sv 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-BWQarWDB.mjs +7000 -0
- package/dist/add-nRRWTjzp.d.mts +35 -0
- package/dist/bin.mjs +25 -1845
- package/dist/{index-7xp7FWpU.d.mts → core-CnPhgWST.d.mts} +294 -46
- package/dist/lib/core.d.mts +2 -0
- package/dist/lib/core.mjs +3 -0
- package/dist/lib/index.d.mts +16 -0
- package/dist/lib/index.mjs +4 -4
- package/dist/lib/testing.d.mts +108 -0
- package/dist/lib/testing.mjs +970 -927
- package/dist/{package-manager-CySZrSUa.mjs → package-manager-DkCPtZM1.mjs} +219 -1328
- package/dist/shared.json +30 -4
- package/dist/templates/addon/assets/DOT-gitignore +27 -0
- package/dist/templates/addon/assets/src/index.js +52 -0
- package/dist/templates/addon/assets/tests/addon.test.js +50 -0
- package/dist/templates/addon/assets/tests/setup/global.js +14 -0
- package/dist/templates/addon/assets/tests/setup/suite.js +130 -0
- package/dist/templates/addon/assets/vitest.config.js +16 -0
- package/dist/templates/addon/files.types=checkjs.json +1 -0
- package/dist/templates/addon/files.types=none.json +1 -0
- package/dist/templates/addon/files.types=typescript.json +1 -0
- package/dist/templates/addon/meta.json +4 -0
- package/dist/templates/addon/package.json +32 -0
- package/dist/templates/demo/files.types=checkjs.json +5 -5
- package/dist/templates/demo/files.types=none.json +5 -5
- package/dist/templates/demo/files.types=typescript.json +5 -5
- package/dist/templates/demo/package.json +1 -1
- package/dist/templates/library/package.json +1 -1
- package/dist/templates/minimal/package.json +1 -1
- package/dist/{core-D715tamU.mjs → utils-DjBRIDJG.mjs} +26494 -25089
- package/package.json +7 -7
- package/dist/index.d.mts +0 -2
- package/dist/index2.d.mts +0 -65
- package/dist/lib/core/index.mjs +0 -4
- package/dist/official-P5OKi7QM.mjs +0 -2586
- package/dist/testing.d.mts +0 -50
package/dist/shared.json
CHANGED
|
@@ -4,7 +4,23 @@
|
|
|
4
4
|
"name": "README.md",
|
|
5
5
|
"include": [],
|
|
6
6
|
"exclude": [],
|
|
7
|
-
"contents": "# sv\n\nEverything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```sh\n# create a new project
|
|
7
|
+
"contents": "# sv\n\nEverything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).\n\n## Creating a project\n\nIf you're seeing this, you've probably already done this step. Congrats!\n\n```sh\n# create a new project\nnpx sv create my-app\n```\n\n## Developing\n\nOnce you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:\n\n```sh\nnpm run dev\n\n# or start the server and open the app in a new browser tab\nnpm run dev -- --open\n```\n\n## Building\n\nTo create a production version of your app:\n\n```sh\nnpm run build\n```\n\nYou can preview the production build with `npm run preview`.\n\n> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"name": "CONTRIBUTING.md",
|
|
11
|
+
"include": [
|
|
12
|
+
"addon"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [],
|
|
15
|
+
"contents": "# Contributing Guide\n\nCheatsheet: [All official add-ons source code](https://github.com/sveltejs/cli/tree/feat/community-add-on-draft-0/packages/sv/lib/addons)\n\n---\n\nSome convenient scripts are provided to help develop the add-on.\n\n```sh\n## create a new minimal project in the `demo` directory\nnpm run demo-create\n\n## add your current add-on to the demo project\nnpm run demo-add\n\n## run the tests\nnpm run test\n```\n\n## Key things to note\n\nYour `add-on` should:\n\n- export a function that returns a `defineAddon` object.\n- have a `package.json` with an `exports` field that points to the main entry point of the add-on.\n\n## Sharing your add-on\n\nWhen you're ready to publish your add-on to npm, run:\n\n```shell\nnpm login\nnpm publish\n```\n\n## Things to be aware of\n\nCommunity add-ons are **not permitted** to have any external dependencies outside of `sv`. If the use of a dependency is absolutely necessary, then they can be bundled using a bundler of your choosing (e.g. Rollup, Rolldown, tsup, etc.).\n"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "README.md",
|
|
19
|
+
"include": [
|
|
20
|
+
"addon"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [],
|
|
23
|
+
"contents": "# [sv](https://svelte.dev/docs/cli/overview) community add-on: [~SV-NAME-TODO~](https://github.com/~SV-NAME-TODO~)\n\n> [!IMPORTANT]\n> Svelte maintainers have not reviewed community add-ons for malicious code. Use at your discretion\n\n## Usage\n\nTo install the add-on, run:\n\n```shell\nnpx sv add ~SV-PROTOCOL-NAME-TODO~\n```\n\n## What you get [TO BE FILLED...]\n\n- A super cool stuff\n- Another one!\n\n## Options [TO BE FILLED...]\n\n### `who`\n\nThe name of the person to say hello to.\n\nDefault: `you`\n\n```shell\nnpx sv add ~SV-PROTOCOL-NAME-TODO~=\"who:your-name\"\n```\n"
|
|
8
24
|
},
|
|
9
25
|
{
|
|
10
26
|
"name": "jsconfig.json",
|
|
@@ -68,7 +84,7 @@
|
|
|
68
84
|
"typescript"
|
|
69
85
|
],
|
|
70
86
|
"exclude": [],
|
|
71
|
-
"contents": "import adapter from '@sveltejs/adapter-auto';\
|
|
87
|
+
"contents": "import adapter from '@sveltejs/adapter-auto';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n"
|
|
72
88
|
},
|
|
73
89
|
{
|
|
74
90
|
"name": "tsconfig.json",
|
|
@@ -102,6 +118,16 @@
|
|
|
102
118
|
"exclude": [],
|
|
103
119
|
"contents": "import { sveltekit } from '@sveltejs/kit/vite';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n\tplugins: [sveltekit()]\n});\n"
|
|
104
120
|
},
|
|
121
|
+
{
|
|
122
|
+
"name": "jsconfig.json",
|
|
123
|
+
"include": [
|
|
124
|
+
"addon"
|
|
125
|
+
],
|
|
126
|
+
"exclude": [
|
|
127
|
+
"typescript"
|
|
128
|
+
],
|
|
129
|
+
"contents": "{\n\t\"compilerOptions\": {\n\t\t\"strict\": true,\n\t\t\"skipLibCheck\": true,\n\t\t\"checkJs\": true,\n\t\t\"module\": \"NodeNext\",\n\t\t\"moduleResolution\": \"NodeNext\"\n\t}\n}\n"
|
|
130
|
+
},
|
|
105
131
|
{
|
|
106
132
|
"name": "svelte.config.js",
|
|
107
133
|
"include": [
|
|
@@ -109,7 +135,7 @@
|
|
|
109
135
|
"checkjs"
|
|
110
136
|
],
|
|
111
137
|
"exclude": [],
|
|
112
|
-
"contents": "import adapter from '@sveltejs/adapter-auto';\
|
|
138
|
+
"contents": "import adapter from '@sveltejs/adapter-auto';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n"
|
|
113
139
|
},
|
|
114
140
|
{
|
|
115
141
|
"name": "svelte.config.js",
|
|
@@ -118,7 +144,7 @@
|
|
|
118
144
|
"typescript"
|
|
119
145
|
],
|
|
120
146
|
"exclude": [],
|
|
121
|
-
"contents": "import adapter from '@sveltejs/adapter-auto';\
|
|
147
|
+
"contents": "import adapter from '@sveltejs/adapter-auto';\n\n/** @type {import('@sveltejs/kit').Config} */\nconst config = {\n\tkit: {\n\t\t// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.\n\t\t// If your environment is not supported, or you settled on a specific environment, switch out the adapter.\n\t\t// See https://svelte.dev/docs/kit/adapters for more information about adapters.\n\t\tadapter: adapter()\n\t}\n};\n\nexport default config;\n"
|
|
122
148
|
},
|
|
123
149
|
{
|
|
124
150
|
"name": "svelte.config.js",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
demo/
|
|
3
|
+
.test-output/
|
|
4
|
+
|
|
5
|
+
# Output
|
|
6
|
+
.output
|
|
7
|
+
.vercel
|
|
8
|
+
.netlify
|
|
9
|
+
.wrangler
|
|
10
|
+
/.svelte-kit
|
|
11
|
+
/build
|
|
12
|
+
/dist
|
|
13
|
+
|
|
14
|
+
# OS
|
|
15
|
+
.DS_Store
|
|
16
|
+
Thumbs.db
|
|
17
|
+
|
|
18
|
+
# Env
|
|
19
|
+
.env
|
|
20
|
+
.env.*
|
|
21
|
+
!.env.example
|
|
22
|
+
!.env.test
|
|
23
|
+
|
|
24
|
+
# Vite
|
|
25
|
+
vite.config.js.timestamp-*
|
|
26
|
+
vite.config.ts.timestamp-*
|
|
27
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineAddon, defineAddonOptions, js, parse, svelte } from 'sv/core';
|
|
2
|
+
|
|
3
|
+
const options = defineAddonOptions()
|
|
4
|
+
.add('who', {
|
|
5
|
+
question: 'To whom should the addon say hello?',
|
|
6
|
+
type: 'string',
|
|
7
|
+
default: ''
|
|
8
|
+
})
|
|
9
|
+
.build();
|
|
10
|
+
|
|
11
|
+
export default defineAddon({
|
|
12
|
+
id: '~SV-NAME-TODO~',
|
|
13
|
+
options,
|
|
14
|
+
|
|
15
|
+
setup: ({ kit, unsupported }) => {
|
|
16
|
+
if (!kit) unsupported('Requires SvelteKit');
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
run: ({ kit, sv, options, language, cancel }) => {
|
|
20
|
+
if (!kit) return cancel('SvelteKit is required');
|
|
21
|
+
|
|
22
|
+
sv.file(`src/lib/~SV-NAME-TODO~/content.txt`, () => {
|
|
23
|
+
return `This is a text file made by the Community Addon Template demo for the add-on: '~SV-NAME-TODO~'!`;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
sv.file(`src/lib/~SV-NAME-TODO~/HelloComponent.svelte`, (content) => {
|
|
27
|
+
const { ast, generateCode } = parse.svelte(content);
|
|
28
|
+
svelte.ensureScript(ast, { language });
|
|
29
|
+
|
|
30
|
+
js.imports.addDefault(ast.instance.content, { as: 'content', from: './content.txt?raw' });
|
|
31
|
+
|
|
32
|
+
svelte.addFragment(ast, '<p>{content}</p>');
|
|
33
|
+
svelte.addFragment(ast, `<h2>Hello ${options.who}!</h2>`);
|
|
34
|
+
|
|
35
|
+
return generateCode();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
sv.file(kit.routesDirectory + '/+page.svelte', (content) => {
|
|
39
|
+
const { ast, generateCode } = parse.svelte(content);
|
|
40
|
+
svelte.ensureScript(ast, { language });
|
|
41
|
+
|
|
42
|
+
js.imports.addDefault(ast.instance.content, {
|
|
43
|
+
as: 'HelloComponent',
|
|
44
|
+
from: `$lib/~SV-NAME-TODO~/HelloComponent.svelte`
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
svelte.addFragment(ast, '<HelloComponent />');
|
|
48
|
+
|
|
49
|
+
return generateCode();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { expect } from '@playwright/test';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import addon from '../src/index.js';
|
|
6
|
+
import { setupTest } from './setup/suite.js';
|
|
7
|
+
|
|
8
|
+
// set to true to enable browser testing
|
|
9
|
+
const browser = false;
|
|
10
|
+
|
|
11
|
+
const { test, prepareServer, testCases } = setupTest(
|
|
12
|
+
{ addon },
|
|
13
|
+
{
|
|
14
|
+
kinds: [{ type: 'default', options: { addon: { who: 'you' } } }],
|
|
15
|
+
filter: (testCase) => testCase.variant.includes('kit'),
|
|
16
|
+
browser
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
test.concurrent.for(testCases)(
|
|
21
|
+
'~SV-NAME-TODO~ $kind.type $variant',
|
|
22
|
+
async (testCase, { page, ...ctx }) => {
|
|
23
|
+
const cwd = ctx.cwd(testCase);
|
|
24
|
+
|
|
25
|
+
const msg =
|
|
26
|
+
"This is a text file made by the Community Addon Template demo for the add-on: '~SV-NAME-TODO~'!";
|
|
27
|
+
|
|
28
|
+
const contentPath = path.resolve(cwd, `src/lib/~SV-NAME-TODO~/content.txt`);
|
|
29
|
+
const contentContent = fs.readFileSync(contentPath, 'utf8');
|
|
30
|
+
|
|
31
|
+
// Check if we have the imports
|
|
32
|
+
expect(contentContent).toContain(msg);
|
|
33
|
+
|
|
34
|
+
// For browser testing
|
|
35
|
+
if (browser) {
|
|
36
|
+
const { close } = await prepareServer({ cwd, page });
|
|
37
|
+
// kill server process when we're done
|
|
38
|
+
ctx.onTestFinished(async () => await close());
|
|
39
|
+
|
|
40
|
+
// expectations
|
|
41
|
+
const textContent = await page.locator('p').last().textContent();
|
|
42
|
+
if (testCase.variant.includes('kit')) {
|
|
43
|
+
expect(textContent).toContain(msg);
|
|
44
|
+
} else {
|
|
45
|
+
// it's not a kit plugin!
|
|
46
|
+
expect(textContent).not.toContain(msg);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { setupGlobal } from 'sv/testing';
|
|
3
|
+
|
|
4
|
+
const TEST_DIR = fileURLToPath(new URL('../../.test-output/', import.meta.url));
|
|
5
|
+
|
|
6
|
+
export default setupGlobal({
|
|
7
|
+
TEST_DIR,
|
|
8
|
+
pre: async () => {
|
|
9
|
+
// global setup (e.g. spin up docker containers)
|
|
10
|
+
},
|
|
11
|
+
post: async () => {
|
|
12
|
+
// tear down... (e.g. cleanup docker containers)
|
|
13
|
+
}
|
|
14
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { inject, test as vitestTest, beforeAll, beforeEach } from 'vitest';
|
|
5
|
+
import { chromium } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
import { add } from 'sv';
|
|
8
|
+
import { createProject, addPnpmBuildDependencies, prepareServer } from 'sv/testing';
|
|
9
|
+
|
|
10
|
+
const cwd = inject('testDir');
|
|
11
|
+
const templatesDir = inject('templatesDir');
|
|
12
|
+
const variants = inject('variants');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @template {import('sv').AddonMap} AddonMap
|
|
16
|
+
* @param {AddonMap} addons
|
|
17
|
+
* @param {import('sv/testing').SetupTestOptions<AddonMap>} [options]
|
|
18
|
+
* @returns {{ test: ReturnType<typeof vitestTest.extend<import('sv/testing').Fixtures>>, testCases: Array<import('sv/testing').AddonTestCase<AddonMap>>, prepareServer: typeof prepareServer }}
|
|
19
|
+
*/
|
|
20
|
+
export function setupTest(addons, options) {
|
|
21
|
+
/** @type {ReturnType<typeof vitestTest.extend<import('sv/testing').Fixtures>>} */
|
|
22
|
+
// @ts-ignore - vitest.extend expects fixtures object but we provide it in beforeEach
|
|
23
|
+
const test = vitestTest.extend({});
|
|
24
|
+
|
|
25
|
+
const withBrowser = options?.browser ?? true;
|
|
26
|
+
|
|
27
|
+
/** @type {ReturnType<typeof createProject>} */
|
|
28
|
+
let create;
|
|
29
|
+
/** @type {Awaited<ReturnType<typeof chromium.launch>>} */
|
|
30
|
+
let browser;
|
|
31
|
+
|
|
32
|
+
if (withBrowser) {
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
browser = await chromium.launch();
|
|
35
|
+
return async () => {
|
|
36
|
+
await browser.close();
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @type {Array<import('sv/testing').AddonTestCase<AddonMap>>} */
|
|
42
|
+
const testCases = [];
|
|
43
|
+
for (const kind of options?.kinds ?? []) {
|
|
44
|
+
for (const variant of variants) {
|
|
45
|
+
const addonTestCase = { variant, kind };
|
|
46
|
+
if (options?.filter === undefined || options.filter(addonTestCase)) {
|
|
47
|
+
testCases.push(addonTestCase);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
let testName;
|
|
53
|
+
beforeAll(async ({ name }) => {
|
|
54
|
+
testName = path.dirname(name).split('/').at(-1) ?? '';
|
|
55
|
+
|
|
56
|
+
// constructs a builder to create test projects
|
|
57
|
+
create = createProject({ cwd, templatesDir, testName });
|
|
58
|
+
|
|
59
|
+
// creates a pnpm workspace in each addon dir
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.resolve(cwd, testName, 'pnpm-workspace.yaml'),
|
|
62
|
+
"packages:\n - '**/*'",
|
|
63
|
+
'utf8'
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// creates a barebones package.json in each addon dir
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.resolve(cwd, testName, 'package.json'),
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
name: `${testName}-workspace-root`,
|
|
71
|
+
private: true
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
for (const addonTestCase of testCases) {
|
|
76
|
+
const { variant, kind } = addonTestCase;
|
|
77
|
+
const cwd = create({ testId: `${kind.type}-${variant}`, variant });
|
|
78
|
+
|
|
79
|
+
// test metadata
|
|
80
|
+
const metaPath = path.resolve(cwd, 'meta.json');
|
|
81
|
+
fs.writeFileSync(metaPath, JSON.stringify({ variant, kind }, null, '\t'), 'utf8');
|
|
82
|
+
|
|
83
|
+
if (options?.preAdd) {
|
|
84
|
+
await options.preAdd({ addonTestCase, cwd });
|
|
85
|
+
}
|
|
86
|
+
const { pnpmBuildDependencies } = await add({
|
|
87
|
+
cwd,
|
|
88
|
+
addons,
|
|
89
|
+
options: kind.options,
|
|
90
|
+
packageManager: 'pnpm'
|
|
91
|
+
});
|
|
92
|
+
await addPnpmBuildDependencies(cwd, 'pnpm', ['esbuild', ...pnpmBuildDependencies]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
execSync('pnpm install', { cwd: path.resolve(cwd, testName), stdio: 'pipe' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// runs before each test case
|
|
99
|
+
/**
|
|
100
|
+
* @param {import('sv/testing').Fixtures & import('vitest').TestContext} ctx
|
|
101
|
+
*/
|
|
102
|
+
beforeEach(async (ctx) => {
|
|
103
|
+
/** @type {Awaited<ReturnType<typeof browser.newContext>>} */
|
|
104
|
+
let browserCtx;
|
|
105
|
+
if (withBrowser) {
|
|
106
|
+
browserCtx = await browser.newContext();
|
|
107
|
+
/** @type {import('sv/testing').Fixtures} */ (/** @type {unknown} */ (ctx)).page =
|
|
108
|
+
await browserCtx.newPage();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {import('sv/testing').AddonTestCase<Addons>} addonTestCase
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
/** @type {import('sv/testing').Fixtures} */ (/** @type {unknown} */ (ctx)).cwd = (
|
|
116
|
+
addonTestCase
|
|
117
|
+
) => {
|
|
118
|
+
return path.join(cwd, testName, `${addonTestCase.kind.type}-${addonTestCase.variant}`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return async () => {
|
|
122
|
+
if (withBrowser) {
|
|
123
|
+
await browserCtx.close();
|
|
124
|
+
}
|
|
125
|
+
// ...other tear downs
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return { test, testCases, prepareServer };
|
|
130
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
const ONE_MINUTE = 1000 * 60;
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
test: {
|
|
7
|
+
include: ['tests/**/*.test.{js,ts}'],
|
|
8
|
+
exclude: ['tests/setup/*'],
|
|
9
|
+
testTimeout: ONE_MINUTE * 3,
|
|
10
|
+
hookTimeout: ONE_MINUTE * 3,
|
|
11
|
+
globalSetup: ['tests/setup/global.js'],
|
|
12
|
+
expect: {
|
|
13
|
+
requireAssertions: true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "~SV-NAME-TODO~",
|
|
3
|
+
"description": "sv add-on for ~SV-NAME-TODO~",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"demo-create": "sv create demo --types ts --template minimal --no-add-ons --no-install",
|
|
9
|
+
"demo-add": "sv add file:../ --cwd demo --no-git-check --no-install",
|
|
10
|
+
"demo-add:ci": "sv add file:../=who:you --cwd demo --no-git-check --no-download-check --no-install",
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"files": ["src", "!src/**/*.test.*"],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"default": "./src/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"sv": "latest"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@playwright/test": "^1.56.1",
|
|
24
|
+
"@types/node": "^24",
|
|
25
|
+
"vitest": "^4.0.7"
|
|
26
|
+
},
|
|
27
|
+
"keywords": ["sv-add"],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"directory": "dist",
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
{
|
|
11
11
|
"name": "src/routes/+page.svelte",
|
|
12
|
-
"contents": "<script>\n\timport
|
|
12
|
+
"contents": "<script>\n\timport welcomeFallback from '$lib/images/svelte-welcome.png';\n\timport welcome from '$lib/images/svelte-welcome.webp';\n\n\timport Counter from './Counter.svelte';\n</script>\n\n<svelte:head>\n\t<title>Home</title>\n\t<meta name=\"description\" content=\"Svelte demo app\" />\n</svelte:head>\n\n<section>\n\t<h1>\n\t\t<span class=\"welcome\">\n\t\t\t<picture>\n\t\t\t\t<source srcset={welcome} type=\"image/webp\" />\n\t\t\t\t<img src={welcomeFallback} alt=\"Welcome\" />\n\t\t\t</picture>\n\t\t</span>\n\n\t\tto your new<br />SvelteKit app\n\t</h1>\n\n\t<h2>\n\t\ttry editing <strong>src/routes/+page.svelte</strong>\n\t</h2>\n\n\t<Counter />\n</section>\n\n<style>\n\tsection {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tflex: 0.6;\n\t}\n\n\th1 {\n\t\twidth: 100%;\n\t}\n\n\t.welcome {\n\t\tdisplay: block;\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 0;\n\t\tpadding: 0 0 calc(100% * 495 / 2048) 0;\n\t}\n\n\t.welcome img {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\ttop: 0;\n\t\tdisplay: block;\n\t}\n</style>\n"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"name": "src/routes/+page.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "src/routes/Header.svelte",
|
|
24
|
-
"contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport
|
|
24
|
+
"contents": "<script>\n\timport { resolve } from '$app/paths';\n\timport { page } from '$app/state';\n\timport github from '$lib/images/github.svg';\n\timport logo from '$lib/images/svelte-logo.svg';\n</script>\n\n<header>\n\t<div class=\"corner\">\n\t\t<a href=\"https://svelte.dev/docs/kit\">\n\t\t\t<img src={logo} alt=\"SvelteKit\" />\n\t\t</a>\n\t</div>\n\n\t<nav>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z\" />\n\t\t</svg>\n\t\t<ul>\n\t\t\t<li aria-current={page.url.pathname === '/' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/')}>Home</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname === '/about' ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/about')}>About</a>\n\t\t\t</li>\n\t\t\t<li aria-current={page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>\n\t\t\t\t<a href={resolve('/sverdle')}>Sverdle</a>\n\t\t\t</li>\n\t\t</ul>\n\t\t<svg viewBox=\"0 0 2 3\" aria-hidden=\"true\">\n\t\t\t<path d=\"M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z\" />\n\t\t</svg>\n\t</nav>\n\n\t<div class=\"corner\">\n\t\t<a href=\"https://github.com/sveltejs/kit\">\n\t\t\t<img src={github} alt=\"GitHub\" />\n\t\t</a>\n\t</div>\n</header>\n\n<style>\n\theader {\n\t\tdisplay: flex;\n\t\tjustify-content: space-between;\n\t}\n\n\t.corner {\n\t\twidth: 3em;\n\t\theight: 3em;\n\t}\n\n\t.corner a {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t}\n\n\t.corner img {\n\t\twidth: 2em;\n\t\theight: 2em;\n\t\tobject-fit: contain;\n\t}\n\n\tnav {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\t--background: rgba(255, 255, 255, 0.7);\n\t}\n\n\tsvg {\n\t\twidth: 2em;\n\t\theight: 3em;\n\t\tdisplay: block;\n\t}\n\n\tpath {\n\t\tfill: var(--background);\n\t}\n\n\tul {\n\t\tposition: relative;\n\t\tpadding: 0;\n\t\tmargin: 0;\n\t\theight: 3em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tlist-style: none;\n\t\tbackground: var(--background);\n\t\tbackground-size: contain;\n\t}\n\n\tli {\n\t\tposition: relative;\n\t\theight: 100%;\n\t}\n\n\tli[aria-current='page']::before {\n\t\t--size: 6px;\n\t\tcontent: '';\n\t\twidth: 0;\n\t\theight: 0;\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: calc(50% - var(--size));\n\t\tborder: var(--size) solid transparent;\n\t\tborder-top: var(--size) solid var(--color-theme-1);\n\t}\n\n\tnav a {\n\t\tdisplay: flex;\n\t\theight: 100%;\n\t\talign-items: center;\n\t\tpadding: 0 0.5rem;\n\t\tcolor: var(--color-text);\n\t\tfont-weight: 700;\n\t\tfont-size: 0.8rem;\n\t\ttext-transform: uppercase;\n\t\tletter-spacing: 0.1em;\n\t\ttext-decoration: none;\n\t\ttransition: color 0.2s linear;\n\t}\n\n\ta:hover {\n\t\tcolor: var(--color-theme-1);\n\t}\n</style>\n"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "src/routes/about/+page.svelte",
|
|
@@ -33,15 +33,15 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "src/routes/sverdle/+page.server.js",
|
|
36
|
-
"contents": "import { fail } from '@sveltejs/kit';\nimport { Game } from './game.js';\n\n/** @satisfies {import('./$types').PageServerLoad} */\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\n/** @satisfies {import('./$types').Actions} */\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = /** @type {string[]} */ (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
|
|
36
|
+
"contents": "import { fail } from '@sveltejs/kit';\n\nimport { Game } from './game.js';\n\n/** @satisfies {import('./$types').PageServerLoad} */\nexport const load = ({ cookies }) => {\n\tconst game = new Game(cookies.get('sverdle'));\n\n\treturn {\n\t\t/**\n\t\t * The player's guessed words so far\n\t\t */\n\t\tguesses: game.guesses,\n\n\t\t/**\n\t\t * An array of strings like '__x_c' corresponding to the guesses, where 'x' means\n\t\t * an exact match, and 'c' means a close match (right letter, wrong place)\n\t\t */\n\t\tanswers: game.answers,\n\n\t\t/**\n\t\t * The correct answer, revealed if the game is over\n\t\t */\n\t\tanswer: game.answers.length >= 6 ? game.answer : null\n\t};\n};\n\n/** @satisfies {import('./$types').Actions} */\nexport const actions = {\n\t/**\n\t * Modify game state in reaction to a keypress. If client-side JavaScript\n\t * is available, this will happen in the browser instead of here\n\t */\n\tupdate: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst key = data.get('key');\n\n\t\tconst i = game.answers.length;\n\n\t\tif (key === 'backspace') {\n\t\t\tgame.guesses[i] = game.guesses[i].slice(0, -1);\n\t\t} else {\n\t\t\tgame.guesses[i] += key;\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\t/**\n\t * Modify game state in reaction to a guessed word. This logic always runs on\n\t * the server, so that people can't cheat by peeking at the JavaScript\n\t */\n\tenter: async ({ request, cookies }) => {\n\t\tconst game = new Game(cookies.get('sverdle'));\n\n\t\tconst data = await request.formData();\n\t\tconst guess = /** @type {string[]} */ (data.getAll('guess'));\n\n\t\tif (!game.enter(guess)) {\n\t\t\treturn fail(400, { badGuess: true });\n\t\t}\n\n\t\tcookies.set('sverdle', game.toString(), { path: '/' });\n\t},\n\n\trestart: async ({ cookies }) => {\n\t\tcookies.delete('sverdle', { path: '/' });\n\t}\n};\n"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "src/routes/sverdle/+page.svelte",
|
|
40
|
-
"contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\t/**\n\t * @typedef {Object} Props\n\t * @property {import('./$types').PageData} data\n\t * @property {import('./$types').ActionData} form\n\t */\n\n\t/**\n\t * @type {Props}\n\t */\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t * @type {Record<string, 'exact' | 'close' | 'missing'>}\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t * @type {Record<string, string>}\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t * @param {MouseEvent} event\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t * @param {KeyboardEvent} event\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
|
|
40
|
+
"contents": "<script>\n\timport { enhance } from '$app/forms';\n\timport { resolve } from '$app/paths';\n\timport { confetti } from '@neoconfetti/svelte';\n\timport { MediaQuery } from 'svelte/reactivity';\n\n\t/**\n\t * @typedef {Object} Props\n\t * @property {import('./$types').PageData} data\n\t * @property {import('./$types').ActionData} form\n\t */\n\n\t/**\n\t * @type {Props}\n\t */\n\tlet { data, form = $bindable() } = $props();\n\n\t/** Whether the user prefers reduced motion */\n\tconst reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');\n\n\t/** Whether or not the user has won */\n\tlet won = $derived(data.answers.at(-1) === 'xxxxx');\n\n\t/** The index of the current guess */\n\tlet i = $derived(won ? -1 : data.answers.length);\n\n\t/** The current guess */\n\tlet currentGuess = $derived(data.guesses[i] || '');\n\n\t/** Whether the current guess can be submitted */\n\tlet submittable = $derived(currentGuess.length === 5);\n\n\tconst { classnames, description } = $derived.by(() => {\n\t\t/**\n\t\t * A map of classnames for all letters that have been guessed,\n\t\t * used for styling the keyboard\n\t\t * @type {Record<string, 'exact' | 'close' | 'missing'>}\n\t\t */\n\t\tlet classnames = {};\n\t\t/**\n\t\t * A map of descriptions for all letters that have been guessed,\n\t\t * used for adding text for assistive technology (e.g. screen readers)\n\t\t * @type {Record<string, string>}\n\t\t */\n\t\tlet description = {};\n\t\tdata.answers.forEach((answer, i) => {\n\t\t\tconst guess = data.guesses[i];\n\t\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\t\tconst letter = guess[i];\n\t\t\t\tif (answer[i] === 'x') {\n\t\t\t\t\tclassnames[letter] = 'exact';\n\t\t\t\t\tdescription[letter] = 'correct';\n\t\t\t\t} else if (!classnames[letter]) {\n\t\t\t\t\tclassnames[letter] = answer[i] === 'c' ? 'close' : 'missing';\n\t\t\t\t\tdescription[letter] = answer[i] === 'c' ? 'present' : 'absent';\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\treturn { classnames, description };\n\t});\n\n\t/**\n\t * Modify the game state without making a trip to the server,\n\t * if client-side JavaScript is enabled\n\t * @param {MouseEvent} event\n\t */\n\tfunction update(event) {\n\t\tevent.preventDefault();\n\t\tconst key = /** @type {HTMLButtonElement} */ (event.target).getAttribute('data-key');\n\n\t\tif (key === 'backspace') {\n\t\t\tcurrentGuess = currentGuess.slice(0, -1);\n\t\t\tif (form?.badGuess) form.badGuess = false;\n\t\t} else if (currentGuess.length < 5) {\n\t\t\tcurrentGuess += key;\n\t\t}\n\t}\n\n\t/**\n\t * Trigger form logic in response to a keydown event, so that\n\t * desktop users can use the keyboard to play the game\n\t * @param {KeyboardEvent} event\n\t */\n\tfunction keydown(event) {\n\t\tif (event.metaKey) return;\n\n\t\tif (event.key === 'Enter' && !submittable) return;\n\n\t\tdocument\n\t\t\t.querySelector(`[data-key=\"${event.key}\" i]`)\n\t\t\t?.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true }));\n\t}\n</script>\n\n<svelte:window onkeydown={keydown} />\n\n<svelte:head>\n\t<title>Sverdle</title>\n\t<meta name=\"description\" content=\"A Wordle clone written in SvelteKit\" />\n</svelte:head>\n\n<h1 class=\"visually-hidden\">Sverdle</h1>\n\n<form\n\tmethod=\"post\"\n\taction=\"?/enter\"\n\tuse:enhance={() => {\n\t\t// prevent default callback from resetting the form\n\t\treturn ({ update }) => {\n\t\t\tupdate({ reset: false });\n\t\t};\n\t}}\n>\n\t<a class=\"how-to-play\" href={resolve('/sverdle/how-to-play')}>How to play</a>\n\n\t<div class=\"grid\" class:playing={!won} class:bad-guess={form?.badGuess}>\n\t\t{#each Array.from(Array(6).keys()) as row (row)}\n\t\t\t{@const current = row === i}\n\t\t\t<h2 class=\"visually-hidden\">Row {row + 1}</h2>\n\t\t\t<div class=\"row\" class:current>\n\t\t\t\t{#each Array.from(Array(5).keys()) as column (column)}\n\t\t\t\t\t{@const guess = current ? currentGuess : data.guesses[row]}\n\t\t\t\t\t{@const answer = data.answers[row]?.[column]}\n\t\t\t\t\t{@const value = guess?.[column] ?? ''}\n\t\t\t\t\t{@const selected = current && column === guess.length}\n\t\t\t\t\t{@const exact = answer === 'x'}\n\t\t\t\t\t{@const close = answer === 'c'}\n\t\t\t\t\t{@const missing = answer === '_'}\n\t\t\t\t\t<div class=\"letter\" class:exact class:close class:missing class:selected>\n\t\t\t\t\t\t{value}\n\t\t\t\t\t\t<span class=\"visually-hidden\">\n\t\t\t\t\t\t\t{#if exact}\n\t\t\t\t\t\t\t\t(correct)\n\t\t\t\t\t\t\t{:else if close}\n\t\t\t\t\t\t\t\t(present)\n\t\t\t\t\t\t\t{:else if missing}\n\t\t\t\t\t\t\t\t(absent)\n\t\t\t\t\t\t\t{:else}\n\t\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t\t{/if}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<input name=\"guess\" disabled={!current} type=\"hidden\" {value} />\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/each}\n\t</div>\n\n\t<div class=\"controls\">\n\t\t{#if won || data.answers.length >= 6}\n\t\t\t{#if !won && data.answer}\n\t\t\t\t<p>the answer was \"{data.answer}\"</p>\n\t\t\t{/if}\n\t\t\t<button data-key=\"enter\" class=\"restart selected\" formaction=\"?/restart\">\n\t\t\t\t{won ? 'you won :)' : `game over :(`} play again?\n\t\t\t</button>\n\t\t{:else}\n\t\t\t<div class=\"keyboard\">\n\t\t\t\t<button data-key=\"enter\" class:selected={submittable} disabled={!submittable}>enter</button>\n\n\t\t\t\t<button\n\t\t\t\t\tonclick={update}\n\t\t\t\t\tdata-key=\"backspace\"\n\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\tname=\"key\"\n\t\t\t\t\tvalue=\"backspace\"\n\t\t\t\t>\n\t\t\t\t\tback\n\t\t\t\t</button>\n\n\t\t\t\t{#each ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'] as row (row)}\n\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t{#each row as letter, index (index)}\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tonclick={update}\n\t\t\t\t\t\t\t\tdata-key={letter}\n\t\t\t\t\t\t\t\tclass={classnames[letter]}\n\t\t\t\t\t\t\t\tdisabled={submittable}\n\t\t\t\t\t\t\t\tformaction=\"?/update\"\n\t\t\t\t\t\t\t\tname=\"key\"\n\t\t\t\t\t\t\t\tvalue={letter}\n\t\t\t\t\t\t\t\taria-label=\"{letter} {description[letter] || ''}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{letter}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t{/each}\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t{/if}\n\t</div>\n</form>\n\n{#if won}\n\t<div\n\t\tstyle=\"position: absolute; left: 50%; top: 30%\"\n\t\tuse:confetti={{\n\t\t\tparticleCount: reducedMotion.current ? 0 : undefined,\n\t\t\tforce: 0.7,\n\t\t\tstageWidth: window.innerWidth,\n\t\t\tstageHeight: window.innerHeight,\n\t\t\tcolors: ['#ff3e00', '#40b3ff', '#676778']\n\t\t}}\n\t></div>\n{/if}\n\n<style>\n\tform {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1rem;\n\t\tflex: 1;\n\t}\n\n\t.how-to-play {\n\t\tcolor: var(--color-text);\n\t}\n\n\t.how-to-play::before {\n\t\tcontent: 'i';\n\t\tdisplay: inline-block;\n\t\tfont-size: 0.8em;\n\t\tfont-weight: 900;\n\t\twidth: 1em;\n\t\theight: 1em;\n\t\tpadding: 0.2em;\n\t\tline-height: 1;\n\t\tborder: 1.5px solid var(--color-text);\n\t\tborder-radius: 50%;\n\t\ttext-align: center;\n\t\tmargin: 0 0.5em 0 0;\n\t\tposition: relative;\n\t\ttop: -0.05em;\n\t}\n\n\t.grid {\n\t\t--width: min(100vw, 40vh, 380px);\n\t\tmax-width: var(--width);\n\t\talign-self: center;\n\t\tjustify-self: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-start;\n\t}\n\n\t.grid .row {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(5, 1fr);\n\t\tgrid-gap: 0.2rem;\n\t\tmargin: 0 0 0.2rem 0;\n\t}\n\n\t@media (prefers-reduced-motion: no-preference) {\n\t\t.grid.bad-guess .row.current {\n\t\t\tanimation: wiggle 0.5s;\n\t\t}\n\t}\n\n\t.grid.playing .row.current {\n\t\tfilter: drop-shadow(3px 3px 10px var(--color-bg-0));\n\t}\n\n\t.letter {\n\t\taspect-ratio: 1;\n\t\twidth: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\ttext-align: center;\n\t\tbox-sizing: border-box;\n\t\ttext-transform: lowercase;\n\t\tborder: none;\n\t\tfont-size: calc(0.08 * var(--width));\n\t\tborder-radius: 2px;\n\t\tbackground: white;\n\t\tmargin: 0;\n\t\tcolor: rgba(0, 0, 0, 0.7);\n\t}\n\n\t.letter.missing {\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tcolor: rgba(0, 0, 0, 0.5);\n\t}\n\n\t.letter.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.letter.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.selected {\n\t\toutline: 2px solid var(--color-theme-1);\n\t}\n\n\t.controls {\n\t\ttext-align: center;\n\t\tjustify-content: center;\n\t\theight: min(18vh, 10rem);\n\t}\n\n\t.keyboard {\n\t\t--gap: 0.2rem;\n\t\tposition: relative;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: var(--gap);\n\t\theight: 100%;\n\t}\n\n\t.keyboard .row {\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\tgap: 0.2rem;\n\t\tflex: 1;\n\t}\n\n\t.keyboard button,\n\t.keyboard button:disabled {\n\t\t--size: min(8vw, 4vh, 40px);\n\t\tbackground-color: white;\n\t\tcolor: black;\n\t\twidth: var(--size);\n\t\tborder: none;\n\t\tborder-radius: 2px;\n\t\tfont-size: calc(var(--size) * 0.5);\n\t\tmargin: 0;\n\t}\n\n\t.keyboard button.exact {\n\t\tbackground: var(--color-theme-2);\n\t\tcolor: white;\n\t}\n\n\t.keyboard button.missing {\n\t\topacity: 0.5;\n\t}\n\n\t.keyboard button.close {\n\t\tborder: 2px solid var(--color-theme-2);\n\t}\n\n\t.keyboard button:focus {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t.keyboard button[data-key='enter'],\n\t.keyboard button[data-key='backspace'] {\n\t\tposition: absolute;\n\t\tbottom: 0;\n\t\twidth: calc(1.5 * var(--size));\n\t\theight: calc(1 / 3 * (100% - 2 * var(--gap)));\n\t\ttext-transform: uppercase;\n\t\tfont-size: calc(0.3 * var(--size));\n\t\tpadding-top: calc(0.15 * var(--size));\n\t}\n\n\t.keyboard button[data-key='enter'] {\n\t\tright: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='backspace'] {\n\t\tleft: calc(50% + 3.5 * var(--size) + 0.8rem);\n\t}\n\n\t.keyboard button[data-key='enter']:disabled {\n\t\topacity: 0.5;\n\t}\n\n\t.restart {\n\t\twidth: 100%;\n\t\tpadding: 1rem;\n\t\tbackground: rgba(255, 255, 255, 0.5);\n\t\tborder-radius: 2px;\n\t\tborder: none;\n\t}\n\n\t.restart:focus,\n\t.restart:hover {\n\t\tbackground: var(--color-theme-1);\n\t\tcolor: white;\n\t\toutline: none;\n\t}\n\n\t@keyframes wiggle {\n\t\t0% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t\t10% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t30% {\n\t\t\ttransform: translateX(4px);\n\t\t}\n\t\t50% {\n\t\t\ttransform: translateX(-6px);\n\t\t}\n\t\t70% {\n\t\t\ttransform: translateX(+4px);\n\t\t}\n\t\t90% {\n\t\t\ttransform: translateX(-2px);\n\t\t}\n\t\t100% {\n\t\t\ttransform: translateX(0);\n\t\t}\n\t}\n</style>\n"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "src/routes/sverdle/game.js",
|
|
44
|
-
"contents": "import {
|
|
44
|
+
"contents": "import { allowed, words } from './words.server.js';\n\nexport class Game {\n\t/**\n\t * Create a game object from the player's cookie, or initialise a new game\n\t * @param {string | undefined} serialized\n\t */\n\tconstructor(serialized = undefined) {\n\t\tif (serialized) {\n\t\t\tconst [index, guesses, answers] = serialized.split('-');\n\n\t\t\tthis.index = +index;\n\t\t\tthis.guesses = guesses ? guesses.split(' ') : [];\n\t\t\tthis.answers = answers ? answers.split(' ') : [];\n\t\t} else {\n\t\t\tthis.index = Math.floor(Math.random() * words.length);\n\t\t\tthis.guesses = ['', '', '', '', '', ''];\n\t\t\tthis.answers = /** @type {string[]} */ ([]);\n\t\t}\n\n\t\tthis.answer = words[this.index];\n\t}\n\n\t/**\n\t * Update game state based on a guess of a five-letter word. Returns\n\t * true if the guess was valid, false otherwise\n\t * @param {string[]} letters\n\t */\n\tenter(letters) {\n\t\tconst word = letters.join('');\n\t\tconst valid = allowed.has(word);\n\n\t\tif (!valid) return false;\n\n\t\tthis.guesses[this.answers.length] = word;\n\n\t\tconst available = Array.from(this.answer);\n\t\tconst answer = Array(5).fill('_');\n\n\t\t// first, find exact matches\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (letters[i] === available[i]) {\n\t\t\t\tanswer[i] = 'x';\n\t\t\t\tavailable[i] = ' ';\n\t\t\t}\n\t\t}\n\n\t\t// then find close matches (this has to happen\n\t\t// in a second step, otherwise an early close\n\t\t// match can prevent a later exact match)\n\t\tfor (let i = 0; i < 5; i += 1) {\n\t\t\tif (answer[i] === '_') {\n\t\t\t\tconst index = available.indexOf(letters[i]);\n\t\t\t\tif (index !== -1) {\n\t\t\t\t\tanswer[i] = 'c';\n\t\t\t\t\tavailable[index] = ' ';\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthis.answers.push(answer.join(''));\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Serialize game state so it can be set as a cookie\n\t */\n\ttoString() {\n\t\treturn `${this.index}-${this.guesses.join(' ')}-${this.answers.join(' ')}`;\n\t}\n}\n"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "src/routes/sverdle/how-to-play/+page.svelte",
|