@ikas/component-cli 1.4.0-beta.17 → 1.4.0-beta.171

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.
Files changed (149) hide show
  1. package/dist/build-tools.d.ts +37 -0
  2. package/dist/build-tools.d.ts.map +1 -0
  3. package/dist/build-tools.js +36 -0
  4. package/dist/build-tools.js.map +1 -0
  5. package/dist/commands/add-sections-to-page.d.ts +3 -0
  6. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-sections-to-page.js +39 -0
  8. package/dist/commands/add-sections-to-page.js.map +1 -0
  9. package/dist/commands/add.d.ts.map +1 -1
  10. package/dist/commands/add.js +10 -1
  11. package/dist/commands/add.js.map +1 -1
  12. package/dist/commands/build.d.ts.map +1 -1
  13. package/dist/commands/build.js +5 -165
  14. package/dist/commands/build.js.map +1 -1
  15. package/dist/commands/config.d.ts +35 -0
  16. package/dist/commands/config.d.ts.map +1 -1
  17. package/dist/commands/config.js +837 -118
  18. package/dist/commands/config.js.map +1 -1
  19. package/dist/commands/create-design-tokens.d.ts +24 -0
  20. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  21. package/dist/commands/create-design-tokens.js +327 -0
  22. package/dist/commands/create-design-tokens.js.map +1 -0
  23. package/dist/commands/create-global-variable.d.ts +3 -0
  24. package/dist/commands/create-global-variable.d.ts.map +1 -0
  25. package/dist/commands/create-global-variable.js +53 -0
  26. package/dist/commands/create-global-variable.js.map +1 -0
  27. package/dist/commands/create-page.d.ts +3 -0
  28. package/dist/commands/create-page.d.ts.map +1 -0
  29. package/dist/commands/create-page.js +31 -0
  30. package/dist/commands/create-page.js.map +1 -0
  31. package/dist/commands/delete-theme-globals.d.ts +4 -0
  32. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  33. package/dist/commands/delete-theme-globals.js +48 -0
  34. package/dist/commands/delete-theme-globals.js.map +1 -0
  35. package/dist/commands/dev.d.ts.map +1 -1
  36. package/dist/commands/dev.js +96 -31
  37. package/dist/commands/dev.js.map +1 -1
  38. package/dist/commands/get-component-props.d.ts +3 -0
  39. package/dist/commands/get-component-props.d.ts.map +1 -0
  40. package/dist/commands/get-component-props.js +32 -0
  41. package/dist/commands/get-component-props.js.map +1 -0
  42. package/dist/commands/get-page-by-type.d.ts +3 -0
  43. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  44. package/dist/commands/get-page-by-type.js +25 -0
  45. package/dist/commands/get-page-by-type.js.map +1 -0
  46. package/dist/commands/get-section-values.d.ts +3 -0
  47. package/dist/commands/get-section-values.d.ts.map +1 -0
  48. package/dist/commands/get-section-values.js +39 -0
  49. package/dist/commands/get-section-values.js.map +1 -0
  50. package/dist/commands/list-entities.d.ts +3 -0
  51. package/dist/commands/list-entities.d.ts.map +1 -0
  52. package/dist/commands/list-entities.js +32 -0
  53. package/dist/commands/list-entities.js.map +1 -0
  54. package/dist/commands/list-page-sections.d.ts.map +1 -1
  55. package/dist/commands/list-page-sections.js +8 -2
  56. package/dist/commands/list-page-sections.js.map +1 -1
  57. package/dist/commands/list-theme-globals.d.ts +3 -0
  58. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  59. package/dist/commands/list-theme-globals.js +22 -0
  60. package/dist/commands/list-theme-globals.js.map +1 -0
  61. package/dist/commands/move-page-section.d.ts +3 -0
  62. package/dist/commands/move-page-section.d.ts.map +1 -0
  63. package/dist/commands/move-page-section.js +62 -0
  64. package/dist/commands/move-page-section.js.map +1 -0
  65. package/dist/commands/publish-theme.d.ts +3 -0
  66. package/dist/commands/publish-theme.d.ts.map +1 -0
  67. package/dist/commands/publish-theme.js +29 -0
  68. package/dist/commands/publish-theme.js.map +1 -0
  69. package/dist/commands/publish.d.ts.map +1 -1
  70. package/dist/commands/publish.js +45 -3
  71. package/dist/commands/publish.js.map +1 -1
  72. package/dist/commands/search-products.d.ts +3 -0
  73. package/dist/commands/search-products.d.ts.map +1 -0
  74. package/dist/commands/search-products.js +40 -0
  75. package/dist/commands/search-products.js.map +1 -0
  76. package/dist/commands/update-global-variable.d.ts +3 -0
  77. package/dist/commands/update-global-variable.d.ts.map +1 -0
  78. package/dist/commands/update-global-variable.js +47 -0
  79. package/dist/commands/update-global-variable.js.map +1 -0
  80. package/dist/commands/update-page-sections.d.ts +3 -0
  81. package/dist/commands/update-page-sections.d.ts.map +1 -0
  82. package/dist/commands/update-page-sections.js +39 -0
  83. package/dist/commands/update-page-sections.js.map +1 -0
  84. package/dist/commands/update-section-prop.d.ts.map +1 -1
  85. package/dist/commands/update-section-prop.js +34 -9
  86. package/dist/commands/update-section-prop.js.map +1 -1
  87. package/dist/commands/upload-image.d.ts +3 -0
  88. package/dist/commands/upload-image.d.ts.map +1 -0
  89. package/dist/commands/upload-image.js +38 -0
  90. package/dist/commands/upload-image.js.map +1 -0
  91. package/dist/commands/upload-images.d.ts +3 -0
  92. package/dist/commands/upload-images.d.ts.map +1 -0
  93. package/dist/commands/upload-images.js +48 -0
  94. package/dist/commands/upload-images.js.map +1 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +46 -1
  97. package/dist/index.js.map +1 -1
  98. package/dist/types.d.ts +17 -0
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/compile.d.ts +17 -2
  101. package/dist/utils/compile.d.ts.map +1 -1
  102. package/dist/utils/compile.js +683 -106
  103. package/dist/utils/compile.js.map +1 -1
  104. package/dist/utils/component-helpers.d.ts +69 -2
  105. package/dist/utils/component-helpers.d.ts.map +1 -1
  106. package/dist/utils/component-helpers.js +193 -25
  107. package/dist/utils/component-helpers.js.map +1 -1
  108. package/dist/utils/editor-action-client.d.ts +1 -1
  109. package/dist/utils/editor-action-client.d.ts.map +1 -1
  110. package/dist/utils/editor-action-client.js +51 -16
  111. package/dist/utils/editor-action-client.js.map +1 -1
  112. package/dist/utils/esm-transform.d.ts +30 -0
  113. package/dist/utils/esm-transform.d.ts.map +1 -1
  114. package/dist/utils/esm-transform.js +169 -2
  115. package/dist/utils/esm-transform.js.map +1 -1
  116. package/dist/utils/find-component-for-file.d.ts +12 -0
  117. package/dist/utils/find-component-for-file.d.ts.map +1 -0
  118. package/dist/utils/find-component-for-file.js +24 -0
  119. package/dist/utils/find-component-for-file.js.map +1 -0
  120. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  121. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  122. package/dist/utils/forbid-deep-imports.js +54 -0
  123. package/dist/utils/forbid-deep-imports.js.map +1 -0
  124. package/dist/utils/forbid-external-packages.d.ts +26 -0
  125. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  126. package/dist/utils/forbid-external-packages.js +99 -0
  127. package/dist/utils/forbid-external-packages.js.map +1 -0
  128. package/dist/utils/load-image.d.ts +16 -0
  129. package/dist/utils/load-image.d.ts.map +1 -0
  130. package/dist/utils/load-image.js +50 -0
  131. package/dist/utils/load-image.js.map +1 -0
  132. package/dist/utils/vendor-storefront.d.ts +13 -0
  133. package/dist/utils/vendor-storefront.d.ts.map +1 -0
  134. package/dist/utils/vendor-storefront.js +91 -0
  135. package/dist/utils/vendor-storefront.js.map +1 -0
  136. package/dist/utils/websocket-server.d.ts +53 -5
  137. package/dist/utils/websocket-server.d.ts.map +1 -1
  138. package/dist/utils/websocket-server.js +233 -43
  139. package/dist/utils/websocket-server.js.map +1 -1
  140. package/package.json +16 -1
  141. package/templates/add/section.css +11 -0
  142. package/dist/commands/create.d.ts +0 -4
  143. package/dist/commands/create.d.ts.map +0 -1
  144. package/dist/commands/create.js +0 -228
  145. package/dist/commands/create.js.map +0 -1
  146. package/dist/commands/proxy.d.ts +0 -39
  147. package/dist/commands/proxy.d.ts.map +0 -1
  148. package/dist/commands/proxy.js +0 -210
  149. package/dist/commands/proxy.js.map +0 -1
