From 0f4d9c69aa31fb8a2eb000663d5c2e59884740b0 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 3 Oct 2025 23:26:41 +0200 Subject: [PATCH 1/8] `--from-playground` & PlaygroundLayout --- .changeset/tasty-friends-think.md | 5 + packages/cli/commands/create.ts | 2 +- packages/create/playground.ts | 241 ++++++++++++++++++++++++++++- packages/create/test/playground.ts | 21 ++- 4 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 .changeset/tasty-friends-think.md diff --git a/.changeset/tasty-friends-think.md b/.changeset/tasty-friends-think.md new file mode 100644 index 000000000..7127bf4db --- /dev/null +++ b/.changeset/tasty-friends-think.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat(cli): `--from-playground` will now bring a PlaygroundLayout to get a more consistent experience with the online playground diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index cf39b4ff0..5f630a6b1 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -244,7 +244,7 @@ async function createProjectFromPlayground(url: string, cwd: string): Promise { diff --git a/packages/create/playground.ts b/packages/create/playground.ts index 242353670..bb15e1dd9 100644 --- a/packages/create/playground.ts +++ b/packages/create/playground.ts @@ -154,6 +154,7 @@ function extractPackageVersion(pkgName: string) { } export function setupPlaygroundProject( + url: string, playground: PlaygroundData, cwd: string, installDependencies: boolean @@ -171,17 +172,251 @@ export function setupPlaygroundProject( } // write file to disk - const filePath = path.join(cwd, 'src', 'routes', file.name); + const filePath = path.join(cwd, 'src', 'lib', 'playground', file.name); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, file.content, 'utf8'); } + // add playground layout to lib + { + const playgroundLayoutPath = path.join(cwd, 'src', 'lib', 'PlaygroundLayout.svelte'); + const { generateCode } = parseSvelte(''); + const newContent = generateCode({ + script: `import favicon from "$lib/assets/favicon.svg"; + + let { children } = $props(); + + const title = "${playground.name}"; + const href = "${url}"; + + let prefersDark = $state(true); + let isDark = $state(true); + + function setTheme(value) { + isDark = value === "dark"; + localStorage.setItem("sv:theme", isDark === prefersDark ? "system" : value); + } + + $effect(() => { + document.documentElement.classList.remove("light", "dark"); + + prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const theme = localStorage.getItem("sv:theme") + isDark = theme === "dark" || (theme === "system" && prefersDark); + + document.documentElement.classList.add(isDark ? "dark" : "light"); + });`, + template: ` + --from-playground {title} + + + +
+ + +
+ {@render children?.()} +
+
+ +` + }); + fs.writeFileSync(playgroundLayoutPath, newContent, 'utf-8'); + } + // add app import to +page.svelte const filePath = path.join(cwd, 'src/routes/+page.svelte'); const content = fs.readFileSync(filePath, 'utf-8'); const { script, generateCode } = parseSvelte(content); - js.imports.addDefault(script.ast, { from: `./${mainFile.name}`, as: 'App' }); - const newContent = generateCode({ script: script.generateCode(), template: `` }); + js.imports.addDefault(script.ast, { as: 'App', from: `$lib/playground/${mainFile.name}` }); + js.imports.addDefault(script.ast, { + as: 'PlaygroundLayout', + from: `$lib/PlaygroundLayout.svelte` + }); + const newContent = generateCode({ + script: script.generateCode(), + template: ` + +` + }); fs.writeFileSync(filePath, newContent, 'utf-8'); // add packages as dependencies to package.json if requested diff --git a/packages/create/test/playground.ts b/packages/create/test/playground.ts index 81fd4e0ea..226aef8ad 100644 --- a/packages/create/test/playground.ts +++ b/packages/create/test/playground.ts @@ -165,11 +165,23 @@ test('real world download and convert playground async', async () => { svelteVersion: '5.38.7' }); - setupPlaygroundProject(playground, directory, true); + setupPlaygroundProject( + 'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8', + playground, + directory, + true + ); const pageFilePath = path.join(directory, 'src/routes/+page.svelte'); const pageContent = fs.readFileSync(pageFilePath, 'utf-8'); expect(pageContent).toContain(''); + expect(pageContent).toContain(''); + + const playgroundLayoutPath = path.join(directory, 'src/lib/PlaygroundLayout.svelte'); + const playgroundLayoutContent = fs.readFileSync(playgroundLayoutPath, 'utf-8'); + expect(playgroundLayoutContent).toContain('localStorage.getItem'); + expect(playgroundLayoutContent).toContain('sv:theme'); + expect(playgroundLayoutContent).toContain('770bbef086034b9f8e337bab57efe8d8'); const packageJsonPath = path.join(directory, 'package.json'); const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); @@ -199,7 +211,12 @@ test('real world download and convert playground without async', async () => { svelteVersion: '5.0.5' }); - setupPlaygroundProject(playground, directory, true); + setupPlaygroundProject( + 'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8', + playground, + directory, + true + ); const packageJsonPath = path.join(directory, 'package.json'); const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); From 543f072fe4c5fe64be8e2d8b33deb8c49c886a27 Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 13 Oct 2025 11:55:08 +0200 Subject: [PATCH 2/8] move to separate file --- packages/cli/commands/create.ts | 14 +- packages/create/playground.ts | 236 ++---------------- .../create/shared/PlaygroundLayout.svelte | 220 ++++++++++++++++ 3 files changed, 249 insertions(+), 221 deletions(-) create mode 100644 packages/create/shared/PlaygroundLayout.svelte diff --git a/packages/cli/commands/create.ts b/packages/cli/commands/create.ts index 2b9110d38..93ba0e43a 100644 --- a/packages/cli/commands/create.ts +++ b/packages/cli/commands/create.ts @@ -194,7 +194,11 @@ async function createProject(cwd: ProjectPath, options: Options) { }); if (options.fromPlayground) { - await createProjectFromPlayground(options.fromPlayground, projectPath); + await createProjectFromPlayground( + options.fromPlayground, + projectPath, + language === 'typescript' + ); } p.log.success('Project created'); @@ -236,7 +240,11 @@ async function createProject(cwd: ProjectPath, options: Options) { return { directory: projectPath, addOnNextSteps, packageManager }; } -async function createProjectFromPlayground(url: string, cwd: string): Promise { +async function createProjectFromPlayground( + url: string, + cwd: string, + typescript: boolean +): Promise { const urlData = parsePlaygroundUrl(url); const playground = await downloadPlaygroundData(urlData); @@ -244,7 +252,7 @@ async function createProjectFromPlayground(url: string, cwd: string): Promise { diff --git a/packages/create/playground.ts b/packages/create/playground.ts index bb15e1dd9..4690964ca 100644 --- a/packages/create/playground.ts +++ b/packages/create/playground.ts @@ -3,6 +3,8 @@ import path from 'node:path'; import * as js from '@sveltejs/cli-core/js'; import { parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers'; import { isVersionUnsupportedBelow } from '@sveltejs/cli-core'; +import { dist } from './utils.ts'; +import type { Common } from './index.ts'; export function validatePlaygroundUrl(link: string): boolean { try { @@ -157,7 +159,8 @@ export function setupPlaygroundProject( url: string, playground: PlaygroundData, cwd: string, - installDependencies: boolean + installDependencies: boolean, + typescript: boolean ): void { const mainFile = playground.files.find((file) => file.name === 'App.svelte'); if (!mainFile) throw new Error('Failed to find `App.svelte` entrypoint.'); @@ -179,225 +182,22 @@ export function setupPlaygroundProject( // add playground layout to lib { + const shared = dist('shared.json'); + const { files } = JSON.parse(fs.readFileSync(shared, 'utf-8')) as Common; + const playgroundLayout = files.find((file) => file.name === 'PlaygroundLayout.svelte'); + if (!playgroundLayout) throw new Error('Failed to find `PlaygroundLayout.svelte`'); + const playgroundLayoutPath = path.join(cwd, 'src', 'lib', 'PlaygroundLayout.svelte'); - const { generateCode } = parseSvelte(''); + // getting raw content + const { script, template } = parseSvelte(playgroundLayout.contents); + // generating new content with the right language style + const { generateCode } = parseSvelte('', { typescript }); const newContent = generateCode({ - script: `import favicon from "$lib/assets/favicon.svg"; - - let { children } = $props(); - - const title = "${playground.name}"; - const href = "${url}"; - - let prefersDark = $state(true); - let isDark = $state(true); - - function setTheme(value) { - isDark = value === "dark"; - localStorage.setItem("sv:theme", isDark === prefersDark ? "system" : value); - } - - $effect(() => { - document.documentElement.classList.remove("light", "dark"); - - prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - const theme = localStorage.getItem("sv:theme") - isDark = theme === "dark" || (theme === "system" && prefersDark); - - document.documentElement.classList.add(isDark ? "dark" : "light"); - });`, - template: ` - --from-playground {title} - - - -
- - -
- {@render children?.()} -
-
- -` + script: script + .generateCode() + .replaceAll('$sv-title-$sv', playground.name) + .replaceAll('$sv-url-$sv', url), + template: template.generateCode() }); fs.writeFileSync(playgroundLayoutPath, newContent, 'utf-8'); } diff --git a/packages/create/shared/PlaygroundLayout.svelte b/packages/create/shared/PlaygroundLayout.svelte new file mode 100644 index 000000000..9cc6dd861 --- /dev/null +++ b/packages/create/shared/PlaygroundLayout.svelte @@ -0,0 +1,220 @@ + + + + --from-playground {title} + + + +
+ + +
+ {@render children?.()} +
+
+ + From 903018e171e738f242ad75f67872bca47d34868d Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 13 Oct 2025 11:57:46 +0200 Subject: [PATCH 3/8] keep also lang stype in page --- packages/create/playground.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create/playground.ts b/packages/create/playground.ts index 4690964ca..ccbe7d705 100644 --- a/packages/create/playground.ts +++ b/packages/create/playground.ts @@ -205,7 +205,7 @@ export function setupPlaygroundProject( // add app import to +page.svelte const filePath = path.join(cwd, 'src/routes/+page.svelte'); const content = fs.readFileSync(filePath, 'utf-8'); - const { script, generateCode } = parseSvelte(content); + const { script, generateCode } = parseSvelte(content, { typescript }); js.imports.addDefault(script.ast, { as: 'App', from: `$lib/playground/${mainFile.name}` }); js.imports.addDefault(script.ast, { as: 'PlaygroundLayout', From cd7dc4ace79a7e18bb840bd6764fb552d8fc8e3b Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 13 Oct 2025 12:14:17 +0200 Subject: [PATCH 4/8] oupsy --- packages/create/test/playground.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create/test/playground.ts b/packages/create/test/playground.ts index 226aef8ad..e76376c52 100644 --- a/packages/create/test/playground.ts +++ b/packages/create/test/playground.ts @@ -169,6 +169,7 @@ test('real world download and convert playground async', async () => { 'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8', playground, directory, + true, true ); @@ -215,6 +216,7 @@ test('real world download and convert playground without async', async () => { 'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8', playground, directory, + true, true ); From b0d9bf669d4595ac17048c33f554d7dc2279cfe8 Mon Sep 17 00:00:00 2001 From: jycouet Date: Mon, 13 Oct 2025 14:03:04 +0200 Subject: [PATCH 5/8] It's only a playground file --- packages/create/index.ts | 2 +- packages/create/playground.ts | 39 +++++++++++-------- .../src/lib}/PlaygroundLayout.svelte | 0 3 files changed, 23 insertions(+), 18 deletions(-) rename packages/create/shared/{ => +playground/src/lib}/PlaygroundLayout.svelte (100%) diff --git a/packages/create/index.ts b/packages/create/index.ts index 7e92162fe..b415953ae 100644 --- a/packages/create/index.ts +++ b/packages/create/index.ts @@ -19,7 +19,7 @@ export type File = { contents: string; }; -export type Condition = TemplateType | LanguageType; +export type Condition = TemplateType | LanguageType | 'playground'; export type Common = { files: Array<{ diff --git a/packages/create/playground.ts b/packages/create/playground.ts index ccbe7d705..cdf1a4340 100644 --- a/packages/create/playground.ts +++ b/packages/create/playground.ts @@ -180,26 +180,31 @@ export function setupPlaygroundProject( fs.writeFileSync(filePath, file.content, 'utf8'); } - // add playground layout to lib + // add playground shared files { const shared = dist('shared.json'); const { files } = JSON.parse(fs.readFileSync(shared, 'utf-8')) as Common; - const playgroundLayout = files.find((file) => file.name === 'PlaygroundLayout.svelte'); - if (!playgroundLayout) throw new Error('Failed to find `PlaygroundLayout.svelte`'); - - const playgroundLayoutPath = path.join(cwd, 'src', 'lib', 'PlaygroundLayout.svelte'); - // getting raw content - const { script, template } = parseSvelte(playgroundLayout.contents); - // generating new content with the right language style - const { generateCode } = parseSvelte('', { typescript }); - const newContent = generateCode({ - script: script - .generateCode() - .replaceAll('$sv-title-$sv', playground.name) - .replaceAll('$sv-url-$sv', url), - template: template.generateCode() - }); - fs.writeFileSync(playgroundLayoutPath, newContent, 'utf-8'); + const playgroundFiles = files.filter((file) => file.include.includes('playground')); + + for (const file of playgroundFiles) { + let contentToWrite = file.contents; + + if (file.name === 'src/lib/PlaygroundLayout.svelte') { + // getting raw content + const { script, template } = parseSvelte(file.contents); + // generating new content with the right language style + const { generateCode } = parseSvelte('', { typescript }); + contentToWrite = generateCode({ + script: script + .generateCode() + .replaceAll('$sv-title-$sv', playground.name) + .replaceAll('$sv-url-$sv', url), + template: template.generateCode() + }); + } + + fs.writeFileSync(path.join(cwd, file.name), contentToWrite, 'utf-8'); + } } // add app import to +page.svelte diff --git a/packages/create/shared/PlaygroundLayout.svelte b/packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte similarity index 100% rename from packages/create/shared/PlaygroundLayout.svelte rename to packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte From 78bf2c30158e6996cd59f91e2ebd8314a24e6dab Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 10:55:26 +0200 Subject: [PATCH 6/8] add a failing test --- packages/create/test/playground.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create/test/playground.ts b/packages/create/test/playground.ts index e76376c52..1ec53364c 100644 --- a/packages/create/test/playground.ts +++ b/packages/create/test/playground.ts @@ -183,6 +183,8 @@ test('real world download and convert playground async', async () => { expect(playgroundLayoutContent).toContain('localStorage.getItem'); expect(playgroundLayoutContent).toContain('sv:theme'); expect(playgroundLayoutContent).toContain('770bbef086034b9f8e337bab57efe8d8'); + // parse & print issue + expect(playgroundLayoutContent).not.toContain('"{()"'); const packageJsonPath = path.join(directory, 'package.json'); const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); From bddeb5a882156efb4ae87acc909055427fbd51c4 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 11:18:49 +0200 Subject: [PATCH 7/8] adding some more failing tests --- packages/create/test/playground.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/create/test/playground.ts b/packages/create/test/playground.ts index 1ec53364c..cb9277495 100644 --- a/packages/create/test/playground.ts +++ b/packages/create/test/playground.ts @@ -185,6 +185,9 @@ test('real world download and convert playground async', async () => { expect(playgroundLayoutContent).toContain('770bbef086034b9f8e337bab57efe8d8'); // parse & print issue expect(playgroundLayoutContent).not.toContain('"{()"'); + expect(playgroundLayoutContent).not.toContain('>'); + expect(playgroundLayoutContent).not.toContain('onclick="{switchTheme}"'); + expect(playgroundLayoutContent).toContain('onclick={switchTheme}'); const packageJsonPath = path.join(directory, 'package.json'); const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8'); From 432899ccdc14d8bd2c5d866130fe30b0fb718333 Mon Sep 17 00:00:00 2001 From: jycouet Date: Sat, 18 Oct 2025 11:19:07 +0200 Subject: [PATCH 8/8] fixing while not having the svelte parser --- packages/create/playground.ts | 7 +++++-- .../+playground/src/lib/PlaygroundLayout.svelte | 14 ++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/create/playground.ts b/packages/create/playground.ts index cdf1a4340..9ab4e0ab5 100644 --- a/packages/create/playground.ts +++ b/packages/create/playground.ts @@ -191,7 +191,7 @@ export function setupPlaygroundProject( if (file.name === 'src/lib/PlaygroundLayout.svelte') { // getting raw content - const { script, template } = parseSvelte(file.contents); + const { script, template, css } = parseSvelte(file.contents); // generating new content with the right language style const { generateCode } = parseSvelte('', { typescript }); contentToWrite = generateCode({ @@ -199,7 +199,10 @@ export function setupPlaygroundProject( .generateCode() .replaceAll('$sv-title-$sv', playground.name) .replaceAll('$sv-url-$sv', url), - template: template.generateCode() + template: template + .generateCode() + .replaceAll('onclick="{switchTheme}"', 'onclick={switchTheme}'), + css: css.generateCode() }); } diff --git a/packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte b/packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte index 9cc6dd861..3ffc9ce9a 100644 --- a/packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte +++ b/packages/create/shared/+playground/src/lib/PlaygroundLayout.svelte @@ -9,18 +9,20 @@ let prefersDark = $state(true); let isDark = $state(true); - function setTheme(/** @type {'dark' | 'light' | 'system'} */ value) { + function switchTheme() { + const value = isDark ? 'light' : 'dark'; + isDark = value === 'dark'; localStorage.setItem('sv:theme', isDark === prefersDark ? 'system' : value); } $effect(() => { document.documentElement.classList.remove('light', 'dark'); - prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const theme = localStorage.getItem('sv:theme'); - isDark = theme === 'dark' || (theme === 'system' && prefersDark); + isDark = !theme ? prefersDark : theme === 'dark' || (theme === 'system' && prefersDark); document.documentElement.classList.add(isDark ? 'dark' : 'light'); }); @@ -55,11 +57,7 @@ --to-playground -