create-spark-html-app 0.8.2 → 0.9.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # create-spark-html-app
2
2
 
3
- Scaffold a [Spark](https://github.com/wilkinnovo/spark) app in seconds — a
3
+ Scaffold a [Spark](https://github.com/wilkinnovo/spark-html) app in seconds — a
4
4
  Bun-powered project (dev / build / preview via `spark-html-bun`) wired to
5
5
  `spark-html` with live, reactive **Spark** components.
6
6
 
@@ -24,6 +24,24 @@ Run it with no name to be prompted:
24
24
  bunx create-spark-html-app@latest
25
25
  ```
26
26
 
27
+ ## Project types — SSR & Prerender
28
+
29
+ ```bash
30
+ bunx create-spark-html-app@latest myapp # client-only (default)
31
+ bunx create-spark-html-app@latest myapp --ssr # SSR with spark-ssr
32
+ bunx create-spark-html-app@latest myapp --prerender # static site with spark-prerender
33
+ ```
34
+
35
+ Without a flag (on a TTY) an interactive picker asks which one you want.
36
+
37
+ `--ssr` scaffolds the three-tier pattern — **pages** declare data with
38
+ `<spark-ssr>`, **components** are pure UI via `<div import>`, **spark.json**
39
+ holds the DB connection — plus a seeded SQLite dev database. No build step:
40
+ `bun run dev` and it serves.
41
+
42
+ `--prerender` scaffolds a minimal static site whose build writes
43
+ fully-rendered HTML into `dist/` via `spark-prerender`.
44
+
27
45
  ## What you get
28
46
 
29
47
  The scaffold comes with the **whole Spark ecosystem pre-wired** — you delete
@@ -54,7 +72,8 @@ proprietary file format. Edit a component, save, and the page updates.
54
72
  ## The Spark family
55
73
 
56
74
  Small, single-purpose packages that share one philosophy: no compiler, no
57
- virtual DOM, no build step required. Add only what you use.
75
+ virtual DOM, no build step required built for humans who love hand-writing
76
+ their web apps. Add only what you use.
58
77
 
59
78
  | Package | What it does |
60
79
  |---|---|
package/bin/index.js CHANGED
@@ -27,7 +27,8 @@ import { createInterface } from 'node:readline/promises';
27
27
  import { stdin, stdout, argv, exit } from 'node:process';
28
28
 
29
29
  const here = dirname(fileURLToPath(import.meta.url));
30
- const templateDir = resolve(here, '..', 'template');
30
+ const templateFor = (type) =>
31
+ resolve(here, '..', type === 'client' ? 'template' : `template-${type}`);
31
32
 
32
33
  // ── tiny ANSI palette (no chalk; one less thing to install) ───────────
33
34
  const supportsColor = stdout.isTTY && process.env.NO_COLOR === undefined;
@@ -99,6 +100,28 @@ async function prompt(question, fallback) {
99
100
  }
100
101
  }
101
102
 
103
+ // ── project type ────────────────────────────────────────────────────────
104
+ // Client-only (the existing default), SSR (spark-ssr), or a prerendered
105
+ // static site (spark-prerender). Flags: --ssr / --prerender; otherwise an
106
+ // interactive picker on a TTY.
107
+ const TYPES = [
108
+ { key: 'client', label: 'Client-only (default)' },
109
+ { key: 'ssr', label: 'SSR (spark-ssr)' },
110
+ { key: 'prerender', label: 'Prerender (spark-prerender)' },
111
+ ];
112
+
113
+ async function pickType() {
114
+ const flags = argv.slice(2);
115
+ if (flags.includes('--ssr')) return 'ssr';
116
+ if (flags.includes('--prerender')) return 'prerender';
117
+ if (flags.includes('--client')) return 'client';
118
+ if (!stdin.isTTY) return 'client';
119
+ stdout.write(`${c.accent('?')} Project type:\n`);
120
+ TYPES.forEach((t, i) => stdout.write(` ${c.dim(String(i + 1) + ')')} ${t.label}\n`));
121
+ const a = (await prompt(`${c.accent('?')} Pick one ${c.dim('(1)')} `, '1')).trim();
122
+ return (TYPES[Number(a) - 1] || TYPES[0]).key;
123
+ }
124
+
102
125
  // ── optional features ──────────────────────────────────────────────────
103
126
  // The template ships with EVERYTHING wired; excluded features are stripped
104
127
  // out of the copied files via `@spark:<name>` … `@spark:end` marker blocks
@@ -106,6 +129,7 @@ async function prompt(question, fallback) {
106
129
  const FEATURES = [
107
130
  { key: 'router', question: 'Include router (multi-page SPA)?', def: true, deps: ['spark-html-router'], files: ['public/components/about.html'] },
108
131
  { key: 'theme', question: 'Include theme (dark/light toggle)?', def: true, deps: ['spark-html-theme'], files: [] },
132
+ { key: 'font', question: 'Include font loading optimizer (no FOUT)?', def: true, deps: ['spark-html-font'], files: [] },
109
133
  { key: 'image', question: 'Include image optimization?', def: true, deps: ['spark-html-image'], files: ['public/components/demo-image.html', 'public/sample.jpg'] },
110
134
  { key: 'sri', question: 'Include SRI integrity checks?', def: true, deps: ['spark-html-sri'], files: [] },
111
135
  { key: 'pwa', question: 'Include PWA support (manifest + offline shell)?', def: false, deps: ['spark-html-manifest'], files: ['public/icon.png'] },
@@ -122,7 +146,7 @@ async function pickFeatures() {
122
146
  if (flags.includes(`--no-${f.key}`)) on[f.key] = false;
123
147
  }
124
148
  // Interactive only on a TTY, and only when no preset flag was given.
125
- const preset = flags.some((a) => /^--(yes|all|minimal|(no-)?(router|theme|image|sri|pwa))$/.test(a));
149
+ const preset = flags.some((a) => /^--(yes|all|minimal|(no-)?(router|theme|font|image|sri|pwa))$/.test(a));
126
150
  if (stdin.isTTY && !preset) {
127
151
  for (const f of FEATURES) {
128
152
  const hint = f.def ? '(Y/n)' : '(y/N)';
@@ -181,10 +205,10 @@ function applyFeatures(targetDir, on) {
181
205
 
182
206
  async function main() {
183
207
  stdout.write(`\n${BOLT} ${c.bold('create-spark-html-app')}\n`);
184
- stdout.write(`${c.dim(' HTML that reacts — no compiler, no virtual DOM.')}\n\n`);
208
+ stdout.write(`${c.dim(' HTML that reacts. Built for humans — no compiler, no virtual DOM.')}\n\n`);
185
209
 
186
210
  // 1 ─ figure out the target directory ────────────────────────────────
187
- let targetArg = argv[2];
211
+ let targetArg = argv.slice(2).find((a) => !a.startsWith('-'));
188
212
  if (!targetArg) {
189
213
  if (!stdin.isTTY) bail('Please pass a project name: create-spark-html-app <name>');
190
214
  targetArg = await prompt(
@@ -206,11 +230,12 @@ async function main() {
206
230
  if (!/^y(es)?$/i.test(ok)) bail('Aborted — nothing was written.');
207
231
  }
208
232
 
209
- // 3 ─ pick features, copy the template, strip what's excluded ────────
210
- const features = await pickFeatures();
233
+ // 3 ─ pick the project type + features, copy the template ────────────
234
+ const type = await pickType();
235
+ const features = type === 'client' ? await pickFeatures() : {};
211
236
  mkdirSync(targetDir, { recursive: true });
212
- cpSync(templateDir, targetDir, { recursive: true });
213
- applyFeatures(targetDir, features);
237
+ cpSync(templateFor(type), targetDir, { recursive: true });
238
+ if (type === 'client') applyFeatures(targetDir, features);
214
239
 
215
240
  // npm renames/strips dotfiles on publish, so the template ships them
216
241
  // with safe underscore prefixes. Restore the real names here.
@@ -228,7 +253,7 @@ async function main() {
228
253
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
229
254
  pkg.name = projectName;
230
255
  // Drop dependencies that belong to excluded features.
231
- for (const f of FEATURES) {
256
+ for (const f of type === 'client' ? FEATURES : []) {
232
257
  if (features[f.key]) continue;
233
258
  for (const dep of f.deps) {
234
259
  if (pkg.dependencies) delete pkg.dependencies[dep];
@@ -247,7 +272,8 @@ async function main() {
247
272
  const deps = pkg[group];
248
273
  if (!deps) continue;
249
274
  for (const name of Object.keys(deps)) {
250
- if (name !== 'spark-html' && !name.startsWith('spark-html-') && name !== 'spark-prerender') continue;
275
+ if (name !== 'spark-html' && !name.startsWith('spark-html-')
276
+ && name !== 'spark-prerender' && name !== 'spark-ssr') continue;
251
277
  const range = await latestRange(name);
252
278
  if (range) {
253
279
  deps[name] = range;
@@ -259,13 +285,16 @@ async function main() {
259
285
 
260
286
  // 5 ─ celebrate + print next steps ───────────────────────────────────
261
287
  const rel = relative(process.cwd(), targetDir) || '.';
262
- const picked = FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only';
263
- stdout.write(`\n${c.green('')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(head, persist, prerender, devtools + ${picked})`)}\n\n`);
288
+ const flavor = type === 'ssr' ? 'SSR — spark-ssr, zero config, no build'
289
+ : type === 'prerender' ? 'prerendered static site spark-prerender'
290
+ : `head, persist, prerender, devtools + ${FEATURES.filter((f) => features[f.key]).map((f) => f.key).join(', ') || 'core only'}`;
291
+ stdout.write(`\n${c.green('✔')} Scaffolded ${c.bold(projectName)} in ${c.cyan(rel)} ${c.dim(`(${flavor})`)}\n\n`);
264
292
  stdout.write(`${c.bold('Next steps:')}\n`);
265
293
  if (rel !== '.') stdout.write(` ${c.dim('1.')} cd ${rel}\n`);
266
294
  stdout.write(` ${c.dim(rel !== '.' ? '2.' : '1.')} bun install\n`);
267
- stdout.write(` ${c.dim(rel !== '.' ? '3.' : '2.')} bun dev\n\n`);
268
- stdout.write(`${BOLT} Then open the dev server and edit ${c.cyan('public/components/hero.html')}.\n\n`);
295
+ stdout.write(` ${c.dim(rel !== '.' ? '3.' : '2.')} bun run dev\n\n`);
296
+ const editHint = type === 'ssr' ? 'pages/index.html' : 'public/components/hero.html';
297
+ stdout.write(`${BOLT} Then open the dev server and edit ${c.cyan(editHint)}.\n\n`);
269
298
  }
270
299
 
271
300
  main().catch((err) => bail(err?.message || String(err)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-spark-html-app",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Scaffold a spark-html app — dev/build/preview on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,14 +8,16 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
- "template"
11
+ "template",
12
+ "template-ssr",
13
+ "template-prerender"
12
14
  ],
13
15
  "engines": {
14
16
  "node": ">=18"
15
17
  },
16
18
  "repository": {
17
19
  "type": "git",
18
- "url": "git+https://github.com/wilkinnovo/spark.git",
20
+ "url": "git+https://github.com/wilkinnovo/spark-html.git",
19
21
  "directory": "packages/create-spark-html-app"
20
22
  },
21
23
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  # ⚡ Spark App
2
2
 
3
- A starter built with [spark-html](https://github.com/wilkinnovo/spark) — single-file
3
+ A starter built with [spark-html](https://github.com/wilkinnovo/spark-html) — single-file
4
4
  HTML components with built-in reactivity. No compiler, no virtual DOM, no build step.
5
5
 
6
6
  The scaffold is a **multi-page SPA** with client-side routing, live demos, and a
@@ -72,5 +72,5 @@ Derive values with `$:`, share state across components with `useStore(name)`, us
72
72
  `bind:value` for two-way binds, and pass props as attributes on the `import`
73
73
  placeholder.
74
74
 
75
- See the [full docs](https://wilkinnovo.github.io/spark/docs) for the complete
75
+ See the [full docs](https://wilkinnovo.github.io/spark-html/docs) for the complete
76
76
  template syntax reference.
@@ -6,9 +6,9 @@
6
6
  <title>Spark App</title>
7
7
  <meta name="description" content="A reactive app built with Spark — single-file HTML components, no compiler, no virtual DOM." />
8
8
  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>" />
9
- <link rel="preconnect" href="https://fonts.googleapis.com" />
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
11
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
9
+ <!-- Fonts are injected by spark-html-font/bun (see spark.config.js):
10
+ preconnects, the stylesheet, and a size-adjusted fallback face so the
11
+ swap never moves the page. -->
12
12
  <style>
13
13
  /* Design system — the same palette and monospace type as the Spark
14
14
  website. Tokens + base + a shared card/button/input system live here in
@@ -34,7 +34,9 @@
34
34
  --spark-ink: #ffd24a;
35
35
  --danger: #ff6b6b;
36
36
  --radius: 12px;
37
- --font: "JetBrains Mono", ui-monospace, monospace;
37
+ /* the injected --font-jetbrains-mono stack includes the fallback face;
38
+ the var() fallback list covers a config without the font step */
39
+ --font: var(--font-jetbrains-mono, "JetBrains Mono", ui-monospace, monospace);
38
40
  }
39
41
  [data-theme="light"] {
40
42
  --bg: #fff;
@@ -21,6 +21,7 @@
21
21
  "spark-html-bun": "latest",
22
22
  "spark-prerender": "latest",
23
23
  "spark-html-devtools": "latest",
24
- "spark-html-image": "latest"
24
+ "spark-html-image": "latest",
25
+ "spark-html-font": "latest"
25
26
  }
26
27
  }
@@ -1,7 +1,7 @@
1
1
  <footer class="foot">
2
2
  Edit any file in <code>public/components/</code> and save — the page updates
3
3
  instantly. Built with
4
- <a href="https://github.com/wilkinnovo/spark" target="_blank" rel="noopener">Spark</a>.
4
+ <a href="https://github.com/wilkinnovo/spark-html" target="_blank" rel="noopener">Spark</a>.
5
5
  </footer>
6
6
 
7
7
  <style>
@@ -1,4 +1,10 @@
1
1
  import prerender from 'spark-prerender/bun';
2
+ // @spark:theme
3
+ import theme from 'spark-html-theme/bun';
4
+ // @spark:end
5
+ // @spark:font
6
+ import font from 'spark-html-font/bun';
7
+ // @spark:end
2
8
  // @spark:image
3
9
  import image from 'spark-html-image/bun';
4
10
  // @spark:end
@@ -26,6 +32,20 @@ import sri from 'spark-html-sri/bun';
26
32
  export default {
27
33
  pipeline: [
28
34
  prerender({ pages: ['index.html'] }),
35
+ // @spark:theme
36
+ // Bakes the tiny no-flash script into each page (dev too) so the saved /
37
+ // OS theme is on <html> before first paint — no wrong-theme flash on load.
38
+ theme(),
39
+ // @spark:end
40
+ // @spark:font
41
+ // Preconnect + the Google stylesheet + a size-adjusted local fallback
42
+ // face, baked into each page (dev too) — the font swap never moves or
43
+ // visibly reflows the page.
44
+ font({
45
+ fallback: ['ui-monospace', 'monospace'],
46
+ fonts: [{ family: 'JetBrains Mono', google: true, weights: [300, 400, 500, 600, 700, 800] }],
47
+ }),
48
+ // @spark:end
29
49
  // @spark:image
30
50
  // Every <img> in pages and components: converted to webp/avif at multiple
31
51
  // widths, srcset + width/height added (no layout shift), loading="lazy".
@@ -0,0 +1,13 @@
1
+ # ⚡ Spark Static App
2
+
3
+ A prerendered static site built with
4
+ [spark-html](https://github.com/wilkinnovo/spark-html) + `spark-prerender`.
5
+ Components stay single-file HTML; the build writes fully-rendered pages into
6
+ `dist/` (plus sitemap/robots hooks) and the browser hydrates over them.
7
+
8
+ ```bash
9
+ bun install
10
+ bun run dev # dev server with HMR
11
+ bun run build # prerendered static output → dist/, deploy anywhere
12
+ bun run preview # preview the production build
13
+ ```
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Spark Static App</title>
7
+ <link rel="stylesheet" href="/src/style.css" />
8
+ </head>
9
+ <body>
10
+ <div import="components/hero"></div>
11
+ <script type="module" src="/src/main.js"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "spark-static-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "spark dev",
8
+ "build": "spark build",
9
+ "preview": "spark preview"
10
+ },
11
+ "dependencies": {
12
+ "spark-html": "latest"
13
+ },
14
+ "devDependencies": {
15
+ "spark-html-bun": "latest",
16
+ "spark-prerender": "latest"
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ <main>
2
+ <h1>⚡ {greeting}</h1>
3
+ <p>This page is prerendered at build time — view-source on the deployed
4
+ site and the content is right there. Edit this component and save.</p>
5
+ <button onclick={cheer}>Clicked {count} times</button>
6
+ </main>
7
+
8
+ <script>
9
+ let greeting = 'HTML that reacts';
10
+ let count = 0;
11
+ let pageTitle = 'Spark Static App';
12
+ let pageDescription = 'A prerendered spark-html starter — SEO-ready static HTML that hydrates into a reactive app.';
13
+ function cheer() { count++; }
14
+ </script>
@@ -0,0 +1,9 @@
1
+ import prerender from 'spark-prerender/bun';
2
+
3
+ // Static site, the Spark way: `spark dev` serves components raw with HMR;
4
+ // `spark build` copies public/, bundles the entry, then prerender() runs the
5
+ // REAL app at build time and writes fully-rendered HTML into dist/ — crawlers
6
+ // and AI tools read real content, the browser hydrates over it.
7
+ export default {
8
+ pipeline: [prerender({ pages: ['index.html'] })],
9
+ };
@@ -0,0 +1,2 @@
1
+ import { mount } from 'spark-html';
2
+ mount();
@@ -0,0 +1,3 @@
1
+ body { font-family: system-ui, sans-serif; max-width: 36rem; margin: 4rem auto; padding: 0 1.5rem; }
2
+ main { text-align: center; }
3
+ button { padding: 0.5rem 1rem; cursor: pointer; }
@@ -0,0 +1,29 @@
1
+ # ⚡ Spark SSR App
2
+
3
+ SSR the Spark way — zero config, no build. The HTML template infers
4
+ everything: `<template each="todo in todos">` means you need `todos`,
5
+ `<spark-ssr table="todos" />` backs it with a table and the REST endpoints
6
+ the handlers imply.
7
+
8
+ ## Develop
9
+
10
+ ```bash
11
+ bun install
12
+ bun run dev # creates + seeds dev.db, then serves on :3000
13
+ ```
14
+
15
+ ## The three-tier pattern
16
+
17
+ - **Page** — `pages/index.html` declares its data with `<spark-ssr>`
18
+ - **Component** — `components/nav.html` is pure UI via `<div import>`
19
+ - **Config** — `spark.json` holds the DB connection (swap sqlite → postgres
20
+ by changing one line)
21
+
22
+ Add a page by adding a file: `pages/about.html` → `/about`,
23
+ `pages/blog/[slug].html` → `/blog/:slug` (`:slug` binds into your queries).
24
+
25
+ ## Deploy
26
+
27
+ ```bash
28
+ bun run build # dist/ with a compiled single binary
29
+ ```
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ dev.db
4
+ uploads
@@ -0,0 +1,4 @@
1
+ <!-- Components are pure UI — they just render what they receive. -->
2
+ <nav>
3
+ <strong>⚡ {title}</strong>
4
+ </nav>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "spark-ssr-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun setup.js && bun spark-ssr",
8
+ "start": "bun spark-ssr",
9
+ "build": "bun spark-ssr build"
10
+ },
11
+ "dependencies": {
12
+ "spark-html": "latest",
13
+ "spark-ssr": "latest"
14
+ }
15
+ }
@@ -0,0 +1,6 @@
1
+ body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 3rem auto; padding: 0 1.5rem; }
2
+ nav { opacity: 0.7; margin-bottom: 2rem; }
3
+ ul { padding: 0; }
4
+ li { list-style: none; display: flex; gap: 0.5rem; align-items: center; padding: 0.35rem 0; }
5
+ li button { margin-left: auto; }
6
+ input[type="text"], input:not([type]) { padding: 0.4rem 0.6rem; }
@@ -0,0 +1,19 @@
1
+ <div import="/components/nav" title="Tasks"></div>
2
+
3
+ <h1>Tasks</h1>
4
+
5
+ <template await="todos">
6
+ <input bind:value="draft" placeholder="New task">
7
+ <button onclick={add}>Add</button>
8
+ <ul>
9
+ <template each="todo in todos">
10
+ <li>
11
+ <input type="checkbox" bind:checked="todo.done" onchange={patch}>
12
+ {todo.title}
13
+ <button onclick={remove}>✕</button>
14
+ </li>
15
+ </template>
16
+ </ul>
17
+ </template>
18
+
19
+ <spark-ssr table="todos" />
@@ -0,0 +1,15 @@
1
+ // One-time (idempotent) dev database setup — `bun run dev` runs it for you.
2
+ // Swap spark.json's db to postgres:// any time; no code changes needed.
3
+ import { Database } from 'bun:sqlite';
4
+
5
+ const db = new Database('./dev.db', { create: true });
6
+ db.run(`CREATE TABLE IF NOT EXISTS todos (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ title TEXT NOT NULL,
9
+ done INTEGER DEFAULT 0
10
+ )`);
11
+ if (db.query('SELECT COUNT(*) AS n FROM todos').get().n === 0) {
12
+ db.run("INSERT INTO todos (title) VALUES ('Try spark-ssr'), ('Edit pages/index.html'), ('Ship it')");
13
+ console.log('⚡ seeded dev.db with a few todos');
14
+ }
15
+ db.close();
@@ -0,0 +1,3 @@
1
+ {
2
+ "db": "sqlite://./dev.db"
3
+ }