@@ -4,34 +4,65 @@ import * as os from "os";
4
4
  import * as path from "path";
5
5
  import { resolveCssImports } from "./css-import-resolver.js";
6
6
  import { ikasComponentUtilsPlugin } from "./observer-runtime.js";
7
+ import { forbidDeepStorefrontImportsPlugin } from "./forbid-deep-imports.js";
8
+ import { forbidExternalPackagesPlugin } from "./forbid-external-packages.js";
9
+ import { isVendoredDependencyFile, vendorStorefrontPlugin } from "./vendor-storefront.js";
7
10
  /**
8
- * Shim file that tells esbuild `h` and `Fragment` are reserved identifiers.
9
- * Unlike `banner` (which is appended after minification), `inject` is processed
10
- * during compilation so the minifier will never reuse these names.
11
+ * Shim that provides the JSX factory/fragment under collision-proof names.
12
+ *
13
+ * The classic JSX pragma must be a bare identifier in scope. Using `h`/`Fragment`
14
+ * directly means any user-authored local named `h` or `Fragment` (e.g. a `.map`
15
+ * callback param) shadows the factory, so JSX in that scope compiles to a call on
16
+ * the local value → runtime "x is not a function". Aliasing to `__ikas_h` /
17
+ * `__ikas_Fragment` (names users won't write) eliminates that shadowing footgun.
18
+ *
19
+ * Unlike `banner` (appended after minification), `inject` is processed during
20
+ * compilation so the minifier will never reuse these names. The bundled import
21
+ * still references preact's `h`/`Fragment`, so the canvas esm-transform keying off
22
+ * those source export names is unaffected.
11
23
  */
12
24
  const PREACT_JSX_SHIM = path.join(os.tmpdir(), "ikas-preact-jsx-shim.mjs");
13
- fs.writeFileSync(PREACT_JSX_SHIM, 'export{h,Fragment}from"preact";\n');
25
+ fs.writeFileSync(PREACT_JSX_SHIM, 'export{h as __ikas_h,Fragment as __ikas_Fragment}from"preact";\n');
14
26
  /**
15
- * External dependencies - these will be provided by the generated storefront
27
+ * External dependencies - these will be provided by the generated storefront.
28
+ *
29
+ * The `@ikas/bp-storefront*` packages are intentionally NOT listed here: they are externalized
30
+ * by `forbidDeepStorefrontImportsPlugin()` instead, which externalizes the allowed ROOT import
31
+ * and rejects private deep subpaths. (esbuild applies this `external` array before plugin
32
+ * onResolve, so keeping them here would let `/dist/...` deep imports slip through unchecked.)
16
33
  */
17
34
  const SERVER_EXTERNALS = [
18
35
  "preact",
19
36
  "preact/hooks",
20
37
  "preact-render-to-string",
21
- "mobx",
22
- "@ikas/bp-storefront",
23
- "@ikas/bp-storefront-models",
24
- "@ikas/bp-storefront-config",
25
- "@ikas/bp-storefront-api"
38
+ "mobx"
26
39
  ];
27
40
  const CLIENT_EXTERNALS = [
28
41
  "preact",
29
42
  "preact/hooks",
30
- "mobx",
31
- "@ikas/bp-storefront",
32
- "@ikas/bp-storefront-models",
33
- "@ikas/bp-storefront-config"
43
+ "mobx"
34
44
  ];
45
+ /**
46
+ * The server and client builds MUST compile with the SAME esbuild `target` so
47
+ * syntax is lowered identically and code-split shared chunks come out
48
+ * byte-identical (same content hash) on both sides.
49
+ *
50
+ * If the targets diverge (previously server=node18 vs client=es2020), esbuild
51
+ * lowers modern syntax differently: e.g. a class field (`x = 1`, ES2022) stays
52
+ * native on node18 but is down-leveled into a `__publicField` helper on es2020.
53
+ * That helper lands in the shared runtime-helper chunk, so the helper chunk's
54
+ * hash differs between builds — and every chunk that imports it by hashed name
55
+ * (`./chunk-<hash>.js`) diverges too. The result is "server-only" shared chunks
56
+ * with no client twin, which the client-derived `usedSharedModuleIds` never
57
+ * lists, so they get pruned/dropped downstream ("Could not resolve
58
+ * ./chunk-XXXX.js" at SSR build).
59
+ *
60
+ * es2020 is the floor the browser build needs; node18 executes es2020 fine, so
61
+ * both builds use es2020 and produce identical shared chunks. Keep this the
62
+ * single source of truth for both `platform: node` and `platform: browser`
63
+ * builds — do not set per-build `target` overrides.
64
+ */
65
+ const SHARED_BUILD_TARGET = "es2020";
35
66
  /**
36
67
  * Compile a single component's TSX and CSS
37
68
  */
@@ -43,10 +74,14 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
43
74
  entryPoints: [path.resolve(process.cwd(), entryPath)],
44
75
  format: "esm",
45
76
  platform: "node",
46
- target: "node18",
77
+ target: SHARED_BUILD_TARGET,
47
78
  write: false,
