@ikas/component-cli 1.4.0-beta.9 → 1.4.0-beta.91

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 (140) hide show
  1. package/dist/commands/add-sections-to-page.d.ts +3 -0
  2. package/dist/commands/add-sections-to-page.d.ts.map +1 -0
  3. package/dist/commands/add-sections-to-page.js +39 -0
  4. package/dist/commands/add-sections-to-page.js.map +1 -0
  5. package/dist/commands/add-to-page.d.ts +3 -0
  6. package/dist/commands/add-to-page.d.ts.map +1 -0
  7. package/dist/commands/add-to-page.js +41 -0
  8. package/dist/commands/add-to-page.js.map +1 -0
  9. package/dist/commands/build.d.ts.map +1 -1
  10. package/dist/commands/build.js +5 -165
  11. package/dist/commands/build.js.map +1 -1
  12. package/dist/commands/config.d.ts.map +1 -1
  13. package/dist/commands/config.js +112 -4
  14. package/dist/commands/config.js.map +1 -1
  15. package/dist/commands/create-design-tokens.d.ts +24 -0
  16. package/dist/commands/create-design-tokens.d.ts.map +1 -0
  17. package/dist/commands/create-design-tokens.js +297 -0
  18. package/dist/commands/create-design-tokens.js.map +1 -0
  19. package/dist/commands/create-global-variable.d.ts +3 -0
  20. package/dist/commands/create-global-variable.d.ts.map +1 -0
  21. package/dist/commands/create-global-variable.js +53 -0
  22. package/dist/commands/create-global-variable.js.map +1 -0
  23. package/dist/commands/create-page.d.ts +3 -0
  24. package/dist/commands/create-page.d.ts.map +1 -0
  25. package/dist/commands/create-page.js +31 -0
  26. package/dist/commands/create-page.js.map +1 -0
  27. package/dist/commands/delete-theme-globals.d.ts +4 -0
  28. package/dist/commands/delete-theme-globals.d.ts.map +1 -0
  29. package/dist/commands/delete-theme-globals.js +48 -0
  30. package/dist/commands/delete-theme-globals.js.map +1 -0
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +77 -49
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/get-component-props.d.ts +3 -0
  35. package/dist/commands/get-component-props.d.ts.map +1 -0
  36. package/dist/commands/get-component-props.js +32 -0
  37. package/dist/commands/get-component-props.js.map +1 -0
  38. package/dist/commands/get-page-by-type.d.ts +3 -0
  39. package/dist/commands/get-page-by-type.d.ts.map +1 -0
  40. package/dist/commands/get-page-by-type.js +25 -0
  41. package/dist/commands/get-page-by-type.js.map +1 -0
  42. package/dist/commands/get-section-values.d.ts +3 -0
  43. package/dist/commands/get-section-values.d.ts.map +1 -0
  44. package/dist/commands/get-section-values.js +39 -0
  45. package/dist/commands/get-section-values.js.map +1 -0
  46. package/dist/commands/import.d.ts +3 -0
  47. package/dist/commands/import.d.ts.map +1 -0
  48. package/dist/commands/import.js +25 -0
  49. package/dist/commands/import.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-imported.d.ts +3 -0
  55. package/dist/commands/list-imported.d.ts.map +1 -0
  56. package/dist/commands/list-imported.js +25 -0
  57. package/dist/commands/list-imported.js.map +1 -0
  58. package/dist/commands/list-page-sections.d.ts +3 -0
  59. package/dist/commands/list-page-sections.d.ts.map +1 -0
  60. package/dist/commands/list-page-sections.js +25 -0
  61. package/dist/commands/list-page-sections.js.map +1 -0
  62. package/dist/commands/list-pages.d.ts +3 -0
  63. package/dist/commands/list-pages.d.ts.map +1 -0
  64. package/dist/commands/list-pages.js +21 -0
  65. package/dist/commands/list-pages.js.map +1 -0
  66. package/dist/commands/list-theme-globals.d.ts +3 -0
  67. package/dist/commands/list-theme-globals.d.ts.map +1 -0
  68. package/dist/commands/list-theme-globals.js +22 -0
  69. package/dist/commands/list-theme-globals.js.map +1 -0
  70. package/dist/commands/publish-theme.d.ts +3 -0
  71. package/dist/commands/publish-theme.d.ts.map +1 -0
  72. package/dist/commands/publish-theme.js +29 -0
  73. package/dist/commands/publish-theme.js.map +1 -0
  74. package/dist/commands/search-products.d.ts +3 -0
  75. package/dist/commands/search-products.d.ts.map +1 -0
  76. package/dist/commands/search-products.js +40 -0
  77. package/dist/commands/search-products.js.map +1 -0
  78. package/dist/commands/update-global-variable.d.ts +3 -0
  79. package/dist/commands/update-global-variable.d.ts.map +1 -0
  80. package/dist/commands/update-global-variable.js +47 -0
  81. package/dist/commands/update-global-variable.js.map +1 -0
  82. package/dist/commands/update-page-sections.d.ts +3 -0
  83. package/dist/commands/update-page-sections.d.ts.map +1 -0
  84. package/dist/commands/update-page-sections.js +39 -0
  85. package/dist/commands/update-page-sections.js.map +1 -0
  86. package/dist/commands/update-section-prop.d.ts +3 -0
  87. package/dist/commands/update-section-prop.d.ts.map +1 -0
  88. package/dist/commands/update-section-prop.js +59 -0
  89. package/dist/commands/update-section-prop.js.map +1 -0
  90. package/dist/commands/upload-image.d.ts +3 -0
  91. package/dist/commands/upload-image.d.ts.map +1 -0
  92. package/dist/commands/upload-image.js +38 -0
  93. package/dist/commands/upload-image.js.map +1 -0
  94. package/dist/commands/upload-images.d.ts +3 -0
  95. package/dist/commands/upload-images.d.ts.map +1 -0
  96. package/dist/commands/upload-images.js +48 -0
  97. package/dist/commands/upload-images.js.map +1 -0
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +55 -0
  100. package/dist/index.js.map +1 -1
  101. package/dist/types.d.ts +11 -1
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/utils/compile.d.ts +4 -1
  104. package/dist/utils/compile.d.ts.map +1 -1
  105. package/dist/utils/compile.js +643 -67
  106. package/dist/utils/compile.js.map +1 -1
  107. package/dist/utils/component-helpers.d.ts +21 -3
  108. package/dist/utils/component-helpers.d.ts.map +1 -1
  109. package/dist/utils/component-helpers.js +111 -4
  110. package/dist/utils/component-helpers.js.map +1 -1
  111. package/dist/utils/editor-action-client.d.ts +28 -0
  112. package/dist/utils/editor-action-client.d.ts.map +1 -0
  113. package/dist/utils/editor-action-client.js +116 -0
  114. package/dist/utils/editor-action-client.js.map +1 -0
  115. package/dist/utils/forbid-deep-imports.d.ts +3 -0
  116. package/dist/utils/forbid-deep-imports.d.ts.map +1 -0
  117. package/dist/utils/forbid-deep-imports.js +54 -0
  118. package/dist/utils/forbid-deep-imports.js.map +1 -0
  119. package/dist/utils/forbid-external-packages.d.ts +16 -0
  120. package/dist/utils/forbid-external-packages.d.ts.map +1 -0
  121. package/dist/utils/forbid-external-packages.js +89 -0
  122. package/dist/utils/forbid-external-packages.js.map +1 -0
  123. package/dist/utils/load-image.d.ts +16 -0
  124. package/dist/utils/load-image.d.ts.map +1 -0
  125. package/dist/utils/load-image.js +50 -0
  126. package/dist/utils/load-image.js.map +1 -0
  127. package/dist/utils/websocket-server.d.ts +40 -2
  128. package/dist/utils/websocket-server.d.ts.map +1 -1
  129. package/dist/utils/websocket-server.js +59 -1
  130. package/dist/utils/websocket-server.js.map +1 -1
  131. package/package.json +1 -1
  132. package/templates/add/section.css +11 -0
  133. package/dist/commands/create.d.ts +0 -9
  134. package/dist/commands/create.d.ts.map +0 -1
  135. package/dist/commands/create.js +0 -9
  136. package/dist/commands/create.js.map +0 -1
  137. package/dist/commands/proxy.d.ts +0 -39
  138. package/dist/commands/proxy.d.ts.map +0 -1
  139. package/dist/commands/proxy.js +0 -212
  140. package/dist/commands/proxy.js.map +0 -1
