Skip to content

Commit 3d5f4de

Browse files
authored
--from-playground & PlaygroundLayout (#731)
* `--from-playground` & PlaygroundLayout * move to separate file * keep also lang stype in page * oupsy * It's only a playground file * add a failing test * adding some more failing tests * fixing while not having the svelte parser
1 parent 6832165 commit 3d5f4de

File tree

6 files changed

+309
-11
lines changed

6 files changed

+309
-11
lines changed

.changeset/tasty-friends-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': patch
3+
---
4+
5+
feat(cli): `--from-playground` will now bring a PlaygroundLayout to get a more consistent experience with the online playground

packages/cli/commands/create.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@ async function createProject(cwd: ProjectPath, options: Options) {
194194
});
195195

196196
if (options.fromPlayground) {
197-
await createProjectFromPlayground(options.fromPlayground, projectPath);
197+
await createProjectFromPlayground(
198+
options.fromPlayground,
199+
projectPath,
200+
language === 'typescript'
201+
);
198202
}
199203

200204
p.log.success('Project created');
@@ -236,15 +240,19 @@ async function createProject(cwd: ProjectPath, options: Options) {
236240
return { directory: projectPath, addOnNextSteps, packageManager };
237241
}
238242

239-
async function createProjectFromPlayground(url: string, cwd: string): Promise<void> {
243+
async function createProjectFromPlayground(
244+
url: string,
245+
cwd: string,
246+
typescript: boolean
247+
): Promise<void> {
240248
const urlData = parsePlaygroundUrl(url);
241249
const playground = await downloadPlaygroundData(urlData);
242250

243251
// Detect external dependencies and ask for confirmation
244252
const dependencies = detectPlaygroundDependencies(playground.files);
245253
const installDependencies = await confirmExternalDependencies(Array.from(dependencies.keys()));
246254

247-
setupPlaygroundProject(playground, cwd, installDependencies);
255+
setupPlaygroundProject(url, playground, cwd, installDependencies, typescript);
248256
}
249257

250258
async function confirmExternalDependencies(dependencies: string[]): Promise<boolean> {

packages/create/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type File = {
1919
contents: string;
2020
};
2121

22-
export type Condition = TemplateType | LanguageType;
22+
export type Condition = TemplateType | LanguageType | 'playground';
2323

2424
export type Common = {
2525
files: Array<{

packages/create/playground.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from 'node:path';
33
import * as js from '@sveltejs/cli-core/js';
44
import { parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
55
import { isVersionUnsupportedBelow } from '@sveltejs/cli-core';
6+
import { dist } from './utils.ts';
7+
import type { Common } from './index.ts';
68

79
export function validatePlaygroundUrl(link: string): boolean {
810
try {
@@ -154,9 +156,11 @@ function extractPackageVersion(pkgName: string) {
154156
}
155157

156158
export function setupPlaygroundProject(
159+
url: string,
157160
playground: PlaygroundData,
158161
cwd: string,
159-
installDependencies: boolean
162+
installDependencies: boolean,
163+
typescript: boolean
160164
): void {
161165
const mainFile = playground.files.find((file) => file.name === 'App.svelte');
162166
if (!mainFile) throw new Error('Failed to find `App.svelte` entrypoint.');
@@ -171,17 +175,56 @@ export function setupPlaygroundProject(
171175
}
172176

173177
// write file to disk
174-
const filePath = path.join(cwd, 'src', 'routes', file.name);
178+
const filePath = path.join(cwd, 'src', 'lib', 'playground', file.name);
175179
fs.mkdirSync(path.dirname(filePath), { recursive: true });
176180
fs.writeFileSync(filePath, file.content, 'utf8');
177181
}
178182

183+
// add playground shared files
184+
{
185+
const shared = dist('shared.json');
186+
const { files } = JSON.parse(fs.readFileSync(shared, 'utf-8')) as Common;
187+
const playgroundFiles = files.filter((file) => file.include.includes('playground'));
188+
189+
for (const file of playgroundFiles) {
190+
let contentToWrite = file.contents;
191+
192+
if (file.name === 'src/lib/PlaygroundLayout.svelte') {
193+
// getting raw content
194+
const { script, template, css } = parseSvelte(file.contents);
195+
// generating new content with the right language style
196+
const { generateCode } = parseSvelte('', { typescript });
197+
contentToWrite = generateCode({
198+
script: script
199+
.generateCode()
200+
.replaceAll('$sv-title-$sv', playground.name)
201+
.replaceAll('$sv-url-$sv', url),
202+
template: template
203+
.generateCode()
204+
.replaceAll('onclick="{switchTheme}"', 'onclick={switchTheme}'),
205+
css: css.generateCode()
206+
});
207+
}
208+
209+
fs.writeFileSync(path.join(cwd, file.name), contentToWrite, 'utf-8');
210+
}
211+
}
212+
179213
// add app import to +page.svelte
180214
const filePath = path.join(cwd, 'src/routes/+page.svelte');
181215
const content = fs.readFileSync(filePath, 'utf-8');
182-
const { script, generateCode } = parseSvelte(content);
183-
js.imports.addDefault(script.ast, { from: `./${mainFile.name}`, as: 'App' });
184-
const newContent = generateCode({ script: script.generateCode(), template: `<App />` });
216+
const { script, generateCode } = parseSvelte(content, { typescript });
217+
js.imports.addDefault(script.ast, { as: 'App', from: `$lib/playground/${mainFile.name}` });
218+
js.imports.addDefault(script.ast, {
219+
as: 'PlaygroundLayout',
220+
from: `$lib/PlaygroundLayout.svelte`
221+
});
222+
const newContent = generateCode({
223+
script: script.generateCode(),
224+
template: `<PlaygroundLayout>
225+
<App />
226+
</PlaygroundLayout>`
227+
});
185228
fs.writeFileSync(filePath, newContent, 'utf-8');
186229

187230
// add packages as dependencies to package.json if requested
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
<script lang="ts">
2+
import favicon from '$lib/assets/favicon.svg';
3+
4+
let { children } = $props();
5+
6+
const title = '$sv-title-$sv';
7+
const href = '$sv-url-$sv';
8+
9+
let prefersDark = $state(true);
10+
let isDark = $state(true);
11+
12+
function switchTheme() {
13+
const value = isDark ? 'light' : 'dark';
14+
15+
isDark = value === 'dark';
16+
localStorage.setItem('sv:theme', isDark === prefersDark ? 'system' : value);
17+
}
18+
19+
$effect(() => {
20+
document.documentElement.classList.remove('light', 'dark');
21+
prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
22+
23+
const theme = localStorage.getItem('sv:theme');
24+
25+
isDark = !theme ? prefersDark : theme === 'dark' || (theme === 'system' && prefersDark);
26+
document.documentElement.classList.add(isDark ? 'dark' : 'light');
27+
});
28+
</script>
29+
30+
<svelte:head>
31+
<title>--from-playground {title}</title>
32+
<script>
33+
{
34+
const theme = localStorage.getItem('sv:theme');
35+
36+
document.documentElement.classList.add(
37+
!theme || theme === 'system'
38+
? window.matchMedia('(prefers-color-scheme: dark)').matches
39+
? 'dark'
40+
: 'light'
41+
: theme
42+
);
43+
}
44+
</script>
45+
</svelte:head>
46+
47+
<div class="layout">
48+
<nav class="navbar">
49+
<div class="nav-left">
50+
<a href="/" class="svelte-icon">
51+
<img src={favicon} alt="Svelte" width="32" height="32" />
52+
</a>
53+
<p class="title">{title}</p>
54+
</div>
55+
<div class="nav-right">
56+
<a {href} class="raised" target="_blank" rel="noopener noreferrer">
57+
--to-playground
58+
<span aria-hidden="true" style="margin-left:0.25em;"> ↗</span>
59+
</a>
60+
<button class="raised theme-toggle" onclick={switchTheme} aria-label="Toggle theme">
61+
<span class="icon"></span>
62+
</button>
63+
</div>
64+
</nav>
65+
66+
<main class="content">
67+
{@render children?.()}
68+
</main>
69+
</div>
70+
71+
<style>
72+
:global(body) {
73+
margin: 0;
74+
}
75+
76+
:global(html) {
77+
margin: 0;
78+
--bg-1: hsl(0, 0%, 100%);
79+
--bg-2: hsl(206, 20%, 90%);
80+
--bg-3: hsl(206, 20%, 80%);
81+
--navbar-bg: #fff;
82+
--fg-1: hsl(0, 0%, 13%);
83+
--fg-2: hsl(0, 0%, 50%);
84+
--fg-3: hsl(0, 0%, 60%);
85+
--link: hsl(208, 77%, 47%);
86+
--border-radius: 4px;
87+
--font:
88+
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
89+
'Helvetica Neue', sans-serif;
90+
color-scheme: light;
91+
background: var(--bg-1);
92+
color: var(--fg-1);
93+
font-family: var(--font);
94+
line-height: 1.5;
95+
height: calc(100vh - 2rem);
96+
accent-color: var(--link) !important;
97+
min-height: 100vh;
98+
background-color: var(--bg-1);
99+
}
100+
101+
:global(html.dark) {
102+
color-scheme: dark;
103+
--bg-1: hsl(0, 0%, 18%);
104+
--bg-2: hsl(0, 0%, 30%);
105+
--bg-3: hsl(0, 0%, 40%);
106+
--navbar-bg: hsl(220, 14%, 16%);
107+
--fg-1: hsl(0, 0%, 75%);
108+
--fg-2: hsl(0, 0%, 40%);
109+
--fg-3: hsl(0, 0%, 30%);
110+
--link: hsl(206, 96%, 72%);
111+
}
112+
113+
.navbar {
114+
color: var(--fg-1);
115+
display: flex;
116+
justify-content: space-between;
117+
align-items: center;
118+
padding: 0em 2.5rem;
119+
height: 3.7rem;
120+
background-color: var(--navbar-bg);
121+
box-shadow:
122+
0 2px 8px 0 rgba(0, 0, 0, 0.08),
123+
0 1.5px 4px 0 rgba(0, 0, 0, 0.04);
124+
}
125+
126+
.nav-left {
127+
display: flex;
128+
align-items: center;
129+
gap: 0.5rem;
130+
}
131+
132+
.svelte-icon {
133+
display: flex;
134+
align-items: center;
135+
text-decoration: none;
136+
transition: opacity 0.2s ease;
137+
}
138+
139+
.svelte-icon:hover {
140+
opacity: 0.8;
141+
}
142+
143+
.title {
144+
font-size: 1.5rem;
145+
font-weight: 400;
146+
margin: 0;
147+
}
148+
149+
.nav-right {
150+
display: flex;
151+
align-items: center;
152+
gap: 1rem;
153+
}
154+
155+
.raised {
156+
background: var(--navbar-bg);
157+
border-left: 0.5px solid var(--fg-3);
158+
border-top: 0.5px solid var(--fg-3);
159+
border-bottom: none;
160+
border-right: none;
161+
border-radius: var(--border-radius);
162+
color: var(--fg-1);
163+
cursor: pointer;
164+
transition: all 0.2s ease;
165+
box-shadow:
166+
0 2px 4px rgba(0, 0, 0, 0.1),
167+
0 1px 2px rgba(0, 0, 0, 0.06);
168+
text-decoration: none;
169+
font-weight: 500;
170+
padding: 0.25rem 0.75rem;
171+
font-size: 0.8rem;
172+
}
173+
174+
.raised:hover {
175+
border-left-color: var(--fg-2);
176+
border-top-color: var(--fg-2);
177+
box-shadow:
178+
0 4px 8px rgba(0, 0, 0, 0.15),
179+
0 2px 4px rgba(0, 0, 0, 0.1);
180+
transform: translate(-1px, -1px);
181+
}
182+
183+
.content {
184+
padding: 1rem;
185+
color: var(--fg-1);
186+
}
187+
188+
.theme-toggle {
189+
display: flex;
190+
align-items: center;
191+
justify-content: center;
192+
width: 1.8rem;
193+
height: 1.8rem;
194+
padding: 0;
195+
min-width: 2rem;
196+
}
197+
198+
.icon {
199+
display: inline-block;
200+
width: 1.5rem;
201+
height: 1.5rem;
202+
-webkit-mask-size: 1.5rem;
203+
mask-size: 1.5rem;
204+
-webkit-mask-repeat: no-repeat;
205+
mask-repeat: no-repeat;
206+
-webkit-mask-position: center;
207+
mask-position: center;
208+
background-color: var(--fg-1);
209+
}
210+
211+
.icon {
212+
mask-image: url('data:image/svg+xml,%3csvg%20xmlns="http://www.w3.org/2000/svg"%20viewBox="0%200%2024%2024"%3e%3cpath%20fill="%23666"%20d="M12%2021q-3.775%200-6.388-2.613T3%2012q0-3.45%202.25-5.988T11%203.05q.625-.075.975.45t-.025%201.1q-.425.65-.638%201.375T11.1%207.5q0%202.25%201.575%203.825T16.5%2012.9q.775%200%201.538-.225t1.362-.625q.525-.35%201.075-.037t.475.987q-.35%203.45-2.937%205.725T12%2021Zm0-2q2.2%200%203.95-1.213t2.55-3.162q-.5.125-1%20.2t-1%20.075q-3.075%200-5.238-2.163T9.1%207.5q0-.5.075-1t.2-1q-1.95.8-3.163%202.55T5%2012q0%202.9%202.05%204.95T12%2019Zm-.25-6.75Z"/%3e%3c/svg%3e');
213+
}
214+
215+
:global(html.dark) .icon {
216+
mask-image: url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2024%2024'%3e%3cpath%20fill='%23d4d4d4'%20d='M12%2019a1%201%200%200%201%20.993.883L13%2020v1a1%201%200%200%201-1.993.117L11%2021v-1a1%201%200%200%201%201-1zm6.313-2.09.094.083.7.7a1%201%200%200%201-1.32%201.497l-.094-.083-.7-.7a1%201%200%200%201%201.218-1.567l.102.07zm-11.306.083a1%201%200%200%201%20.083%201.32l-.083.094-.7.7a1%201%200%200%201-1.497-1.32l.083-.094.7-.7a1%201%200%200%201%201.414%200zM4%2011a1%201%200%200%201%20.117%201.993L4%2013H3a1%201%200%200%201-.117-1.993L3%2011h1zm17%200a1%201%200%200%201%20.117%201.993L21%2013h-1a1%201%200%200%201-.117-1.993L20%2011h1zM6.213%204.81l.094.083.7.7a1%201%200%200%201-1.32%201.497l-.094-.083-.7-.7A1%201%200%200%201%206.11%204.74l.102.07zm12.894.083a1%201%200%200%201%20.083%201.32l-.083.094-.7.7a1%201%200%200%201-1.497-1.32l.083-.094.7-.7a1%201%200%200%201%201.414%200zM12%202a1%201%200%200%201%20.993.883L13%203v1a1%201%200%200%201-1.993.117L11%204V3a1%201%200%200%201%201-1zm0%205a5%205%200%201%201-4.995%205.217L7%2012l.005-.217A5%205%200%200%201%2012%207z'/%3e%3c/svg%3e");
217+
}
218+
</style>

packages/create/test/playground.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,11 +165,29 @@ test('real world download and convert playground async', async () => {
165165
svelteVersion: '5.38.7'
166166
});
167167

168-
setupPlaygroundProject(playground, directory, true);
168+
setupPlaygroundProject(
169+
'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8',
170+
playground,
171+
directory,
172+
true,
173+
true
174+
);
169175

170176
const pageFilePath = path.join(directory, 'src/routes/+page.svelte');
171177
const pageContent = fs.readFileSync(pageFilePath, 'utf-8');
172178
expect(pageContent).toContain('<App />');
179+
expect(pageContent).toContain('<PlaygroundLayout>');
180+
181+
const playgroundLayoutPath = path.join(directory, 'src/lib/PlaygroundLayout.svelte');
182+
const playgroundLayoutContent = fs.readFileSync(playgroundLayoutPath, 'utf-8');
183+
expect(playgroundLayoutContent).toContain('localStorage.getItem');
184+
expect(playgroundLayoutContent).toContain('sv:theme');
185+
expect(playgroundLayoutContent).toContain('770bbef086034b9f8e337bab57efe8d8');
186+
// parse & print issue
187+
expect(playgroundLayoutContent).not.toContain('"{()"');
188+
expect(playgroundLayoutContent).not.toContain('&gt;');
189+
expect(playgroundLayoutContent).not.toContain('onclick="{switchTheme}"');
190+
expect(playgroundLayoutContent).toContain('onclick={switchTheme}');
173191

174192
const packageJsonPath = path.join(directory, 'package.json');
175193
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');
@@ -199,7 +217,13 @@ test('real world download and convert playground without async', async () => {
199217
svelteVersion: '5.0.5'
200218
});
201219

202-
setupPlaygroundProject(playground, directory, true);
220+
setupPlaygroundProject(
221+
'https://svelte.dev/playground/770bbef086034b9f8e337bab57efe8d8',
222+
playground,
223+
directory,
224+
true,
225+
true
226+
);
203227

204228
const packageJsonPath = path.join(directory, 'package.json');
205229
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf-8');

0 commit comments

Comments
 (0)