Skip to content

Commit 6e4cbb3

Browse files
authored
feat: "download app" possibility (#1051)
* feat: "download app" possibility closes #827 * lint * maybe this? * use SvelteKit instead * integrate into menu * move here * remove outdated comment * add css from playground
1 parent a940812 commit 6e4cbb3

File tree

8 files changed

+158
-3
lines changed

8 files changed

+158
-3
lines changed

apps/svelte.dev/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
/src/routes/_home/Supporters/contributors.js
77
/src/routes/_home/Supporters/donors.jpg
88
/src/routes/_home/Supporters/donors.js
9+
/scripts/svelte-template
10+
/static/svelte-template.json
911

1012
# git-repositories of synced docs go here
1113
/repos/

apps/svelte.dev/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"ansi-to-html": "^0.7.2",
3636
"base64-js": "^1.5.1",
3737
"cookie": "^0.7.0",
38+
"do-not-zip": "^1.0.0",
3839
"d3-geo": "^3.1.0",
3940
"d3-geo-projection": "^4.0.0",
4041
"editor": "workspace:*",
@@ -72,6 +73,7 @@
7273
"prettier-plugin-svelte": "^3.3.2",
7374
"satori": "^0.10.13",
7475
"satori-html": "^0.3.2",
76+
"sv": "^0.6.8",
7577
"svelte": "5.14.0",
7678
"svelte-check": "^4.1.1",
7779
"svelte-preprocess": "^6.0.3",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// @ts-check
2+
import { readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { create } from 'sv';
6+
7+
// This download the currente Vite template from Github, adjusts it to our needs, and saves it to static/svelte-template.json
8+
// This is used by the Svelte REPL as part of the "download project" feature
9+
10+
const force = process.env.FORCE_UPDATE === 'true';
11+
const output_file = fileURLToPath(new URL('../static/svelte-template.json', import.meta.url));
12+
const output_dir = fileURLToPath(new URL('./svelte-template', import.meta.url));
13+
14+
try {
15+
if (!force && statSync(output_file)) {
16+
console.info(`[update/template] ${output_file} exists. Skipping`);
17+
process.exit(0);
18+
}
19+
} catch {
20+
// create Svelte-Kit skelton app
21+
create(output_dir, { template: 'minimal', types: 'typescript', name: 'your-app' });
22+
23+
function get_all_files(dir) {
24+
const files = [];
25+
const items = readdirSync(dir, { withFileTypes: true });
26+
27+
for (const item of items) {
28+
const full_path = join(dir, item.name);
29+
if (item.isDirectory()) {
30+
files.push(...get_all_files(full_path));
31+
} else {
32+
files.push(full_path.replaceAll('\\', '/'));
33+
}
34+
}
35+
36+
return files;
37+
}
38+
39+
const all_files = get_all_files(output_dir);
40+
const files = [];
41+
42+
for (let path of all_files) {
43+
const bytes = readFileSync(path);
44+
const string = bytes.toString();
45+
let data = bytes.compare(Buffer.from(string)) === 0 ? string : [...bytes];
46+
47+
if (path.endsWith('routes/+page.svelte')) {
48+
data = `<script>\n\timport '../app.css';\n\timport App from './App.svelte';\n</script>\n\n<App />\n`;
49+
}
50+
51+
files.push({ path: path.slice(output_dir.length + 1), data });
52+
}
53+
54+
files.push({
55+
path: 'src/routes/+page.js',
56+
data:
57+
"// Because we don't know whether or not your playground app can run in a server environment, we disable server-side rendering.\n" +
58+
'// Make sure to test whether or not you can re-enable it, as SSR improves perceived performance and site accessibility.\n' +
59+
'// Read more about this option here: https://svelte.dev/docs/kit/page-options#ssr\n' +
60+
'export const ssr = false;\n'
61+
});
62+
63+
// add CSS styles from playground to the project
64+
const html = readFileSync(
65+
join(output_dir, '../../../../packages/repl/src/lib/Output/srcdoc/index.html'),
66+
{ encoding: 'utf-8' }
67+
);
68+
const css = html
69+
.slice(html.indexOf('<style>') + 7, html.indexOf('</style>'))
70+
.split('\n')
71+
.map((line) =>
72+
// remove leading \t
73+
line.slice(3)
74+
)
75+
.join('\n')
76+
.trimStart();
77+
files.push({
78+
path: 'src/app.css',
79+
data: css
80+
});
81+
82+
writeFileSync(output_file, JSON.stringify(files));
83+
84+
// remove output dir afterwards to prevent it messing with Vite watcher
85+
rmSync(output_dir, { force: true, recursive: true });
86+
}

apps/svelte.dev/scripts/update.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ const env = {
99

1010
fork(`${dir}/get_contributors.js`, { env });
1111
fork(`${dir}/get_donors.js`, { env });
12+
fork(`${dir}/get_svelte_template.js`, { env });

apps/svelte.dev/src/routes/(authed)/playground/[id]/+page.svelte

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<script lang="ts">
2+
// @ts-expect-error no types
3+
import * as doNotZip from 'do-not-zip';
24
import { browser } from '$app/environment';
35
import { afterNavigate, goto, replaceState } from '$app/navigation';
46
import type { Gist } from '$lib/db/types';
@@ -113,6 +115,43 @@
113115
}
114116
}
115117
118+
async function download() {
119+
const { files: components, imports } = repl.toJSON();
120+
121+
const files: Array<{ path: string; data: string }> = await (
122+
await fetch('/svelte-template.json')
123+
).json();
124+
125+
if (imports.length > 0) {
126+
const idx = files.findIndex(({ path }) => path === 'package.json');
127+
const pkg = JSON.parse(files[idx].data);
128+
const { devDependencies } = pkg;
129+
imports.forEach((mod) => {
130+
const match = /^(@[^/]+\/)?[^@/]+/.exec(mod)!;
131+
devDependencies[match[0]] = 'latest';
132+
});
133+
pkg.devDependencies = devDependencies;
134+
files[idx].data = JSON.stringify(pkg, null, ' ');
135+
}
136+
137+
files.push(
138+
...components.map((component) => ({
139+
path: `src/routes/${component.name}`,
140+
data: (component as File).contents
141+
}))
142+
);
143+
144+
const url = URL.createObjectURL(doNotZip.toBlob(files));
145+
const link = document.createElement('a');
146+
link.href = url;
147+
link.download = 'svelte-app.zip';
148+
link.style.display = 'none';
149+
document.body.appendChild(link);
150+
link.click();
151+
URL.revokeObjectURL(url);
152+
link.remove();
153+
}
154+
116155
async function update_hash() {
117156
// Only change hash when necessary to avoid polluting everyone's browser history
118157
if (modified) {
@@ -200,6 +239,7 @@
200239
{can_escape}
201240
injectedJS={mapbox_setup}
202241
{onchange}
242+
{download}
203243
previewTheme={$theme.current}
204244
/>
205245
</div>

packages/repl/src/lib/Input/ComponentSelector.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
workspace: Workspace;
1111
can_migrate: boolean;
1212
migrate: () => void;
13+
download?: () => void;
1314
}
1415
15-
let { runes, onchange, workspace, can_migrate, migrate }: Props = $props();
16+
let { runes, onchange, workspace, can_migrate, migrate, download }: Props = $props();
1617
1718
let input = $state() as HTMLInputElement;
1819
let input_value = $state(workspace.current.name);
@@ -170,6 +171,10 @@
170171
</label>
171172

172173
<button disabled={!can_migrate} onclick={migrate}>Migrate to Svelte 5, if possible</button>
174+
175+
{#if download}
176+
<button onclick={download}>Download app</button>
177+
{/if}
173178
</Toolbox>
174179
</div>
175180
</div>

packages/repl/src/lib/Repl.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
injectedCSS?: string;
2424
previewTheme?: 'light' | 'dark';
2525
onchange?: () => void;
26+
download?: () => void;
2627
}
2728
2829
let {
@@ -37,7 +38,8 @@
3738
injectedJS = '',
3839
injectedCSS = '',
3940
previewTheme = 'light',
40-
onchange = () => {}
41+
onchange = () => {},
42+
download
4143
}: Props = $props();
4244
4345
// TODO pass in real data
@@ -174,7 +176,7 @@
174176
>
175177
{#snippet a()}
176178
<section>
177-
<ComponentSelector {runes} {onchange} {workspace} {can_migrate} {migrate} />
179+
<ComponentSelector {runes} {onchange} {workspace} {can_migrate} {migrate} {download} />
178180

179181
<Editor {workspace} />
180182
</section>

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)