@@ -4,33 +4,42 @@ 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";
7
9
  /**
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.
10
+ * Shim that provides the JSX factory/fragment under collision-proof names.
11
+ *
12
+ * The classic JSX pragma must be a bare identifier in scope. Using `h`/`Fragment`
13
+ * directly means any user-authored local named `h` or `Fragment` (e.g. a `.map`
14
+ * callback param) shadows the factory, so JSX in that scope compiles to a call on
15
+ * the local value → runtime "x is not a function". Aliasing to `__ikas_h` /
16
+ * `__ikas_Fragment` (names users won't write) eliminates that shadowing footgun.
17
+ *
18
+ * Unlike `banner` (appended after minification), `inject` is processed during
19
+ * compilation so the minifier will never reuse these names. The bundled import
20
+ * still references preact's `h`/`Fragment`, so the canvas esm-transform keying off
21
+ * those source export names is unaffected.
11
22
  */
12
23
  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');
24
+ fs.writeFileSync(PREACT_JSX_SHIM, 'export{h as __ikas_h,Fragment as __ikas_Fragment}from"preact";\n');
14
25
  /**
15
- * External dependencies - these will be provided by the generated storefront
26
+ * External dependencies - these will be provided by the generated storefront.
27
+ *
28
+ * The `@ikas/bp-storefront*` packages are intentionally NOT listed here: they are externalized
29
+ * by `forbidDeepStorefrontImportsPlugin()` instead, which externalizes the allowed ROOT import
30
+ * and rejects private deep subpaths. (esbuild applies this `external` array before plugin
31
+ * onResolve, so keeping them here would let `/dist/...` deep imports slip through unchecked.)
16
32
  */
17
33
  const SERVER_EXTERNALS = [
18
34
  "preact",
19
35
  "preact/hooks",
20
36
  "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"
37
+ "mobx"
26
38
  ];
27
39
  const CLIENT_EXTERNALS = [
28
40
  "preact",
29
41
  "preact/hooks",
30
- "mobx",
31
- "@ikas/bp-storefront",
32
- "@ikas/bp-storefront-models",
33
- "@ikas/bp-storefront-config"
42
+ "mobx"
34
43
  ];
35
44
  /**
36
45
  * Compile a single component's TSX and CSS
@@ -46,7 +55,11 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
46
55
  target: "node18",
47
56
  write: false,
48
57
  external: SERVER_EXTERNALS,
49
- plugins: [ikasComponentUtilsPlugin()],
58
+ plugins: [
59
+ forbidDeepStorefrontImportsPlugin(),
60
+ forbidExternalPackagesPlugin(),
61
+ ikasComponentUtilsPlugin(),
62
+ ],
50
63
  });
51
64
  // Build client bundle (for hydration)
52
65
  const clientResult = await esbuild.build({
@@ -57,7 +70,11 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
57
70
  target: "es2020",
58
71
  write: false,
59
72
  external: CLIENT_EXTERNALS,
60
- plugins: [ikasComponentUtilsPlugin()],
73
+ plugins: [
74
+ forbidDeepStorefrontImportsPlugin(),
75
+ forbidExternalPackagesPlugin(),
76
+ ikasComponentUtilsPlugin(),
77
+ ],
61
78
  });
62
79
  // Read CSS (with import resolution)
63
80
  const cssPath = path.resolve(process.cwd(), stylesPath);
@@ -86,11 +103,15 @@ export async function compileComponent(entryPath, stylesPath, componentId) {
86
103
  }
87
104
  }
88
105
  /**
89
- * Scope CSS selectors with a component-specific class prefix
106
+ * Scope CSS selectors with a component-specific class prefix, then namespace any
107
+ * document-global CSS identifiers (`@keyframes`, `@font-face` font-family,
108
+ * `@counter-style`) DEFINED in this component so two components — or the host theme —
109
+ * declaring the same name can't collide in the page's global namespace.
90
110
  */
