spark-html-manifest 0.1.1 → 0.1.2

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,20 +1,18 @@
1
1
  # ⚡ spark-html-manifest
2
2
 
3
3
  PWA setup for [spark-html](https://www.npmjs.com/package/spark-html) sites
4
- from a **single config** — a Vite plugin that generates
4
+ from a **single config** — a `spark-html-bun` build step that generates
5
5
  `manifest.webmanifest`, resizes your icons from one source image, injects
6
6
  the `<head>` tags, and (optionally) emits a minimal offline app-shell
7
7
  service worker. No manual icon exports, no copy-paste boilerplate.
8
8
 
9
9
  ```js
10
- // vite.config.js
11
- import spark from 'spark-html/vite';
12
- import prerender from 'spark-prerender/vite';
13
- import manifest from 'spark-html-manifest/vite';
10
+ // spark.config.js
11
+ import prerender from 'spark-prerender/bun';
12
+ import manifest from 'spark-html-manifest/bun';
14
13
 
15
14
  export default {
16
- plugins: [
17
- spark(),
15
+ pipeline: [
18
16
  prerender(),
19
17
  manifest({
20
18
  name: 'My Spark App',
@@ -27,7 +25,7 @@ export default {
27
25
  };
28
26
  ```
29
27
 
30
- That's the whole setup. `npm run build` now produces:
28
+ That's the whole setup. `bun run build` now produces:
31
29
 
32
30
  - `manifest.webmanifest` — name, colors, display mode, icons
33
31
  - `icons/spark-192.png`, `icons/spark-512.png` — resized from your source
@@ -69,7 +67,7 @@ manifest({
69
67
  `offline: true` emits a deliberately small service worker:
70
68
 
71
69
  - the **app shell** (`shell` URLs) is precached at install;
72
- - Vite's hash-named `/assets/…` files are **cache-first** (they're immutable);
70
+ - the build's hash-named `/assets/…` files are **cache-first** (they're immutable);
73
71
  - everything else same-origin is **network-first** with cache fallback — the
74
72
  app opens offline but is never a deploy behind while online;
75
73
  - offline navigation to any route falls back to the shell.
@@ -116,7 +114,7 @@ virtual DOM, no build step required. Add only what you use.
116
114
  | [`spark-html-manifest`](https://www.npmjs.com/package/spark-html-manifest) | PWA manifest + icons + head tags (and optional service worker) from one config. |
117
115
  | [`spark-html-offline`](https://www.npmjs.com/package/spark-html-offline) | Offline URL imports — a service worker that caches CDN components. |
118
116
  | [`spark-html-sri`](https://www.npmjs.com/package/spark-html-sri) | Subresource Integrity — hash + verify assets and remote components. |
119
- | [`create-spark-html-app`](https://www.npmjs.com/package/create-spark-html-app) | Scaffold a Vite + spark-html app in one command. |
117
+ | [`create-spark-html-app`](https://www.npmjs.com/package/create-spark-html-app) | Scaffold a spark-html app in one command. |
120
118
  | [`prettier-plugin-spark`](https://www.npmjs.com/package/prettier-plugin-spark) | Prettier for components — formats `<script>`/`<style>`, markup stays byte-for-byte. |
121
119
  | [`spark-html-language-server`](https://www.npmjs.com/package/spark-html-language-server) | LSP — diagnostics, go-to-definition, prop autocomplete, hover docs. |
122
120
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spark-html-manifest",
3
- "version": "0.1.1",
4
- "description": "PWA manifest + icons + offline app shell for spark-html sites from a single config — a Vite plugin that generates manifest.webmanifest, resizes icons, injects the head tags, and optionally emits a minimal service worker.",
3
+ "version": "0.1.2",
4
+ "description": "PWA manifest + icons + offline app shell for spark-html sites from a single config — a spark-html-bun build step that generates manifest.webmanifest, resizes icons, injects the head tags, and optionally emits a minimal service worker.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./src/index.d.ts",
@@ -11,9 +11,8 @@
11
11
  "types": "./src/index.d.ts",
12
12
  "default": "./src/index.js"
13
13
  },
14
- "./vite": {
15
- "types": "./src/vite.d.ts",
16
- "default": "./src/vite.js"
14
+ "./bun": {
15
+ "default": "./src/bun.js"
17
16
  }
18
17
  },
19
18
  "scripts": {
@@ -37,7 +36,7 @@
37
36
  "webmanifest",
38
37
  "icons",
39
38
  "service-worker",
40
- "vite-plugin"
39
+ "bun"
41
40
  ],
42
41
  "license": "MIT"
43
42
  }
package/src/bun.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * spark-html-manifest/bun — one config → a full PWA, as a spark-html-bun
3
+ * pipeline step. Same output, split the two ways the Bun
4
+ * runner consumes:
5
+ *
6
+ * • run({ outDir }) — emit manifest.webmanifest, resize the source icon into
7
+ * every configured size (sharp, imported lazily), optionally emit the
8
+ * app-shell worker, then stamp <link rel="manifest"> + <meta theme-color>
9
+ * (+ worker registration) into every built page. Put it after prerender()
10
+ * so per-route pages are covered.
11
+ * • devRoutes() — serve the manifest (and worker) straight from config so
12
+ * devtools' "installable" checks work in dev too.
13
+ * • transformHtml() — inject the same head tags into served pages in dev.
14
+ *
15
+ * import manifest from 'spark-html-manifest/bun';
16
+ * export default { pipeline: [prerender(), manifest({
17
+ * name: 'My App', themeColor: '#ffd24a', icon: 'public/icon.png', offline: true,
18
+ * })] };
19
+ */
20
+ import { join, resolve, dirname } from 'node:path';
21
+ import { readFile, writeFile, readdir, stat, mkdir } from 'node:fs/promises';
22
+ import { existsSync } from 'node:fs';
23
+ import { manifestJson, manifestHtml, swSource, iconPath, ICON_SIZES } from './index.js';
24
+
25
+ async function htmlFiles(dir) {
26
+ const out = [];
27
+ for (const name of await readdir(dir)) {
28
+ const full = join(dir, name);
29
+ const s = await stat(full);
30
+ if (s.isDirectory()) out.push(...await htmlFiles(full));
31
+ else if (name.endsWith('.html')) out.push(full);
32
+ }
33
+ return out;
34
+ }
35
+
36
+ export default function sparkManifest(config = {}) {
37
+ const filename = config.filename || 'manifest.webmanifest';
38
+ const offline = config.offline || false;
39
+ const swFile = (offline && offline.file) || 'spark-manifest-sw.js';
40
+ const headBlock = () =>
41
+ manifestHtml(config, { href: filename, sw: offline ? swFile : undefined });
42
+
43
+ return {
44
+ name: 'spark-html-manifest',
45
+
46
+ async run({ outDir }) {
47
+ const root = resolve(outDir);
48
+ if (!existsSync(root)) return;
49
+
50
+ // Emit manifest + worker.
51
+ await writeFile(join(root, filename), JSON.stringify(manifestJson(config), null, 2), 'utf8');
52
+ if (offline) {
53
+ await writeFile(join(root, swFile), swSource(typeof offline === 'object' ? offline : {}), 'utf8');
54
+ }
55
+
56
+ // Icons (unless the author supplied explicit ones).
57
+ if (config.icon && !config.icons) {
58
+ if (!existsSync(config.icon)) {
59
+ console.warn(`[spark-html-manifest] icon source not found: ${config.icon} — icons skipped`);
60
+ } else {
61
+ // sharp is imported lazily so the plugin never breaks a build where
62
+ // native deps can't load — you just generate icons elsewhere.
63
+ let sharp;
64
+ try {
65
+ sharp = (await import('sharp')).default;
66
+ } catch (e) {
67
+ console.warn(`[spark-html-manifest] sharp unavailable — icons skipped (${e.message})`);
68
+ sharp = null;
69
+ }
70
+ if (sharp) {
71
+ const sizes = config.sizes || ICON_SIZES;
72
+ for (const size of sizes) {
73
+ const dest = join(root, iconPath(config, size));
74
+ await mkdir(dirname(dest), { recursive: true });
75
+ await sharp(config.icon).resize(size, size, { fit: 'cover' }).png().toFile(dest);
76
+ }
77
+ console.log(`[spark-html-manifest] ${sizes.length} icon(s) generated from ${config.icon}`);
78
+ }
79
+ }
80
+ }
81
+
82
+ // Stamp head tags into every page (after prerender wrote its pages).
83
+ const block = headBlock();
84
+ let pages = 0;
85
+ for (const file of await htmlFiles(root)) {
86
+ const html = await readFile(file, 'utf8');
87
+ if (!/<\/head>/i.test(html)) continue; // fragment — skip
88
+ if (html.includes('data-spark-manifest')) continue; // already injected
89
+ await writeFile(file, html.replace(/<\/head>/i, `${block}\n</head>`), 'utf8');
90
+ pages++;
91
+ }
92
+ if (pages) console.log(`[spark-html-manifest] injected PWA tags into ${pages} page(s)`);
93
+ },
94
+
95
+ // Dev: serve the manifest (and worker) straight from config.
96
+ devRoutes() {
97
+ const routes = {
98
+ [`/${filename}`]: {
99
+ type: 'application/manifest+json',
100
+ body: () => JSON.stringify(manifestJson(config), null, 2),
101
+ },
102
+ };
103
+ if (offline) {
104
+ routes[`/${swFile}`] = {
105
+ type: 'text/javascript',
106
+ body: () => swSource(typeof offline === 'object' ? offline : {}),
107
+ };
108
+ }
109
+ return routes;
110
+ },
111
+
112
+ transformHtml(html, { dev }) {
113
+ if (!dev) return html;
114
+ if (html.includes('data-spark-manifest') || !/<\/head>/i.test(html)) return html;
115
+ return html.replace(/<\/head>/i, `${headBlock()}\n</head>`);
116
+ },
117
+ };
118
+ }
package/src/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * spark-html-manifest — PWA setup from a single config.
3
3
  *
4
- * name, icons, colors, display mode in one place; the vite plugin turns it
4
+ * name, icons, colors, display mode in one place; the bun build step turns it
5
5
  * into manifest.webmanifest + resized icons + the <head> tags + (optionally)
6
6
  * a minimal app-shell service worker. No manual icon exports, no copy-paste
7
7
  * boilerplate.
8
8
  *
9
- * // vite.config.js
10
- * import manifest from 'spark-html-manifest/vite';
9
+ * // spark.config.js
10
+ * import manifest from 'spark-html-manifest/bun';
11
11
  * plugins: [spark(), prerender(), manifest({
12
12
  * name: 'My App',
13
13
  * themeColor: '#ffd24a',
@@ -136,7 +136,7 @@ self.addEventListener('fetch', function (event) {
136
136
  var url;
137
137
  try { url = new URL(req.url); } catch (e) { return; }
138
138
  if (url.origin !== self.location.origin) return;
139
- // Vite's hash-named assets are immutable — cache-first is always correct.
139
+ // The build's hash-named assets are immutable — cache-first is always correct.
140
140
  var immutable = /\\/assets\\/[^/]*[-.][A-Za-z0-9_-]{8,}\\.\\w+$/.test(url.pathname);
141
141
  event.respondWith(caches.open(CACHE).then(function (cache) {
142
142
  return cache.match(req, { ignoreSearch: req.mode === 'navigate' }).then(function (cached) {
package/src/vite.d.ts DELETED
@@ -1,23 +0,0 @@
1
- import type { ManifestConfig, AppShellSwOptions } from './index.js';
2
-
3
- export interface ManifestVitePluginOptions extends ManifestConfig {
4
- /** Source image — resized to every size in `sizes` (requires sharp). */
5
- icon?: string;
6
- /** Emitted manifest file name (default 'manifest.webmanifest'). */
7
- filename?: string;
8
- /** Emit + register the offline app-shell worker. */
9
- offline?: boolean | AppShellSwOptions;
10
- }
11
-
12
- /**
13
- * Vite plugin: manifest.webmanifest + resized icons + head tags +
14
- * (optionally) the app-shell service worker. Put it after prerender().
15
- */
16
- export default function sparkManifest(options: ManifestVitePluginOptions): {
17
- name: string;
18
- configResolved(config: unknown): void;
19
- configureServer(server: unknown): void;
20
- transformIndexHtml(html: string): string;
21
- generateBundle(): Promise<void>;
22
- closeBundle: { order: 'post'; handler(): Promise<void> };
23
- };
package/src/vite.js DELETED
@@ -1,128 +0,0 @@
1
- /**
2
- * spark-html-manifest/vite — one config → a full PWA.
3
- *
4
- * Build: emits manifest.webmanifest, resizes the source icon into every
5
- * configured size (sharp, imported lazily), optionally emits the app-shell
6
- * worker, and injects <link rel="manifest"> + <meta name="theme-color">
7
- * (+ worker registration) into every built page — runs in closeBundle
8
- * (order post, after spark-prerender wrote its per-route HTML).
9
- * Dev: serves the manifest + injects the tags so devtools' "installable"
10
- * checks work locally too.
11
- *
12
- * import manifest from 'spark-html-manifest/vite';
13
- * plugins: [spark(), prerender(), manifest({
14
- * name: 'My App', themeColor: '#ffd24a', icon: 'public/icon.png', offline: true,
15
- * })]
16
- */
17
- import { join, resolve } from 'node:path';
18
- import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
19
- import { existsSync } from 'node:fs';
20
- import { manifestJson, manifestHtml, swSource, iconPath, ICON_SIZES } from './index.js';
21
-
22
- async function htmlFiles(dir) {
23
- const out = [];
24
- for (const name of await readdir(dir)) {
25
- const full = join(dir, name);
26
- const s = await stat(full);
27
- if (s.isDirectory()) out.push(...await htmlFiles(full));
28
- else if (name.endsWith('.html')) out.push(full);
29
- }
30
- return out;
31
- }
32
-
33
- /**
34
- * @param {object} config Everything manifestJson takes, plus:
35
- * @param {string} [config.icon] Source image — resized to every size in `sizes`.
36
- * @param {string} [config.filename='manifest.webmanifest']
37
- * @param {boolean|object} [config.offline=false] Emit + register the app-shell
38
- * worker; pass { shell, version, file } to tune it.
39
- */
40
- export default function sparkManifest(config = {}) {
41
- const filename = config.filename || 'manifest.webmanifest';
42
- const offline = config.offline || false;
43
- const swFile = (offline && offline.file) || 'spark-manifest-sw.js';
44
- const headBlock = () =>
45
- manifestHtml(config, { href: filename, sw: offline ? swFile : undefined });
46
- let outDir = 'dist';
47
-
48
- return {
49
- name: 'spark-html-manifest',
50
- configResolved(viteConfig) {
51
- if (viteConfig && viteConfig.build && viteConfig.build.outDir) outDir = viteConfig.build.outDir;
52
- },
53
-
54
- // Dev: serve the manifest (and worker) straight from config.
55
- configureServer(server) {
56
- server.middlewares.use((req, res, next) => {
57
- const path = (req.url || '').split('?')[0];
58
- if (path === `/${filename}`) {
59
- res.setHeader('Content-Type', 'application/manifest+json');
60
- res.end(JSON.stringify(manifestJson(config), null, 2));
61
- } else if (offline && path === `/${swFile}`) {
62
- res.setHeader('Content-Type', 'text/javascript');
63
- res.end(swSource(typeof offline === 'object' ? offline : {}));
64
- } else {
65
- next();
66
- }
67
- });
68
- },
69
- transformIndexHtml(html) {
70
- if (html.includes('data-spark-manifest')) return html;
71
- return html.replace(/<\/head>/i, `${headBlock()}\n</head>`);
72
- },
73
-
74
- // Build: emit manifest + icons + worker.
75
- async generateBundle() {
76
- this.emitFile({
77
- type: 'asset',
78
- fileName: filename,
79
- source: JSON.stringify(manifestJson(config), null, 2),
80
- });
81
- if (offline) {
82
- const shellOpts = typeof offline === 'object' ? offline : {};
83
- this.emitFile({ type: 'asset', fileName: swFile, source: swSource(shellOpts) });
84
- }
85
- if (!config.icon || config.icons) return; // explicit icons — nothing to generate
86
- if (!existsSync(config.icon)) {
87
- console.warn(`[spark-html-manifest] icon source not found: ${config.icon} — icons skipped`);
88
- return;
89
- }
90
- // sharp is imported lazily so the plugin never breaks a build where
91
- // native deps can't load — you just generate icons elsewhere.
92
- let sharp;
93
- try {
94
- sharp = (await import('sharp')).default;
95
- } catch (e) {
96
- console.warn(`[spark-html-manifest] sharp unavailable — icons skipped (${e.message})`);
97
- return;
98
- }
99
- for (const size of config.sizes || ICON_SIZES) {
100
- const png = await sharp(config.icon)
101
- .resize(size, size, { fit: 'cover' })
102
- .png()
103
- .toBuffer();
104
- this.emitFile({ type: 'asset', fileName: iconPath(config, size), source: png });
105
- }
106
- console.log(`[spark-html-manifest] ${(config.sizes || ICON_SIZES).length} icon(s) generated from ${config.icon}`);
107
- },
108
-
109
- // After prerender wrote its pages: stamp the head tags into every page.
110
- closeBundle: {
111
- order: 'post',
112
- async handler() {
113
- const root = resolve(outDir);
114
- if (!existsSync(root)) return;
115
- const block = headBlock();
116
- let pages = 0;
117
- for (const file of await htmlFiles(root)) {
118
- const html = await readFile(file, 'utf8');
119
- if (!/<\/head>/i.test(html)) continue; // fragment — skip
120
- if (html.includes('data-spark-manifest')) continue; // already injected
121
- await writeFile(file, html.replace(/<\/head>/i, `${block}\n</head>`), 'utf8');
122
- pages++;
123
- }
124
- if (pages) console.log(`[spark-html-manifest] injected PWA tags into ${pages} page(s)`);
125
- },
126
- },
127
- };
128
- }