@uniweb/build 0.8.19 → 0.8.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,8 @@
17
17
  "./dev": "./src/dev/index.js",
18
18
  "./prerender": "./src/prerender.js",
19
19
  "./i18n": "./src/i18n/index.js",
20
- "./search": "./src/search/index.js"
20
+ "./search": "./src/search/index.js",
21
+ "./import-map-plugin": "./src/import-map-plugin.js"
21
22
  },
22
23
  "files": [
23
24
  "src"
@@ -52,9 +53,9 @@
52
53
  "@uniweb/theming": "0.1.2"
53
54
  },
54
55
  "optionalDependencies": {
55
- "@uniweb/schemas": "0.2.1",
56
56
  "@uniweb/content-reader": "1.1.4",
57
- "@uniweb/runtime": "0.6.14"
57
+ "@uniweb/runtime": "0.6.15",
58
+ "@uniweb/schemas": "0.2.1"
58
59
  },
59
60
  "peerDependencies": {
60
61
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -63,7 +64,7 @@
63
64
  "@tailwindcss/vite": "^4.0.0",
64
65
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
65
66
  "vite-plugin-svgr": "^4.0.0",
66
- "@uniweb/core": "0.5.13"
67
+ "@uniweb/core": "0.5.14"
67
68
  },
68
69
  "peerDependenciesMeta": {
69
70
  "vite": {
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Import Map Plugin
3
+ *
4
+ * Shared Vite plugin that emits import-map bridge modules so that
5
+ * foundations loaded via dynamic import() can resolve bare specifiers
6
+ * (react, @uniweb/core, etc.) to the same instances used by the host app.
7
+ *
8
+ * Production: emits deterministic chunks at _importmap/*.js with explicit
9
+ * named re-exports, and injects a <script type="importmap"> into the HTML.
10
+ *
11
+ * Used by:
12
+ * - Site builds (runtime mode + extensions) — packages/build/src/site/config.js
13
+ * - Runtime shell build — packages/runtime/vite.config.app.js
14
+ * - Dynamic-runtime (editor preview) — packages/uniweb-editor/dynamic-runtime/
15
+ *
16
+ * @module @uniweb/build/import-map-plugin
17
+ */
18
+
19
+ /** Default externals shared between foundations and hosts */
20
+ const DEFAULT_EXTERNALS = [
21
+ 'react',
22
+ 'react-dom',
23
+ 'react/jsx-runtime',
24
+ 'react/jsx-dev-runtime',
25
+ '@uniweb/core',
26
+ ]
27
+
28
+ const IMPORT_MAP_PREFIX = '\0importmap:'
29
+
30
+ /** Valid JS identifier — filters out non-identifier keys from CJS modules */
31
+ const isValidId = (k) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
32
+
33
+ /**
34
+ * Create the import map Vite plugin.
35
+ *
36
+ * @param {Object} [options]
37
+ * @param {string[]} [options.externals] - Package specifiers to bridge (default: react, react-dom, @uniweb/core, etc.)
38
+ * @param {string} [options.name] - Plugin name (default: 'uniweb:import-map')
39
+ * @param {string} [options.basePath] - Base path prefix for import map URLs in HTML (default: '/')
40
+ * @param {string} [options.resolveFrom] - Absolute path to resolve bare specifiers from inside virtual modules.
41
+ * Needed when the host project doesn't have the externals as direct dependencies (e.g., site builds
42
+ * under pnpm strict mode resolve from the foundation directory instead).
43
+ * @param {Object} [options.devBridges] - Map of specifier → dev-mode URL for import map injection in dev.
44
+ * When provided, the import map is injected in both dev and prod (with different URLs).
45
+ * When omitted, the import map is only injected in prod (dev uses other mechanisms like transformRequest).
46
+ * @returns {import('vite').Plugin}
47
+ */
48
+ export function importMapPlugin({
49
+ externals = DEFAULT_EXTERNALS,
50
+ name = 'uniweb:import-map',
51
+ basePath = '/',
52
+ resolveFrom,
53
+ devBridges,
54
+ } = {}) {
55
+ let isBuild = false
56
+
57
+ return {
58
+ name,
59
+
60
+ configResolved(config) {
61
+ isBuild = config.command === 'build'
62
+ },
63
+
64
+ resolveId(id, importer) {
65
+ if (id.startsWith(IMPORT_MAP_PREFIX)) return id
66
+ // Bare specifiers inside our virtual modules (e.g. '@uniweb/core' re-exported
67
+ // from '\0importmap:@uniweb/core') can't be resolved by Rollup because virtual
68
+ // modules have no filesystem context. When a resolveFrom path is provided,
69
+ // resolve from there (e.g. the foundation directory under pnpm strict mode).
70
+ if (resolveFrom && importer?.startsWith(IMPORT_MAP_PREFIX) && externals.includes(id)) {
71
+ return this.resolve(id, resolveFrom, { skipSelf: true })
72
+ }
73
+ },
74
+
75
+ async load(id) {
76
+ if (!id.startsWith(IMPORT_MAP_PREFIX)) return
77
+ const pkg = id.slice(IMPORT_MAP_PREFIX.length)
78
+
79
+ // Generate explicit named re-exports (not `export *`) because CJS
80
+ // packages like React only expose a default via `export *`, losing
81
+ // individual named exports (useState, jsx, etc.) that foundations need.
82
+ try {
83
+ const mod = await import(pkg)
84
+ const names = Object.keys(mod).filter((k) => k !== '__esModule' && isValidId(k))
85
+ const hasDefault = 'default' in mod
86
+ const named = names.filter((k) => k !== 'default')
87
+ const lines = []
88
+ if (named.length) {
89
+ lines.push(`export { ${named.join(', ')} } from '${pkg}'`)
90
+ }
91
+ if (hasDefault) {
92
+ lines.push(`export { default } from '${pkg}'`)
93
+ }
94
+ return lines.join('\n') || 'export {}'
95
+ } catch {
96
+ // Fallback: generic re-export (may not preserve named exports for CJS)
97
+ return `export * from '${pkg}'`
98
+ }
99
+ },
100
+
101
+ // Emit deterministic chunks for each external (production only).
102
+ // preserveSignature: 'exports-only' tells Rollup to preserve the original
103
+ // export names (useState, jsx, etc.) instead of mangling them.
104
+ buildStart() {
105
+ if (!isBuild) return
106
+ for (const ext of externals) {
107
+ this.emitFile({
108
+ type: 'chunk',
109
+ id: `${IMPORT_MAP_PREFIX}${ext}`,
110
+ fileName: `_importmap/${ext.replace(/\//g, '-')}.js`,
111
+ preserveSignature: 'exports-only',
112
+ })
113
+ }
114
+ },
115
+
116
+ // Inject the import map into the HTML.
117
+ // In prod: always injects with basePath-prefixed _importmap/ URLs.
118
+ // In dev: only injects if devBridges are provided (otherwise, the consumer
119
+ // handles dev-mode resolution via other mechanisms like transformRequest).
120
+ transformIndexHtml: {
121
+ order: 'pre',
122
+ handler(html) {
123
+ const imports = {}
124
+
125
+ if (isBuild) {
126
+ for (const ext of externals) {
127
+ imports[ext] = `${basePath}_importmap/${ext.replace(/\//g, '-')}.js`
128
+ }
129
+ } else if (devBridges) {
130
+ Object.assign(imports, devBridges)
131
+ } else {
132
+ // No dev injection — consumer handles dev mode separately
133
+ return html
134
+ }
135
+
136
+ const importMap = JSON.stringify({ imports }, null, 2)
137
+ const script = ` <script type="importmap">\n${importMap}\n </script>\n`
138
+ // Import map must appear before any module scripts
139
+ return html.replace('<head>', '<head>\n' + script)
140
+ },
141
+ },
142
+ }
143
+ }
144
+
145
+ export { DEFAULT_EXTERNALS }
@@ -24,6 +24,7 @@ import { existsSync, readFileSync } from 'node:fs'
24
24
  import { resolve, dirname, join } from 'node:path'
25
25
  import yaml from 'js-yaml'
26
26
  import { generateEntryPoint, shouldRegenerateForFile } from '../generate-entry.js'
27
+ import { importMapPlugin } from '../import-map-plugin.js'
27
28
 
28
29
  /**
29
30
  * Normalize a base path for Vite compatibility
@@ -343,105 +344,73 @@ export async function defineSiteConfig(options = {}) {
343
344
 
344
345
  if (noopFoundationPlugin) plugins.push(noopFoundationPlugin)
345
346
 
346
- // Import map plugin for runtime mode production builds
347
+ // Import map plugin for runtime mode production builds.
347
348
  // Emits re-export modules for each externalized package (react, @uniweb/core, etc.)
348
- // so the browser can resolve bare specifiers in the dynamically-imported foundation
349
- const IMPORT_MAP_EXTERNALS = [
350
- 'react',
351
- 'react-dom',
352
- 'react/jsx-runtime',
353
- 'react/jsx-dev-runtime',
354
- '@uniweb/core'
355
- ]
356
- const IMPORT_MAP_PREFIX = '\0importmap:'
357
-
358
- const importMapPlugin = needsImportMap ? (() => {
359
- let isBuild = false
360
-
361
- return {
362
- name: 'uniweb:import-map',
363
-
364
- configResolved(config) {
365
- isBuild = config.command === 'build'
366
- },
367
-
368
- resolveId(id, importer) {
369
- if (id.startsWith(IMPORT_MAP_PREFIX)) return id
370
- // Bare specifiers inside our virtual modules (e.g. '@uniweb/core' re-exported
371
- // from '\0importmap:@uniweb/core') can't be resolved by Rollup because virtual
372
- // modules have no filesystem context. Resolve from the foundation directory where
373
- // @uniweb/core is a direct dependency (the site may not have it under pnpm strict).
374
- if (importer?.startsWith(IMPORT_MAP_PREFIX) && IMPORT_MAP_EXTERNALS.includes(id)) {
375
- const resolveFrom = foundationInfo.path
376
- ? resolve(foundationInfo.path, 'package.json')
377
- : resolve(siteRoot, 'main.js')
378
- return this.resolve(id, resolveFrom, { skipSelf: true })
379
- }
380
- },
349
+ // so the browser can resolve bare specifiers in the dynamically-imported foundation.
350
+ // In dev mode, Vite's transformRequest() handles bare specifier resolution instead.
351
+ if (needsImportMap) {
352
+ plugins.push(importMapPlugin({
353
+ basePath: base || '/',
354
+ // Under pnpm strict mode, the site may not have @uniweb/core in its own
355
+ // node_modules. Resolve from the foundation directory where it's a direct dep.
356
+ resolveFrom: foundationInfo.path
357
+ ? resolve(foundationInfo.path, 'package.json')
358
+ : resolve(siteRoot, 'main.js'),
359
+ }))
360
+ }
381
361
 
382
- async load(id) {
383
- if (!id.startsWith(IMPORT_MAP_PREFIX)) return
384
- const pkg = id.slice(IMPORT_MAP_PREFIX.length)
385
- // Dynamically discover exports at build time by importing the package.
386
- // We generate explicit named re-exports (not `export *`) because CJS
387
- // packages like React only expose a default via `export *`, losing
388
- // individual named exports (useState, jsx, etc.) that foundations need.
389
- try {
390
- const mod = await import(pkg)
391
- const names = Object.keys(mod).filter(k => k !== '__esModule')
392
- const hasDefault = 'default' in mod
393
- const named = names.filter(k => k !== 'default')
394
- const lines = []
395
- if (named.length) {
396
- lines.push(`export { ${named.join(', ')} } from '${pkg}'`)
397
- }
398
- if (hasDefault) {
399
- lines.push(`export { default } from '${pkg}'`)
362
+ // Preload hints for runtime-loaded foundations and extensions.
363
+ // In runtime mode, foundation JS is loaded via import() and CSS is injected
364
+ // dynamically in JavaScript — the browser doesn't discover them until JS executes.
365
+ // These <link> tags let the browser start fetching during HTML parsing.
366
+ // Shell mode is excluded: URLs come from __DATA__ at serve time (unicloud handles it).
367
+ if (isRuntimeMode && !isShellMode) {
368
+ plugins.push({
369
+ name: 'uniweb:foundation-preload',
370
+ transformIndexHtml: {
371
+ order: 'post',
372
+ handler() {
373
+ const tags = []
374
+
375
+ // Foundation JS modulepreload
376
+ if (foundationConfig.url) {
377
+ tags.push({
378
+ tag: 'link',
379
+ attrs: { rel: 'modulepreload', href: foundationConfig.url },
380
+ injectTo: 'head',
381
+ })
400
382
  }
401
- return lines.join('\n') || `export {}`
402
- } catch {
403
- // Fallback: generic re-export (may not preserve named exports for CJS)
404
- return `export * from '${pkg}'`
405
- }
406
- },
407
383
 
408
- // Emit deterministic chunks for each external (production only).
409
- // preserveSignature: 'exports-only' tells Rollup to preserve the original
410
- // export names (useState, jsx, etc.) instead of mangling them.
411
- // In dev mode, Vite's transformRequest() resolves bare specifiers instead.
412
- buildStart() {
413
- if (!isBuild) return
414
- for (const ext of IMPORT_MAP_EXTERNALS) {
415
- this.emitFile({
416
- type: 'chunk',
417
- id: `${IMPORT_MAP_PREFIX}${ext}`,
418
- fileName: `_importmap/${ext.replace(/\//g, '-')}.js`,
419
- preserveSignature: 'exports-only'
420
- })
421
- }
422
- },
384
+ // Foundation CSS injected as a real <link> so the browser fetches it
385
+ // during HTML parsing instead of waiting for loadFoundationCSS() in JS.
386
+ // The runtime's dynamic <link> deduplicates (same URL, already cached).
387
+ if (foundationConfig.cssUrl) {
388
+ tags.push({
389
+ tag: 'link',
390
+ attrs: { rel: 'stylesheet', href: foundationConfig.cssUrl },
391
+ injectTo: 'head',
392
+ })
393
+ }
423
394
 
424
- // Inject the import map into the HTML (production only).
425
- // In dev mode, Vite's transformRequest() handles bare specifier resolution.
426
- transformIndexHtml: {
427
- order: 'pre',
428
- handler(html) {
429
- if (!isBuild) return html
430
- const basePath = base || '/'
431
- const imports = {}
432
- for (const ext of IMPORT_MAP_EXTERNALS) {
433
- imports[ext] = `${basePath}_importmap/${ext.replace(/\//g, '-')}.js`
395
+ // Extension JS modulepreload (CSS left to runtime we can't reliably
396
+ // derive CSS URLs for all extension formats)
397
+ const extensions = siteConfig.extensions || []
398
+ for (const ext of extensions) {
399
+ const url = typeof ext === 'string' ? ext : ext?.url
400
+ if (url) {
401
+ tags.push({
402
+ tag: 'link',
403
+ attrs: { rel: 'modulepreload', href: url },
404
+ injectTo: 'head',
405
+ })
406
+ }
434
407
  }
435
- const importMap = JSON.stringify({ imports }, null, 2)
436
- const script = ` <script type="importmap">\n${importMap}\n </script>\n`
437
- // Import map must appear before any module scripts
438
- return html.replace('<head>', '<head>\n' + script)
439
- }
440
- }
441
- }
442
- })() : null
443
408
 
444
- if (importMapPlugin) plugins.push(importMapPlugin)
409
+ return tags
410
+ },
411
+ },
412
+ })
413
+ }
445
414
 
446
415
  // Build foundation config for runtime
447
416
  const foundationConfig = {