48
79
  external: SERVER_EXTERNALS,
49
- plugins: [ikasComponentUtilsPlugin()],
80
+ plugins: [
81
+ forbidDeepStorefrontImportsPlugin(),
82
+ forbidExternalPackagesPlugin(),
83
+ ikasComponentUtilsPlugin(),
84
+ ],
50
85
  });
51
86
  // Build client bundle (for hydration)
52
87
  const clientResult = await esbuild.build({
@@ -54,10 +89,14 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
54
89
  entryPoints: [path.resolve(process.cwd(), entryPath)],
55
90
  format: "esm",
56
91
  platform: "browser",
57
- target: "es2020",
92
+ target: SHARED_BUILD_TARGET,
58
93
  write: false,
59
94
  external: CLIENT_EXTERNALS,
60
- plugins: [ikasComponentUtilsPlugin()],
95
+ plugins: [
96
+ forbidDeepStorefrontImportsPlugin(),
97
+ forbidExternalPackagesPlugin(),
98
+ ikasComponentUtilsPlugin(),
99
+ ],
61
100
  });
62
101
  // Read CSS (with import resolution)
63
102
  const cssPath = path.resolve(process.cwd(), stylesPath);
@@ -86,11 +125,15 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
86
125
  }
87
126
  }
88
127
  /**
89
- * Scope CSS selectors with a component-specific class prefix
128
+ * Scope CSS selectors with a component-specific class prefix, then namespace any
129
+ * document-global CSS identifiers (`@keyframes`, `@font-face` font-family,
130
+ * `@counter-style`) DEFINED in this component so two components — or the host theme —
131
+ * declaring the same name can't collide in the page's global namespace.
90
132
  */
91
133
  export function scopeCSS(css, componentId) {
92
134
  const scopeClass = `cc_${componentId.replace(/[^a-zA-Z0-9]/g, "_")}`;
93
- return scopeCSSWithClasses(css, [scopeClass]);
135
+ const scoped = scopeCSSWithClasses(css, [scopeClass]);
136
+ return renameCollidingGlobals(scoped, `${scopeClass}_`);
94
137
  }
95
138
  /**
96
139
  * Scope CSS selectors with multiple class prefixes (for shared chunks).
@@ -100,63 +143,519 @@ export function scopeCSSMulti(css, componentIds) {
100
143
  const scopeClasses = componentIds.map(id => `cc_${id.replace(/[^a-zA-Z0-9]/g, "_")}`);
101
144
  return scopeCSSWithClasses(css, scopeClasses);
102
145
  }
146
+ // Conditional group at-rules whose blocks nest normal style rules and must be
147
+ // scoped recursively. This is an ALLOWLIST on purpose: at-rules NOT listed here
148
+ // (@keyframes, @font-face, @page, @property, @counter-style, ...) have non-selector
149
+ // bodies — e.g. @keyframes contains `0%`/`from` keyframe selectors that must NOT be
150
+ // prefixed — so their bodies are copied through untouched. Vendor-prefixed conditional
151
+ // rules (@-moz-document) match via the optional prefix group; @-webkit-keyframes does
152
+ // NOT match (the alternation excludes "keyframes"), which is what we want.
153
+ //
154
+ // NOTE: This scoper is duplicated in editor-models/src/helpers/code-component-css-scope.ts
155
+ // (the editor-preview / code-generator path). The CLI ships standalone to npm and cannot
156
+ // import that private package, so the two intentionally diverge. Keep parsing fixes in sync.
157
+ const NESTING_AT_RULES = /^@(?:-\w+-)?(media|supports|container|layer|document|scope|starting-style)\b/i;
103
158
  /**
104
- * Internal: scope CSS selectors with one or more scope class prefixes
159
+ * Internal: scope CSS selectors with one or more scope class prefixes.
160
+ *
161
+ * Character-based walker, NOT line-based: a rule's selector list is everything up
162
+ * to its opening `{`, so lists written across multiple lines (`.a,\n.b { ... }`)
163
+ * are scoped as a whole. The previous line-based implementation only scoped the
164
+ * selector fragment sharing a line with `{`, leaving the other selectors unscoped
165
+ * (they either lost the cascade to scoped rules or leaked into other components).
166
+ *
167
+ * Comments, string literals, and `\` escapes are treated as inert spans (via
168
+ * `skipInertSpan`) by every structural scanner, so a `{`, `}`, `;`, or `,` that
169
+ * appears inside a comment / string / escape can never be mistaken for real
170
+ * structure (which would otherwise drop the scope prefix and leak styles globally).
105
171
  */
