@techninja/clearstack 0.3.43 → 0.3.44

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/bin/cli.js CHANGED
@@ -68,7 +68,13 @@ async function run(action) {
68
68
  report(process.cwd(), { json: !!flags.json });
69
69
  } else if (action === 'build') {
70
70
  const sub = args.find((a) => a !== 'build' && !a.startsWith('-'));
71
- if (sub === 'og-images' || sub === 'images') {
71
+ if (sub === 'sitemap') {
72
+ const { buildSitemap } = await import('../lib/build-sitemap.js');
73
+ buildSitemap({ projectDir: process.cwd(), outDir: flags.out || 'dist', baseUrl: flags.url || '' });
74
+ } else if (sub === 'modulepreload' || sub === 'preload') {
75
+ const { buildModulePreload } = await import('../lib/build-modulepreload.js');
76
+ buildModulePreload({ projectDir: process.cwd(), outDir: flags.out || 'dist' });
77
+ } else if (sub === 'og-images' || sub === 'images') {
72
78
  const mod = await import('../lib/build-og-images.js');
73
79
  const common = { projectDir: process.cwd(), outDir: flags.out || 'dist', logo: flags.logo || '', siteName: flags.site || '' };
74
80
  if (flags.slug) await mod.buildOneOGImage({ ...common, slug: flags.slug });
@@ -80,7 +86,11 @@ async function run(action) {
80
86
  outDir: flags.out || 'dist',
81
87
  baseUrl: flags.url || '',
82
88
  });
89
+ const { buildSitemap } = await import('../lib/build-sitemap.js');
90
+ buildSitemap({ projectDir: process.cwd(), outDir: flags.out || 'dist', baseUrl: flags.url || '' });
83
91
  if (sub === 'all') {
92
+ const { buildModulePreload } = await import('../lib/build-modulepreload.js');
93
+ buildModulePreload({ projectDir: process.cwd(), outDir: flags.out || 'dist' });
84
94
  const { buildOGImages } = await import('../lib/build-og-images.js');
85
95
  await buildOGImages({
86
96
  projectDir: process.cwd(),
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Module preload builder — statically crawls ES module imports and injects
3
+ * <link rel="modulepreload"> tags into index.html so the browser fetches all
4
+ * modules in parallel instead of chaining waterfall requests.
5
+ * @module lib/build-modulepreload
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { resolve, dirname, relative, extname } from 'node:path';
10
+
11
+ /**
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)
16
+ */
17
+ export function crawlModules(srcDir, entryFile, ignoreDirs = ['vendor', 'deps']) {
18
+ const visited = new Set();
19
+ const order = [];
20
+
21
+ /**
22
+ *
23
+ */
24
+ function crawl(relPath) {
25
+ if (visited.has(relPath)) return;
26
+ visited.add(relPath);
27
+ if (ignoreDirs.some((d) => relPath.includes(`/${d}/`) || relPath.startsWith(`${d}/`))) return;
28
+
29
+ const absPath = resolve(srcDir, relPath);
30
+ if (!existsSync(absPath) || extname(absPath) !== '.js') return;
31
+
32
+ const src = readFileSync(absPath, 'utf-8');
33
+ const importRe = /(?:^|\n)\s*import\s[^'"]*['"]([^'"]+)['"]/g;
34
+ let m;
35
+ while ((m = importRe.exec(src)) !== null) {
36
+ const spec = m[1];
37
+ if (spec.startsWith('#')) {
38
+ // Import map alias — resolve via known prefixes
39
+ const mapped = resolveAlias(spec, relPath);
40
+ if (mapped) crawl(mapped);
41
+ } else if (spec.startsWith('./') || spec.startsWith('../')) {
42
+ const abs = resolve(dirname(resolve(srcDir, relPath)), spec);
43
+ const rel = relative(srcDir, abs.endsWith('.js') ? abs : abs + '.js');
44
+ crawl(rel);
45
+ }
46
+ // Skip bare specifiers (vendor/hybrids handled separately)
47
+ }
48
+ order.push(relPath);
49
+ }
50
+
51
+ crawl(entryFile);
52
+ return order;
53
+ }
54
+
55
+ /** Resolve #alias/ import map specifiers to relative paths. */
56
+ function resolveAlias(spec, fromFile) {
57
+ const aliases = {
58
+ '#store/': 'store/',
59
+ '#utils/': 'utils/',
60
+ '#atoms/': 'components/atoms/',
61
+ '#molecules/': 'components/molecules/',
62
+ '#organisms/': 'components/organisms/',
63
+ '#templates/': 'components/templates/',
64
+ '#pages/': 'pages/',
65
+ };
66
+ for (const [prefix, dir] of Object.entries(aliases)) {
67
+ if (spec.startsWith(prefix)) return dir + spec.slice(prefix.length);
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Inject modulepreload tags into index.html.
74
+ * @param {{ projectDir: string, srcDir?: string, outDir?: string, entry?: string, ignore?: string[] }} opts
75
+ * @returns {{ modules: number }}
76
+ */
77
+ export function buildModulePreload(opts) {
78
+ const {
79
+ projectDir,
80
+ srcDir = resolve(projectDir, 'src'),
81
+ outDir = resolve(projectDir, 'dist'),
82
+ entry = 'router/index.js',
83
+ ignore = ['vendor', 'deps'],
84
+ } = opts;
85
+
86
+ const indexPath = resolve(outDir, 'index.html');
87
+ if (!existsSync(indexPath)) {
88
+ console.log('⚠ dist/index.html not found — run build first');
89
+ return { modules: 0 };
90
+ }
91
+
92
+ const modules = crawlModules(srcDir, entry, ignore);
93
+ const tags = modules.map((m) => ` <link rel="modulepreload" href="/${m}">`).join('\n');
94
+
95
+ let html = readFileSync(indexPath, 'utf-8');
96
+ if (html.includes('rel="modulepreload"')) {
97
+ // Remove existing preload tags before re-injecting
98
+ html = html.replace(/\s*<link rel="modulepreload"[^>]*>/g, '');
99
+ }
100
+ html = html.replace('</head>', `${tags}\n</head>`);
101
+ writeFileSync(indexPath, html);
102
+
103
+ console.log(`✅ Modulepreload: ${modules.length} modules → dist/index.html`);
104
+ return { modules: modules.length };
105
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Sitemap generator — builds sitemap.xml from clearstack.routes.json.
3
+ * Static routes get one URL each; dynamic routes iterate their data source.
4
+ * @module lib/build-sitemap
5
+ */
6
+
7
+ import { mkdirSync, writeFileSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+ import { loadRoutes, resolveDataSource } from './build-og.js';
10
+ import { interpolate } from './og-template.js';
11
+
12
+ /** @param {string} s */
13
+ const esc = (s) => s.replace(/&/g, '&amp;').replace(/'/g, '&apos;');
14
+
15
+ /** Default priority by route type. */
16
+ function defaultPriority(pattern) {
17
+ if (pattern === '/') return '1.0';
18
+ if (pattern.includes(':')) return '0.5';
19
+ return '0.7';
20
+ }
21
+
22
+ /** Default changefreq by route type. */
23
+ function defaultChangefreq(pattern) {
24
+ if (pattern === '/') return 'daily';
25
+ if (pattern.includes(':')) return 'weekly';
26
+ return 'monthly';
27
+ }
28
+
29
+ /**
30
+ * @param {string} date - ISO date string or empty
31
+ * @returns {string}
32
+ */
33
+ function fmtDate(date) {
34
+ if (!date) return '';
35
+ try { return new Date(date).toISOString().slice(0, 10); } catch { return ''; }
36
+ }
37
+
38
+ /**
39
+ * Build sitemap.xml from route config.
40
+ * @param {{ projectDir: string, outDir?: string, baseUrl: string, context?: Record<string, unknown> }} opts
41
+ * @returns {{ urls: number }}
42
+ */
43
+ export function buildSitemap(opts) {
44
+ const { projectDir, outDir = 'dist', baseUrl, context = {} } = opts;
45
+ const routes = loadRoutes(projectDir);
46
+ if (!routes) {
47
+ console.log('⚠ No routes config found. Create clearstack.routes.json');
48
+ return { urls: 0 };
49
+ }
50
+
51
+ const urls = [];
52
+
53
+ for (const [pattern, config] of Object.entries(routes)) {
54
+ const sm = config.sitemap || {};
55
+ const priority = sm.priority ?? defaultPriority(pattern);
56
+ const changefreq = sm.changefreq ?? defaultChangefreq(pattern);
57
+ const isDynamic = pattern.includes(':');
58
+
59
+ if (!isDynamic) {
60
+ const lastmod = fmtDate(sm.lastmod || '');
61
+ urls.push(entry(`${baseUrl}${pattern}`, changefreq, String(priority), lastmod));
62
+ } else if (config.data) {
63
+ const items = resolveDataSource(config.data, projectDir);
64
+ const paramName = pattern.match(/:(\w+)/)?.[1] || 'id';
65
+ for (const item of items) {
66
+ const slug = item.slug || item.id || item.sku || item[paramName];
67
+ if (!slug) continue;
68
+ const path = pattern.replace(`:${paramName}`, String(slug));
69
+ const data = { [paramName]: item, item, ...context };
70
+ const resolvedPath = interpolate(path, data);
71
+ const lastmod = fmtDate(item.date || item.modified || item.created || '');
72
+ urls.push(entry(`${baseUrl}${resolvedPath}`, changefreq, String(priority), lastmod));
73
+ }
74
+ }
75
+ }
76
+
77
+ const xml = [
78
+ '<?xml version="1.0" encoding="UTF-8"?>',
79
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
80
+ ...urls,
81
+ '</urlset>',
82
+ ].join('\n');
83
+
84
+ const out = resolve(projectDir, outDir);
85
+ mkdirSync(out, { recursive: true });
86
+ writeFileSync(resolve(out, 'sitemap.xml'), xml);
87
+ console.log(`✅ Sitemap: ${urls.length} URLs → ${outDir}/sitemap.xml`);
88
+ return { urls: urls.length };
89
+ }
90
+
91
+ /** Build a single <url> entry. */
92
+ function entry(loc, changefreq, priority, lastmod) {
93
+ return [
94
+ ' <url>',
95
+ ` <loc>${esc(loc)}</loc>`,
96
+ lastmod ? ` <lastmod>${lastmod}</lastmod>` : '',
97
+ ` <changefreq>${changefreq}</changefreq>`,
98
+ ` <priority>${priority}</priority>`,
99
+ ' </url>',
100
+ ].filter(Boolean).join('\n');
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.43",
3
+ "version": "0.3.44",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -4,6 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>{{name}}</title>
7
+ <!-- FOUC prevention — matches --color-bg in tokens.css -->
8
+ <style>html,body{background:#0f172a;color:#e2e8f0;margin:0}</style>
7
9
  <link rel="stylesheet" href="/styles/reset.css" />
8
10
  <link rel="stylesheet" href="/styles/tokens.css" />
9
11
  <link rel="stylesheet" href="/styles/shared.css" />
@@ -27,7 +29,22 @@
27
29
  </script>
28
30
  </head>
29
31
  <body>
32
+ <noscript>
33
+ <div style="font-family:system-ui;padding:2rem;max-width:40rem;margin:4rem auto;text-align:center">
34
+ <h1>JavaScript required</h1>
35
+ <p>{{name}} requires JavaScript to run. Please enable it in your browser settings.</p>
36
+ </div>
37
+ </noscript>
30
38
  <app-router></app-router>
31
39
  <script type="module" src="/router/index.js"></script>
40
+ <script>
41
+ // Global error boundary — catches unhandled promise rejections and JS errors
42
+ window.addEventListener('unhandledrejection', (e) => {
43
+ console.error('[app] Unhandled rejection:', e.reason);
44
+ });
45
+ window.addEventListener('error', (e) => {
46
+ console.error('[app] Uncaught error:', e.message, e.filename, e.lineno);
47
+ });
48
+ </script>
32
49
  </body>
33
50
  </html>