91
111
  export function scopeCSS(css, componentId) {
92
112
  const scopeClass = `cc_${componentId.replace(/[^a-zA-Z0-9]/g, "_")}`;
93
- return scopeCSSWithClasses(css, [scopeClass]);
113
+ const scoped = scopeCSSWithClasses(css, [scopeClass]);
114
+ return renameCollidingGlobals(scoped, `${scopeClass}_`);
94
115
  }
95
116
  /**
96
117
  * Scope CSS selectors with multiple class prefixes (for shared chunks).
@@ -100,63 +121,519 @@ export function scopeCSSMulti(css, componentIds) {
100
121
  const scopeClasses = componentIds.map(id => `cc_${id.replace(/[^a-zA-Z0-9]/g, "_")}`);
101
122
  return scopeCSSWithClasses(css, scopeClasses);
102
123
  }
124
+ // Conditional group at-rules whose blocks nest normal style rules and must be
125
+ // scoped recursively. This is an ALLOWLIST on purpose: at-rules NOT listed here
126
+ // (@keyframes, @font-face, @page, @property, @counter-style, ...) have non-selector
127
+ // bodies — e.g. @keyframes contains `0%`/`from` keyframe selectors that must NOT be
128
+ // prefixed — so their bodies are copied through untouched. Vendor-prefixed conditional
129
+ // rules (@-moz-document) match via the optional prefix group; @-webkit-keyframes does
130
+ // NOT match (the alternation excludes "keyframes"), which is what we want.
131
+ //
132
+ // NOTE: This scoper is duplicated in editor-models/src/helpers/code-component-css-scope.ts
133
+ // (the editor-preview / code-generator path). The CLI ships standalone to npm and cannot
134
+ // import that private package, so the two intentionally diverge. Keep parsing fixes in sync.
135
+ const NESTING_AT_RULES = /^@(?:-\w+-)?(media|supports|container|layer|document|scope|starting-style)\b/i;
103
136
  /**
104
- * Internal: scope CSS selectors with one or more scope class prefixes
137
+ * Internal: scope CSS selectors with one or more scope class prefixes.
138
+ *
139
+ * Character-based walker, NOT line-based: a rule's selector list is everything up
140
+ * to its opening `{`, so lists written across multiple lines (`.a,\n.b { ... }`)
141
+ * are scoped as a whole. The previous line-based implementation only scoped the
142
+ * selector fragment sharing a line with `{`, leaving the other selectors unscoped
143
+ * (they either lost the cascade to scoped rules or leaked into other components).
144
+ *
145
+ * Comments, string literals, and `\` escapes are treated as inert spans (via
146
+ * `skipInertSpan`) by every structural scanner, so a `{`, `}`, `;`, or `,` that
147
+ * appears inside a comment / string / escape can never be mistaken for real
148
+ * structure (which would otherwise drop the scope prefix and leak styles globally).
105
149
  */
106
150
  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);
151
+ let out = "";
152
+ for (let i = 0; i < css.length;) {
153
+ const ch = css[i];
154
+ // Copy comments verbatim
155
+ if (ch === "/" && css[i + 1] === "*") {
156
+ const end = css.indexOf("*/", i + 2);
157
+ if (end === -1)
158
+ return out + css.slice(i);
159
+ out += css.slice(i, end + 2);
160
+ i = end + 2;
115
161
  continue;
116
162
  }
117
- if (trimmed.startsWith("@media")) {
118
- inMediaQuery = true;
119
- scopedLines.push(line);
163
+ // Copy whitespace and stray closing braces
164
+ if (/\s/.test(ch) || ch === "}") {
165
+ out += ch;
166
+ i++;
120
167
  continue;
121
168
  }
122
- if (trimmed.startsWith("@keyframes") || trimmed.startsWith("@-webkit-keyframes")) {
123
- keyframesDepth = 1;
124
- scopedLines.push(line);
169
+ if (ch === "@") {
170
+ const headerEnd = findAtRuleHeaderEnd(css, i);
171
+ // Block-less at-rule (@import, @charset, @layer a;, ...) — copy through
172
+ if (headerEnd >= css.length || css[headerEnd] === ";") {
173
+ out += css.slice(i, Math.min(headerEnd + 1, css.length));
174
+ i = headerEnd + 1;
175
+ continue;
176
+ }
177
+ const header = css.slice(i, headerEnd);
178
+ const blockEnd = findBlockEnd(css, headerEnd + 1);
179
+ const body = css.slice(headerEnd + 1, blockEnd);
180
+ const transformed = NESTING_AT_RULES.test(header.trim())
181
+ ? scopeCSSWithClasses(body, scopeClasses)
182
+ : body;
183
+ // Always emit the closing brace: when the source block is unterminated
184
+ // (blockEnd === css.length) this repairs it, so concatenated component CSS
185
+ // can't leak one component's block over the next.
186
+ out += `${header}{${transformed}}`;
187
+ i = blockEnd + 1;
188
+ continue;
189
+ }
190
+ // Normal style rule: selector list runs up to the rule's opening `{`
191
+ const selEnd = findSelectorListEnd(css, i);
192
+ if (selEnd >= css.length) {
193
+ // Trailing content with no block (e.g. a dangling declaration at EOF) — copy through
194
+ out += css.slice(i);
195
+ break;
196
+ }
197
+ if (css[selEnd] === ";") {
198
+ // A stray top-level `;` (or a stray declaration fragment ending in `;`) — not a rule.
199
+ // DROP it. Copying it through is not enough: per the CSS syntax spec a browser folds a
200
+ // stray `;` into the NEXT rule's prelude when consuming a qualified rule, which makes
201
+ // that selector invalid and drops the whole following rule. Dropping the stray token
202
+ // keeps the next rule valid and correctly scoped.
203
+ i = selEnd + 1;
204
+ continue;
205
+ }
206
+ if (css[selEnd] === "}") {
207
+ // Content before a stray closing brace — copy through verbatim and let the main loop
208
+ // emit the `}`.
209
+ out += css.slice(i, selEnd);
210
+ i = selEnd;
211
+ continue;
212
+ }
213
+ const selectorList = css.slice(i, selEnd);
214
+ const blockEnd = findBlockEnd(css, selEnd + 1);
215
+ const body = css.slice(selEnd + 1, blockEnd);
216
+ const scopedSelector = splitSelectorList(selectorList)
217
+ .map(s => s.trim())
218
+ .filter(Boolean)
219
+ .flatMap(s => scopeClasses.map(cls => `.${cls} ${s}`))
220
+ .join(", ");
221
+ // Always emit the closing brace (see at-rule branch above for rationale).
222
+ out += `${scopedSelector} {${body}}`;
223
+ i = blockEnd + 1;
224
+ }
225
+ return out;
226
+ }
227
+ /**
228
+ * If an inert span — a `/* ... *\/` comment, a `"..."` / `'...'` string literal, an
229
+ * unquoted `url(...)` token, or a `\x` escape — starts at `s[i]`, return the index just
230
+ * past it; otherwise return -1.
231
+ *
232
+ * Every structural scanner below funnels through this so that delimiters (`{ } ; , ( )`)
233
+ * living inside comments, strings, urls, or escapes are never treated as structure. String
234
+ * scanning honors `\` escapes and stops at a raw newline, matching CSS "bad-string" error
235
+ * recovery (an unterminated string ends at the line break) so a missing quote can't
236
+ * swallow every rule that follows it.
237
+ */
238
+ function skipInertSpan(s, i) {
239
+ const c = s[i];
240
+ if (c === "/" && s[i + 1] === "*") {
241
+ const end = s.indexOf("*/", i + 2);
242
+ return end === -1 ? s.length : end + 2;
243
+ }
244
+ if (c === '"' || c === "'") {
245
+ for (let j = i + 1; j < s.length; j++) {
246
+ const d = s[j];
247
+ if (d === "\\") {
248
+ j++; // escape: skip the next char
249
+ continue;
250
+ }
251
+ if (d === c)
252
+ return j + 1; // closing quote
253
+ if (d === "\n" || d === "\r" || d === "\f")
254
+ return j; // bad-string ends at newline
255
+ }
256
+ return s.length;
257
+ }
258
+ // Unquoted url(...) token: per the CSS tokenizer its content is opaque, so braces,
259
+ // semicolons, and parens inside it must NOT be read as structure (an unbalanced `}` in
260
+ // `url(x}y.png)` or `{` in an inline SVG data-uri would otherwise mis-terminate the
261
+ // enclosing block). A QUOTED url (`url("...")`) is a normal function + string and is left
262
+ // to the string branch above. `url` must be a standalone ident, not the tail of a longer
263
+ // identifier like `myurl(`.
264
+ if ((c === "u" || c === "U") && /^url\(/i.test(s.slice(i, i + 4))) {
265
+ const prev = i > 0 ? s[i - 1] : "";
266
+ if (!/[A-Za-z0-9_-]/.test(prev)) {
267
+ let j = i + 4;
268
+ while (j < s.length && /\s/.test(s[j]))
269
+ j++; // optional leading whitespace
270
+ if (s[j] !== '"' && s[j] !== "'") {
271
+ for (; j < s.length; j++) {
272
+ if (s[j] === "\\") {
273
+ j++; // escaped char inside the url (e.g. `url(foo\)bar)`)
274
+ continue;
275
+ }
276
+ if (s[j] === ")")
277
+ return j + 1; // end of url token
278
+ }
279
+ return s.length; // unterminated url() — consume to EOF (bad-url)
280
+ }
281
+ }
282
+ }
283
+ if (c === "\\")
284
+ return i + 2; // escaped char outside a string (e.g. `.a\,b`)
285
+ return -1;
286
+ }
287
+ /** Find the first top-level `{` or `;` that ends an at-rule's header. Respects `()` / `[]`
288
+ * and inert spans (strings/comments/escapes). */
289
+ function findAtRuleHeaderEnd(css, start) {
290
+ let pDepth = 0;
291
+ for (let j = start; j < css.length;) {
292
+ const skip = skipInertSpan(css, j);
293
+ if (skip !== -1) {
294
+ j = skip;
295
+ continue;
296
+ }
297
+ const c = css[j];
298
+ if (c === "(" || c === "[")
299
+ pDepth++;
300
+ else if (c === ")" || c === "]")
301
+ pDepth--;
302
+ else if (pDepth === 0 && (c === "{" || c === ";"))
303
+ return j;
304
+ j++;
305
+ }
306
+ return css.length;
307
+ }
308
+ /**
309
+ * Find the matching `}` for the block whose body starts at `blockStart` (char after `{`).
310
+ * Inert spans are skipped so braces inside `content: "}"`, comments, or escapes don't
311
+ * terminate the block early.
312
+ */
313
+ function findBlockEnd(css, blockStart) {
314
+ let depth = 1;
315
+ for (let j = blockStart; j < css.length;) {
316
+ const skip = skipInertSpan(css, j);
317
+ if (skip !== -1) {
318
+ j = skip;
319
+ continue;
320
+ }
321
+ const c = css[j];
322
+ if (c === "{")
323
+ depth++;
324
+ else if (c === "}") {
325
+ depth--;
326
+ if (depth === 0)
327
+ return j;
328
+ }
329
+ j++;
330
+ }
331
+ return css.length;
332
+ }
333
+ /** Find the `{`, `}`, or `;` that terminates a selector list. A `}` or `;` here means the
334
+ * run was not a real style rule. Respects `()` / `[]` and inert spans. */
335
+ function findSelectorListEnd(css, start) {
336
+ let pDepth = 0;
337
+ for (let j = start; j < css.length;) {
338
+ const skip = skipInertSpan(css, j);
339
+ if (skip !== -1) {
340
+ j = skip;
341
+ continue;
342
+ }
343
+ const c = css[j];
344
+ if (c === "(" || c === "[")
345
+ pDepth++;
346
+ else if (c === ")" || c === "]")
347
+ pDepth--;
348
+ else if (pDepth === 0 && (c === "{" || c === "}" || c === ";"))
349
+ return j;
350
+ j++;
351
+ }
352
+ return css.length;
353
+ }
354
+ /** Split a selector list on top-level commas. Respects `()` / `[]` and inert spans, so a
355
+ * comma inside a comment, string, or `\` escape (e.g. `.a\,b`) does not split the list. */
356
+ function splitSelectorList(s) {
357
+ const result = [];
358
+ let depth = 0;
359
+ let start = 0;
360
+ for (let i = 0; i < s.length;) {
361
+ const skip = skipInertSpan(s, i);
362
+ if (skip !== -1) {
363
+ i = skip;
364
+ continue;
365
+ }
366
+ const ch = s[i];
367
+ if (ch === "(" || ch === "[")
368
+ depth++;
369
+ else if (ch === ")" || ch === "]")
370
+ depth--;
371
+ else if (ch === "," && depth === 0) {
372
+ result.push(s.slice(start, i));
373
+ start = i + 1;
374
+ }
375
+ i++;
376
+ }
377
+ result.push(s.slice(start));
378
+ return result;
379
+ }
380
+ /* ============================================================================
381
+ * Global-identifier namespacing.
382
+ *
383
+ * `@keyframes`, `@font-face` font-family names, and `@counter-style` names live in a
384
+ * DOCUMENT-GLOBAL namespace — scoping selectors with `.cc_<id>` does not isolate them, so
385
+ * two components (or the theme) defining the same name would collide (last definition wins
386
+ * document-wide). `renameCollidingGlobals` prefixes every such name DEFINED in this CSS,
387
+ * plus its references within the SAME CSS, with the component's scope prefix.
388
+ *
389
+ * Only names defined in THIS css are renamed — a reference to a name defined elsewhere
390
+ * (global.css, a shared chunk, the theme) is left untouched, so cross-file references keep
391
+ * resolving. Shared-chunk CSS (scopeCSSMulti) is intentionally NOT renamed, to avoid
392
+ * breaking a chunk-defined keyframe referenced from a consuming component's own CSS.
393
+ *
394
+ * NOTE: ported from packages/editor-models/src/helpers/code-component-css-scope.ts
395
+ * (renameCollidingGlobals + helpers, the global.css path's `cc_<projectId>_` variant).
396
+ * Keep parsing/rename fixes in sync between the two.
397
+ * ============================================================================ */
398
+ // Declarations whose VALUE may reference a global identifier of the given type.
399
+ const KEYFRAME_REF_PROPS = new Set(["animation", "animation-name"]);
400
+ const FONT_FAMILY_REF_PROPS = new Set(["font", "font-family"]);
401
+ const COUNTER_STYLE_REF_PROPS = new Set(["list-style", "list-style-type"]);
402
+ function renameCollidingGlobals(css, prefix) {
403
+ // Comment/string/url-masked copy: defining-site regex scans run against this so a name
404
+ // inside a comment or `content: "..."` string can't contribute a rename or be rewritten.
405
+ const scanCss = maskInertSpans(css);
406
+ const keyframeNames = new Set();
407
+ const fontFamilyNames = new Set();
408
+ const counterStyleNames = new Set();
409
+ let out = css;
410
+ out = out.replace(/@keyframes(\s+)([\w-]+)/g, (m, ws, name, offset) => {
411
+ if (scanCss.substr(offset, 10) !== "@keyframes")
412
+ return m;
413
+ keyframeNames.add(name);
414
+ return `@keyframes${ws}${prefix}${name}`;
415
+ });
416
+ out = out.replace(/@counter-style(\s+)([\w-]+)/g, (m, ws, name, offset) => {
417
+ if (scanCss.substr(offset, 14) !== "@counter-style")
418
+ return m;
419
+ counterStyleNames.add(name);
420
+ return `@counter-style${ws}${prefix}${name}`;
421
+ });
422
+ out = rewriteFontFaceBlocks(out, fontFamilyNames, prefix);
423
+ if (keyframeNames.size === 0 && fontFamilyNames.size === 0 && counterStyleNames.size === 0) {
424
+ return out;
425
+ }
426
+ return rewriteDeclarationReferences(out, keyframeNames, fontFamilyNames, counterStyleNames, prefix);
427
+ }
428
+ /** Replace every inert span (comment / string / url() / escape) with equal-length spaces so
429
+ * position-based regex scans still align but can't match tokens hidden inside them. */
430
+ function maskInertSpans(css) {
431
+ let out = "";
432
+ for (let i = 0; i < css.length;) {
433
+ const skip = skipInertSpan(css, i);
434
+ if (skip !== -1) {
435
+ out += " ".repeat(skip - i);
436
+ i = skip;
125
437
  continue;
126
438
  }
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--;
439
+ out += css[i];
440
+ i++;
441
+ }
442
+ return out;
443
+ }
444
+ function rewriteFontFaceBlocks(css, collect, prefix) {
445
+ const matches = [];
446
+ css.replace(/@font-face\s*\{/g, (m, offset) => {
447
+ matches.push({ index: offset, length: m.length });
448
+ return m;
449
+ });
450
+ if (matches.length === 0)
451
+ return css;
452
+ let out = "";
453
+ let lastIdx = 0;
454
+ for (const match of matches) {
455
+ const openBraceIdx = match.index + match.length - 1;
456
+ if (openBraceIdx < lastIdx)
457
+ continue;
458
+ const endIdx = findBlockEnd(css, openBraceIdx + 1);
459
+ const before = css.slice(lastIdx, openBraceIdx + 1);
460
+ const body = css.slice(openBraceIdx + 1, endIdx);
461
+ const rewrittenBody = body.replace(/(font-family\s*:\s*)(?:"([^"]+)"|'([^']+)'|([\w-]+))/i, (_m, pre, dq, sq, bare) => {
462
+ const name = dq || sq || bare;
463
+ if (!name)
464
+ return _m;
465
+ collect.add(name);
466
+ const prefixed = `${prefix}${name}`;
467
+ if (dq)
468
+ return `${pre}"${prefixed}"`;
469
+ if (sq)
470
+ return `${pre}'${prefixed}'`;
471
+ return `${pre}${prefixed}`;
472
+ });
473
+ out += before + rewrittenBody + "}";
474
+ lastIdx = endIdx + 1;
475
+ }
476
+ out += css.slice(lastIdx);
477
+ return out;
478
+ }
479
+ function rewriteDeclarationReferences(css, keyframeNames, fontFamilyNames, counterStyleNames, prefix) {
480
+ const keyframeRe = buildBareIdentRegex(keyframeNames);
481
+ const fontFamilyRe = buildFontFamilyValueRegex(fontFamilyNames);
482
+ const counterStyleRe = buildBareIdentRegex(counterStyleNames);
483
+ // counter(<name>, <style>?) / counters(<name>, <string>, <style>?) — the counter-style
484
+ // identifier, when present, is always the LAST optional argument.
485
+ const counterFnRe = counterStyleNames.size > 0
486
+ ? new RegExp(`(\\bcounters?\\([^)]*?,\\s*)(${bareUnion(counterStyleNames)})(\\s*\\))`, "gi")
487
+ : null;
488
+ return mapRuleBlocks(css, block => {
489
+ return mapDeclarations(block, (prop, value) => {
490
+ let v = value;
491
+ const lower = prop.toLowerCase();
492
+ if (keyframeRe && KEYFRAME_REF_PROPS.has(lower)) {
493
+ v = v.replace(keyframeRe, m => `${prefix}${m}`);
134
494
  }
135
- scopedLines.push(line);
495
+ if (fontFamilyRe && FONT_FAMILY_REF_PROPS.has(lower)) {
496
+ v = v.replace(fontFamilyRe, (m, dq, sq, bare) => {
497
+ const name = dq || sq || bare;
498
+ if (!name || !fontFamilyNames.has(name))
499
+ return m;
500
+ if (dq !== undefined)
501
+ return `"${prefix}${name}"`;
502
+ if (sq !== undefined)
503
+ return `'${prefix}${name}'`;
504
+ return `${prefix}${name}`;
505
+ });
506
+ }
507
+ if (counterStyleRe && COUNTER_STYLE_REF_PROPS.has(lower)) {
508
+ v = v.replace(counterStyleRe, m => `${prefix}${m}`);
509
+ }
510
+ if (counterFnRe) {
511
+ v = v.replace(counterFnRe, (_m, pre, name, post) => `${pre}${prefix}${name}${post}`);
512
+ }
513
+ return v;
514
+ });
515
+ });
516
+ }
517
+ function bareUnion(names) {
518
+ return Array.from(names).map(escapeRegex).join("|");
519
+ }
520
+ function buildBareIdentRegex(names) {
521
+ if (names.size === 0)
522
+ return null;
523
+ return new RegExp(`(?<![\\w-])(?:${bareUnion(names)})(?![\\w-])`, "g");
524
+ }
525
+ function buildFontFamilyValueRegex(names) {
526
+ if (names.size === 0)
527
+ return null;
528
+ const u = bareUnion(names);
529
+ return new RegExp(`"(${u})"|'(${u})'|(?<![\\w-])(${u})(?![\\w-])`, "g");
530
+ }
531
+ function escapeRegex(s) {
532
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
533
+ }
534
+ /**
535
+ * Walk every top-level rule block `{ ... }` (recursing into nested at-rule contents) and
536
+ * pass the block body through `transform`. Inert spans are skipped so a `{` inside a string
537
+ * or `url(a{b)` is not mistaken for a block. At-rule preambles and inter-rule whitespace
538
+ * are left untouched.
539
+ */
540
+ function mapRuleBlocks(css, transform) {
541
+ let out = "";
542
+ for (let i = 0; i < css.length;) {
543
+ const skip = skipInertSpan(css, i);
544
+ if (skip !== -1) {
545
+ out += css.slice(i, skip);
546
+ i = skip;
136
547
  continue;
137
548
  }
138
- if (trimmed === "}" && inMediaQuery) {
139
- inMediaQuery = false;
140
- scopedLines.push(line);
549
+ if (css[i] === "{") {
550
+ const blockEnd = findBlockEnd(css, i + 1);
551
+ const body = css.slice(i + 1, blockEnd);
552
+ const processed = hasTopLevelBrace(body) ? mapRuleBlocks(body, transform) : transform(body);
553
+ out += "{" + processed + "}";
554
+ i = blockEnd + 1;
141
555
  continue;
142
556
  }
143
- if (!trimmed.includes("{") || trimmed.startsWith("@")) {
144
- scopedLines.push(line);
557
+ out += css[i];
558
+ i++;
559
+ }
560
+ return out;
561
+ }
562
+ /** Inert-span-aware: true iff a real `{` appears outside comments/strings/url()/escapes. */
563
+ function hasTopLevelBrace(s) {
564
+ for (let i = 0; i < s.length;) {
565
+ const skip = skipInertSpan(s, i);
566
+ if (skip !== -1) {
567
+ i = skip;
568
+ continue;
569
+ }
570
+ if (s[i] === "{")
571
+ return true;
572
+ i++;
573
+ }
574
+ return false;
575
+ }
576
+ /**
577
+ * Split a rule block body into `prop: value` declarations on top-level `;`, apply
578
+ * `transform(prop, value)` to each, and rejoin preserving separators. Inert spans are
579
+ * skipped so a `;` inside a string or `url(a;b)` doesn't split a declaration.
580
+ */
581
+ function mapDeclarations(body, transform) {
582
+ const parts = [];
583
+ let start = 0;
584
+ let pDepth = 0;
585
+ for (let i = 0; i < body.length;) {
586
+ const skip = skipInertSpan(body, i);
587
+ if (skip !== -1) {
588
+ i = skip;
145
589
  continue;
146
590
  }
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}`);
591
+ const c = body[i];
592
+ if (c === "(" || c === "[" || c === "{")
593
+ pDepth++;
594
+ else if (c === ")" || c === "]" || c === "}")
595
+ pDepth--;
596
+ else if (c === ";" && pDepth === 0) {
597
+ parts.push(body.slice(start, i));
598
+ start = i + 1;
154
599
  }
155
- else {
156
- scopedLines.push(line);
600
+ i++;
601
+ }
602
+ parts.push(body.slice(start));
603
+ return parts
604
+ .map((part, idx) => {
605
+ const isLast = idx === parts.length - 1;
606
+ const colonIdx = findTopLevelColon(part);
607
+ if (colonIdx === -1)
608
+ return part + (isLast ? "" : ";");
609
+ const valueRaw = part.slice(colonIdx + 1);
610
+ const leading = valueRaw.match(/^\s*/)?.[0] ?? "";
611
+ const trailing = valueRaw.match(/\s*$/)?.[0] ?? "";
612
+ const prop = part.slice(0, colonIdx).trim();
613
+ const value = valueRaw.slice(leading.length, valueRaw.length - trailing.length);
614
+ const transformed = transform(prop, value);
615
+ return part.slice(0, colonIdx) + ":" + leading + transformed + trailing + (isLast ? "" : ";");
616
+ })
617
+ .join("");
618
+ }
619
+ function findTopLevelColon(s) {
620
+ let pDepth = 0;
621
+ for (let i = 0; i < s.length;) {
622
+ const skip = skipInertSpan(s, i);
623
+ if (skip !== -1) {
624
+ i = skip;
625
+ continue;
157
626
  }
627
+ const c = s[i];
628
+ if (c === "(" || c === "[")
629
+ pDepth++;
630
+ else if (c === ")" || c === "]")
631
+ pDepth--;
632
+ else if (c === ":" && pDepth === 0)
633
+ return i;
634
+ i++;
158
635
  }
159
- return scopedLines.join("\n");
636
+ return -1;
160
637
  }
161
638
  /**
162
639
  * Common esbuild options shared between server and client builds
@@ -165,20 +642,89 @@ function commonBuildOptions() {
165
642
  return {
166
643
  bundle: true,
167
644
  jsx: "transform",
168
- jsxFactory: "h",
169
- jsxFragment: "Fragment",
645
+ jsxFactory: "__ikas_h",
646
+ jsxFragment: "__ikas_Fragment",
170
647
  inject: [PREACT_JSX_SHIM],
171
648
  tsconfigRaw: {
172
649
  compilerOptions: {
173
650
  jsx: "react",
174
- jsxFactory: "h",
175
- jsxFragmentFactory: "Fragment"
651
+ jsxFactory: "__ikas_h",
652
+ jsxFragmentFactory: "__ikas_Fragment"
176
653
  }
177
654
  },
178
655
  minify: true,
179
656
  sourcemap: false,
180
657
  };
181
658
  }
659
+ /**
660
+ * Analyze an esbuild metafile to extract the chunk dependency graph:
661
+ * - directEntryChunks: entryId → set of chunks the entry imports directly
662
+ * - chunkDeps: chunkId → list of chunks it imports
663
+ * - sortedChunkIds: chunks in dependency-first (topological) order
664
+ *
665
+ * Used to track each build (server and client) independently, since the two
666
+ * builds split into differently-named `chunk-[hash]` files.
667
+ */
668
+ function analyzeChunkGraph(metafile, entryIds) {
669
+ const chunkNames = new Set();
670
+ for (const outputPath of Object.keys(metafile.outputs)) {
671
+ const baseName = path.basename(outputPath, ".js");
672
+ if (!entryIds.has(baseName) && baseName.startsWith("chunk-")) {
673
+ chunkNames.add(baseName);
674
+ }
675
+ }
676
+ const directEntryChunks = new Map();
677
+ const chunkDeps = new Map();
678
+ for (const chunkId of chunkNames)
679
+ chunkDeps.set(chunkId, []);
680
+ for (const [outputPath, outputMeta] of Object.entries(metafile.outputs)) {
681
+ const baseName = path.basename(outputPath, ".js");
682
+ const imports = outputMeta.imports || [];
683
+ if (entryIds.has(baseName)) {
684
+ const set = directEntryChunks.get(baseName) || new Set();
685
+ for (const imp of imports) {
686
+ const impBase = path.basename(imp.path, ".js");
687
+ if (chunkNames.has(impBase))
688
+ set.add(impBase);
689
+ }
690
+ directEntryChunks.set(baseName, set);
691
+ }
692
+ else if (chunkNames.has(baseName)) {
693
+ for (const imp of imports) {
694
+ const impBase = path.basename(imp.path, ".js");
695
+ if (chunkNames.has(impBase) && impBase !== baseName) {
696
+ chunkDeps.get(baseName).push(impBase);
697
+ }
698
+ }
699
+ }
700
+ }
701
+ // Topological sort: dependency-first (a chunk appears after the chunks it imports).
702
+ const depInDegree = new Map();
703
+ for (const [chunkId, deps] of chunkDeps)
704
+ depInDegree.set(chunkId, deps.length);
705
+ const queue = [];
706
+ for (const [chunkId, deg] of depInDegree)
707
+ if (deg === 0)
708
+ queue.push(chunkId);
709
+ const sortedChunkIds = [];
710
+ while (queue.length > 0) {
711
+ const current = queue.shift();
712
+ sortedChunkIds.push(current);
713
+ for (const [chunkId, deps] of chunkDeps) {
714
+ if (deps.includes(current)) {
715
+ depInDegree.set(chunkId, depInDegree.get(chunkId) - 1);
716
+ if (depInDegree.get(chunkId) === 0)
717
+ queue.push(chunkId);
718
+ }
719
+ }
720
+ }
721
+ // Cycle fallback — append anything not yet emitted (shouldn't happen).
722
+ for (const chunkId of chunkDeps.keys()) {
723
+ if (!sortedChunkIds.includes(chunkId))
724
+ sortedChunkIds.push(chunkId);
725
+ }
726
+ return { directEntryChunks, chunkDeps, sortedChunkIds };
727
+ }
182
728
  /**
183
729
  * Compile all components together with esbuild code splitting.
184
730
  * Shared code is automatically extracted into chunk files.
@@ -206,19 +752,30 @@ export async function compileAllComponents(components, globalStylesPath) {
206
752
  entryPoints[comp.id] = path.resolve(process.cwd(), comp.entryPath);
207
753
  }
208
754
  try {
209
- // Server build with code splitting
210
- await esbuild.build({
755
+ // Server build with code splitting.
756
+ // metafile is enabled here too: the server build splits independently from the
757
+ // client build (different platform/externals → different `chunk-[hash]` names),
758
+ // so server-only chunks must be tracked separately. Otherwise they never make it
759
+ // into any entry's `entryChunkIds` → `usedSharedModuleIds`, get pruned from the
760
+ // persisted theme, and the storefront build fails with
761
+ // `Could not resolve "./chunk-XXXX.js" from "component.server.js"`.
762
+ const serverBuildResult = await esbuild.build({
211
763
  ...commonBuildOptions(),
212
764
  entryPoints,
213
765
  format: "esm",
214
766
  platform: "node",
215
767
  target: "node18",
216
768
  external: SERVER_EXTERNALS,
217
- plugins: [ikasComponentUtilsPlugin()],
769
+ plugins: [
770
+ forbidDeepStorefrontImportsPlugin(),
771
+ forbidExternalPackagesPlugin(),
772
+ ikasComponentUtilsPlugin(),
773
+ ],
218
774
  splitting: true,
219
775
  outdir: serverDir,
220
776
  entryNames: "[name]",
221
777
  chunkNames: "chunk-[hash]",
778
+ metafile: true,
222
779
  });
223
780
  // Client build with code splitting (metafile enabled for dependency tracking)
224
781
  const clientBuildResult = await esbuild.build({
@@ -228,7 +785,11 @@ export async function compileAllComponents(components, globalStylesPath) {
228
785
  platform: "browser",
229
786
  target: "es2020",
230
787
  external: CLIENT_EXTERNALS,
231
- plugins: [ikasComponentUtilsPlugin()],
788
+ plugins: [
789
+ forbidDeepStorefrontImportsPlugin(),
790
+ forbidExternalPackagesPlugin(),
791
+ ikasComponentUtilsPlugin(),
792
+ ],
232
793
  splitting: true,
233
794
  outdir: clientDir,
234
795
  entryNames: "[name]",
@@ -449,22 +1010,37 @@ export async function compileAllComponents(components, globalStylesPath) {
449
1010
  directEntryChunks.get(entryId)?.add(chunkId);
450
1011
  }
451
1012
  }
1013
+ // The client metafile only knows about CLIENT chunks. The server build splits
1014
+ // independently (different names), so analyze its metafile too and fold the
1015
+ // server-side entry→chunk closure into `entryChunkIds`. Chunk name spaces are
1016
+ // disjoint between the two builds, so the graphs simply merge.
1017
+ const serverGraph = analyzeChunkGraph(serverBuildResult.metafile, entryIds);
1018
+ const mergedChunkDeps = new Map();
1019
+ for (const [k, v] of chunkDeps)
1020
+ mergedChunkDeps.set(k, v);
1021
+ for (const [k, v] of serverGraph.chunkDeps)
1022
+ mergedChunkDeps.set(k, v);
1023
+ // Combined dependency-first order (client chunks then server chunks; disjoint).
1024
+ const combinedSortedChunkIds = [...sortedChunkIds, ...serverGraph.sortedChunkIds];
452
1025
  const entryChunkIds = new Map();
453
1026
  for (const comp of components) {
454
1027
  const reachable = new Set();
455
- const queue = Array.from(directEntryChunks.get(comp.id) || []);
1028
+ const queue = [
1029
+ ...(directEntryChunks.get(comp.id) || []),
1030
+ ...(serverGraph.directEntryChunks.get(comp.id) || []),
1031
+ ];
456
1032
  while (queue.length > 0) {
457
1033
  const next = queue.shift();
458
1034
  if (reachable.has(next))
459
1035
  continue;
460
1036
  reachable.add(next);
461
- for (const dep of chunkDeps.get(next) || []) {
1037
+ for (const dep of mergedChunkDeps.get(next) || []) {
462
1038
  if (!reachable.has(dep))
463
1039
  queue.push(dep);
464
1040
  }
465
1041
  }
466
1042
  // Emit in topo order so consumers can evaluate dependency-first.
467
- entryChunkIds.set(comp.id, sortedChunkIds.filter(id => reachable.has(id)));
1043
+ entryChunkIds.set(comp.id, combinedSortedChunkIds.filter(id => reachable.has(id)));
468
1044
  }
469
1045
  return { success: true, entries, chunks, css, chunkCss, globalCss, entryChunkIds };
470
1046
  }