106
172
  function scopeCSSWithClasses(css, scopeClasses) {
107
- const lines = css.split("\n");
108
- const scopedLines = [];
109
- let inMediaQuery = false;
110
- let keyframesDepth = 0;
111
- for (const line of lines) {
112
- const trimmed = line.trim();
113
- if (!trimmed) {
114
- scopedLines.push(line);
173
+ let out = "";
174
+ for (let i = 0; i < css.length;) {
175
+ const ch = css[i];
176
+ // Copy comments verbatim
177
+ if (ch === "/" && css[i + 1] === "*") {
178
+ const end = css.indexOf("*/", i + 2);
179
+ if (end === -1)
180
+ return out + css.slice(i);
181
+ out += css.slice(i, end + 2);
182
+ i = end + 2;
183
+ continue;
184
+ }
185
+ // Copy whitespace and stray closing braces
186
+ if (/\s/.test(ch) || ch === "}") {
187
+ out += ch;
188
+ i++;
189
+ continue;
190
+ }
191
+ if (ch === "@") {
192
+ const headerEnd = findAtRuleHeaderEnd(css, i);
193
+ // Block-less at-rule (@import, @charset, @layer a;, ...) — copy through
194
+ if (headerEnd >= css.length || css[headerEnd] === ";") {
195
+ out += css.slice(i, Math.min(headerEnd + 1, css.length));
196
+ i = headerEnd + 1;
197
+ continue;
198
+ }
199
+ const header = css.slice(i, headerEnd);
200
+ const blockEnd = findBlockEnd(css, headerEnd + 1);
201
+ const body = css.slice(headerEnd + 1, blockEnd);
202
+ const transformed = NESTING_AT_RULES.test(header.trim())
203
+ ? scopeCSSWithClasses(body, scopeClasses)
204
+ : body;
205
+ // Always emit the closing brace: when the source block is unterminated
206
+ // (blockEnd === css.length) this repairs it, so concatenated component CSS
207
+ // can't leak one component's block over the next.
208
+ out += `${header}{${transformed}}`;
209
+ i = blockEnd + 1;
210
+ continue;
211
+ }
212
+ // Normal style rule: selector list runs up to the rule's opening `{`
213
+ const selEnd = findSelectorListEnd(css, i);
214
+ if (selEnd >= css.length) {
215
+ // Trailing content with no block (e.g. a dangling declaration at EOF) — copy through
216
+ out += css.slice(i);
217
+ break;
218
+ }
219
+ if (css[selEnd] === ";") {
220
+ // A stray top-level `;` (or a stray declaration fragment ending in `;`) — not a rule.
221
+ // DROP it. Copying it through is not enough: per the CSS syntax spec a browser folds a
222
+ // stray `;` into the NEXT rule's prelude when consuming a qualified rule, which makes
223
+ // that selector invalid and drops the whole following rule. Dropping the stray token
224
+ // keeps the next rule valid and correctly scoped.
225
+ i = selEnd + 1;
226
+ continue;
227
+ }
228
+ if (css[selEnd] === "}") {
229
+ // Content before a stray closing brace — copy through verbatim and let the main loop
230
+ // emit the `}`.
231
+ out += css.slice(i, selEnd);
232
+ i = selEnd;
233
+ continue;
234
+ }
235
+ const selectorList = css.slice(i, selEnd);
236
+ const blockEnd = findBlockEnd(css, selEnd + 1);
237
+ const body = css.slice(selEnd + 1, blockEnd);
238
+ const scopedSelector = splitSelectorList(selectorList)
239
+ .map(s => s.trim())
240
+ .filter(Boolean)
241
+ .flatMap(s => scopeClasses.map(cls => `.${cls} ${s}`))
242
+ .join(", ");
243
+ // Always emit the closing brace (see at-rule branch above for rationale).
244
+ out += `${scopedSelector} {${body}}`;
245
+ i = blockEnd + 1;
246
+ }
247
+ return out;
248
+ }
249
+ /**
250
+ * If an inert span — a `/* ... *\/` comment, a `"..."` / `'...'` string literal, an
251
+ * unquoted `url(...)` token, or a `\x` escape — starts at `s[i]`, return the index just
252
+ * past it; otherwise return -1.
253
+ *
254
+ * Every structural scanner below funnels through this so that delimiters (`{ } ; , ( )`)
255
+ * living inside comments, strings, urls, or escapes are never treated as structure. String
256
+ * scanning honors `\` escapes and stops at a raw newline, matching CSS "bad-string" error
257
+ * recovery (an unterminated string ends at the line break) so a missing quote can't
258
+ * swallow every rule that follows it.
259
+ */
260
+ function skipInertSpan(s, i) {
261
+ const c = s[i];
262
+ if (c === "/" && s[i + 1] === "*") {
263
+ const end = s.indexOf("*/", i + 2);
264
+ return end === -1 ? s.length : end + 2;
265
+ }
266
+ if (c === '"' || c === "'") {
267
+ for (let j = i + 1; j < s.length; j++) {
268
+ const d = s[j];
269
+ if (d === "\\") {
270
+ j++; // escape: skip the next char
271
+ continue;
272
+ }
273
+ if (d === c)
274
+ return j + 1; // closing quote
275
+ if (d === "\n" || d === "\r" || d === "\f")
276
+ return j; // bad-string ends at newline
277
+ }
278
+ return s.length;
279
+ }
280
+ // Unquoted url(...) token: per the CSS tokenizer its content is opaque, so braces,
281
+ // semicolons, and parens inside it must NOT be read as structure (an unbalanced `}` in
282
+ // `url(x}y.png)` or `{` in an inline SVG data-uri would otherwise mis-terminate the
283
+ // enclosing block). A QUOTED url (`url("...")`) is a normal function + string and is left
284
+ // to the string branch above. `url` must be a standalone ident, not the tail of a longer
285
+ // identifier like `myurl(`.
286
+ if ((c === "u" || c === "U") && /^url\(/i.test(s.slice(i, i + 4))) {
287
+ const prev = i > 0 ? s[i - 1] : "";
288
+ if (!/[A-Za-z0-9_-]/.test(prev)) {
289
+ let j = i + 4;
290
+ while (j < s.length && /\s/.test(s[j]))
291
+ j++; // optional leading whitespace
292
+ if (s[j] !== '"' && s[j] !== "'") {
293
+ for (; j < s.length; j++) {
294
+ if (s[j] === "\\") {
295
+ j++; // escaped char inside the url (e.g. `url(foo\)bar)`)
296
+ continue;
297
+ }
298
+ if (s[j] === ")")
299
+ return j + 1; // end of url token
300
+ }
301
+ return s.length; // unterminated url() — consume to EOF (bad-url)
302
+ }
303
+ }
304
+ }
305
+ if (c === "\\")
306
+ return i + 2; // escaped char outside a string (e.g. `.a\,b`)
307
+ return -1;
308
+ }
309
+ /** Find the first top-level `{` or `;` that ends an at-rule's header. Respects `()` / `[]`
310
+ * and inert spans (strings/comments/escapes). */
311
+ function findAtRuleHeaderEnd(css, start) {
312
+ let pDepth = 0;
313
+ for (let j = start; j < css.length;) {
314
+ const skip = skipInertSpan(css, j);
315
+ if (skip !== -1) {
316
+ j = skip;
115
317
  continue;
116
318
  }
117
- if (trimmed.startsWith("@media")) {
118
- inMediaQuery = true;
119
- scopedLines.push(line);
319
+ const c = css[j];
320
+ if (c === "(" || c === "[")
321
+ pDepth++;
322
+ else if (c === ")" || c === "]")
323
+ pDepth--;
324
+ else if (pDepth === 0 && (c === "{" || c === ";"))
325
+ return j;
326
+ j++;
327
+ }
328
+ return css.length;
329
+ }
330
+ /**
331
+ * Find the matching `}` for the block whose body starts at `blockStart` (char after `{`).
332
+ * Inert spans are skipped so braces inside `content: "}"`, comments, or escapes don't
333
+ * terminate the block early.
334
+ */
335
+ function findBlockEnd(css, blockStart) {
336
+ let depth = 1;
337
+ for (let j = blockStart; j < css.length;) {
338
+ const skip = skipInertSpan(css, j);
339
+ if (skip !== -1) {
340
+ j = skip;
120
341
  continue;
121
342
  }
122
- if (trimmed.startsWith("@keyframes") || trimmed.startsWith("@-webkit-keyframes")) {
123
- keyframesDepth = 1;
124
- scopedLines.push(line);
343
+ const c = css[j];
344
+ if (c === "{")
345
+ depth++;
346
+ else if (c === "}") {
347
+ depth--;
348
+ if (depth === 0)
349
+ return j;
350
+ }
351
+ j++;
352
+ }
353
+ return css.length;
354
+ }
355
+ /** Find the `{`, `}`, or `;` that terminates a selector list. A `}` or `;` here means the
356
+ * run was not a real style rule. Respects `()` / `[]` and inert spans. */
357
+ function findSelectorListEnd(css, start) {
358
+ let pDepth = 0;
359
+ for (let j = start; j < css.length;) {
360
+ const skip = skipInertSpan(css, j);
361
+ if (skip !== -1) {
362
+ j = skip;
125
363
  continue;
126
364
  }
127
- // Inside @keyframes: track brace depth, pass lines through unscoped
128
- if (keyframesDepth > 0) {
129
- for (const ch of trimmed) {
130
- if (ch === "{")
131
- keyframesDepth++;
132
- else if (ch === "}")
133
- keyframesDepth--;
365
+ const c = css[j];
366
+ if (c === "(" || c === "[")
367
+ pDepth++;
368
+ else if (c === ")" || c === "]")
369
+ pDepth--;
370
+ else if (pDepth === 0 && (c === "{" || c === "}" || c === ";"))
371
+ return j;
372
+ j++;
373
+ }
374
+ return css.length;
375
+ }
376
+ /** Split a selector list on top-level commas. Respects `()` / `[]` and inert spans, so a
377
+ * comma inside a comment, string, or `\` escape (e.g. `.a\,b`) does not split the list. */
378
+ function splitSelectorList(s) {
379
+ const result = [];
380
+ let depth = 0;
381
+ let start = 0;
382
+ for (let i = 0; i < s.length;) {
383
+ const skip = skipInertSpan(s, i);
384
+ if (skip !== -1) {
385
+ i = skip;
386
+ continue;
387
+ }
388
+ const ch = s[i];
389
+ if (ch === "(" || ch === "[")
390
+ depth++;
391
+ else if (ch === ")" || ch === "]")
392
+ depth--;
393
+ else if (ch === "," && depth === 0) {
394
+ result.push(s.slice(start, i));
395
+ start = i + 1;
396
+ }
397
+ i++;
398
+ }
399
+ result.push(s.slice(start));
400
+ return result;
401
+ }
402
+ /* ============================================================================
403
+ * Global-identifier namespacing.
404
+ *
405
+ * `@keyframes`, `@font-face` font-family names, and `@counter-style` names live in a
406
+ * DOCUMENT-GLOBAL namespace — scoping selectors with `.cc_<id>` does not isolate them, so
407
+ * two components (or the theme) defining the same name would collide (last definition wins
408
+ * document-wide). `renameCollidingGlobals` prefixes every such name DEFINED in this CSS,
409
+ * plus its references within the SAME CSS, with the component's scope prefix.
410
+ *
411
+ * Only names defined in THIS css are renamed — a reference to a name defined elsewhere
412
+ * (global.css, a shared chunk, the theme) is left untouched, so cross-file references keep
413
+ * resolving. Shared-chunk CSS (scopeCSSMulti) is intentionally NOT renamed, to avoid
414
+ * breaking a chunk-defined keyframe referenced from a consuming component's own CSS.
415
+ *
416
+ * NOTE: ported from packages/editor-models/src/helpers/code-component-css-scope.ts
417
+ * (renameCollidingGlobals + helpers, the global.css path's `cc_<projectId>_` variant).
418
+ * Keep parsing/rename fixes in sync between the two.
419
+ * ============================================================================ */
420
+ // Declarations whose VALUE may reference a global identifier of the given type.
421
+ const KEYFRAME_REF_PROPS = new Set(["animation", "animation-name"]);
422
+ const FONT_FAMILY_REF_PROPS = new Set(["font", "font-family"]);
423
+ const COUNTER_STYLE_REF_PROPS = new Set(["list-style", "list-style-type"]);
424
+ function renameCollidingGlobals(css, prefix) {
425
+ // Comment/string/url-masked copy: defining-site regex scans run against this so a name
426
+ // inside a comment or `content: "..."` string can't contribute a rename or be rewritten.
427
+ const scanCss = maskInertSpans(css);
428
+ const keyframeNames = new Set();
429
+ const fontFamilyNames = new Set();
430
+ const counterStyleNames = new Set();
431
+ let out = css;
432
+ out = out.replace(/@keyframes(\s+)([\w-]+)/g, (m, ws, name, offset) => {
433
+ if (scanCss.substr(offset, 10) !== "@keyframes")
434
+ return m;
435
+ keyframeNames.add(name);
436
+ return `@keyframes${ws}${prefix}${name}`;
437
+ });
438
+ out = out.replace(/@counter-style(\s+)([\w-]+)/g, (m, ws, name, offset) => {
439
+ if (scanCss.substr(offset, 14) !== "@counter-style")
440
+ return m;
441
+ counterStyleNames.add(name);
442
+ return `@counter-style${ws}${prefix}${name}`;
443
+ });
444
+ out = rewriteFontFaceBlocks(out, fontFamilyNames, prefix);
445
+ if (keyframeNames.size === 0 && fontFamilyNames.size === 0 && counterStyleNames.size === 0) {
446
+ return out;
447
+ }
448
+ return rewriteDeclarationReferences(out, keyframeNames, fontFamilyNames, counterStyleNames, prefix);
449
+ }
450
+ /** Replace every inert span (comment / string / url() / escape) with equal-length spaces so
451
+ * position-based regex scans still align but can't match tokens hidden inside them. */
452
+ function maskInertSpans(css) {
453
+ let out = "";
454
+ for (let i = 0; i < css.length;) {
455
+ const skip = skipInertSpan(css, i);
456
+ if (skip !== -1) {
457
+ out += " ".repeat(skip - i);
458
+ i = skip;
459
+ continue;
460
+ }
461
+ out += css[i];
462
+ i++;
463
+ }
464
+ return out;
465
+ }
466
+ function rewriteFontFaceBlocks(css, collect, prefix) {
467
+ const matches = [];
468
+ css.replace(/@font-face\s*\{/g, (m, offset) => {
469
+ matches.push({ index: offset, length: m.length });
470
+ return m;
471
+ });
472
+ if (matches.length === 0)
473
+ return css;
474
+ let out = "";
475
+ let lastIdx = 0;
476
+ for (const match of matches) {
477
+ const openBraceIdx = match.index + match.length - 1;
478
+ if (openBraceIdx < lastIdx)
479
+ continue;
480
+ const endIdx = findBlockEnd(css, openBraceIdx + 1);
481
+ const before = css.slice(lastIdx, openBraceIdx + 1);
482
+ const body = css.slice(openBraceIdx + 1, endIdx);
483
+ const rewrittenBody = body.replace(/(font-family\s*:\s*)(?:"([^"]+)"|'([^']+)'|([\w-]+))/i, (_m, pre, dq, sq, bare) => {
484
+ const name = dq || sq || bare;
485
+ if (!name)
486
+ return _m;
487
+ collect.add(name);
488
+ const prefixed = `${prefix}${name}`;
489
+ if (dq)
490
+ return `${pre}"${prefixed}"`;
491
+ if (sq)
492
+ return `${pre}'${prefixed}'`;
493
+ return `${pre}${prefixed}`;
494
+ });
495
+ out += before + rewrittenBody + "}";
496
+ lastIdx = endIdx + 1;
497
+ }
498
+ out += css.slice(lastIdx);
499
+ return out;
500
+ }
501
+ function rewriteDeclarationReferences(css, keyframeNames, fontFamilyNames, counterStyleNames, prefix) {
502
+ const keyframeRe = buildBareIdentRegex(keyframeNames);
503
+ const fontFamilyRe = buildFontFamilyValueRegex(fontFamilyNames);
504
+ const counterStyleRe = buildBareIdentRegex(counterStyleNames);
505
+ // counter(<name>, <style>?) / counters(<name>, <string>, <style>?) — the counter-style
506
+ // identifier, when present, is always the LAST optional argument.
507
+ const counterFnRe = counterStyleNames.size > 0
508
+ ? new RegExp(`(\\bcounters?\\([^)]*?,\\s*)(${bareUnion(counterStyleNames)})(\\s*\\))`, "gi")
509
+ : null;
510
+ return mapRuleBlocks(css, block => {
511
+ return mapDeclarations(block, (prop, value) => {
512
+ let v = value;
513
+ const lower = prop.toLowerCase();
514
+ if (keyframeRe && KEYFRAME_REF_PROPS.has(lower)) {
515
+ v = v.replace(keyframeRe, m => `${prefix}${m}`);
516
+ }
517
+ if (fontFamilyRe && FONT_FAMILY_REF_PROPS.has(lower)) {
518
+ v = v.replace(fontFamilyRe, (m, dq, sq, bare) => {
519
+ const name = dq || sq || bare;
520
+ if (!name || !fontFamilyNames.has(name))
521
+ return m;
522
+ if (dq !== undefined)
523
+ return `"${prefix}${name}"`;
524
+ if (sq !== undefined)
525
+ return `'${prefix}${name}'`;
526
+ return `${prefix}${name}`;
527
+ });
528
+ }
529
+ if (counterStyleRe && COUNTER_STYLE_REF_PROPS.has(lower)) {
530
+ v = v.replace(counterStyleRe, m => `${prefix}${m}`);
531
+ }
532
+ if (counterFnRe) {
533
+ v = v.replace(counterFnRe, (_m, pre, name, post) => `${pre}${prefix}${name}${post}`);
134
534
  }
135
- scopedLines.push(line);
535
+ return v;
536
+ });
537
+ });
538
+ }
539
+ function bareUnion(names) {
540
+ return Array.from(names).map(escapeRegex).join("|");
541
+ }
542
+ function buildBareIdentRegex(names) {
543
+ if (names.size === 0)
544
+ return null;
545
+ return new RegExp(`(?<![\\w-])(?:${bareUnion(names)})(?![\\w-])`, "g");
546
+ }
547
+ function buildFontFamilyValueRegex(names) {
548
+ if (names.size === 0)
549
+ return null;
550
+ const u = bareUnion(names);
551
+ return new RegExp(`"(${u})"|'(${u})'|(?<![\\w-])(${u})(?![\\w-])`, "g");
552
+ }
553
+ function escapeRegex(s) {
554
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
555
+ }
556
+ /**
557
+ * Walk every top-level rule block `{ ... }` (recursing into nested at-rule contents) and
558
+ * pass the block body through `transform`. Inert spans are skipped so a `{` inside a string
559
+ * or `url(a{b)` is not mistaken for a block. At-rule preambles and inter-rule whitespace
560
+ * are left untouched.
561
+ */
562
+ function mapRuleBlocks(css, transform) {
563
+ let out = "";
564
+ for (let i = 0; i < css.length;) {
565
+ const skip = skipInertSpan(css, i);
566
+ if (skip !== -1) {
567
+ out += css.slice(i, skip);
568
+ i = skip;
569
+ continue;
570
+ }
571
+ if (css[i] === "{") {
572
+ const blockEnd = findBlockEnd(css, i + 1);
573
+ const body = css.slice(i + 1, blockEnd);
574
+ const processed = hasTopLevelBrace(body) ? mapRuleBlocks(body, transform) : transform(body);
575
+ out += "{" + processed + "}";
576
+ i = blockEnd + 1;
136
577
  continue;
137
578
  }
138
- if (trimmed === "}" && inMediaQuery) {
139
- inMediaQuery = false;
140
- scopedLines.push(line);
579
+ out += css[i];
580
+ i++;
581
+ }
582
+ return out;
583
+ }
584
+ /** Inert-span-aware: true iff a real `{` appears outside comments/strings/url()/escapes. */
585
+ function hasTopLevelBrace(s) {
586
+ for (let i = 0; i < s.length;) {
587
+ const skip = skipInertSpan(s, i);
588
+ if (skip !== -1) {
589
+ i = skip;
141
590
  continue;
142
591
  }
143
- if (!trimmed.includes("{") || trimmed.startsWith("@")) {
144
- scopedLines.push(line);
592
+ if (s[i] === "{")
593
+ return true;
594
+ i++;
595
+ }
596
+ return false;
597
+ }
598
+ /**
599
+ * Split a rule block body into `prop: value` declarations on top-level `;`, apply
600
+ * `transform(prop, value)` to each, and rejoin preserving separators. Inert spans are
601
+ * skipped so a `;` inside a string or `url(a;b)` doesn't split a declaration.
602
+ */
603
+ function mapDeclarations(body, transform) {
604
+ const parts = [];
605
+ let start = 0;
606
+ let pDepth = 0;
607
+ for (let i = 0; i < body.length;) {
608
+ const skip = skipInertSpan(body, i);
609
+ if (skip !== -1) {
610
+ i = skip;
145
611
  continue;
146
612
  }
147
- const [selector, rest] = line.split("{");
148
- if (selector && rest !== undefined) {
149
- const scopedSelector = selector
150
- .split(",")
151
- .flatMap((s) => scopeClasses.map(cls => `.${cls} ${s.trim()}`))
152
- .join(", ");
153
- scopedLines.push(`${scopedSelector} {${rest}`);
613
+ const c = body[i];
614
+ if (c === "(" || c === "[" || c === "{")
615
+ pDepth++;
616
+ else if (c === ")" || c === "]" || c === "}")
617
+ pDepth--;
618
+ else if (c === ";" && pDepth === 0) {
619
+ parts.push(body.slice(start, i));
620
+ start = i + 1;
154
621
  }
155
- else {
156
- scopedLines.push(line);
622
+ i++;
623
+ }
624
+ parts.push(body.slice(start));
625
+ return parts
626
+ .map((part, idx) => {
627
+ const isLast = idx === parts.length - 1;
628
+ const colonIdx = findTopLevelColon(part);
629
+ if (colonIdx === -1)
630
+ return part + (isLast ? "" : ";");
631
+ const valueRaw = part.slice(colonIdx + 1);
632
+ const leading = valueRaw.match(/^\s*/)?.[0] ?? "";
633
+ const trailing = valueRaw.match(/\s*$/)?.[0] ?? "";
634
+ const prop = part.slice(0, colonIdx).trim();
635
+ const value = valueRaw.slice(leading.length, valueRaw.length - trailing.length);
636
+ const transformed = transform(prop, value);
637
+ return part.slice(0, colonIdx) + ":" + leading + transformed + trailing + (isLast ? "" : ";");
638
+ })
639
+ .join("");
640
+ }
641
+ function findTopLevelColon(s) {
642
+ let pDepth = 0;
643
+ for (let i = 0; i < s.length;) {
644
+ const skip = skipInertSpan(s, i);
645
+ if (skip !== -1) {
646
+ i = skip;
647
+ continue;
157
648
  }
649
+ const c = s[i];
650
+ if (c === "(" || c === "[")
651
+ pDepth++;
652
+ else if (c === ")" || c === "]")
653
+ pDepth--;
654
+ else if (c === ":" && pDepth === 0)
655
+ return i;
656
+ i++;
158
657
  }
159
- return scopedLines.join("\n");
658
+ return -1;
160
659
  }
161
660
  /**
162
661
  * Common esbuild options shared between server and client builds
@@ -165,26 +664,60 @@ function commonBuildOptions() {
165
664
  return {
166
665
  bundle: true,
167
666
  jsx: "transform",
168
- jsxFactory: "h",
169
- jsxFragment: "Fragment",
667
+ jsxFactory: "__ikas_h",
668
+ jsxFragment: "__ikas_Fragment",
170
669
  inject: [PREACT_JSX_SHIM],
171
670
  tsconfigRaw: {
172
671
  compilerOptions: {
173
672
  jsx: "react",
174
- jsxFactory: "h",
175
- jsxFragmentFactory: "Fragment"
673
+ jsxFactory: "__ikas_h",
674
+ jsxFragmentFactory: "__ikas_Fragment"
176
675
  }
177
676
  },
178
677
  minify: true,
179
678
  sourcemap: false,
180
679
  };
181
680
  }
681
+ /**
682
+ * What `import.meta.env` becomes in a vendored build.
683
+ *
684
+ * The SDK reads Vite variables (`VITE_BASE_URL`, `VITE_PUBLIC_ANALYTICS_URL`, `VITE_ENV`). While it
685
+ * was external, the theme's own Vite build substituted them. Vendoring compiles it with esbuild
686
+ * instead, and `import.meta` is illegal inside the `new Function` the evaluator uses anyway.
687
+ *
688
+ * They are NOT baked in at publish time, even though that would be simpler. `VITE_BASE_URL` names
689
+ * the ikas API the *theme* talks to; a built-in login that authenticated against a different one
690
+ * would hand back a token the rest of the page cannot use. It has to be whatever the host says it
691
+ * is, so it is read from the host at evaluation time — configuration from the host, code from the
692
+ * build, which is the same line every other decision here follows.
693
+ *
694
+ * A host that never sets this gets a thrown TypeError on first evaluation, caught and reported per
695
+ * component. That is deliberate: the alternative, defaulting to `{}`, would silently produce
696
+ * `undefined/ms/google/login/authenticate` and fail as a confusing 404 much later.
697
+ */
698
+ const VENDORED_IMPORT_META_ENV = "globalThis.__ikasBuiltinEnv";
699
+ /**
700
+ * Build the resolution plugins shared by the server and client passes.
701
+ *
702
+ * The storefront plugin is either/or, never both: `forbidDeepStorefrontImportsPlugin` externalizes
703
+ * the SDK root, `vendorStorefrontPlugin` lets it resolve so it gets bundled. Running both would
704
+ * mean whichever registered its `onResolve` first decides, which is not a contract worth having.
705
+ */
706
+ function resolutionPlugins(vendorStorefront) {
707
+ return vendorStorefront
708
+ ? [
709
+ vendorStorefrontPlugin(),
710
+ forbidExternalPackagesPlugin({ allowFrom: isVendoredDependencyFile }),
711
+ ikasComponentUtilsPlugin(),
712
+ ]
713
+ : [forbidDeepStorefrontImportsPlugin(), forbidExternalPackagesPlugin(), ikasComponentUtilsPlugin()];
714
+ }
182
715
  /**
183
716
  * Compile all components together with esbuild code splitting.
184
717
  * Shared code is automatically extracted into chunk files.
185
718
  * When there's no shared code, output is identical to per-component compilation.
186
719
  */
187
- export async function compileAllComponents(components, globalStylesPath) {
720
+ export async function compileAllComponents(components, globalStylesPath, options) {
188
721
  if (components.length === 0) {
189
722
  return {
190
723
  success: true,
@@ -206,19 +739,33 @@ export async function compileAllComponents(components, globalStylesPath) {
206
739
  entryPoints[comp.id] = path.resolve(process.cwd(), comp.entryPath);
207
740
  }
208
741
  try {
209
- // Server build with code splitting
210
- await esbuild.build({
742
+ // Server build with code splitting (metafile enabled for dependency tracking)
743
+ const serverBuildResult = await esbuild.build({
211
744
  ...commonBuildOptions(),
212
745
  entryPoints,
213
746
  format: "esm",
214
- platform: "node",
215
- target: "node18",
747
+ // Vendoring flips the SSR build to browser resolution. Two reasons, both load-bearing:
748
+ //
749
+ // 1. `platform: "node"` picks the node flavour of a vendored dependency, which reaches for
750
+ // node builtins (`uuid` → `crypto`) that no storefront can hand a published artifact.
751
+ // The browser flavour uses `globalThis.crypto`, which SSR on Node 18+ has anyway.
752
+ // 2. esbuild escapes `</script` in string literals on browser and not on node. That single
753
+ // byte changes a chunk's content hash, so the two builds emitted different chunk names
754
+ // for identical code and every importer inherited the split — 154 of 251 chunks had no
755
+ // twin. Same platform, same hashes, one chunk graph.
756
+ //
757
+ // The build is still consumed by `preact-render-to-string` in Node; only module resolution
758
+ // changes, and SERVER_EXTERNALS keeps the SSR renderer external either way.
759
+ platform: options?.vendorStorefront ? "browser" : "node",
760
+ target: SHARED_BUILD_TARGET,
216
761
  external: SERVER_EXTERNALS,
217
- plugins: [ikasComponentUtilsPlugin()],
762
+ ...(options?.vendorStorefront ? { define: { "import.meta.env": VENDORED_IMPORT_META_ENV } } : {}),
763
+ plugins: resolutionPlugins(!!options?.vendorStorefront),
218
764
  splitting: true,
219
765
  outdir: serverDir,
220
766
  entryNames: "[name]",
221
767
  chunkNames: "chunk-[hash]",
768
+ metafile: true,
222
769
  });
223
770
  // Client build with code splitting (metafile enabled for dependency tracking)
224
771
  const clientBuildResult = await esbuild.build({
@@ -226,9 +773,10 @@ export async function compileAllComponents(components, globalStylesPath) {
226
773
  entryPoints,
227
774
  format: "esm",
228
775
  platform: "browser",
229
- target: "es2020",
776
+ target: SHARED_BUILD_TARGET,
230
777
  external: CLIENT_EXTERNALS,
231
- plugins: [ikasComponentUtilsPlugin()],
778
+ ...(options?.vendorStorefront ? { define: { "import.meta.env": VENDORED_IMPORT_META_ENV } } : {}),
779
+ plugins: resolutionPlugins(!!options?.vendorStorefront),
232
780
  splitting: true,
233
781
  outdir: clientDir,
234
782
  entryNames: "[name]",
@@ -269,17 +817,29 @@ export async function compileAllComponents(components, globalStylesPath) {
269
817
  chunks.set(key, { serverJs, clientJs });
270
818
  }
271
819
  }
272
- // Build dependency maps from metafile
273
- const metafile = clientBuildResult.metafile;
820
+ // Build dependency maps from BOTH the server and client metafiles.
821
+ //
822
+ // Only the client metafile used to be consulted here. But SERVER_EXTERNALS
823
+ // differs from CLIENT_EXTERNALS (e.g. preact-render-to-string is external on
824
+ // the server only), so esbuild splits the two builds differently and emits
825
+ // server-only shared chunks whose names have no client twin. Those chunks are
826
+ // written to disk and captured in `chunks` above, yet — with a client-only
827
+ // graph — they never entered any component's chunk closure, so publish pruned
828
+ // them as orphaned. The result: `compiledServerJs` went missing on the shared
829
+ // module and the server bundle imported a `./chunk-XXXX.js` that was never
830
+ // emitted ("Could not resolve ./chunk-XXXX.js"). Merging both graphs keeps
831
+ // server and client chunk closures symmetric.
832
+ const clientMetafile = clientBuildResult.metafile;
833
+ const serverMetafile = serverBuildResult.metafile;
274
834
  const entryDirSet = new Map(); // entryId → absolute dir of entry file
275
835
  for (const comp of components) {
276
836
  entryDirSet.set(comp.id, path.dirname(path.resolve(process.cwd(), comp.entryPath)));
277
837
  }
278
- // Map each output to its input source files
838
+ // CSS is a browser-only concern, so entry/chunk input source maps are derived
839
+ // from the CLIENT metafile only (they feed the CSS collection passes below).
279
840
  const entryInputs = new Map(); // entryId → list of input paths
280
841
  const chunkInputs = new Map(); // chunkId → list of input paths
281
- const chunkConsumers = new Map(); // chunkId → list of entry IDs that import it
282
- for (const [outputPath, outputMeta] of Object.entries(metafile.outputs)) {
842
+ for (const [outputPath, outputMeta] of Object.entries(clientMetafile.outputs)) {
283
843
  const baseName = path.basename(outputPath, ".js");
284
844
  const inputPaths = Object.keys(outputMeta.inputs || {});
285
845
  if (entryIds.has(baseName)) {
@@ -289,40 +849,49 @@ export async function compileAllComponents(components, globalStylesPath) {
289
849
  chunkInputs.set(baseName, inputPaths);
290
850
  }
291
851
  }
292
- // Determine chunk→consumers: which entries import each chunk
293
- for (const [outputPath, outputMeta] of Object.entries(metafile.outputs)) {
294
- const baseName = path.basename(outputPath, ".js");
295
- if (!entryIds.has(baseName))
296
- continue;
297
- const imports = outputMeta.imports || [];
298
- for (const imp of imports) {
299
- const impBaseName = path.basename(imp.path, ".js");
300
- if (chunkInputs.has(impBaseName)) {
301
- if (!chunkConsumers.has(impBaseName)) {
302
- chunkConsumers.set(impBaseName, []);
303
- }
304
- chunkConsumers.get(impBaseName).push(baseName);
305
- }
306
- }
307
- }
308
- // Build chunk→chunk dependency graph and topologically sort chunks
309
- // so that chunks are evaluated in dependency-first order
852
+ // Recognize any emitted chunk server OR client — by its output basename.
853
+ // `chunks` was populated above from both the server and client output dirs.
854
+ const allChunkIds = new Set(chunks.keys());
855
+ const chunkConsumers = new Map(); // chunkId → list of entry IDs that import it
310
856
  const chunkDeps = new Map(); // chunkId → list of chunk IDs it depends on
311
- for (const chunkId of chunkInputs.keys()) {
857
+ for (const chunkId of allChunkIds) {
312
858
  chunkDeps.set(chunkId, []);
313
859
  }
314
- for (const [outputPath, outputMeta] of Object.entries(metafile.outputs)) {
315
- const baseName = path.basename(outputPath, ".js");
316
- if (!chunkInputs.has(baseName))
317
- continue;
318
- const imports = outputMeta.imports || [];
319
- for (const imp of imports) {
320
- const impBaseName = path.basename(imp.path, ".js");
321
- if (chunkInputs.has(impBaseName) && impBaseName !== baseName) {
322
- chunkDeps.get(baseName).push(impBaseName);
860
+ // Fold one metafile's output graph into the shared consumer/dependency maps.
861
+ // Called for both builds so server-only chunks are tracked alongside client ones.
862
+ const ingestMetafile = (mf) => {
863
+ for (const [outputPath, outputMeta] of Object.entries(mf.outputs)) {
864
+ const baseName = path.basename(outputPath, ".js");
865
+ const imports = outputMeta.imports || [];
866
+ if (entryIds.has(baseName)) {
867
+ // entry chunk consumer edges
868
+ for (const imp of imports) {
869
+ const impBaseName = path.basename(imp.path, ".js");
870
+ if (!allChunkIds.has(impBaseName))
871
+ continue;
872
+ if (!chunkConsumers.has(impBaseName)) {
873
+ chunkConsumers.set(impBaseName, []);
874
+ }
875
+ const consumers = chunkConsumers.get(impBaseName);
876
+ if (!consumers.includes(baseName))
877
+ consumers.push(baseName);
878
+ }
879
+ }
880
+ else if (allChunkIds.has(baseName)) {
881
+ // chunk → chunk dependency edges
882
+ for (const imp of imports) {
883
+ const impBaseName = path.basename(imp.path, ".js");
884
+ if (allChunkIds.has(impBaseName) && impBaseName !== baseName) {
885
+ const deps = chunkDeps.get(baseName);
886
+ if (!deps.includes(impBaseName))
887
+ deps.push(impBaseName);
888
+ }
889
+ }
323
890
  }
324
891
  }
325
- }
892
+ };
893
+ ingestMetafile(clientMetafile);
894
+ ingestMetafile(serverMetafile);
326
895
  // Kahn's algorithm for topological sort
327
896
  const inDegree = new Map();
328
897
  for (const chunkId of chunkDeps.keys()) {
@@ -480,8 +1049,16 @@ export async function compileAllComponents(components, globalStylesPath) {
480
1049
  };
481
1050
  }
482
1051
  finally {
483
- // Clean up temp directory
484
- fs.rmSync(tmpBase, { recursive: true, force: true });
1052
+ // Clean up temp directory. Best-effort: on Windows an antivirus/indexer can briefly hold
1053
+ // handles on the freshly written output files, making rmSync throw EBUSY/EPERM — and a
1054
+ // throw here (inside finally) would replace a successful compilation result with an error.
1055
+ // A leaked tmp dir is harmless; retry a few times, then give up silently.
1056
+ try {
1057
+ fs.rmSync(tmpBase, { recursive: true, force: true, maxRetries: 3 });
1058
+ }
1059
+ catch {
1060
+ // ignore — never fail the build over tmp cleanup
1061
+ }
485
1062
  }
486
1063
  }
487
1064
  /**