chore: merge prototyping branch
Most of the core configuration is done, and now the project has a reasonable path for the future. More features will be added and refactored in future pull requests and commits.
This commit is contained in:
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
5
.husky/pre-commit
Normal file
5
.husky/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm dlx commitlint --edit ${1}
|
||||
pnpm dlx lint-staged
|
||||
4
.husky/prepare-commit-msg
Normal file
4
.husky/prepare-commit-msg
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm dlx devmoji -e --lint
|
||||
4
.lintstagedrc
Normal file
4
.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "./.git",
|
||||
"*": "ESLINT_USE_FLAT_CONFIG=true eslint . --fix"
|
||||
}
|
||||
25
.vscode/settings.json
vendored
Normal file
25
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"eslint.experimental.useFlatConfig": true,
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true,
|
||||
"source.organizeImports": false,
|
||||
},
|
||||
// The following is optional.
|
||||
// It's better to put under project setting `.vscode/settings.json`
|
||||
// to avoid conflicts with working with different eslint configs
|
||||
// that does not support all formats.
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
]
|
||||
}
|
||||
4
commitlint.config.cjs
Normal file
4
commitlint.config.cjs
Normal file
@@ -0,0 +1,4 @@
|
||||
/** @type {import('@commitlint/types').UserConfig} */
|
||||
const config = { extends: ['@commitlint/config-conventional'] };
|
||||
|
||||
module.exports = config;
|
||||
7
eslint.config.js
Normal file
7
eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'readable';
|
||||
|
||||
export default defineConfig({
|
||||
environment: {
|
||||
node: true,
|
||||
},
|
||||
});
|
||||
13
fixtures/svelte/.eslintignore
Normal file
13
fixtures/svelte/.eslintignore
Normal file
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
29
fixtures/svelte/.eslintrc.cjs
Normal file
29
fixtures/svelte/.eslintrc.cjs
Normal file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: false,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
12
fixtures/svelte/.gitignore
vendored
Normal file
12
fixtures/svelte/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.vercel
|
||||
.output
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
4
fixtures/svelte/.lintstagedrc
Normal file
4
fixtures/svelte/.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "../../.git",
|
||||
"*": "ESLINT_USE_FLAT_CONFIG=true eslint . --fix"
|
||||
}
|
||||
2
fixtures/svelte/.npmrc
Normal file
2
fixtures/svelte/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
||||
engine-strict=true
|
||||
resolution-mode=highest
|
||||
38
fixtures/svelte/README.md
Normal file
38
fixtures/svelte/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
29
fixtures/svelte/package.json
Normal file
29
fixtures/svelte/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "svelte",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "ESLINT_USE_FLAT_CONFIG=true eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource/fira-mono": "^4.5.10",
|
||||
"@neoconfetti/svelte": "^1.0.0",
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
12
fixtures/svelte/src/app.d.ts
vendored
Normal file
12
fixtures/svelte/src/app.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
12
fixtures/svelte/src/app.html
Normal file
12
fixtures/svelte/src/app.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
16
fixtures/svelte/src/lib/images/github.svg
Normal file
16
fixtures/svelte/src/lib/images/github.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 -3 30 30">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5229 6.47715 22 12 22C17.5229 22 22 17.5229 22 12C22 6.47715 17.5229 2 12 2ZM0 12C0 5.3726 5.3726 0 12 0C18.6274 0 24 5.3726 24 12C24 18.6274 18.6274 24 12 24C5.3726 24 0 18.6274 0 12Z"
|
||||
fill="rgba(0,0,0,0.7)"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.59162 22.7357C9.49492 22.6109 9.49492 21.4986 9.59162 19.399C8.55572 19.4347 7.90122 19.3628 7.62812 19.1833C7.21852 18.9139 6.80842 18.0833 6.44457 17.4979C6.08072 16.9125 5.27312 16.8199 4.94702 16.6891C4.62091 16.5582 4.53905 16.0247 5.84562 16.4282C7.15222 16.8316 7.21592 17.9303 7.62812 18.1872C8.04032 18.4441 9.02572 18.3317 9.47242 18.1259C9.91907 17.9201 9.88622 17.1538 9.96587 16.8503C10.0666 16.5669 9.71162 16.5041 9.70382 16.5018C9.26777 16.5018 6.97697 16.0036 6.34772 13.7852C5.71852 11.5669 6.52907 10.117 6.96147 9.49369C7.24972 9.07814 7.22422 8.19254 6.88497 6.83679C8.11677 6.67939 9.06732 7.06709 9.73672 7.99999C9.73737 8.00534 10.6143 7.47854 12.0001 7.47854C13.386 7.47854 13.8777 7.90764 14.2571 7.99999C14.6365 8.09234 14.94 6.36699 17.2834 6.83679C16.7942 7.79839 16.3844 8.99999 16.6972 9.49369C17.0099 9.98739 18.2372 11.5573 17.4833 13.7852C16.9807 15.2706 15.9927 16.1761 14.5192 16.5018C14.3502 16.5557 14.2658 16.6427 14.2658 16.7627C14.2658 16.9427 14.4942 16.9624 14.8233 17.8058C15.0426 18.368 15.0585 19.9739 14.8708 22.6234C14.3953 22.7445 14.0254 22.8257 13.7611 22.8673C13.2924 22.9409 12.7835 22.9822 12.2834 22.9982C11.7834 23.0141 11.6098 23.0123 10.9185 22.948C10.4577 22.9051 10.0154 22.8343 9.59162 22.7357Z"
|
||||
fill="rgba(0,0,0,0.7)"
|
||||
stroke="none"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
fixtures/svelte/src/lib/images/svelte-logo.svg
Normal file
1
fixtures/svelte/src/lib/images/svelte-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:#ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
fixtures/svelte/src/lib/images/svelte-welcome.png
Normal file
BIN
fixtures/svelte/src/lib/images/svelte-welcome.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
BIN
fixtures/svelte/src/lib/images/svelte-welcome.webp
Normal file
BIN
fixtures/svelte/src/lib/images/svelte-welcome.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
53
fixtures/svelte/src/routes/+layout.svelte
Normal file
53
fixtures/svelte/src/routes/+layout.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script>
|
||||
import Header from './Header.svelte';
|
||||
import './styles.css';
|
||||
</script>
|
||||
|
||||
<div class="app">
|
||||
<Header />
|
||||
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to learn SvelteKit</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
width: 100%;
|
||||
max-width: 64rem;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
footer {
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
fixtures/svelte/src/routes/+page.svelte
Normal file
59
fixtures/svelte/src/routes/+page.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script>
|
||||
import Counter from './Counter.svelte';
|
||||
import welcome from '$lib/images/svelte-welcome.webp';
|
||||
import welcome_fallback from '$lib/images/svelte-welcome.png';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Home</title>
|
||||
<meta name="description" content="Svelte demo app" />
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<h1>
|
||||
<span class="welcome">
|
||||
<picture>
|
||||
<source srcset={welcome} type="image/webp" />
|
||||
<img src={welcome_fallback} alt="Welcome" />
|
||||
</picture>
|
||||
</span>
|
||||
|
||||
to your new<br />SvelteKit app
|
||||
</h1>
|
||||
|
||||
<h2>
|
||||
try editing <strong>src/routes/+page.svelte</strong>
|
||||
</h2>
|
||||
|
||||
<Counter />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding: 0 0 calc(100% * 495 / 2048) 0;
|
||||
}
|
||||
|
||||
.welcome img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
3
fixtures/svelte/src/routes/+page.ts
Normal file
3
fixtures/svelte/src/routes/+page.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// since there's no dynamic data here, we can prerender
|
||||
// it so that it gets served as a static asset in production
|
||||
export const prerender = true;
|
||||
102
fixtures/svelte/src/routes/Counter.svelte
Normal file
102
fixtures/svelte/src/routes/Counter.svelte
Normal file
@@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { spring } from 'svelte/motion';
|
||||
|
||||
let count = 0;
|
||||
|
||||
const displayed_count = spring();
|
||||
$: displayed_count.set(count);
|
||||
$: offset = modulo($displayed_count, 1);
|
||||
|
||||
function modulo(n: number, m: number) {
|
||||
// handle negative numbers
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="counter">
|
||||
<button on:click={() => (count -= 1)} aria-label="Decrease the counter by one">
|
||||
<svg aria-hidden="true" viewBox="0 0 1 1">
|
||||
<path d="M0,0.5 L1,0.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="counter-viewport">
|
||||
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
|
||||
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
|
||||
<strong>{Math.floor($displayed_count)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button on:click={() => (count += 1)} aria-label="Increase the counter by one">
|
||||
<svg aria-hidden="true" viewBox="0 0 1 1">
|
||||
<path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.counter {
|
||||
display: flex;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.counter button {
|
||||
width: 2em;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
touch-action: manipulation;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.counter button:hover {
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
path {
|
||||
vector-effect: non-scaling-stroke;
|
||||
stroke-width: 2px;
|
||||
stroke: #444;
|
||||
}
|
||||
|
||||
.counter-viewport {
|
||||
width: 8em;
|
||||
height: 4em;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.counter-viewport strong {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: 400;
|
||||
color: var(--color-theme-1);
|
||||
font-size: 4rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.counter-digits {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
top: -100%;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
129
fixtures/svelte/src/routes/Header.svelte
Normal file
129
fixtures/svelte/src/routes/Header.svelte
Normal file
@@ -0,0 +1,129 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import logo from '$lib/images/svelte-logo.svg';
|
||||
import github from '$lib/images/github.svg';
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<div class="corner">
|
||||
<a href="https://kit.svelte.dev">
|
||||
<img src={logo} alt="SvelteKit" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<svg viewBox="0 0 2 3" aria-hidden="true">
|
||||
<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
|
||||
</svg>
|
||||
<ul>
|
||||
<li aria-current={$page.url.pathname === '/' ? 'page' : undefined}>
|
||||
<a href="/">Home</a>
|
||||
</li>
|
||||
<li aria-current={$page.url.pathname === '/about' ? 'page' : undefined}>
|
||||
<a href="/about">About</a>
|
||||
</li>
|
||||
<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
|
||||
<a href="/sverdle">Sverdle</a>
|
||||
</li>
|
||||
</ul>
|
||||
<svg viewBox="0 0 2 3" aria-hidden="true">
|
||||
<path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" />
|
||||
</svg>
|
||||
</nav>
|
||||
|
||||
<div class="corner">
|
||||
<a href="https://github.com/sveltejs/kit">
|
||||
<img src={github} alt="GitHub" />
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.corner {
|
||||
width: 3em;
|
||||
height: 3em;
|
||||
}
|
||||
|
||||
.corner a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.corner img {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
--background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 2em;
|
||||
height: 3em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: var(--background);
|
||||
}
|
||||
|
||||
ul {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 3em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
background: var(--background);
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
li[aria-current='page']::before {
|
||||
--size: 6px;
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - var(--size));
|
||||
border: var(--size) solid transparent;
|
||||
border-top: var(--size) solid var(--color-theme-1);
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 700;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s linear;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-theme-1);
|
||||
}
|
||||
</style>
|
||||
26
fixtures/svelte/src/routes/about/+page.svelte
Normal file
26
fixtures/svelte/src/routes/about/+page.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<svelte:head>
|
||||
<title>About</title>
|
||||
<meta name="description" content="About this app" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text-column">
|
||||
<h1>About this app</h1>
|
||||
|
||||
<p>
|
||||
This is a <a href="https://kit.svelte.dev">SvelteKit</a> app. You can make your own by typing the
|
||||
following into your command line and following the prompts:
|
||||
</p>
|
||||
|
||||
<pre>npm create svelte@latest</pre>
|
||||
|
||||
<p>
|
||||
The page you're looking at is purely static HTML, with no client-side interactivity needed.
|
||||
Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening
|
||||
the devtools network panel and reloading.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The <a href="/sverdle">Sverdle</a> page illustrates SvelteKit's data loading and form handling. Try
|
||||
using it with JavaScript disabled!
|
||||
</p>
|
||||
</div>
|
||||
9
fixtures/svelte/src/routes/about/+page.ts
Normal file
9
fixtures/svelte/src/routes/about/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
// we don't need any JS on this page, though we'll load
|
||||
// it in dev so that we get hot module replacement
|
||||
export const csr = dev;
|
||||
|
||||
// since there's no dynamic data here, we can prerender
|
||||
// it so that it gets served as a static asset in production
|
||||
export const prerender = true;
|
||||
107
fixtures/svelte/src/routes/styles.css
Normal file
107
fixtures/svelte/src/routes/styles.css
Normal file
@@ -0,0 +1,107 @@
|
||||
@import '@fontsource/fira-mono';
|
||||
|
||||
:root {
|
||||
--font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: 'Fira Mono', monospace;
|
||||
--color-bg-0: rgb(202, 216, 228);
|
||||
--color-bg-1: hsl(209, 36%, 86%);
|
||||
--color-bg-2: hsl(224, 44%, 95%);
|
||||
--color-theme-1: #ff3e00;
|
||||
--color-theme-2: #4075a6;
|
||||
--color-text: rgba(0, 0, 0, 0.7);
|
||||
--column-width: 42rem;
|
||||
--column-margin-top: 4rem;
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background-attachment: fixed;
|
||||
background-color: var(--color-bg-1);
|
||||
background-size: 100vw 100vh;
|
||||
background-image: radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
rgba(255, 255, 255, 0.75) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
),
|
||||
linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
p {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-theme-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 16px;
|
||||
font-family: var(--font-mono);
|
||||
background-color: rgba(255, 255, 255, 0.45);
|
||||
border-radius: 3px;
|
||||
box-shadow: 2px 2px 6px rgb(255 255 255 / 25%);
|
||||
padding: 0.5em;
|
||||
overflow-x: auto;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.text-column {
|
||||
display: flex;
|
||||
max-width: 48rem;
|
||||
flex: 0.6;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
h1 {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
69
fixtures/svelte/src/routes/sverdle/+page.server.ts
Normal file
69
fixtures/svelte/src/routes/sverdle/+page.server.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { Game } from './game';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
|
||||
export const load = (({ cookies }) => {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
return {
|
||||
/**
|
||||
* The player's guessed words so far
|
||||
*/
|
||||
guesses: game.guesses,
|
||||
|
||||
/**
|
||||
* An array of strings like '__x_c' corresponding to the guesses, where 'x' means
|
||||
* an exact match, and 'c' means a close match (right letter, wrong place)
|
||||
*/
|
||||
answers: game.answers,
|
||||
|
||||
/**
|
||||
* The correct answer, revealed if the game is over
|
||||
*/
|
||||
answer: game.answers.length >= 6 ? game.answer : null
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
/**
|
||||
* Modify game state in reaction to a keypress. If client-side JavaScript
|
||||
* is available, this will happen in the browser instead of here
|
||||
*/
|
||||
update: async ({ request, cookies }) => {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
const data = await request.formData();
|
||||
const key = data.get('key');
|
||||
|
||||
const i = game.answers.length;
|
||||
|
||||
if (key === 'backspace') {
|
||||
game.guesses[i] = game.guesses[i].slice(0, -1);
|
||||
} else {
|
||||
game.guesses[i] += key;
|
||||
}
|
||||
|
||||
cookies.set('sverdle', game.toString());
|
||||
},
|
||||
|
||||
/**
|
||||
* Modify game state in reaction to a guessed word. This logic always runs on
|
||||
* the server, so that people can't cheat by peeking at the JavaScript
|
||||
*/
|
||||
enter: async ({ request, cookies }) => {
|
||||
const game = new Game(cookies.get('sverdle'));
|
||||
|
||||
const data = await request.formData();
|
||||
const guess = data.getAll('guess') as string[];
|
||||
|
||||
if (!game.enter(guess)) {
|
||||
return fail(400, { badGuess: true });
|
||||
}
|
||||
|
||||
cookies.set('sverdle', game.toString());
|
||||
},
|
||||
|
||||
restart: async ({ cookies }) => {
|
||||
cookies.delete('sverdle');
|
||||
}
|
||||
} satisfies Actions;
|
||||
406
fixtures/svelte/src/routes/sverdle/+page.svelte
Normal file
406
fixtures/svelte/src/routes/sverdle/+page.svelte
Normal file
@@ -0,0 +1,406 @@
|
||||
<script lang="ts">
|
||||
import { confetti } from '@neoconfetti/svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageData, ActionData } from './$types';
|
||||
import { reduced_motion } from './reduced-motion';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
export let form: ActionData;
|
||||
|
||||
/** Whether or not the user has won */
|
||||
$: won = data.answers.at(-1) === 'xxxxx';
|
||||
|
||||
/** The index of the current guess */
|
||||
$: i = won ? -1 : data.answers.length;
|
||||
|
||||
/** Whether the current guess can be submitted */
|
||||
$: submittable = data.guesses[i]?.length === 5;
|
||||
|
||||
/**
|
||||
* A map of classnames for all letters that have been guessed,
|
||||
* used for styling the keyboard
|
||||
*/
|
||||
let classnames: Record<string, 'exact' | 'close' | 'missing'>;
|
||||
|
||||
/**
|
||||
* A map of descriptions for all letters that have been guessed,
|
||||
* used for adding text for assistive technology (e.g. screen readers)
|
||||
*/
|
||||
let description: Record<string, string>;
|
||||
|
||||
$: {
|
||||
classnames = {};
|
||||
description = {};
|
||||
|
||||
data.answers.forEach((answer, i) => {
|
||||
const guess = data.guesses[i];
|
||||
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const letter = guess[i];
|
||||
|
||||
if (answer[i] === 'x') {
|
||||
classnames[letter] = 'exact';
|
||||
description[letter] = 'correct';
|
||||
} else if (!classnames[letter]) {
|
||||
classnames[letter] = answer[i] === 'c' ? 'close' : 'missing';
|
||||
description[letter] = answer[i] === 'c' ? 'present' : 'absent';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the game state without making a trip to the server,
|
||||
* if client-side JavaScript is enabled
|
||||
*/
|
||||
function update(event: MouseEvent) {
|
||||
const guess = data.guesses[i];
|
||||
const key = (event.target as HTMLButtonElement).getAttribute(
|
||||
'data-key'
|
||||
);
|
||||
|
||||
if (key === 'backspace') {
|
||||
data.guesses[i] = guess.slice(0, -1);
|
||||
if (form?.badGuess) form.badGuess = false;
|
||||
} else if (guess.length < 5) {
|
||||
data.guesses[i] += key;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger form logic in response to a keydown event, so that
|
||||
* desktop users can use the keyboard to play the game
|
||||
*/
|
||||
function keydown(event: KeyboardEvent) {
|
||||
if (event.metaKey) return;
|
||||
|
||||
document
|
||||
.querySelector(`[data-key="${event.key}" i]`)
|
||||
?.dispatchEvent(new MouseEvent('click', { cancelable: true }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={keydown} />
|
||||
|
||||
<svelte:head>
|
||||
<title>Sverdle</title>
|
||||
<meta name="description" content="A Wordle clone written in SvelteKit" />
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="visually-hidden">Sverdle</h1>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/enter"
|
||||
use:enhance={() => {
|
||||
// prevent default callback from resetting the form
|
||||
return ({ update }) => {
|
||||
update({ reset: false });
|
||||
};
|
||||
}}
|
||||
>
|
||||
<a class="how-to-play" href="/sverdle/how-to-play">How to play</a>
|
||||
|
||||
<div class="grid" class:playing={!won} class:bad-guess={form?.badGuess}>
|
||||
{#each Array.from(Array(6).keys()) as row (row)}
|
||||
{@const current = row === i}
|
||||
<h2 class="visually-hidden">Row {row + 1}</h2>
|
||||
<div class="row" class:current>
|
||||
{#each Array.from(Array(5).keys()) as column (column)}
|
||||
{@const answer = data.answers[row]?.[column]}
|
||||
{@const value = data.guesses[row]?.[column] ?? ''}
|
||||
{@const selected = current && column === data.guesses[row].length}
|
||||
{@const exact = answer === 'x'}
|
||||
{@const close = answer === 'c'}
|
||||
{@const missing = answer === '_'}
|
||||
<div class="letter" class:exact class:close class:missing class:selected>
|
||||
{value}
|
||||
<span class="visually-hidden">
|
||||
{#if exact}
|
||||
(correct)
|
||||
{:else if close}
|
||||
(present)
|
||||
{:else if missing}
|
||||
(absent)
|
||||
{:else}
|
||||
empty
|
||||
{/if}
|
||||
</span>
|
||||
<input name="guess" disabled={!current} type="hidden" {value} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
{#if won || data.answers.length >= 6}
|
||||
{#if !won && data.answer}
|
||||
<p>the answer was "{data.answer}"</p>
|
||||
{/if}
|
||||
<button data-key="enter" class="restart selected" formaction="?/restart">
|
||||
{won ? 'you won :)' : `game over :(`} play again?
|
||||
</button>
|
||||
{:else}
|
||||
<div class="keyboard">
|
||||
<button data-key="enter" class:selected={submittable} disabled={!submittable}>enter</button>
|
||||
|
||||
<button
|
||||
on:click|preventDefault={update}
|
||||
data-key="backspace"
|
||||
formaction="?/update"
|
||||
name="key"
|
||||
value="backspace"
|
||||
>
|
||||
back
|
||||
</button>
|
||||
|
||||
{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row}
|
||||
<div class="row">
|
||||
{#each row as letter}
|
||||
<button
|
||||
on:click|preventDefault={update}
|
||||
data-key={letter}
|
||||
class={classnames[letter]}
|
||||
disabled={data.guesses[i].length === 5}
|
||||
formaction="?/update"
|
||||
name="key"
|
||||
value={letter}
|
||||
aria-label="{letter} {description[letter] || ''}"
|
||||
>
|
||||
{letter}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#if won}
|
||||
<div
|
||||
style="position: absolute; left: 50%; top: 30%"
|
||||
use:confetti={{
|
||||
particleCount: $reduced_motion ? 0 : undefined,
|
||||
force: 0.7,
|
||||
stageWidth: window.innerWidth,
|
||||
stageHeight: window.innerHeight,
|
||||
colors: ['#ff3e00', '#40b3ff', '#676778']
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
form {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.how-to-play {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.how-to-play::before {
|
||||
content: 'i';
|
||||
display: inline-block;
|
||||
font-size: 0.8em;
|
||||
font-weight: 900;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.2em;
|
||||
line-height: 1;
|
||||
border: 1.5px solid var(--color-text);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
margin: 0 0.5em 0 0;
|
||||
position: relative;
|
||||
top: -0.05em;
|
||||
}
|
||||
|
||||
.grid {
|
||||
--width: min(100vw, 40vh, 380px);
|
||||
max-width: var(--width);
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.grid .row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-gap: 0.2rem;
|
||||
margin: 0 0 0.2rem 0;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.grid.bad-guess .row.current {
|
||||
animation: wiggle 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.grid.playing .row.current {
|
||||
filter: drop-shadow(3px 3px 10px var(--color-bg-0));
|
||||
}
|
||||
|
||||
.letter {
|
||||
aspect-ratio: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
text-transform: lowercase;
|
||||
border: none;
|
||||
font-size: calc(0.08 * var(--width));
|
||||
border-radius: 2px;
|
||||
background: white;
|
||||
margin: 0;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.letter.missing {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.letter.exact {
|
||||
background: var(--color-theme-2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.letter.close {
|
||||
border: 2px solid var(--color-theme-2);
|
||||
}
|
||||
|
||||
.selected {
|
||||
outline: 2px solid var(--color-theme-1);
|
||||
}
|
||||
|
||||
.controls {
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
height: min(18vh, 10rem);
|
||||
}
|
||||
|
||||
.keyboard {
|
||||
--gap: 0.2rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.keyboard .row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.keyboard button,
|
||||
.keyboard button:disabled {
|
||||
--size: min(8vw, 4vh, 40px);
|
||||
background-color: white;
|
||||
color: black;
|
||||
width: var(--size);
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-size: calc(var(--size) * 0.5);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.keyboard button.exact {
|
||||
background: var(--color-theme-2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.keyboard button.missing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.keyboard button.close {
|
||||
border: 2px solid var(--color-theme-2);
|
||||
}
|
||||
|
||||
.keyboard button:focus {
|
||||
background: var(--color-theme-1);
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.keyboard button[data-key='enter'],
|
||||
.keyboard button[data-key='backspace'] {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: calc(1.5 * var(--size));
|
||||
height: calc(1 / 3 * (100% - 2 * var(--gap)));
|
||||
text-transform: uppercase;
|
||||
font-size: calc(0.3 * var(--size));
|
||||
padding-top: calc(0.15 * var(--size));
|
||||
}
|
||||
|
||||
.keyboard button[data-key='enter'] {
|
||||
right: calc(50% + 3.5 * var(--size) + 0.8rem);
|
||||
}
|
||||
|
||||
.keyboard button[data-key='backspace'] {
|
||||
left: calc(50% + 3.5 * var(--size) + 0.8rem);
|
||||
}
|
||||
|
||||
.keyboard button[data-key='enter']:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.restart {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.restart:focus,
|
||||
.restart:hover {
|
||||
background: var(--color-theme-1);
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
10% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
30% {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-6px);
|
||||
}
|
||||
70% {
|
||||
transform: translateX(+4px);
|
||||
}
|
||||
90% {
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
fixtures/svelte/src/routes/sverdle/game.ts
Normal file
75
fixtures/svelte/src/routes/sverdle/game.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { words, allowed } from './words.server';
|
||||
|
||||
export class Game {
|
||||
index: number;
|
||||
guesses: string[];
|
||||
answers: string[];
|
||||
answer: string;
|
||||
|
||||
/**
|
||||
* Create a game object from the player's cookie, or initialise a new game
|
||||
*/
|
||||
constructor(serialized: string | undefined = undefined) {
|
||||
if (serialized) {
|
||||
const [index, guesses, answers] = serialized.split('-');
|
||||
|
||||
this.index = +index;
|
||||
this.guesses = guesses ? guesses.split(' ') : [];
|
||||
this.answers = answers ? answers.split(' ') : [];
|
||||
} else {
|
||||
this.index = Math.floor(Math.random() * words.length);
|
||||
this.guesses = ['', '', '', '', '', ''];
|
||||
this.answers = [];
|
||||
}
|
||||
|
||||
this.answer = words[this.index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game state based on a guess of a five-letter word. Returns
|
||||
* true if the guess was valid, false otherwise
|
||||
*/
|
||||
enter(letters: string[]) {
|
||||
const word = letters.join('');
|
||||
const valid = allowed.has(word);
|
||||
|
||||
if (!valid) return false;
|
||||
|
||||
this.guesses[this.answers.length] = word;
|
||||
|
||||
const available = Array.from(this.answer);
|
||||
const answer = Array(5).fill('_');
|
||||
|
||||
// first, find exact matches
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
if (letters[i] === available[i]) {
|
||||
answer[i] = 'x';
|
||||
available[i] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// then find close matches (this has to happen
|
||||
// in a second step, otherwise an early close
|
||||
// match can prevent a later exact match)
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
if (answer[i] === '_') {
|
||||
const index = available.indexOf(letters[i]);
|
||||
if (index !== -1) {
|
||||
answer[i] = 'c';
|
||||
available[index] = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.answers.push(answer.join(''));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize game state so it can be set as a cookie
|
||||
*/
|
||||
toString() {
|
||||
return `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;
|
||||
}
|
||||
}
|
||||
95
fixtures/svelte/src/routes/sverdle/how-to-play/+page.svelte
Normal file
95
fixtures/svelte/src/routes/sverdle/how-to-play/+page.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<svelte:head>
|
||||
<title>How to play Sverdle</title>
|
||||
<meta name="description" content="How to play Sverdle" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text-column">
|
||||
<h1>How to play Sverdle</h1>
|
||||
|
||||
<p>
|
||||
Sverdle is a clone of <a href="https://www.nytimes.com/games/wordle/index.html">Wordle</a>, the
|
||||
word guessing game. To play, enter a five-letter English word. For example:
|
||||
</p>
|
||||
|
||||
<div class="example">
|
||||
<span class="close">r</span>
|
||||
<span class="missing">i</span>
|
||||
<span class="close">t</span>
|
||||
<span class="missing">z</span>
|
||||
<span class="exact">y</span>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
The <span class="exact">y</span> is in the right place. <span class="close">r</span> and
|
||||
<span class="close">t</span>
|
||||
are the right letters, but in the wrong place. The other letters are wrong, and can be discarded.
|
||||
Let's make another guess:
|
||||
</p>
|
||||
|
||||
<div class="example">
|
||||
<span class="exact">p</span>
|
||||
<span class="exact">a</span>
|
||||
<span class="exact">r</span>
|
||||
<span class="exact">t</span>
|
||||
<span class="exact">y</span>
|
||||
</div>
|
||||
|
||||
<p>This time we guessed right! You have <strong>six</strong> guesses to get the word.</p>
|
||||
|
||||
<p>
|
||||
Unlike the original Wordle, Sverdle runs on the server instead of in the browser, making it
|
||||
impossible to cheat. It uses <code><form></code> and cookies to submit data, meaning you can
|
||||
even play with JavaScript disabled!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
span {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 0.8em;
|
||||
width: 2.4em;
|
||||
height: 2.4em;
|
||||
background-color: white;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
border-width: 2px;
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.missing {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.close {
|
||||
border-style: solid;
|
||||
border-color: var(--color-theme-2);
|
||||
}
|
||||
|
||||
.exact {
|
||||
background: var(--color-theme-2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.example {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 1rem 0;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.example span {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
p span {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
border-radius: 1px;
|
||||
font-size: 0.4em;
|
||||
transform: scale(2) translate(0, -10%);
|
||||
margin: 0 1em;
|
||||
}
|
||||
</style>
|
||||
9
fixtures/svelte/src/routes/sverdle/how-to-play/+page.ts
Normal file
9
fixtures/svelte/src/routes/sverdle/how-to-play/+page.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
// we don't need any JS on this page, though we'll load
|
||||
// it in dev so that we get hot module replacement
|
||||
export const csr = dev;
|
||||
|
||||
// since there's no dynamic data here, we can prerender
|
||||
// it so that it gets served as a static asset in production
|
||||
export const prerender = true;
|
||||
23
fixtures/svelte/src/routes/sverdle/reduced-motion.ts
Normal file
23
fixtures/svelte/src/routes/sverdle/reduced-motion.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { readable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const reduced_motion_query = '(prefers-reduced-motion: reduce)';
|
||||
|
||||
const get_initial_motion_preference = () => {
|
||||
if (!browser) return false;
|
||||
return window.matchMedia(reduced_motion_query).matches;
|
||||
};
|
||||
|
||||
export const reduced_motion = readable(get_initial_motion_preference(), (set) => {
|
||||
if (browser) {
|
||||
const set_reduced_motion = (event: MediaQueryListEvent) => {
|
||||
set(event.matches);
|
||||
};
|
||||
const media_query_list = window.matchMedia(reduced_motion_query);
|
||||
media_query_list.addEventListener('change', set_reduced_motion);
|
||||
|
||||
return () => {
|
||||
media_query_list.removeEventListener('change', set_reduced_motion);
|
||||
};
|
||||
}
|
||||
});
|
||||
12980
fixtures/svelte/src/routes/sverdle/words.server.ts
Normal file
12980
fixtures/svelte/src/routes/sverdle/words.server.ts
Normal file
File diff suppressed because it is too large
Load Diff
BIN
fixtures/svelte/static/favicon.png
Normal file
BIN
fixtures/svelte/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
3
fixtures/svelte/static/robots.txt
Normal file
3
fixtures/svelte/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
18
fixtures/svelte/svelte.config.js
Normal file
18
fixtures/svelte/svelte.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import { vitePreprocess } from '@sveltejs/kit/vite';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
fixtures/svelte/tsconfig.json
Normal file
17
fixtures/svelte/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
6
fixtures/svelte/vite.config.ts
Normal file
6
fixtures/svelte/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "readable-monorepo",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "ESLINT_USE_FLAT_CONFIG=true eslint .",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"readable": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
"@commitlint/config-conventional": "^17.6.6",
|
||||
"@commitlint/types": "^17.4.4",
|
||||
"eslint": "^8.44.0",
|
||||
"husky": "^8.0.3"
|
||||
}
|
||||
}
|
||||
4
packages/core/.lintstagedrc
Normal file
4
packages/core/.lintstagedrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"gitDir": "../../.git",
|
||||
"*": "ESLINT_USE_FLAT_CONFIG=true eslint . --fix"
|
||||
}
|
||||
3
packages/core/index.d.ts
vendored
Normal file
3
packages/core/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { Config, ESConfig } from './src/types';
|
||||
|
||||
export async function defineConfig(config: Config): Promise<ESConfig[]>;
|
||||
5
packages/core/jsconfig.json
Normal file
5
packages/core/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"exclude": ["./node_modules/**", "./dist/**"],
|
||||
"include": ["./index.d.ts", "./src/**/*.ts", "./src/**/*.js"],
|
||||
}
|
||||
41
packages/core/package.json
Normal file
41
packages/core/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "readable",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"module": "./src/index.js",
|
||||
"source": "./src/index.js",
|
||||
"exports": {
|
||||
"default": "./src/index.js",
|
||||
"import": "./src/index.js",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "./src/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "ESLINT_USE_FLAT_CONFIG=true eslint ."
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/eslint__js": "^8.42.0",
|
||||
"@types/node": "^20.4.2",
|
||||
"eslint": "^8.45.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": "^2.1.0",
|
||||
"@eslint/js": "^8.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
||||
"@typescript-eslint/parser": "^6.1.0",
|
||||
"eslint-plugin-jsdoc": "^46.4.4",
|
||||
"globals": "^13.20.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.45.0",
|
||||
"typescript": "^5.1.6"
|
||||
}
|
||||
}
|
||||
26
packages/core/src/@types/eslint-plugin-jsdoc.d.ts
vendored
Normal file
26
packages/core/src/@types/eslint-plugin-jsdoc.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ESLint } from 'eslint';
|
||||
|
||||
/**
|
||||
* @see {@link https://www.npmjs.org/package/eslint-plugin-jsdoc npm package}
|
||||
*
|
||||
* @summary JSDoc specific linting rules for ESLint.
|
||||
*
|
||||
* ---
|
||||
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
|
||||
* config types. ESlint already has backwards compatibility for plugins not created in the
|
||||
* new flat config.
|
||||
*/
|
||||
declare module 'eslint-plugin-jsdoc' {
|
||||
interface jsDocESlintPlugin extends ESLint.Plugin {
|
||||
configs: ESLint.Plugin['configs'] & {
|
||||
recommended: ESLint.ConfigData
|
||||
'recommended-error': ESLint.ConfigData
|
||||
'recommended-typescript': ESLint.ConfigData
|
||||
'recommended-typescript-error': ESLint.ConfigData
|
||||
'recommended-typescript-flavor': ESLint.ConfigData
|
||||
'recommended-typescript-flavor-error': ESLint.ConfigData
|
||||
}
|
||||
}
|
||||
declare const plugin: jsDocESlintPlugin;
|
||||
export default plugin;
|
||||
}
|
||||
10
packages/core/src/@types/globals.d.ts
vendored
Normal file
10
packages/core/src/@types/globals.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
declare module 'globals' {
|
||||
const globals: {
|
||||
builtin: Record<string, boolean>
|
||||
browser: Record<string, boolean>
|
||||
node: Record<string, boolean>
|
||||
nodeBuiltin: Record<string, boolean>
|
||||
commonjs: Record<string, boolean>
|
||||
};
|
||||
export default globals;
|
||||
}
|
||||
48
packages/core/src/@types/typescript-eslint.d.ts
vendored
Normal file
48
packages/core/src/@types/typescript-eslint.d.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { ESLint, Linter } from 'eslint';
|
||||
|
||||
/**
|
||||
* @see {@link https://www.npmjs.com/package/@typescript-eslint/eslint-plugin npm package}
|
||||
*
|
||||
* @summary An ESLint plugin which provides lint rules for TypeScript codebases.
|
||||
*
|
||||
* ---
|
||||
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
|
||||
* config types. ESlint already has backwards compatibility for plugins not created in the
|
||||
* new flat config.
|
||||
*/
|
||||
declare module '@typescript-eslint/eslint-plugin' {
|
||||
interface typescriptEslintPlugin extends ESLint.Plugin {
|
||||
configs: {
|
||||
recommended: {
|
||||
rules: Linter.RulesRecord
|
||||
}
|
||||
'recommended-requiring-type-checking': {
|
||||
rules: Linter.RulesRecord
|
||||
}
|
||||
'eslint-recommended': {
|
||||
rules: Linter.RulesRecord
|
||||
}
|
||||
strict: {
|
||||
rules: Linter.RulesRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
declare const plugin: typescriptEslintPlugin;
|
||||
export default plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link https://www.npmjs.com/package/@typescript-eslint/parser npm package}
|
||||
*
|
||||
* @summary An ESLint parser which leverages TypeScript ESTree to allow for ESLint
|
||||
* to lint TypeScript source code.
|
||||
*
|
||||
* ---
|
||||
* **Note:** Types in this project where overridden to be compatible with ESLint new flat
|
||||
* config types. ESlint already has backwards compatibility for parsers not created in the
|
||||
* new flat config.
|
||||
*/
|
||||
declare module '@typescript-eslint/parser' {
|
||||
declare const parser: Linter.ParserModule;
|
||||
export default parser;
|
||||
}
|
||||
35
packages/core/src/configs/common.js
Normal file
35
packages/core/src/configs/common.js
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
/**
|
||||
* Common configuration related to language features of Javascript and Typescript
|
||||
*
|
||||
* @type {import('../types').ESConfig}
|
||||
*/
|
||||
const config = {
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': ['error', {
|
||||
'ts-ignore': 'allow-with-description',
|
||||
}],
|
||||
'@typescript-eslint/ban-tslint-comment': 'error',
|
||||
|
||||
'@typescript-eslint/no-require-imports': 'error',
|
||||
|
||||
// Extension rules
|
||||
|
||||
'no-dupe-class-members': 'off',
|
||||
'@typescript-eslint/no-dupe-class-members': 'error',
|
||||
|
||||
'no-invalid-this': 'off',
|
||||
'@typescript-eslint/no-invalid-this': 'error',
|
||||
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
|
||||
'no-use-before-define': 'off',
|
||||
'@typescript-eslint/no-use-before-define': 'error',
|
||||
|
||||
'no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
58
packages/core/src/configs/environments.js
Normal file
58
packages/core/src/configs/environments.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import globals from 'globals';
|
||||
|
||||
/**
|
||||
* @param {import('../types').Config['environment']} environment
|
||||
* Manual configuration of environments, if undefined,
|
||||
* the function tries to detect the environment automatically
|
||||
* @returns {import('../types').ESConfig[]}
|
||||
* ESLint configuration with global variables and environment
|
||||
*/
|
||||
export function environments(environment) {
|
||||
|
||||
environment ||= {
|
||||
node:
|
||||
typeof window === 'undefined' &&
|
||||
typeof process !== 'undefined' &&
|
||||
typeof require !== 'function',
|
||||
deno:
|
||||
typeof window !== 'undefined' &&
|
||||
// @ts-expect-error because this package is develop in node
|
||||
typeof Deno !== 'undefined',
|
||||
browser:
|
||||
typeof window !== 'undefined',
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
languageOptions: {
|
||||
ecmaVersion: environment.ecmaVersion ?? 'latest',
|
||||
globals: {
|
||||
...globals.builtin,
|
||||
...environment.customGlobals,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.cjs', '**/*.cts'],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.commonjs,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.mjs', '**/*.ts', '**/*.mts'],
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...(environment.node ? globals.nodeBuiltin : {}),
|
||||
...(environment.browser || environment.deno ? globals.browser : {}),
|
||||
...(environment.deno ? { Deno: true } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
90
packages/core/src/configs/formatting.js
Normal file
90
packages/core/src/configs/formatting.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Formatting rules/configuration for Javascript and Typescript
|
||||
*
|
||||
* @type {import('../types').ESConfig}
|
||||
*/
|
||||
const config = {
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
rules: {
|
||||
// Formatting rules
|
||||
|
||||
'brace-style': 'off',
|
||||
'@typescript-eslint/brace-style': ['error', 'stroustrup', { allowSingleLine: true }],
|
||||
|
||||
'comma-dangle': 'off',
|
||||
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
|
||||
|
||||
'indent': 'off',
|
||||
'@typescript-eslint/indent': ['error', process.env.READABLE_ESLINT_OPTIONS?.indent === 'space' ? 2 : 'tab', {
|
||||
SwitchCase: 1,
|
||||
VariableDeclarator: 1,
|
||||
outerIIFEBody: 1,
|
||||
MemberExpression: 1,
|
||||
FunctionDeclaration: { parameters: 1, body: 1 },
|
||||
FunctionExpression: { parameters: 1, body: 1 },
|
||||
CallExpression: { arguments: 1 },
|
||||
ArrayExpression: 1,
|
||||
ObjectExpression: 1,
|
||||
ImportDeclaration: 1,
|
||||
flatTernaryExpressions: false,
|
||||
offsetTernaryExpressions: true,
|
||||
ignoreComments: false,
|
||||
ignoredNodes: [
|
||||
'TemplateLiteral *',
|
||||
'JSXElement',
|
||||
'JSXElement > *',
|
||||
'JSXAttribute',
|
||||
'JSXIdentifier',
|
||||
'JSXNamespacedName',
|
||||
'JSXMemberExpression',
|
||||
'JSXSpreadAttribute',
|
||||
'JSXExpressionContainer',
|
||||
'JSXOpeningElement',
|
||||
'JSXClosingElement',
|
||||
'JSXFragment',
|
||||
'JSXOpeningFragment',
|
||||
'JSXClosingFragment',
|
||||
'JSXText',
|
||||
'JSXEmptyExpression',
|
||||
'JSXSpreadChild',
|
||||
'TSTypeParameterInstantiation',
|
||||
'FunctionExpression > .params[decorators.length > 0]',
|
||||
'FunctionExpression > .params > :matches(Decorator, :not(:first-child))',
|
||||
'ClassBody.body > PropertyDefinition[decorators.length > 0] > .key',
|
||||
],
|
||||
}],
|
||||
|
||||
'keyword-spacing': 'off',
|
||||
'@typescript-eslint/keyword-spacing': ['error', { before: true, after: true }],
|
||||
|
||||
'lines-between-class-members': 'off',
|
||||
'@typescript-eslint/lines-between-class-members': ['error'],
|
||||
|
||||
'no-extra-parens': 'off',
|
||||
'@typescript-eslint/no-extra-parens': ['error', 'functions'],
|
||||
|
||||
'object-curly-spacing': 'off',
|
||||
'@typescript-eslint/object-curly-spacing': ['error', 'always'],
|
||||
|
||||
'quotes': 'off',
|
||||
'@typescript-eslint/quotes': ['error', process.env.READABLE_ESLINT_OPTIONS?.quotes ?? 'single'],
|
||||
|
||||
'semi': 'off',
|
||||
'@typescript-eslint/semi': ['error', 'always'],
|
||||
|
||||
'space-before-blocks': 'off',
|
||||
'@typescript-eslint/space-before-blocks': ['error', process.env.READABLE_ESLINT_OPTIONS?.semi ?? 'always'],
|
||||
|
||||
'space-before-function-paren': 'off',
|
||||
'@typescript-eslint/space-before-function-paren': ['error', {
|
||||
anonymous: 'always',
|
||||
named: 'never',
|
||||
asyncArrow: 'always',
|
||||
}],
|
||||
|
||||
'space-infix-ops': 'off',
|
||||
'@typescript-eslint/space-infix-ops': 'error',
|
||||
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
5
packages/core/src/configs/index.js
Normal file
5
packages/core/src/configs/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as common } from './common.js';
|
||||
export { default as formatting } from './formatting.js';
|
||||
export { default as jsdoc } from './jsdoc.js';
|
||||
export { default as typescript } from './typescript.js';
|
||||
export * from './environments.js';
|
||||
20
packages/core/src/configs/jsdoc.js
Normal file
20
packages/core/src/configs/jsdoc.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* JSDoc rules overrides
|
||||
*
|
||||
* @type {import('../types').ESConfig}
|
||||
*/
|
||||
const config = {
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
rules: {
|
||||
'jsdoc/tag-lines': ['error', 'always', {
|
||||
count: 1,
|
||||
applyToEndTag: false,
|
||||
startLines: 1,
|
||||
endLines: 0,
|
||||
tags: {
|
||||
param: { lines: 'never' },
|
||||
},
|
||||
}],
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
63
packages/core/src/configs/typescript.js
Normal file
63
packages/core/src/configs/typescript.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import jsdoc from 'eslint-plugin-jsdoc';
|
||||
|
||||
/**
|
||||
* Typescript specific configuration
|
||||
*
|
||||
* @type {import('../types').ESConfig}
|
||||
*/
|
||||
const config = {
|
||||
files: ['**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
// See plugins['jsdoc'] on index.js for more info on this error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
rules: {
|
||||
|
||||
// See plugins['jsdoc'] on index.js for more info on this error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
...jsdoc.configs['recommended-typescript-error'].rules,
|
||||
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/array-type': 'error',
|
||||
'@typescript-eslint/class-literal-property-style': 'error',
|
||||
'@typescript-eslint/consistent-generic-constructors': 'error',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'error',
|
||||
'@typescript-eslint/consistent-type-assertions': 'error',
|
||||
'@typescript-eslint/consistent-type-definitions': 'error',
|
||||
'@typescript-eslint/consistent-type-exports': ['error'],
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports', disallowTypeAnnotations: true }],
|
||||
'@typescript-eslint/no-confusing-non-null-assertion': 'error',
|
||||
'@typescript-eslint/member-delimiter-style': ['error', { multiline: { delimiter: 'none' } }],
|
||||
'@typescript-eslint/type-annotation-spacing': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'error',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/prefer-function-type': 'error',
|
||||
'@typescript-eslint/prefer-namespace-keyword': 'error',
|
||||
|
||||
...(
|
||||
/** @type {() => import('eslint').Linter.RulesRecord} */
|
||||
() => {
|
||||
const inferrableTypes = process.env.READABLE_ESLINT_OPTIONS?.inferrableTypes ?? 'never';
|
||||
|
||||
if (typeof inferrableTypes === 'string') {
|
||||
return {
|
||||
'@typescript-eslint/explicit-function-return-type': inferrableTypes === 'always' ? 'off' : 'error',
|
||||
'@typescript-eslint/no-inferrable-types': inferrableTypes === 'always' ? 'off' : 'error',
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
'@typescript-eslint/explicit-function-return-type': inferrableTypes[1].returnValues ? 'off' : 'error',
|
||||
'@typescript-eslint/no-inferrable-types': [
|
||||
inferrableTypes[0] === 'always' ? 'off' : 'error',
|
||||
{
|
||||
ignoreParameters: inferrableTypes[1].parameters ?? false,
|
||||
ignoreProperties: inferrableTypes[1].properties ?? false,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
)(),
|
||||
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
12
packages/core/src/env.d.ts
vendored
Normal file
12
packages/core/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Config } from './types';
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
READABLE_ESLINT_STRICT: Config['strict']
|
||||
READABLE_ESLINT_OPTIONS: Config['options']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
15
packages/core/src/eslintrc-compact.js
Normal file
15
packages/core/src/eslintrc-compact.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import javascript from '@eslint/js';
|
||||
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// mimic CommonJS variables
|
||||
export const __filename = fileURLToPath(import.meta.url);
|
||||
export const __dirname = path.dirname(__filename);
|
||||
|
||||
export const eslintrc = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: javascript.configs.recommended,
|
||||
allConfig: javascript.configs.all,
|
||||
});
|
||||
80
packages/core/src/index.js
Normal file
80
packages/core/src/index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import { eslintrc } from './eslintrc-compact.js';
|
||||
import tsESlint from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import jsdoc from 'eslint-plugin-jsdoc';
|
||||
import js from '@eslint/js';
|
||||
import * as configs from './configs/index.js';
|
||||
import { getTsConfigs } from './tsconfigs.js';
|
||||
|
||||
/**
|
||||
* @param {import('./types').Config} userConfig
|
||||
* User configuration
|
||||
* @returns {Promise<import('./types').ESConfig[]>}
|
||||
* The complete list of configs for ESLint
|
||||
*/
|
||||
export async function defineConfig(userConfig) {
|
||||
|
||||
userConfig.strict ??= true;
|
||||
userConfig.rootDir ??= process.cwd();
|
||||
userConfig.tsconfig ??= await getTsConfigs(userConfig.rootDir);
|
||||
|
||||
process.env.READABLE_ESLINT_STRICT = userConfig.strict;
|
||||
process.env.READABLE_ESLINT_OPTIONS = {
|
||||
inferrableTypes: userConfig.strict ? 'always' : 'never',
|
||||
...userConfig.options,
|
||||
};
|
||||
|
||||
const userOverrides = (typeof userConfig.overrides !== 'function'
|
||||
? userConfig.overrides
|
||||
: await userConfig.overrides(eslintrc)) ?? [];
|
||||
|
||||
return [
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'**/dist',
|
||||
'**/fixtures',
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ['**/*.js', '**/*.cjs', '**/*.mjs', '**/*.ts', '**/*.cts', '**/*.mts'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tsESlint,
|
||||
/**
|
||||
* @todo
|
||||
* Fix eslint-plugin-jsdoc type definitions.
|
||||
* _Typescript should have detected [eslint-plugin-jsdoc.d.ts](./@types/eslint-plugin-jsdoc.d.ts)._
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
'jsdoc': jsdoc,
|
||||
},
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: userConfig.tsconfig,
|
||||
tsconfigRootDir: userConfig.rootDir,
|
||||
},
|
||||
},
|
||||
// See plugins['jsdoc'] for more info on this error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
rules: {
|
||||
...tsESlint.configs.recommended.rules,
|
||||
...tsESlint.configs['recommended-requiring-type-checking'].rules,
|
||||
...tsESlint.configs['eslint-recommended'].rules,
|
||||
...(userConfig.strict ? tsESlint.configs.strict.rules : null),
|
||||
// See plugins['jsdoc'] for more info on this error
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
...jsdoc.configs['recommended-typescript-flavor-error'].rules,
|
||||
},
|
||||
},
|
||||
configs.common,
|
||||
configs.formatting,
|
||||
configs.jsdoc,
|
||||
configs.typescript,
|
||||
...configs.environments(userConfig.environment),
|
||||
...userOverrides,
|
||||
];
|
||||
}
|
||||
|
||||
87
packages/core/src/tsconfigs.js
Normal file
87
packages/core/src/tsconfigs.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join, normalize } from 'node:path';
|
||||
|
||||
/** @type {(...path: string[]) => string} */
|
||||
function toPath(...path) {
|
||||
return normalize(join(...path));
|
||||
}
|
||||
|
||||
/** @type {(...path: string[]) => boolean} */
|
||||
function exists(...path) {
|
||||
return existsSync(toPath(...path));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} directory what the root directory to detect an workspace/monorepo configuration file
|
||||
* @returns {Promise<string[]>} list of possible paths of packages' tsconfig.json and jsconfig.json files
|
||||
*/
|
||||
async function getMonorepoConfigs(directory) {
|
||||
|
||||
/** @type {string[]} */
|
||||
const paths = [];
|
||||
|
||||
if (exists(directory, 'pnpm-workspace.yaml') || exists(directory, 'pnpm-workspace.yml')) {
|
||||
|
||||
const YAML = await import('yaml');
|
||||
|
||||
const yamlFilePath = exists(directory, 'pnpm-workspace.yaml')
|
||||
? join(directory, 'pnpm-workspace.yaml')
|
||||
: join(directory, 'pnpm-workspace.yml');
|
||||
|
||||
/** @type {{packages?: string[], [properties: string]: unknown}} */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const pnpmWorkspaces = YAML.parse(await readFile(yamlFilePath, 'utf-8'));
|
||||
|
||||
const files = pnpmWorkspaces.packages?.map(w => [
|
||||
toPath(directory, w, 'tsconfig.json'),
|
||||
toPath(directory, w, 'jsconfig.json'),
|
||||
]).flat() ?? [];
|
||||
|
||||
paths.push(...files);
|
||||
|
||||
}
|
||||
else if (exists(directory, 'package.json')) {
|
||||
/** @type {{workspaces?: string[], [properties: string]: unknown}} */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const packageJson = JSON.parse(await readFile(join(directory, 'package.json'), 'utf-8'));
|
||||
|
||||
const files = packageJson.workspaces?.map(w => [
|
||||
toPath(directory, w, 'tsconfig.json'),
|
||||
toPath(directory, w, 'jsconfig.json'),
|
||||
]).flat() ?? [];
|
||||
|
||||
paths.push(...files);
|
||||
|
||||
}
|
||||
|
||||
return paths;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} directory what the root directory to work on
|
||||
* @returns {Promise<string[]>} list of tsconfig.json and jsconfig.json file paths
|
||||
*/
|
||||
export async function getTsConfigs(directory) {
|
||||
|
||||
const rootTSConfig = exists(directory, 'tsconfig.eslint.json')
|
||||
? toPath(directory, 'tsconfig.eslint.json')
|
||||
: exists(directory, 'tsconfig.json')
|
||||
? toPath(directory, 'tsconfig.json')
|
||||
: undefined;
|
||||
|
||||
const rootJSConfig = exists(directory, 'jsconfig.eslint.json')
|
||||
? toPath(directory, 'jsconfig.eslint.json')
|
||||
: exists(directory, 'jsconfig.json')
|
||||
? toPath(directory, 'jsconfig.json')
|
||||
: undefined;
|
||||
|
||||
const monorepoConfigs = await getMonorepoConfigs(directory);
|
||||
|
||||
const paths = /** @type {string[]} */
|
||||
([rootTSConfig, rootJSConfig, ...monorepoConfigs]).filter(p => p);
|
||||
|
||||
return paths;
|
||||
|
||||
}
|
||||
112
packages/core/src/types.d.ts
vendored
Normal file
112
packages/core/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
import type { FlatCompat } from '@eslint/eslintrc';
|
||||
import type { Linter } from 'eslint';
|
||||
|
||||
export type ESConfig = Readonly<Linter.FlatConfig>;
|
||||
|
||||
export interface Config {
|
||||
tsconfig?: string | string[] | true
|
||||
strict?: boolean
|
||||
rootDir?: string
|
||||
/**
|
||||
* @summary
|
||||
* Environment and language settings
|
||||
*
|
||||
* If no globals/environments are defined, the configuration tries to detect the
|
||||
* environment using `typeof`. See each option for more explanation
|
||||
*/
|
||||
environment?: {
|
||||
/**
|
||||
* @summary
|
||||
* Enables NodeJS environment globals.
|
||||
*
|
||||
* **Note:** this does not enables CommonJS globals, if you are using
|
||||
* CommonJS, use a file ending in `.cjs` or `.cts`
|
||||
*
|
||||
* @example // Detects if
|
||||
* typeof window === 'undefined' &&
|
||||
* typeof process !== 'undefined' &&
|
||||
* typeof require !== 'undefined'
|
||||
*/
|
||||
node?: boolean
|
||||
/**
|
||||
* @summary
|
||||
* Enables the global `Deno` namespace and browser/web standards globals
|
||||
*
|
||||
* @example // Detects if
|
||||
* typeof window !== 'undefined' &&
|
||||
* typeof Deno !== 'undefined'
|
||||
*/
|
||||
deno?: boolean
|
||||
/**
|
||||
* @summary
|
||||
* Enables browser/web standards globals
|
||||
*
|
||||
* @example // Detects if
|
||||
* typeof window !== 'undefined'
|
||||
*/
|
||||
browser?: boolean
|
||||
/**
|
||||
* @summary
|
||||
* What JavaScript (ECMAScript) that will be evaluated
|
||||
*
|
||||
* **Defaults to `latest`**
|
||||
*/
|
||||
ecmaVersion?: Linter.ParserOptions['ecmaVersion']
|
||||
/**
|
||||
* @summary
|
||||
* User defined globals for edge-cases or if available aren't enough
|
||||
*
|
||||
* **Does not overrides previous enabled ones**
|
||||
*/
|
||||
customGlobals?: Record<string, boolean>
|
||||
}
|
||||
options?: {
|
||||
indent?: 'tab' | 'space'
|
||||
quotes?: 'single' | 'double'
|
||||
semi?: 'never' | 'always'
|
||||
/**
|
||||
* Typescript's type-checking is able to infer types from parameters.
|
||||
* So using an explicit `:` type annotation isn't obligatory.
|
||||
*
|
||||
* But, **by default in strict mode**, type annotations are always mandated to make
|
||||
* the code more readable, explicit and robust to changes.
|
||||
*
|
||||
* See {@link https://typescript-eslint.io/rules/no-inferrable-types typescript-eslint documentation }
|
||||
* for more info.
|
||||
* ---
|
||||
* **Option: `never`** (default)
|
||||
* Types are always explicit in Typescript
|
||||
*
|
||||
* @example ```ts
|
||||
// Typescript
|
||||
const id: number = 10;
|
||||
const name: string = 'foo';
|
||||
```
|
||||
* ---
|
||||
* **Option: `always`**
|
||||
* Types are always inferred in Typescript
|
||||
*
|
||||
* @example ```ts
|
||||
// Typescript
|
||||
const id = 10;
|
||||
const name = 'foo';
|
||||
```
|
||||
*/
|
||||
inferrableTypes?: inferrableTypesOptions
|
||||
}
|
||||
overrides?:
|
||||
| Linter.FlatConfig[]
|
||||
| ((eslintrc: FlatCompat) => Linter.FlatConfig[] | Promise<Linter.FlatConfig[]>)
|
||||
}
|
||||
|
||||
export type inferrableTypesOptions = [
|
||||
'never' | 'always',
|
||||
{
|
||||
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreparameters} */
|
||||
parameters?: boolean
|
||||
/** @see {@link https://typescript-eslint.io/rules/no-inferrable-types#ignoreproperties} */
|
||||
properties?: boolean
|
||||
/** @see {@link https://typescript-eslint.io/rules/explicit-function-return-type} */
|
||||
returnValues?: boolean
|
||||
},
|
||||
] | 'never' | 'always';
|
||||
3681
pnpm-lock.yaml
generated
Normal file
3681
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'fixtures/*'
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"module": "ES2022",
|
||||
"target": "ES2022",
|
||||
"alwaysStrict": true,
|
||||
"outDir": "./dir"
|
||||
},
|
||||
"include": ["eslint.config.js", "commitlint.config.cjs"],
|
||||
"exclude": ["./node_modules/**", "./dist/**"]
|
||||
}
|
||||
Reference in New Issue
Block a user