@techninja/clearstack 0.3.47 → 0.3.49

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.
@@ -2,21 +2,33 @@
2
2
  * Module preload builder — statically crawls ES module imports and injects
3
3
  * <link rel="modulepreload"> tags into index.html so the browser fetches all
4
4
  * modules in parallel instead of chaining waterfall requests.
5
+ *
6
+ * Importmap safety: modules that directly import bare specifiers are excluded
7
+ * from preload (the browser can race importmap registration when parsing
8
+ * preloaded modules). Bare specifier targets (vendor files) are preloaded
9
+ * first so they are in the module cache before any dependent module runs.
5
10
  * @module lib/build-modulepreload
6
11
  */
7
12
 
8
13
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
14
  import { resolve, dirname, relative, extname } from 'node:path';
10
15
 
16
+ /** Extract the importmap object from an HTML string, or return {}. */
17
+ function parseImportMap(html) {
18
+ const m = html.match(/<script type="importmap">([\s\S]*?)<\/script>/);
19
+ try { return m ? JSON.parse(m[1]).imports ?? {} : {}; } catch { return {}; }
20
+ }
21
+
11
22
  /**
12
- * @param {string} srcDir - The src/ directory to crawl
13
- * @param {string} entryFile - Relative path to entry (e.g. 'router/index.js')
14
- * @param {string[]} ignoreDirs - Dirs to skip (e.g. ['vendor', 'deps'])
15
- * @returns {string[]} Ordered list of module paths (entry last)
23
+ * @param {string} srcDir
24
+ * @param {string} entryFile
25
+ * @param {string[]} ignoreDirs
26
+ * @returns {{ order: string[], hasBareImport: Set<string> }}
16
27
  */
17
28
  export function crawlModules(srcDir, entryFile, ignoreDirs = ['vendor', 'deps']) {
18
29
  const visited = new Set();
19
30
  const order = [];
31
+ const hasBareImport = new Set();
20
32
 
21
33
  /**
22
34
  *
@@ -35,25 +47,28 @@ export function crawlModules(srcDir, entryFile, ignoreDirs = ['vendor', 'deps'])
35
47
  while ((m = importRe.exec(src)) !== null) {
36
48
  const spec = m[1];
37
49
  if (spec.startsWith('#')) {
38
- // Import map alias — resolve via known prefixes
39
- const mapped = resolveAlias(spec, relPath);
50
+ const mapped = resolveAlias(spec);
40
51
  if (mapped) crawl(mapped);
41
52
  } else if (spec.startsWith('./') || spec.startsWith('../')) {
42
53
  const abs = resolve(dirname(resolve(srcDir, relPath)), spec);
43
54
  const rel = relative(srcDir, abs.endsWith('.js') ? abs : abs + '.js');
44
55
  crawl(rel);
56
+ } else {
57
+ // Bare specifier — mark this module as unsafe to preload
58
+ hasBareImport.add(relPath);
45
59
  }
46
- // Skip bare specifiers (vendor/hybrids handled separately)
47
60
  }
48
61
  order.push(relPath);
49
62
  }
50
63
 
51
64
  crawl(entryFile);
52
- return order;
65
+ return { order, hasBareImport };
53
66
  }
54
67
 
55
- /** Resolve #alias/ import map specifiers to relative paths. */
56
- function resolveAlias(spec, fromFile) {
68
+ /**
69
+ *
70
+ */
71
+ function resolveAlias(spec) {
57
72
  const aliases = {
58
73
  '#store/': 'store/',
59
74
  '#utils/': 'utils/',
@@ -71,7 +86,9 @@ function resolveAlias(spec, fromFile) {
71
86
 
72
87
  /**
73
88
  * Inject modulepreload tags into index.html.
74
- * @param {{ projectDir: string, srcDir?: string, outDir?: string, entry?: string, ignore?: string[] }} opts
89
+ * Vendor files (importmap targets) are preloaded first, then app modules
90
+ * that contain no bare specifier imports.
91
+ * @param {{ projectDir: string, srcDir?: string, outDir?: string, entry?: string, ignore?: string[], hashSuffix?: string }} opts
75
92
  * @returns {{ modules: number }}
76
93
  */
77
94
  export function buildModulePreload(opts) {
@@ -81,6 +98,7 @@ export function buildModulePreload(opts) {
81
98
  outDir = resolve(projectDir, 'dist'),
82
99
  entry = 'router/index.js',
83
100
  ignore = ['vendor', 'deps'],
101
+ hashSuffix = '',
84
102
  } = opts;
85
103
 
86
104
  const indexPath = resolve(outDir, 'index.html');
@@ -89,17 +107,32 @@ export function buildModulePreload(opts) {
89
107
  return { modules: 0 };
90
108
  }
91
109
 
92
- const modules = crawlModules(srcDir, entry, ignore);
93
- const tags = modules.map((m) => ` <link rel="modulepreload" href="/${m}">`).join('\n');
110
+ const html0 = readFileSync(indexPath, 'utf-8');
111
+ const importMap = parseImportMap(html0);
112
+
113
+ const { order, hasBareImport } = crawlModules(srcDir, entry, ignore);
114
+
115
+ // Vendor files from importmap — preload these first (no bare imports inside them)
116
+ const vendorPaths = [...new Set(Object.values(importMap))]
117
+ .filter((v) => v.startsWith('/') && v.includes('.js'));
118
+
119
+ // App modules safe to preload — exclude any that directly import a bare specifier
120
+ const appPaths = order.filter((m) => !hasBareImport.has(m));
121
+
122
+ const v = hashSuffix ? `?v=${hashSuffix}` : '';
123
+ const tags = [
124
+ ...vendorPaths.map((p) => ` <link rel="modulepreload" href="${p.includes('?') ? p : p + v}">`),
125
+ ...appPaths.map((m) => ` <link rel="modulepreload" href="/${m}${v}">`),
126
+ ].join('\n');
94
127
 
95
- let html = readFileSync(indexPath, 'utf-8');
128
+ let html = html0;
96
129
  if (html.includes('rel="modulepreload"')) {
97
- // Remove existing preload tags before re-injecting
98
130
  html = html.replace(/\s*<link rel="modulepreload"[^>]*>/g, '');
99
131
  }
100
132
  html = html.replace('</head>', `${tags}\n</head>`);
101
133
  writeFileSync(indexPath, html);
102
134
 
103
- console.log(`✅ Modulepreload: ${modules.length} modules → dist/index.html`);
104
- return { modules: modules.length };
135
+ const skipped = hasBareImport.size;
136
+ console.log(`✅ Modulepreload: ${vendorPaths.length} vendor + ${appPaths.length} app modules → dist/index.html (${skipped} skipped — bare imports)`);
137
+ return { modules: vendorPaths.length + appPaths.length };
105
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.47",
3
+ "version": "0.3.49",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {