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