create-spark-html-app 0.6.0 → 0.7.1
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/README.md +50 -5
- package/bin/index.js +100 -2
- package/package.json +1 -1
- package/template/README.md +5 -0
- package/template/index.html +5 -0
- package/template/package.json +5 -1
- package/template/public/components/demo-image.html +21 -0
- package/template/public/components/demo-persist.html +30 -0
- package/template/public/components/home.html +7 -0
- package/template/public/components/nav.html +6 -0
- package/template/public/icon.png +0 -0
- package/template/public/sample.jpg +0 -0
- package/template/src/main.js +37 -0
- package/template/vite.config.js +33 -2
package/README.md
CHANGED
|
@@ -25,14 +25,59 @@ Run it with no name to be prompted:
|
|
|
25
25
|
npm create spark-html-app@latest
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
## What you get
|
|
29
|
+
|
|
30
|
+
The scaffold comes with the **whole Spark ecosystem pre-wired** — you delete
|
|
31
|
+
what you don't need instead of wiring what you do:
|
|
32
|
+
|
|
33
|
+
| Always on | Optional (prompted) |
|
|
34
|
+
|-----------|---------------------|
|
|
35
|
+
| `spark-html` — the runtime | `spark-html-router` — multi-page SPA *(default yes)* |
|
|
36
|
+
| `spark-html-head` — reactive title/meta | `spark-html-theme` — dark/light toggle *(yes)* |
|
|
37
|
+
| `spark-html-persist` — localStorage store demo | `spark-html-image` — webp/avif + srcset at build *(yes)* |
|
|
38
|
+
| `spark-prerender` — SEO HTML + sitemap/robots | `spark-html-sri` — integrity checks *(yes)* |
|
|
39
|
+
| `spark-html-devtools` — dev-only inspector | `spark-html-manifest` — PWA manifest + icons + offline shell *(no)* |
|
|
40
|
+
|
|
41
|
+
Every included feature ships with a live demo component, ready to run.
|
|
42
|
+
|
|
43
|
+
Non-interactive? Pass flags instead of answering prompts:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx create-spark-html-app my-app --yes # accept the defaults
|
|
47
|
+
npx create-spark-html-app my-app --all # everything on
|
|
48
|
+
npx create-spark-html-app my-app --minimal # core only
|
|
49
|
+
npx create-spark-html-app my-app --pwa --no-image # per-feature
|
|
50
|
+
```
|
|
51
|
+
|
|
33
52
|
Everything is plain HTML and JavaScript — no compiler, no virtual DOM, no
|
|
34
53
|
proprietary file format. Edit a component, save, and the page updates.
|
|
35
54
|
|
|
55
|
+
## The Spark family
|
|
56
|
+
|
|
57
|
+
Small, single-purpose packages that share one philosophy: no compiler, no
|
|
58
|
+
virtual DOM, no build step required. Add only what you use.
|
|
59
|
+
|
|
60
|
+
| Package | What it does |
|
|
61
|
+
|---|---|
|
|
62
|
+
| [`spark-html`](https://www.npmjs.com/package/spark-html) | The runtime — components, reactivity, stores, forms, scoped styles. 13 kB gzip, 0 deps. |
|
|
63
|
+
| [`spark-html-router`](https://www.npmjs.com/package/spark-html-router) | `<template route>` routing — nested routes/layouts, `route.query`, active links. |
|
|
64
|
+
| [`spark-html-theme`](https://www.npmjs.com/package/spark-html-theme) | Dark/light/system theming in one line — persisted, no flash. |
|
|
65
|
+
| [`spark-html-head`](https://www.npmjs.com/package/spark-html-head) | Reactive `<title>`/`<meta>` per route + a `head` store. |
|
|
66
|
+
| [`spark-html-motion`](https://www.npmjs.com/package/spark-html-motion) | Enter/leave transitions on if/each blocks — `transition="fade|slide|scale"`. |
|
|
67
|
+
| [`spark-html-devtools`](https://www.npmjs.com/package/spark-html-devtools) | In-page devtools — live stores, component tree, patch activity. |
|
|
68
|
+
| [`spark-html-query`](https://www.npmjs.com/package/spark-html-query) | Declarative async data — a self-fetching store (`loading`/`error`/`data`/`refetch`). |
|
|
69
|
+
| [`spark-html-persist`](https://www.npmjs.com/package/spark-html-persist) | Persist stores to localStorage/sessionStorage in one line. |
|
|
70
|
+
| [`spark-html-websocket`](https://www.npmjs.com/package/spark-html-websocket) | A WebSocket as a reactive store — auto-reconnect, JSON, `send()`. |
|
|
71
|
+
| [`spark-prerender`](https://www.npmjs.com/package/spark-prerender) | Build-time SEO prerender + sitemap/robots — no SSR server. |
|
|
72
|
+
| [`spark-html-image`](https://www.npmjs.com/package/spark-html-image) | Build-time image optimization — webp/avif + responsive `srcset`, zero config. |
|
|
73
|
+
| [`spark-html-font`](https://www.npmjs.com/package/spark-html-font) | Font loading optimizer — preload + size-adjusted fallbacks, no FOUT. |
|
|
74
|
+
| [`spark-html-manifest`](https://www.npmjs.com/package/spark-html-manifest) | PWA manifest + icons + head tags (and optional service worker) from one config. |
|
|
75
|
+
| [`spark-html-offline`](https://www.npmjs.com/package/spark-html-offline) | Offline URL imports — a service worker that caches CDN components. |
|
|
76
|
+
| [`spark-html-sri`](https://www.npmjs.com/package/spark-html-sri) | Subresource Integrity — hash + verify assets and remote components. |
|
|
77
|
+
| [`create-spark-html-app`](https://www.npmjs.com/package/create-spark-html-app) | Scaffold a Vite + spark-html app in one command. |
|
|
78
|
+
| [`prettier-plugin-spark`](https://www.npmjs.com/package/prettier-plugin-spark) | Prettier for components — formats `<script>`/`<style>`, markup stays byte-for-byte. |
|
|
79
|
+
| [`spark-html-language-server`](https://www.npmjs.com/package/spark-html-language-server) | LSP — diagnostics, go-to-definition, prop autocomplete, hover docs. |
|
|
80
|
+
|
|
36
81
|
## License
|
|
37
82
|
|
|
38
83
|
MIT
|
package/bin/index.js
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
readdirSync,
|
|
19
19
|
readFileSync,
|
|
20
20
|
renameSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
statSync,
|
|
21
23
|
writeFileSync,
|
|
22
24
|
} from 'node:fs';
|
|
23
25
|
import { createInterface } from 'node:readline/promises';
|
|
@@ -96,6 +98,86 @@ async function prompt(question, fallback) {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
// ── optional features ──────────────────────────────────────────────────
|
|
102
|
+
// The template ships with EVERYTHING wired; excluded features are stripped
|
|
103
|
+
// out of the copied files via `@spark:<name>` … `@spark:end` marker blocks
|
|
104
|
+
// (`@spark:!name` blocks are kept only when the feature is OFF).
|
|
105
|
+
const FEATURES = [
|
|
106
|
+
{ key: 'router', question: 'Include router (multi-page SPA)?', def: true, deps: ['spark-html-router'], files: ['public/components/about.html'] },
|
|
107
|
+
{ key: 'theme', question: 'Include theme (dark/light toggle)?', def: true, deps: ['spark-html-theme'], files: [] },
|
|
108
|
+
{ key: 'image', question: 'Include image optimization?', def: true, deps: ['spark-html-image'], files: ['public/components/demo-image.html', 'public/sample.jpg'] },
|
|
109
|
+
{ key: 'sri', question: 'Include SRI integrity checks?', def: true, deps: ['spark-html-sri'], files: [] },
|
|
110
|
+
{ key: 'pwa', question: 'Include PWA support (manifest + offline shell)?', def: false, deps: ['spark-html-manifest'], files: ['public/icon.png'] },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
async function pickFeatures() {
|
|
114
|
+
const flags = argv.slice(3);
|
|
115
|
+
const on = {};
|
|
116
|
+
for (const f of FEATURES) on[f.key] = f.def;
|
|
117
|
+
if (flags.includes('--all')) for (const f of FEATURES) on[f.key] = true;
|
|
118
|
+
if (flags.includes('--minimal')) for (const f of FEATURES) on[f.key] = false;
|
|
119
|
+
for (const f of FEATURES) {
|
|
120
|
+
if (flags.includes(`--${f.key}`)) on[f.key] = true;
|
|
121
|
+
if (flags.includes(`--no-${f.key}`)) on[f.key] = false;
|
|
122
|
+
}
|
|
123
|
+
// Interactive only on a TTY, and only when no preset flag was given.
|
|
124
|
+
const preset = flags.some((a) => /^--(yes|all|minimal|(no-)?(router|theme|image|sri|pwa))$/.test(a));
|
|
125
|
+
if (stdin.isTTY && !preset) {
|
|
126
|
+
for (const f of FEATURES) {
|
|
127
|
+
const hint = f.def ? '(Y/n)' : '(y/N)';
|
|
128
|
+
const a = (await prompt(`${c.accent('?')} ${f.question} ${c.dim(hint)} `, '')).toLowerCase();
|
|
129
|
+
if (a) on[f.key] = /^y(es)?$/.test(a);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return on;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Remove `@spark:` marker blocks for excluded features (and the marker
|
|
136
|
+
// lines themselves for included ones) across every copied text file.
|
|
137
|
+
function stripMarkers(text, on) {
|
|
138
|
+
const out = [];
|
|
139
|
+
let skipDepth = 0;
|
|
140
|
+
for (const line of text.split('\n')) {
|
|
141
|
+
const open = line.match(/@spark:(!?)([a-z]+)\s*(-->)?\s*$/);
|
|
142
|
+
if (open && open[2] !== 'end') {
|
|
143
|
+
const keep = open[1] === '!' ? !on[open[2]] : !!on[open[2]];
|
|
144
|
+
if (skipDepth || !keep) skipDepth++;
|
|
145
|
+
continue; // marker lines never ship
|
|
146
|
+
}
|
|
147
|
+
if (/@spark:end/.test(line)) {
|
|
148
|
+
if (skipDepth) skipDepth--;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!skipDepth) out.push(line);
|
|
152
|
+
}
|
|
153
|
+
return out.join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function walkFiles(dir) {
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const name of readdirSync(dir)) {
|
|
159
|
+
const full = join(dir, name);
|
|
160
|
+
if (statSync(full).isDirectory()) out.push(...walkFiles(full));
|
|
161
|
+
else out.push(full);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function applyFeatures(targetDir, on) {
|
|
167
|
+
// 1 ─ strip marker blocks in every text file.
|
|
168
|
+
for (const file of walkFiles(targetDir)) {
|
|
169
|
+
if (!/\.(js|html|css)$/.test(file)) continue;
|
|
170
|
+
const text = readFileSync(file, 'utf8');
|
|
171
|
+
if (!text.includes('@spark:')) continue;
|
|
172
|
+
writeFileSync(file, stripMarkers(text, on), 'utf8');
|
|
173
|
+
}
|
|
174
|
+
// 2 ─ delete files that belong to excluded features.
|
|
175
|
+
for (const f of FEATURES) {
|
|
176
|
+
if (on[f.key]) continue;
|
|
177
|
+
for (const rel of f.files) rmSync(join(targetDir, rel), { force: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
99
181
|
async function main() {
|
|
100
182
|
stdout.write(`\n${BOLT} ${c.bold('create-spark-html-app')}\n`);
|
|
101
183
|
stdout.write(`${c.dim(' HTML that reacts — no compiler, no virtual DOM.')}\n\n`);
|
|
@@ -123,9 +205,11 @@ async function main() {
|
|
|
123
205
|
if (!/^y(es)?$/i.test(ok)) bail('Aborted — nothing was written.');
|
|
124
206
|
}
|
|
125
207
|
|
|
126
|
-
// 3 ─ copy the template
|
|
208
|
+
// 3 ─ pick features, copy the template, strip what's excluded ────────
|
|
209
|
+
const features = await pickFeatures();
|
|
127
210
|
mkdirSync(targetDir, { recursive: true });
|
|
128
211
|
cpSync(templateDir, targetDir, { recursive: true });
|
|
212
|
+
applyFeatures(targetDir, features);
|
|
129
213
|
|
|
130
214
|
// npm renames/strips dotfiles on publish, so the template ships them
|
|
131
215
|
// with safe underscore prefixes. Restore the real names here.
|
|
@@ -142,6 +226,19 @@ async function main() {
|
|
|
142
226
|
const pkgPath = join(targetDir, 'package.json');
|
|
143
227
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
144
228
|
pkg.name = projectName;
|
|
229
|
+
// Drop dependencies that belong to excluded features.
|
|
230
|
+
for (const f of FEATURES) {
|
|
231
|
+
if (features[f.key]) continue;
|
|
232
|
+
for (const dep of f.deps) {
|
|
233
|
+
if (pkg.dependencies) delete pkg.dependencies[dep];
|
|
234
|
+
if (pkg.devDependencies) delete pkg.devDependencies[dep];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// PWA config carries the app's display name.
|
|
238
|
+
if (features.pwa) {
|
|
239
|
+
const vitePath = join(targetDir, 'vite.config.js');
|
|
240
|
+
writeFileSync(vitePath, readFileSync(vitePath, 'utf8').replace("name: 'Spark App'", `name: '${projectName}'`), 'utf8');
|
|
241
|
+
}
|
|
145
242
|
// Always start on the newest published versions of the spark packages. If the
|
|
146
243
|
// registry can't be reached (or a package isn't published yet), the template's
|
|
147
244
|
// "latest" default still resolves on install.
|
|
@@ -161,7 +258,8 @@ async function main() {
|
|
|
161
258
|
|
|
162
259
|
// 5 ─ celebrate + print next steps ───────────────────────────────────
|
|
163
260
|
const rel = relative(process.cwd(), targetDir) || '.';
|
|
164
|
-
|
|
261
|
+
const picked = FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only';
|
|
262
|
+
stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(head, persist, prerender, devtools + ${picked})`)}\n\n`);
|
|
165
263
|
stdout.write(`${c.bold('Next steps:')}\n`);
|
|
166
264
|
if (rel !== '.') stdout.write(` ${c.dim('1.')} cd ${rel}\n`);
|
|
167
265
|
stdout.write(` ${c.dim(rel !== '.' ? '2.' : '1.')} npm install\n`);
|
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -5,6 +5,11 @@ HTML components with built-in reactivity. No compiler, no virtual DOM, no build
|
|
|
5
5
|
|
|
6
6
|
The scaffold is a **multi-page SPA** with client-side routing, live demos, and a
|
|
7
7
|
shared design system — edit any component and save to see it update instantly.
|
|
8
|
+
Depending on the options you picked at scaffold time it also wires up
|
|
9
|
+
`spark-html-theme` (dark/light), `spark-html-image` (build-time webp/avif),
|
|
10
|
+
`spark-html-sri` (integrity checks), and `spark-html-manifest` (PWA manifest +
|
|
11
|
+
icons + offline app shell). `spark-html-head`, `spark-html-persist`,
|
|
12
|
+
`spark-prerender`, and `spark-html-devtools` are always included.
|
|
8
13
|
|
|
9
14
|
## Develop
|
|
10
15
|
|
package/template/index.html
CHANGED
|
@@ -197,8 +197,13 @@
|
|
|
197
197
|
<body>
|
|
198
198
|
<div import="components/nav"></div>
|
|
199
199
|
<main class="routes">
|
|
200
|
+
<!-- @spark:router -->
|
|
200
201
|
<template route="/"><div import="components/home"></div></template>
|
|
201
202
|
<template route="/about"><div import="components/about"></div></template>
|
|
203
|
+
<!-- @spark:end -->
|
|
204
|
+
<!-- @spark:!router -->
|
|
205
|
+
<div import="components/home"></div>
|
|
206
|
+
<!-- @spark:end -->
|
|
202
207
|
</main>
|
|
203
208
|
<div import="components/footer"></div>
|
|
204
209
|
|
package/template/package.json
CHANGED
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"spark-html": "latest",
|
|
13
13
|
"spark-html-router": "latest",
|
|
14
14
|
"spark-html-theme": "latest",
|
|
15
|
-
"spark-html-head": "latest"
|
|
15
|
+
"spark-html-head": "latest",
|
|
16
|
+
"spark-html-persist": "latest",
|
|
17
|
+
"spark-html-sri": "latest",
|
|
18
|
+
"spark-html-manifest": "latest"
|
|
16
19
|
},
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"spark-prerender": "latest",
|
|
19
22
|
"spark-html-devtools": "latest",
|
|
23
|
+
"spark-html-image": "latest",
|
|
20
24
|
"vite": "^8.1.0"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<section class="card">
|
|
2
|
+
<header class="card-header">
|
|
3
|
+
<h2>Images <span class="tag">spark-html-image</span></h2>
|
|
4
|
+
</header>
|
|
5
|
+
<p class="hint">
|
|
6
|
+
A plain <code><img></code> — at build time it becomes a
|
|
7
|
+
<code><picture></code> with webp/avif srcset, width/height, and lazy
|
|
8
|
+
loading. Run <code>npm run build</code> and inspect <code>dist/</code>.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<img class="shot" src="/sample.jpg" alt="Sample image — optimized at build time" />
|
|
12
|
+
</section>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.shot {
|
|
16
|
+
width: 100%;
|
|
17
|
+
border-radius: 8px;
|
|
18
|
+
border: 1px solid var(--border);
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<section class="card">
|
|
2
|
+
<header class="card-header">
|
|
3
|
+
<h2>Persist <span class="tag">spark-html-persist</span></h2>
|
|
4
|
+
</header>
|
|
5
|
+
<p class="hint">
|
|
6
|
+
This store lives in <code>localStorage</code> — change it, then <b>reload the page</b>.
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p class="visits">You've loaded this app <b>{visits}</b> {visits === 1 ? 'time' : 'times'}.</p>
|
|
10
|
+
<label>
|
|
11
|
+
A note that survives reloads
|
|
12
|
+
<input bind:value="prefs.name" placeholder="type something, then hit reload" />
|
|
13
|
+
</label>
|
|
14
|
+
</section>
|
|
15
|
+
|
|
16
|
+
<script>
|
|
17
|
+
// Created in src/main.js: persist('prefs', { name: '', visits: 0 })
|
|
18
|
+
const prefs = useStore('prefs');
|
|
19
|
+
let visits = 0;
|
|
20
|
+
|
|
21
|
+
onMount(() => {
|
|
22
|
+
prefs.visits++; // saved to localStorage immediately
|
|
23
|
+
visits = prefs.visits; // local mirror for this render
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
.visits { font-size: 13.5px; margin-bottom: 14px; }
|
|
29
|
+
.visits b { color: var(--spark-ink); }
|
|
30
|
+
</style>
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
<div import="components/demo-props"></div>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
+
<div class="grid">
|
|
15
|
+
<div import="components/demo-persist"></div>
|
|
16
|
+
<!-- @spark:image -->
|
|
17
|
+
<div import="components/demo-image"></div>
|
|
18
|
+
<!-- @spark:end -->
|
|
19
|
+
</div>
|
|
20
|
+
|
|
14
21
|
<div import="components/demo-await"></div>
|
|
15
22
|
</section>
|
|
16
23
|
|
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
</a>
|
|
7
7
|
<div class="nav-links">
|
|
8
8
|
<a href="/">Home</a>
|
|
9
|
+
<!-- @spark:router -->
|
|
9
10
|
<a href="/about">About</a>
|
|
11
|
+
<!-- @spark:end -->
|
|
10
12
|
</div>
|
|
13
|
+
<!-- @spark:theme -->
|
|
11
14
|
<button class="theme" onclick="{theme.toggle}" title="Toggle theme">
|
|
12
15
|
{theme.resolved === 'dark' ? '☾' : '☀'}
|
|
13
16
|
</button>
|
|
17
|
+
<!-- @spark:end -->
|
|
14
18
|
</div>
|
|
15
19
|
</nav>
|
|
16
20
|
|
|
17
21
|
<script>
|
|
22
|
+
// @spark:theme
|
|
18
23
|
const theme = useStore('theme');
|
|
24
|
+
// @spark:end
|
|
19
25
|
</script>
|
|
20
26
|
|
|
21
27
|
<style>
|
|
Binary file
|
|
Binary file
|
package/template/src/main.js
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
import { store } from "spark-html";
|
|
2
|
+
// @spark:!router
|
|
3
|
+
import { mount } from "spark-html";
|
|
4
|
+
// @spark:end
|
|
5
|
+
// @spark:router
|
|
2
6
|
import { router } from "spark-html-router";
|
|
7
|
+
// @spark:end
|
|
8
|
+
// @spark:theme
|
|
3
9
|
import { theme } from "spark-html-theme";
|
|
10
|
+
// @spark:end
|
|
4
11
|
import { head } from "spark-html-head";
|
|
12
|
+
import { persist } from "spark-html-persist";
|
|
13
|
+
// @spark:sri
|
|
14
|
+
import { sri } from "spark-html-sri";
|
|
15
|
+
// @spark:end
|
|
5
16
|
import { devtools } from "spark-html-devtools";
|
|
6
17
|
|
|
7
18
|
const dev = import.meta.env?.DEV;
|
|
8
19
|
if (dev) devtools(); // dev only
|
|
9
20
|
|
|
21
|
+
// @spark:sri
|
|
22
|
+
// Subresource Integrity: verifies every component fetch against the
|
|
23
|
+
// manifest the build baked into the page, and allow-lists remote URL
|
|
24
|
+
// imports (TOFU). Fails open on localhost, enforces in production.
|
|
25
|
+
sri();
|
|
26
|
+
// @spark:end
|
|
27
|
+
|
|
10
28
|
head({
|
|
11
29
|
title: { "/": "Home", "/about": "About", "*": "Not found" },
|
|
12
30
|
titleTemplate: (t) => `${t} · My Site`,
|
|
@@ -16,9 +34,28 @@ head({
|
|
|
16
34
|
// Shared stores connect components without providers or prop drilling.
|
|
17
35
|
store("app", { sparks: 0 });
|
|
18
36
|
|
|
37
|
+
// A store that survives reloads — hydrates from localStorage on boot,
|
|
38
|
+
// saves on every change (see components/demo-persist.html).
|
|
39
|
+
persist("prefs", { name: "", visits: 0 });
|
|
40
|
+
|
|
41
|
+
// @spark:theme
|
|
19
42
|
// One-line dark/light/system theming (the ⚡ logo toggles it).
|
|
20
43
|
theme();
|
|
44
|
+
// @spark:end
|
|
45
|
+
|
|
46
|
+
// @spark:pwa
|
|
47
|
+
// PWA: manifest.webmanifest, icons, and the offline app-shell worker are
|
|
48
|
+
// generated by spark-html-manifest/vite (see vite.config.js) — the worker
|
|
49
|
+
// registration is injected into every built page automatically.
|
|
50
|
+
// Want offline-capable CDN component imports too? See spark-html-offline
|
|
51
|
+
// (a page registers one worker per scope, so pick the one you need).
|
|
52
|
+
// @spark:end
|
|
21
53
|
|
|
54
|
+
// @spark:router
|
|
22
55
|
// Client-side router: reads <template route> blocks, intercepts <a> clicks,
|
|
23
56
|
// and manages SPA navigation. Call it once — replaces mount().
|
|
24
57
|
router({ devOverlay: dev, quiet: !dev });
|
|
58
|
+
// @spark:end
|
|
59
|
+
// @spark:!router
|
|
60
|
+
mount(document.body);
|
|
61
|
+
// @spark:end
|
package/template/vite.config.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { defineConfig } from 'vite';
|
|
2
2
|
import spark from 'spark-html/vite';
|
|
3
3
|
import prerender from 'spark-prerender/vite';
|
|
4
|
+
// @spark:image
|
|
5
|
+
import image from 'spark-html-image/vite';
|
|
6
|
+
// @spark:end
|
|
7
|
+
// @spark:pwa
|
|
8
|
+
import manifest from 'spark-html-manifest/vite';
|
|
9
|
+
// @spark:end
|
|
10
|
+
// @spark:sri
|
|
11
|
+
import sri from 'spark-html-sri/vite';
|
|
12
|
+
// @spark:end
|
|
4
13
|
|
|
5
14
|
// Spark needs no build step — Vite is just a convenient dev server and
|
|
6
15
|
// bundler. The plugin serves component fragments raw and full-reloads
|
|
@@ -9,8 +18,8 @@ import prerender from 'spark-prerender/vite';
|
|
|
9
18
|
//
|
|
10
19
|
// `prerender()` makes `npm run build` SEO-friendly: it runs your app at
|
|
11
20
|
// build time and writes fully-rendered HTML into dist/ (crawlers and AI
|
|
12
|
-
// tools read real content; the browser still hydrates over it)
|
|
13
|
-
// if you don't need SEO.
|
|
21
|
+
// tools read real content; the browser still hydrates over it), plus
|
|
22
|
+
// sitemap.xml + robots.txt. Remove it if you don't need SEO.
|
|
14
23
|
export default defineConfig({
|
|
15
24
|
optimizeDeps: {
|
|
16
25
|
// Ensure spark-html is pre-bundled so all modules share the same stores Map.
|
|
@@ -19,6 +28,28 @@ export default defineConfig({
|
|
|
19
28
|
},
|
|
20
29
|
plugins: [
|
|
21
30
|
spark(),
|
|
31
|
+
// @spark:image
|
|
32
|
+
// Every <img> in pages and components: converted to webp/avif at
|
|
33
|
+
// multiple widths, wrapped in <picture> with srcset, width/height
|
|
34
|
+
// added (no layout shift), loading="lazy". Zero config.
|
|
35
|
+
image(),
|
|
36
|
+
// @spark:end
|
|
22
37
|
prerender({ pages: ['index.html'] }),
|
|
38
|
+
// @spark:pwa
|
|
39
|
+
// One config → manifest.webmanifest + resized icons + <head> tags +
|
|
40
|
+
// an offline app-shell service worker (registered automatically).
|
|
41
|
+
manifest({
|
|
42
|
+
name: 'Spark App',
|
|
43
|
+
themeColor: '#ffd24a',
|
|
44
|
+
icon: 'public/icon.png',
|
|
45
|
+
offline: true,
|
|
46
|
+
}),
|
|
47
|
+
// @spark:end
|
|
48
|
+
// @spark:sri
|
|
49
|
+
// Hashes every built asset + component, stamps integrity/crossorigin
|
|
50
|
+
// onto script/link tags, and bakes the verify manifest into each page.
|
|
51
|
+
// Keep it AFTER prerender() so it sees the final pages.
|
|
52
|
+
sri(),
|
|
53
|
+
// @spark:end
|
|
23
54
|
],
|
|
24
55
|
});
|