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.
Files changed (124) hide show
  1. package/README.md +3 -11
  2. package/esm/cli/app/shell.d.ts.map +1 -1
  3. package/esm/cli/app/shell.js +9 -5
  4. package/esm/cli/commands/demo/demo.js +1 -1
  5. package/esm/cli/commands/init/catalog.d.ts.map +1 -1
  6. package/esm/cli/commands/init/catalog.js +13 -5
  7. package/esm/cli/commands/init/command-help.js +4 -4
  8. package/esm/cli/commands/init/types.d.ts +1 -1
  9. package/esm/cli/commands/init/types.d.ts.map +1 -1
  10. package/esm/cli/commands/serve/command.d.ts.map +1 -1
  11. package/esm/cli/commands/serve/command.js +0 -4
  12. package/esm/cli/commands/start/command.d.ts.map +1 -1
  13. package/esm/cli/commands/start/command.js +16 -9
  14. package/esm/cli/help/tips.js +6 -6
  15. package/esm/cli/mcp/remote-file-tools.js +1 -1
  16. package/esm/cli/mcp/tools/catalog-tools.d.ts +3 -3
  17. package/esm/cli/mcp/tools/catalog-tools.d.ts.map +1 -1
  18. package/esm/cli/mcp/tools/catalog-tools.js +21 -13
  19. package/esm/cli/mcp/tools/project-tools.js +1 -1
  20. package/esm/cli/templates/index.js +11 -11
  21. package/esm/cli/templates/manifest.d.ts +22 -15
  22. package/esm/cli/templates/manifest.js +24 -17
  23. package/esm/cli/templates/types.d.ts +1 -1
  24. package/esm/cli/templates/types.d.ts.map +1 -1
  25. package/esm/cli/utils/index.d.ts.map +1 -1
  26. package/esm/cli/utils/index.js +13 -1
  27. package/esm/deno.js +1 -1
  28. package/esm/src/html/html-shell-generator.d.ts.map +1 -1
  29. package/esm/src/html/html-shell-generator.js +2 -0
  30. package/esm/src/html/styles-builder/project-css-cache.d.ts +8 -1
  31. package/esm/src/html/styles-builder/project-css-cache.d.ts.map +1 -1
  32. package/esm/src/html/styles-builder/project-css-cache.js +13 -2
  33. package/esm/src/html/styles-builder/tailwind-compiler.d.ts +2 -0
  34. package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
  35. package/esm/src/html/styles-builder/tailwind-compiler.js +52 -19
  36. package/esm/src/modules/react-loader/css-import-collector.d.ts +29 -0
  37. package/esm/src/modules/react-loader/css-import-collector.d.ts.map +1 -0
  38. package/esm/src/modules/react-loader/css-import-collector.js +41 -0
  39. package/esm/src/modules/react-loader/ssr-module-loader/loader.d.ts.map +1 -1
  40. package/esm/src/modules/react-loader/ssr-module-loader/loader.js +6 -0
  41. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.d.ts.map +1 -1
  42. package/esm/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.js +5 -0
  43. package/esm/src/platform/adapters/fs/factory.d.ts.map +1 -1
  44. package/esm/src/platform/adapters/fs/factory.js +5 -1
  45. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts +1 -0
  46. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.d.ts.map +1 -1
  47. package/esm/src/platform/adapters/fs/veryfront/websocket-manager.js +19 -5
  48. package/esm/src/platform/compat/process.d.ts.map +1 -1
  49. package/esm/src/platform/compat/process.js +20 -3
  50. package/esm/src/proxy/main.js +31 -12
  51. package/esm/src/proxy/token-manager.d.ts +2 -0
  52. package/esm/src/proxy/token-manager.d.ts.map +1 -1
  53. package/esm/src/proxy/token-manager.js +47 -8
  54. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts +23 -0
  55. package/esm/src/rendering/orchestrator/css-candidate-manifest.d.ts.map +1 -0
  56. package/esm/src/rendering/orchestrator/css-candidate-manifest.js +132 -0
  57. package/esm/src/rendering/orchestrator/html.d.ts +11 -1
  58. package/esm/src/rendering/orchestrator/html.d.ts.map +1 -1
  59. package/esm/src/rendering/orchestrator/html.js +103 -18
  60. package/esm/src/rendering/orchestrator/pipeline.d.ts.map +1 -1
  61. package/esm/src/rendering/orchestrator/pipeline.js +14 -2
  62. package/esm/src/server/bootstrap.d.ts +2 -0
  63. package/esm/src/server/bootstrap.d.ts.map +1 -1
  64. package/esm/src/server/bootstrap.js +10 -0
  65. package/esm/src/server/handlers/preview/markdown-html-generator.d.ts.map +1 -1
  66. package/esm/src/server/handlers/preview/markdown-html-generator.js +11 -5
  67. package/esm/src/server/production-server.js +10 -2
  68. package/esm/src/studio/bridge-template.d.ts +2 -0
  69. package/esm/src/studio/bridge-template.d.ts.map +1 -1
  70. package/esm/src/studio/bridge-template.js +3390 -52
  71. package/esm/src/transforms/css-modules/naming.d.ts +33 -0
  72. package/esm/src/transforms/css-modules/naming.d.ts.map +1 -0
  73. package/esm/src/transforms/css-modules/naming.js +128 -0
  74. package/esm/src/transforms/esm/import-parser.d.ts +1 -0
  75. package/esm/src/transforms/esm/import-parser.d.ts.map +1 -1
  76. package/esm/src/transforms/esm/import-parser.js +16 -5
  77. package/esm/src/transforms/pipeline/index.d.ts.map +1 -1
  78. package/esm/src/transforms/pipeline/index.js +3 -1
  79. package/esm/src/transforms/pipeline/stages/index.d.ts +1 -0
  80. package/esm/src/transforms/pipeline/stages/index.d.ts.map +1 -1
  81. package/esm/src/transforms/pipeline/stages/index.js +1 -0
  82. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts +18 -0
  83. package/esm/src/transforms/pipeline/stages/ssr-css-strip.d.ts.map +1 -0
  84. package/esm/src/transforms/pipeline/stages/ssr-css-strip.js +168 -0
  85. package/package.json +1 -1
  86. package/src/cli/app/shell.ts +9 -5
  87. package/src/cli/commands/demo/demo.ts +1 -1
  88. package/src/cli/commands/init/catalog.ts +13 -5
  89. package/src/cli/commands/init/command-help.ts +4 -4
  90. package/src/cli/commands/init/types.ts +5 -5
  91. package/src/cli/commands/serve/command.ts +0 -5
  92. package/src/cli/commands/start/command.ts +15 -10
  93. package/src/cli/help/tips.ts +6 -6
  94. package/src/cli/mcp/remote-file-tools.ts +1 -1
  95. package/src/cli/mcp/tools/catalog-tools.ts +21 -13
  96. package/src/cli/mcp/tools/project-tools.ts +1 -1
  97. package/src/cli/templates/index.ts +11 -11
  98. package/src/cli/templates/manifest.js +24 -17
  99. package/src/cli/templates/types.ts +5 -5
  100. package/src/cli/utils/index.ts +12 -1
  101. package/src/deno.js +1 -1
  102. package/src/src/html/html-shell-generator.ts +2 -0
  103. package/src/src/html/styles-builder/project-css-cache.ts +24 -1
  104. package/src/src/html/styles-builder/tailwind-compiler.ts +67 -26
  105. package/src/src/modules/react-loader/css-import-collector.ts +50 -0
  106. package/src/src/modules/react-loader/ssr-module-loader/loader.ts +7 -0
  107. package/src/src/modules/react-loader/ssr-module-loader/ssr-dependency-validator.ts +6 -0
  108. package/src/src/platform/adapters/fs/factory.ts +5 -1
  109. package/src/src/platform/adapters/fs/veryfront/websocket-manager.ts +21 -5
  110. package/src/src/platform/compat/process.ts +28 -4
  111. package/src/src/proxy/main.ts +32 -12
  112. package/src/src/proxy/token-manager.ts +54 -8
  113. package/src/src/rendering/orchestrator/css-candidate-manifest.ts +176 -0
  114. package/src/src/rendering/orchestrator/html.ts +128 -16
  115. package/src/src/rendering/orchestrator/pipeline.ts +183 -165
  116. package/src/src/server/bootstrap.ts +16 -0
  117. package/src/src/server/handlers/preview/markdown-html-generator.ts +12 -5
  118. package/src/src/server/production-server.ts +12 -2
  119. package/src/src/studio/bridge-template.ts +3392 -52
  120. package/src/src/transforms/css-modules/naming.ts +152 -0
  121. package/src/src/transforms/esm/import-parser.ts +15 -5
  122. package/src/src/transforms/pipeline/index.ts +3 -0
  123. package/src/src/transforms/pipeline/stages/index.ts +1 -0
  124. 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
- localImports.push({ specifier, absolutePath: resolved });
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
- localImports.push({ specifier, absolutePath: resolved });
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
+ };