veryfront 0.1.26 → 0.1.28
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/README.md +3 -11
- package/esm/cli/app/shell.d.ts.map +1 -1
- package/esm/cli/app/shell.js +9 -5
- package/esm/cli/commands/demo/demo.js +1 -1
- package/esm/cli/commands/init/catalog.d.ts.map +1 -1
- package/esm/cli/commands/init/catalog.js +13 -5
- package/esm/cli/commands/init/command-help.js +4 -4
- package/esm/cli/commands/init/types.d.ts +1 -1
- package/esm/cli/commands/init/types.d.ts.map +1 -1
- package/esm/cli/commands/serve/command.d.ts.map +1 -1
- package/esm/cli/commands/serve/command.js +0 -4
- package/esm/cli/commands/start/command.d.ts.map +1 -1
- package/esm/cli/commands/start/command.js +16 -9
- package/esm/cli/help/tips.js +6 -6
- package/esm/cli/mcp/remote-file-tools.js +1 -1
- package/esm/cli/mcp/tools/catalog-tools.d.ts +3 -3
- package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
- package/esm/cli/mcp/tools/catalog-tools.js +21 -13
- package/esm/cli/mcp/tools/project-tools.js +1 -1
- package/esm/cli/templates/index.js +11 -11
- package/esm/cli/templates/manifest.d.ts +22 -15
- package/esm/cli/templates/manifest.js +24 -17
- package/esm/cli/templates/types.d.ts +1 -1
- package/esm/cli/templates/types.d.ts.map +1 -1
- package/esm/cli/utils/index.d.ts.map +1 -1
- package/esm/cli/utils/index.js +13 -1
- package/esm/deno.js +1 -1
- package/esm/src/html/html-shell-generator.d.ts.map +1 -1
- package/esm/src/html/html-shell-generator.js +2 -0
- package/esm/src/html/styles-builder/project-css-cache.d.ts +8 -1
- package/esm/src/html/styles-builder/project-css-cache.d.ts.map +1 -1
- package/esm/src/html/styles-builder/project-css-cache.js +13 -2
- package/esm/src/html/styles-builder/tailwind-compiler.d.ts +2 -0
- package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
- package/esm/src/html/styles-builder/tailwind-compiler.js +52 -19
- package/esm/src/modules/react-loader/css-import-collector.d.ts +29 -0
- package/esm/src/modules/react-loader/css-import-collector.d.ts.map +1 -0
- package/esm/src/modules/react-loader/css-import-collector.js +41 -0
- package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/loader.js +6 -0
- package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
- package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +5 -0
- package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/factory.js +5 -1
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -0
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +19 -5
- package/esm/src/platform/compat/process.d.ts.map +1 -1
- package/esm/src/platform/compat/process.js +20 -3
- package/esm/src/proxy/main.js +31 -12
- package/esm/src/proxy/token-manager.d.ts +2 -0
- package/esm/src/proxy/token-manager.d.ts.map +1 -1
- package/esm/src/proxy/token-manager.js +47 -8
- package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +23 -0
- package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -0
- package/esm/src/rendering/orchestrator/css-candidate-manifest.js +132 -0
- package/esm/src/rendering/orchestrator/html.d.ts +11 -1
- package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/html.js +103 -18
- package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
- package/esm/src/rendering/orchestrator/pipeline.js +14 -2
- package/esm/src/server/bootstrap.d.ts +2 -0
- package/esm/src/server/bootstrap.d.ts.map +1 -1
- package/esm/src/server/bootstrap.js +10 -0
- package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
- package/esm/src/server/handlers/preview/markdown-html-generator.js +11 -5
- package/esm/src/server/production-server.js +10 -2
- package/esm/src/studio/bridge-template.d.ts +2 -0
- package/esm/src/studio/bridge-template.d.ts.map +1 -1
- package/esm/src/studio/bridge-template.js +3390 -52
- package/esm/src/transforms/css-modules/naming.d.ts +33 -0
- package/esm/src/transforms/css-modules/naming.d.ts.map +1 -0
- package/esm/src/transforms/css-modules/naming.js +128 -0
- package/esm/src/transforms/esm/import-parser.d.ts +1 -0
- package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
- package/esm/src/transforms/esm/import-parser.js +16 -5
- package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/index.js +3 -1
- package/esm/src/transforms/pipeline/stages/index.d.ts +1 -0
- package/esm/src/transforms/pipeline/stages/index.d.ts.map +1 -1
- package/esm/src/transforms/pipeline/stages/index.js +1 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts +18 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts.map +1 -0
- package/esm/src/transforms/pipeline/stages/ssr-css-strip.js +168 -0
- package/package.json +1 -1
- package/src/cli/app/shell.ts +9 -5
- package/src/cli/commands/demo/demo.ts +1 -1
- package/src/cli/commands/init/catalog.ts +13 -5
- package/src/cli/commands/init/command-help.ts +4 -4
- package/src/cli/commands/init/types.ts +5 -5
- package/src/cli/commands/serve/command.ts +0 -5
- package/src/cli/commands/start/command.ts +15 -10
- package/src/cli/help/tips.ts +6 -6
- package/src/cli/mcp/remote-file-tools.ts +1 -1
- package/src/cli/mcp/tools/catalog-tools.ts +21 -13
- package/src/cli/mcp/tools/project-tools.ts +1 -1
- package/src/cli/templates/index.ts +11 -11
- package/src/cli/templates/manifest.js +24 -17
- package/src/cli/templates/types.ts +5 -5
- package/src/cli/utils/index.ts +12 -1
- package/src/deno.js +1 -1
- package/src/src/html/html-shell-generator.ts +2 -0
- package/src/src/html/styles-builder/project-css-cache.ts +24 -1
- package/src/src/html/styles-builder/tailwind-compiler.ts +67 -26
- package/src/src/modules/react-loader/css-import-collector.ts +50 -0
- package/src/src/modules/react-loader/ssr-module-loader/loader.ts +7 -0
- package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +6 -0
- package/src/src/platform/adapters/fs/factory.ts +5 -1
- package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +21 -5
- package/src/src/platform/compat/process.ts +28 -4
- package/src/src/proxy/main.ts +32 -12
- package/src/src/proxy/token-manager.ts +54 -8
- package/src/src/rendering/orchestrator/css-candidate-manifest.ts +176 -0
- package/src/src/rendering/orchestrator/html.ts +128 -16
- package/src/src/rendering/orchestrator/pipeline.ts +183 -165
- package/src/src/server/bootstrap.ts +16 -0
- package/src/src/server/handlers/preview/markdown-html-generator.ts +12 -5
- package/src/src/server/production-server.ts +12 -2
- package/src/src/studio/bridge-template.ts +3392 -52
- package/src/src/transforms/css-modules/naming.ts +152 -0
- package/src/src/transforms/esm/import-parser.ts +15 -5
- package/src/src/transforms/pipeline/index.ts +3 -0
- package/src/src/transforms/pipeline/stages/index.ts +1 -0
- package/src/src/transforms/pipeline/stages/ssr-css-strip.ts +201 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Module naming and selector rewriting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic class-name scoping that is stable across
|
|
5
|
+
* transform/runtime boundaries and HTML CSS aggregation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CSS_MODULE_EXTENSION = ".module.css";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a module key to a stable slash-based format.
|
|
12
|
+
* Removes query/hash suffixes and normalizes duplicate separators.
|
|
13
|
+
*/
|
|
14
|
+
export function normalizeCssModuleKey(path: string): string {
|
|
15
|
+
const withoutFilePrefix = path.startsWith("file://") ? path.slice("file://".length) : path;
|
|
16
|
+
const withoutQuery = withoutFilePrefix.replace(/[?#].*$/, "");
|
|
17
|
+
const slashed = withoutQuery.replace(/\\/g, "/");
|
|
18
|
+
const collapsed = slashed.replace(/\/{2,}/g, "/");
|
|
19
|
+
if (collapsed.startsWith("/")) return collapsed;
|
|
20
|
+
if (collapsed.startsWith("http://") || collapsed.startsWith("https://")) return collapsed;
|
|
21
|
+
return `/${collapsed.replace(/^\/+/, "")}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function dirname(path: string): string {
|
|
25
|
+
const normalized = normalizeCssModuleKey(path);
|
|
26
|
+
const lastSlash = normalized.lastIndexOf("/");
|
|
27
|
+
return lastSlash <= 0 ? "/" : normalized.slice(0, lastSlash);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePathSegments(path: string): string {
|
|
31
|
+
const normalized = normalizeCssModuleKey(path);
|
|
32
|
+
if (normalized.startsWith("http://") || normalized.startsWith("https://")) return normalized;
|
|
33
|
+
|
|
34
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
35
|
+
const resolved: string[] = [];
|
|
36
|
+
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (part === ".") continue;
|
|
39
|
+
if (part === "..") {
|
|
40
|
+
resolved.pop();
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
resolved.push(part);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `/${resolved.join("/")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a CSS import specifier to a deterministic module key.
|
|
51
|
+
* Supports relative imports, @/ aliases, absolute paths, and URLs.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveCssModuleKey(
|
|
54
|
+
specifier: string,
|
|
55
|
+
importerFilePath: string,
|
|
56
|
+
projectDir: string,
|
|
57
|
+
): string {
|
|
58
|
+
if (specifier.startsWith("http://") || specifier.startsWith("https://")) {
|
|
59
|
+
return normalizeCssModuleKey(specifier);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (specifier.startsWith("@/")) {
|
|
63
|
+
const aliasPath = specifier.slice(2).replace(/^\/+/, "");
|
|
64
|
+
return normalizePathSegments(`${normalizeCssModuleKey(projectDir)}/${aliasPath}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (specifier.startsWith("/")) {
|
|
68
|
+
return normalizePathSegments(specifier);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
72
|
+
const importerDir = dirname(importerFilePath);
|
|
73
|
+
return normalizePathSegments(`${importerDir}/${specifier}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Bare specifiers are uncommon for CSS in this system, but keep deterministic behavior.
|
|
77
|
+
return normalizeCssModuleKey(specifier);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hashString(input: string): string {
|
|
81
|
+
let hash = 5381;
|
|
82
|
+
for (let i = 0; i < input.length; i++) {
|
|
83
|
+
hash = ((hash << 5) + hash) ^ input.charCodeAt(i);
|
|
84
|
+
}
|
|
85
|
+
return (hash >>> 0).toString(36);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function sanitizeToken(token: string): string {
|
|
89
|
+
return token.replace(/[^\w-]/g, "_");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build deterministic module scope info.
|
|
94
|
+
*/
|
|
95
|
+
export function getCssModuleScope(moduleKey: string): { base: string; hash: string } {
|
|
96
|
+
const normalized = normalizeCssModuleKey(moduleKey);
|
|
97
|
+
const filename = normalized.split("/").pop() || "module";
|
|
98
|
+
const base = sanitizeToken(
|
|
99
|
+
filename.endsWith(CSS_MODULE_EXTENSION)
|
|
100
|
+
? filename.slice(0, -CSS_MODULE_EXTENSION.length)
|
|
101
|
+
: filename.replace(/\.css$/, ""),
|
|
102
|
+
) || "module";
|
|
103
|
+
const hash = hashString(normalized).slice(0, 6);
|
|
104
|
+
return { base, hash };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Convert a local class name to its scoped CSS Module class.
|
|
109
|
+
*/
|
|
110
|
+
export function toScopedCssModuleClass(moduleKey: string, localName: string): string {
|
|
111
|
+
const { base, hash } = getCssModuleScope(moduleKey);
|
|
112
|
+
const normalizedLocal = sanitizeToken(localName);
|
|
113
|
+
return `${base}_${normalizedLocal}__${hash}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function maskGlobalSelectors(css: string): { masked: string; restore: (input: string) => string } {
|
|
117
|
+
const segments: string[] = [];
|
|
118
|
+
const masked = css.replace(/:global\(([^()]*)\)/g, (match) => {
|
|
119
|
+
const token = `__VF_CSS_GLOBAL_${segments.length}__`;
|
|
120
|
+
segments.push(match);
|
|
121
|
+
return token;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
masked,
|
|
126
|
+
restore: (input: string) => {
|
|
127
|
+
let result = input;
|
|
128
|
+
for (const [i, segment] of segments.entries()) {
|
|
129
|
+
result = result.replaceAll(`__VF_CSS_GLOBAL_${i}__`, segment);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Rewrite `.module.css` selectors to deterministic scoped class names.
|
|
138
|
+
* Keeps `:global(...)` segments untouched.
|
|
139
|
+
*/
|
|
140
|
+
export function rewriteCssModuleContent(content: string, moduleKey: string): string {
|
|
141
|
+
const { masked, restore } = maskGlobalSelectors(content);
|
|
142
|
+
// After :global() masking, every `.identifier` in the CSS is a local class
|
|
143
|
+
// selector. No lookbehind needed — numeric decimals like `0.5em` won't
|
|
144
|
+
// match because digits aren't in [_a-zA-Z].
|
|
145
|
+
const rewritten = masked.replace(
|
|
146
|
+
/\.([_a-zA-Z][_a-zA-Z0-9-]*)/g,
|
|
147
|
+
(_match, className: string) => {
|
|
148
|
+
return `.${toScopedCssModuleClass(moduleKey, className)}`;
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
return restore(rewritten);
|
|
152
|
+
}
|
|
@@ -26,12 +26,13 @@ export interface MissingImport {
|
|
|
26
26
|
|
|
27
27
|
export interface ParseLocalImportsResult {
|
|
28
28
|
imports: LocalImport[];
|
|
29
|
+
cssImports: LocalImport[];
|
|
29
30
|
crossProjectImports: CrossProjectImport[];
|
|
30
31
|
missing: MissingImport[];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mdx"];
|
|
34
|
-
const HAS_EXTENSION_RE = /\.(tsx?|jsx?|mjs|cjs|mdx)$/;
|
|
35
|
+
const HAS_EXTENSION_RE = /\.(tsx?|jsx?|mjs|cjs|mdx|css)$/;
|
|
35
36
|
|
|
36
37
|
export async function parseLocalImports(
|
|
37
38
|
code: string,
|
|
@@ -40,7 +41,7 @@ export async function parseLocalImports(
|
|
|
40
41
|
adapter?: RuntimeAdapter,
|
|
41
42
|
): Promise<ParseLocalImportsResult> {
|
|
42
43
|
if (filePath.endsWith(".css") || filePath.endsWith(".json")) {
|
|
43
|
-
return { imports: [], crossProjectImports: [], missing: [] };
|
|
44
|
+
return { imports: [], cssImports: [], crossProjectImports: [], missing: [] };
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
const esbuild = await getEsbuild();
|
|
@@ -58,6 +59,7 @@ export async function parseLocalImports(
|
|
|
58
59
|
|
|
59
60
|
const imports = await parseImports(result.code);
|
|
60
61
|
const localImports: LocalImport[] = [];
|
|
62
|
+
const cssImports: LocalImport[] = [];
|
|
61
63
|
const crossProjectImports: CrossProjectImport[] = [];
|
|
62
64
|
const missingImports: MissingImport[] = [];
|
|
63
65
|
|
|
@@ -68,7 +70,11 @@ export async function parseLocalImports(
|
|
|
68
70
|
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
69
71
|
const resolved = await resolveLocalImportPath(filePath, specifier, adapter);
|
|
70
72
|
if (resolved) {
|
|
71
|
-
|
|
73
|
+
if (resolved.endsWith(".css")) {
|
|
74
|
+
cssImports.push({ specifier, absolutePath: resolved });
|
|
75
|
+
} else {
|
|
76
|
+
localImports.push({ specifier, absolutePath: resolved });
|
|
77
|
+
}
|
|
72
78
|
continue;
|
|
73
79
|
}
|
|
74
80
|
|
|
@@ -84,7 +90,11 @@ export async function parseLocalImports(
|
|
|
84
90
|
const aliasPath = specifier.slice(2);
|
|
85
91
|
const resolved = await resolveAliasImportPath(aliasPath, projectDir, adapter);
|
|
86
92
|
if (resolved) {
|
|
87
|
-
|
|
93
|
+
if (resolved.endsWith(".css")) {
|
|
94
|
+
cssImports.push({ specifier, absolutePath: resolved });
|
|
95
|
+
} else {
|
|
96
|
+
localImports.push({ specifier, absolutePath: resolved });
|
|
97
|
+
}
|
|
88
98
|
continue;
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -109,7 +119,7 @@ export async function parseLocalImports(
|
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
121
|
|
|
112
|
-
return { imports: localImports, crossProjectImports, missing: missingImports };
|
|
122
|
+
return { imports: localImports, cssImports, crossProjectImports, missing: missingImports };
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
async function checkFileExists(path: string, adapter?: RuntimeAdapter): Promise<boolean> {
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import {
|
|
24
24
|
compilePlugin,
|
|
25
|
+
cssStripPlugin,
|
|
25
26
|
finalizePlugin,
|
|
26
27
|
parsePlugin,
|
|
27
28
|
resolveImportsPlugin,
|
|
@@ -39,6 +40,7 @@ import { extractFrameworkBundlePaths } from "../shared/framework-bundle-paths.js
|
|
|
39
40
|
const SSR_PIPELINE: TransformPlugin[] = [
|
|
40
41
|
parsePlugin,
|
|
41
42
|
compilePlugin,
|
|
43
|
+
cssStripPlugin, // Strip CSS imports before they hit import resolution
|
|
42
44
|
resolveImportsPlugin, // Unified import resolution
|
|
43
45
|
ssrVfModulesPlugin, // Resolve /_vf_modules/ to framework files with React transforms
|
|
44
46
|
ssrHttpStubPlugin,
|
|
@@ -49,6 +51,7 @@ const SSR_PIPELINE: TransformPlugin[] = [
|
|
|
49
51
|
const BROWSER_PIPELINE: TransformPlugin[] = [
|
|
50
52
|
parsePlugin,
|
|
51
53
|
compilePlugin,
|
|
54
|
+
cssStripPlugin, // Strip CSS imports before they hit import resolution
|
|
52
55
|
resolveImportsPlugin, // Unified import resolution
|
|
53
56
|
finalizePlugin,
|
|
54
57
|
];
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export { parsePlugin } from "./parse.js";
|
|
8
8
|
export { compilePlugin } from "./compile.js";
|
|
9
|
+
export { cssStripPlugin } from "./ssr-css-strip.js";
|
|
9
10
|
export { resolveImportsPlugin } from "./resolve-imports.js";
|
|
10
11
|
export { ssrVfModulesPlugin } from "./ssr-vf-modules.js";
|
|
11
12
|
export { ssrHttpStubPlugin } from "./ssr-http-stub.js";
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS Strip Stage - removes CSS import statements from compiled code.
|
|
3
|
+
*
|
|
4
|
+
* CSS files are not valid JS modules and will crash both the SSR module
|
|
5
|
+
* loader and browser module system if left in compiled code. This plugin
|
|
6
|
+
* strips them and records the CSS specifiers in pipeline metadata for
|
|
7
|
+
* downstream collection (used by the SSR rendering pipeline to include
|
|
8
|
+
* the CSS content in the HTML output).
|
|
9
|
+
*
|
|
10
|
+
* For CSS Module imports (`import styles from "./X.module.css"`), the
|
|
11
|
+
* import is replaced with a Proxy stub that returns the property name
|
|
12
|
+
* as the class name. This matches the Next.js convention where
|
|
13
|
+
* `styles.container` → `"container"` (identity mapping), which works
|
|
14
|
+
* correctly with Tailwind CSS class-based styling.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { TransformPlugin } from "../types.js";
|
|
18
|
+
import { TransformStage } from "../types.js";
|
|
19
|
+
import { parseImports, rewriteImports } from "../../esm/lexer.js";
|
|
20
|
+
import {
|
|
21
|
+
getCssModuleScope,
|
|
22
|
+
resolveCssModuleKey,
|
|
23
|
+
toScopedCssModuleClass,
|
|
24
|
+
} from "../../css-modules/naming.js";
|
|
25
|
+
|
|
26
|
+
function isCSSImport(specifier: string | undefined): boolean {
|
|
27
|
+
return specifier?.endsWith(".css") || false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isCssModuleImport(specifier: string | undefined): boolean {
|
|
31
|
+
return specifier?.endsWith(".module.css") || false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cssModuleProxyExpression(): string {
|
|
35
|
+
return "new Proxy({}, { get: (_, p) => String(p) })";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scopedCssModuleProxyExpression(moduleKey: string): string {
|
|
39
|
+
const scope = getCssModuleScope(moduleKey);
|
|
40
|
+
return `new Proxy({}, { get: (_, p) => typeof p === "string" ? "${scope.base}_" + String(p).replace(/[^\\w-]/g, "_") + "__${scope.hash}" : "" })`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type NamedImportBinding = { imported: string; local: string };
|
|
44
|
+
|
|
45
|
+
function parseNamedImportBindings(namedClause: string): NamedImportBinding[] {
|
|
46
|
+
const bindings: NamedImportBinding[] = [];
|
|
47
|
+
|
|
48
|
+
for (const rawPart of namedClause.split(",")) {
|
|
49
|
+
const part = rawPart.trim();
|
|
50
|
+
if (!part) continue;
|
|
51
|
+
|
|
52
|
+
const aliasMatch = part.match(/^([_$a-zA-Z][\w$-]*)\s+as\s+([_$a-zA-Z][\w$]*)$/);
|
|
53
|
+
if (aliasMatch) {
|
|
54
|
+
const imported = aliasMatch[1];
|
|
55
|
+
const local = aliasMatch[2];
|
|
56
|
+
if (!imported || !local) continue;
|
|
57
|
+
bindings.push({ imported, local });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (/^[_$a-zA-Z][\w$]*$/.test(part)) {
|
|
62
|
+
bindings.push({ imported: part, local: part });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return bindings;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a replacement for a static CSS import statement.
|
|
71
|
+
*
|
|
72
|
+
* - Side-effect import: `import "./globals.css"` → comment
|
|
73
|
+
* - Default import: `import styles from "./X.module.css"` → Proxy stub
|
|
74
|
+
* - Named imports: `import { a } from "./X.css"` → null stubs
|
|
75
|
+
*/
|
|
76
|
+
function generateCSSStub(statement: string, specifier: string): string {
|
|
77
|
+
const trimmed = statement.trim();
|
|
78
|
+
|
|
79
|
+
// Re-export from CSS: export { default as styles } from './module.css'
|
|
80
|
+
// → strip entirely, the CSS is collected separately
|
|
81
|
+
if (/^export\s/.test(trimmed)) {
|
|
82
|
+
return `/* css re-export stripped: ${specifier} */`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Side-effect import: import "./globals.css"
|
|
86
|
+
if (/^import\s+['"`]/.test(trimmed)) {
|
|
87
|
+
return `/* css import: ${specifier} */`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fromIndex = trimmed.lastIndexOf(" from ");
|
|
91
|
+
if (fromIndex === -1) {
|
|
92
|
+
return `/* css import: ${specifier} */`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cssModuleKey = isCssModuleImport(specifier) ? specifier : undefined;
|
|
96
|
+
const importClause = trimmed.slice(6, fromIndex).trim(); // Skip "import "
|
|
97
|
+
|
|
98
|
+
// Default import: import styles from "./Button.module.css"
|
|
99
|
+
// → const styles = new Proxy({}, { get: (_, p) => String(p) })
|
|
100
|
+
// This makes styles.container return "container" (identity mapping)
|
|
101
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(importClause)) {
|
|
102
|
+
const expr = cssModuleKey
|
|
103
|
+
? scopedCssModuleProxyExpression(cssModuleKey)
|
|
104
|
+
: cssModuleProxyExpression();
|
|
105
|
+
return `const ${importClause} = ${expr}; /* css module: ${specifier} */`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Namespace import: import * as styles from "./X.module.css"
|
|
109
|
+
const nsMatch = importClause.match(/^\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*)$/);
|
|
110
|
+
if (nsMatch) {
|
|
111
|
+
const expr = cssModuleKey
|
|
112
|
+
? scopedCssModuleProxyExpression(cssModuleKey)
|
|
113
|
+
: cssModuleProxyExpression();
|
|
114
|
+
return `const ${nsMatch[1]} = ${expr}; /* css module: ${specifier} */`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Named imports: import { container, header } from "./X.module.css"
|
|
118
|
+
const namedMatch = importClause.match(/^\{([^}]+)\}$/);
|
|
119
|
+
if (namedMatch?.[1]) {
|
|
120
|
+
const bindings = parseNamedImportBindings(namedMatch[1]);
|
|
121
|
+
if (bindings.length > 0) {
|
|
122
|
+
const stubs = bindings
|
|
123
|
+
.map((binding) => {
|
|
124
|
+
const value = cssModuleKey
|
|
125
|
+
? toScopedCssModuleClass(cssModuleKey, binding.imported)
|
|
126
|
+
: binding.imported;
|
|
127
|
+
return `${binding.local} = "${value}"`;
|
|
128
|
+
})
|
|
129
|
+
.join(", ");
|
|
130
|
+
return `const ${stubs}; /* css module: ${specifier} */`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Mixed: import styles, { container } from "./X.module.css"
|
|
135
|
+
const mixedMatch = importClause.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*\{([^}]+)\}$/);
|
|
136
|
+
if (mixedMatch?.[1] && mixedMatch[2]) {
|
|
137
|
+
const defaultName = mixedMatch[1];
|
|
138
|
+
const bindings = parseNamedImportBindings(mixedMatch[2]);
|
|
139
|
+
const namedStubs = bindings
|
|
140
|
+
.map((binding) => {
|
|
141
|
+
const value = cssModuleKey
|
|
142
|
+
? toScopedCssModuleClass(cssModuleKey, binding.imported)
|
|
143
|
+
: binding.imported;
|
|
144
|
+
return `${binding.local} = "${value}"`;
|
|
145
|
+
})
|
|
146
|
+
.join(", ");
|
|
147
|
+
const defaultExpr = cssModuleKey
|
|
148
|
+
? scopedCssModuleProxyExpression(cssModuleKey)
|
|
149
|
+
: cssModuleProxyExpression();
|
|
150
|
+
return namedStubs.length > 0
|
|
151
|
+
? `const ${defaultName} = ${defaultExpr}, ${namedStubs}; /* css module: ${specifier} */`
|
|
152
|
+
: `const ${defaultName} = ${defaultExpr}; /* css module: ${specifier} */`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `/* css import: ${specifier} */`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate a replacement for dynamic CSS imports.
|
|
160
|
+
* Keeps syntax valid in expression position (e.g. await import("./x.css")).
|
|
161
|
+
*/
|
|
162
|
+
function generateDynamicCSSStub(specifier: string): string {
|
|
163
|
+
if (isCssModuleImport(specifier)) {
|
|
164
|
+
return `Promise.resolve({ default: ${
|
|
165
|
+
scopedCssModuleProxyExpression(specifier)
|
|
166
|
+
} }) /* css import: ${specifier} */`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `Promise.resolve({}) /* css import: ${specifier} */`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const cssStripPlugin: TransformPlugin = {
|
|
173
|
+
name: "css-strip",
|
|
174
|
+
stage: TransformStage.COMPILE + 0.5, // Run after esbuild compile, before import resolution
|
|
175
|
+
|
|
176
|
+
async transform(ctx) {
|
|
177
|
+
const imports = await parseImports(ctx.code);
|
|
178
|
+
|
|
179
|
+
const hasCssImports = imports.some((imp) => isCSSImport(imp.n));
|
|
180
|
+
if (!hasCssImports) return ctx.code;
|
|
181
|
+
|
|
182
|
+
const cssSpecifiers: string[] = [];
|
|
183
|
+
|
|
184
|
+
const result = await rewriteImports(ctx.code, (imp, statement) => {
|
|
185
|
+
if (!isCSSImport(imp.n)) return null;
|
|
186
|
+
cssSpecifiers.push(imp.n!);
|
|
187
|
+
const moduleKey = isCssModuleImport(imp.n)
|
|
188
|
+
? resolveCssModuleKey(imp.n!, ctx.filePath, ctx.projectDir)
|
|
189
|
+
: undefined;
|
|
190
|
+
const specifierForStub = moduleKey ?? imp.n!;
|
|
191
|
+
if (imp.d > -1) return generateDynamicCSSStub(specifierForStub);
|
|
192
|
+
return generateCSSStub(statement, specifierForStub);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (cssSpecifiers.length > 0) {
|
|
196
|
+
ctx.metadata.set("cssImports", cssSpecifiers);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
},
|
|
201
|
+
};
|