@uniweb/build 0.16.23 → 0.18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.16.23",
3
+ "version": "0.18.0",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -61,13 +61,13 @@
61
61
  "yaml": "^2.5.0",
62
62
  "@uniweb/schemas": "0.2.10",
63
63
  "@uniweb/theming": "0.1.15",
64
- "@uniweb/projections": "0.2.5",
65
- "@uniweb/content-writer": "0.3.3"
64
+ "@uniweb/content-writer": "0.3.3",
65
+ "@uniweb/projections": "0.2.5"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@uniweb/schemas": "0.2.10",
69
68
  "@uniweb/content-reader": "1.2.2",
70
- "@uniweb/runtime": "0.9.10",
69
+ "@uniweb/schemas": "0.2.10",
70
+ "@uniweb/runtime": "0.11.0",
71
71
  "@uniweb/semantic-parser": "1.2.1"
72
72
  },
73
73
  "peerDependencies": {
@@ -31,11 +31,52 @@ const IMPORT_MAP_PREFIX = '\0importmap:'
31
31
  /** Valid JS identifier — filters out non-identifier keys from CJS modules */
32
32
  const isValidId = (k) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
33
33
 
34
+ /**
35
+ * An external may be a bare specifier string, or an object declaring its
36
+ * surface explicitly.
37
+ *
38
+ * ⛔ WHY THE OBJECT FORM EXISTS — read before "simplifying" it away.
39
+ *
40
+ * The string form enumerates a module's exports by `await import(pkg)` in the
41
+ * Vite process, i.e. in NODE. That works for plain `.js`, and **throws for
42
+ * anything reached through a `.jsx` file** — Node cannot parse JSX. The catch
43
+ * below then falls back to `export * from pkg`, and `export *` **never
44
+ * re-exports `default`**. So a bridged module whose public surface IS its
45
+ * default export silently emits a bridge with that export missing.
46
+ *
47
+ * Measured 2026-08-08 on `@uniweb/runtime/provider` (whose only export is
48
+ * `export default function RuntimeProvider`): the fallback produced ZERO
49
+ * exports, so Rollup reused the emitted filename as a shared chunk and the
50
+ * "bridge" shipped containing `ErrorBoundary` instead. The file existed, the
51
+ * manifest listed it, the channel published it and its digests verified — and
52
+ * `import RuntimeProvider from '<bridge>'` throws "does not provide an export
53
+ * named 'default'". Nothing upstream of the browser could see it.
54
+ *
55
+ * `hasDefault` is what `export *` cannot express, so it is the one thing worth
56
+ * declaring. Named exports stay self-maintaining through `export *`.
57
+ *
58
+ * @typedef {string | { spec: string, hasDefault?: boolean, named?: string[] }} External
59
+ */
60
+ const specOf = (e) => (typeof e === 'string' ? e : e.spec)
61
+
62
+ /**
63
+ * The bridge filename for a specifier: `@uniweb/runtime/provider` →
64
+ * `@uniweb-runtime-provider.js`.
65
+ *
66
+ * Exported because THREE producers must agree on it — the emitted chunk, the
67
+ * `<script type="importmap">`, and the runtime shell's `manifest.json`. A
68
+ * consumer resolves a bridge by the manifest's URL, so a manifest that names a
69
+ * file the build did not emit is a 404 at import time, not a build error.
70
+ */
71
+ export const bridgeFileName = (spec) => `${spec.replace(/\//g, '-')}.js`
72
+
34
73
  /**
35
74
  * Create the import map Vite plugin.
36
75
  *
37
76
  * @param {Object} [options]
38
- * @param {string[]} [options.externals] - Package specifiers to bridge (default: react, react-dom, @uniweb/core, etc.)
77
+ * @param {External[]} [options.externals] - Specifiers to bridge (default: react, react-dom, @uniweb/core, etc.).
78
+ * Each entry is a bare specifier string, or `{ spec, hasDefault?, named? }` when the module's
79
+ * surface cannot be enumerated by importing it in Node — see the `External` typedef.
39
80
  * @param {string} [options.name] - Plugin name (default: 'uniweb:import-map')
40
81
  * @param {string} [options.basePath] - Base path prefix for import map URLs in HTML (default: '/')
41
82
  * @param {string} [options.resolveFrom] - Absolute path to resolve bare specifiers from inside virtual modules.
@@ -68,7 +109,7 @@ export function importMapPlugin({
68
109
  // from '\0importmap:@uniweb/core') can't be resolved by Rollup because virtual
69
110
  // modules have no filesystem context. When a resolveFrom path is provided,
70
111
  // resolve from there (e.g. the foundation directory under pnpm strict mode).
71
- if (resolveFrom && importer?.startsWith(IMPORT_MAP_PREFIX) && externals.includes(id)) {
112
+ if (resolveFrom && importer?.startsWith(IMPORT_MAP_PREFIX) && externals.some((e) => specOf(e) === id)) {
72
113
  return this.resolve(id, resolveFrom, { skipSelf: true })
73
114
  }
74
115
  },
@@ -76,6 +117,24 @@ export function importMapPlugin({
76
117
  async load(id) {
77
118
  if (!id.startsWith(IMPORT_MAP_PREFIX)) return
78
119
  const pkg = id.slice(IMPORT_MAP_PREFIX.length)
120
+ const declared = externals.find((e) => specOf(e) === pkg)
121
+
122
+ // A declared surface wins outright — no Node import is attempted, so a
123
+ // JSX-reaching module is bridged correctly instead of falling into the
124
+ // lossy catch below. See the `External` typedef for why this exists.
125
+ if (typeof declared === 'object' && (declared.named || declared.hasDefault !== undefined)) {
126
+ const lines = []
127
+ if (declared.named?.length) {
128
+ lines.push(`export { ${declared.named.join(', ')} } from '${pkg}'`)
129
+ } else {
130
+ // Named exports stay self-maintaining; only `default` is declared.
131
+ lines.push(`export * from '${pkg}'`)
132
+ }
133
+ if (declared.hasDefault) {
134
+ lines.push(`export { default } from '${pkg}'`)
135
+ }
136
+ return lines.join('\n')
137
+ }
79
138
 
80
139
  // Generate explicit named re-exports (not `export *`) because CJS
81
140
  // packages like React only expose a default via `export *`, losing
@@ -93,8 +152,17 @@ export function importMapPlugin({
93
152
  lines.push(`export { default } from '${pkg}'`)
94
153
  }
95
154
  return lines.join('\n') || 'export {}'
96
- } catch {
97
- // Fallback: generic re-export (may not preserve named exports for CJS)
155
+ } catch (err) {
156
+ // Loud, because this fallback CANNOT carry a default export. Staying
157
+ // silent here is what let a bridge ship with its only export missing
158
+ // (see the `External` typedef). If the module has a default, declare
159
+ // `{ spec, hasDefault: true }`; if it genuinely has none, declare
160
+ // `{ spec, hasDefault: false }` to assert that and silence this.
161
+ this.warn(
162
+ `[import-map] could not enumerate "${pkg}" (${err.message.split('\n')[0]}). ` +
163
+ `Falling back to \`export *\`, which DROPS a default export. ` +
164
+ `Declare it as { spec: '${pkg}', hasDefault: true|false } to be explicit.`
165
+ )
98
166
  return `export * from '${pkg}'`
99
167
  }
100
168
  },
@@ -105,10 +173,11 @@ export function importMapPlugin({
105
173
  buildStart() {
106
174
  if (!isBuild) return
107
175
  for (const ext of externals) {
176
+ const spec = specOf(ext)
108
177
  this.emitFile({
109
178
  type: 'chunk',
110
- id: `${IMPORT_MAP_PREFIX}${ext}`,
111
- fileName: `_importmap/${ext.replace(/\//g, '-')}.js`,
179
+ id: `${IMPORT_MAP_PREFIX}${spec}`,
180
+ fileName: `_importmap/${bridgeFileName(spec)}`,
112
181
  preserveSignature: 'exports-only',
113
182
  })
114
183
  }
@@ -125,7 +194,8 @@ export function importMapPlugin({
125
194
 
126
195
  if (isBuild) {
127
196
  for (const ext of externals) {
128
- imports[ext] = `${basePath}_importmap/${ext.replace(/\//g, '-')}.js`
197
+ const spec = specOf(ext)
198
+ imports[spec] = `${basePath}_importmap/${bridgeFileName(spec)}`
129
199
  }
130
200
  } else if (devBridges) {
131
201
  Object.assign(imports, devBridges)