@swissjs/swite 0.4.1 → 0.4.2

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 (36) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/__tests__/import-rewriter-bug.test.ts +122 -122
  3. package/__tests__/security-r001-r002.test.ts +190 -190
  4. package/dist/cli.js +0 -0
  5. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  6. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  7. package/docs/architecture/build-pipeline.md +97 -97
  8. package/docs/architecture/dev-server.md +87 -87
  9. package/docs/architecture/hmr.md +78 -78
  10. package/docs/architecture/import-rewriting.md +101 -101
  11. package/docs/architecture/index.md +16 -16
  12. package/docs/architecture/python-integration.md +93 -93
  13. package/docs/architecture/resolution.md +92 -92
  14. package/docs/cli/build.md +78 -78
  15. package/docs/cli/dev.md +90 -90
  16. package/docs/cli/index.md +15 -15
  17. package/docs/cli/start.md +45 -45
  18. package/docs/development/contributing.md +74 -74
  19. package/docs/development/index.md +12 -12
  20. package/docs/development/internals.md +101 -101
  21. package/docs/guide/configuration.md +89 -89
  22. package/docs/guide/index.md +13 -13
  23. package/docs/guide/project-structure.md +75 -75
  24. package/docs/guide/quickstart.md +113 -113
  25. package/docs/index.md +16 -16
  26. package/package.json +10 -9
  27. package/src/config/env.ts +98 -98
  28. package/src/dev-engine/handlers/ui-handler.ts +30 -30
  29. package/src/dev-engine/handlers/uix-handler.ts +21 -21
  30. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  31. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  32. package/src/dev-engine/middleware/static-files.ts +813 -813
  33. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  34. package/src/resolution/path/path-fixup.ts +27 -27
  35. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  36. package/src/resolution/symlink-registry.ts +114 -114
@@ -1,75 +1,75 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Project Structure
8
-
9
- Conventions for a SWITE-powered application.
10
-
11
- ---
12
-
13
- ## Application layout
14
-
15
- ```
16
- my-app/
17
- ├── src/
18
- │ ├── index.ui # entry point — referenced by public/index.html
19
- │ ├── App.ui # root component
20
- │ ├── components/ # reusable UI components
21
- │ └── types/ # shared TypeScript types
22
- ├── public/
23
- │ └── index.html # HTML shell — SWITE injects import map and CSS links
24
- ├── .swite/
25
- │ └── import-map.json # generated at build time; optional at dev time
26
- ├── dist/ # production build output (gitignored)
27
- ├── package.json
28
- └── swiss.config.ts # optional config
29
- ```
30
-
31
- ---
32
-
33
- ## Source file conventions
34
-
35
- | Extension | Handler | Description |
36
- |-----------|---------|-------------|
37
- | `.ui` | UIHandler | SwissJS component syntax; compiled by `@swissjs/compiler` then TypeScript-stripped by esbuild |
38
- | `.uix` | UIXHandler | JSX variant of SwissJS component syntax; same pipeline as `.ui` |
39
- | `.ts` | TSHandler | Plain TypeScript; TypeScript-stripped by esbuild only |
40
- | `.js` | JSHandler | Plain JavaScript; import-rewritten but not compiled |
41
- | `.mjs` | MJSHandler | Same as `.js`; falls back to `.js` if `.mjs` does not exist |
42
-
43
- SWITE handles extension negotiation: a request for `/src/Foo.js` will be served from `Foo.ts`, `Foo.ui`, or `Foo.uix` if the `.js` file does not exist on disk. Similarly, a request for `/src/Foo.ts` falls back to `.ui` or `.uix`.
44
-
45
- ---
46
-
47
- ## public/ directory
48
-
49
- The `public/` directory is served as static files. SWITE's SPA fallback reads `public/index.html` and:
50
-
51
- - Injects cache-busting query parameters on the entry point `<script>` tag
52
- - Injects an `<script type="importmap">` block from `.swite/import-map.json` (merging with any existing importmap in HTML)
53
- - Injects `<link rel="stylesheet">` tags for CSS files discovered in the entry point source
54
-
55
- Files under `public/` are served with `no-cache` headers during development.
56
-
57
- ---
58
-
59
- ## Config file
60
-
61
- SWITE looks for `swiss.config.ts` (preferred) or `swiss.config.js` at the project root. Both are optional. If neither exists, SWITE starts with defaults: port 3000, host `localhost`, no Python service.
62
-
63
- See [Configuration](./configuration.md) for the full field reference.
64
-
65
- ---
66
-
67
- ## Workspace layout
68
-
69
- When SWITE detects a monorepo (by walking up the directory tree looking for `pnpm-workspace.yaml` or a `package.json` with `workspaces`), it:
70
-
71
- - Scans workspace `node_modules` for package resolution
72
- - Serves `lib/`, `libraries/`, and `modules/` directories as static file prefixes
73
- - Compiles source files under `packages/` through the handler pipeline
74
-
75
- The workspace root is detected automatically; it can also be set explicitly via `SwiteConfig.rootDir`.
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Project Structure
8
+
9
+ Conventions for a SWITE-powered application.
10
+
11
+ ---
12
+
13
+ ## Application layout
14
+
15
+ ```
16
+ my-app/
17
+ ├── src/
18
+ │ ├── index.ui # entry point — referenced by public/index.html
19
+ │ ├── App.ui # root component
20
+ │ ├── components/ # reusable UI components
21
+ │ └── types/ # shared TypeScript types
22
+ ├── public/
23
+ │ └── index.html # HTML shell — SWITE injects import map and CSS links
24
+ ├── .swite/
25
+ │ └── import-map.json # generated at build time; optional at dev time
26
+ ├── dist/ # production build output (gitignored)
27
+ ├── package.json
28
+ └── swiss.config.ts # optional config
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Source file conventions
34
+
35
+ | Extension | Handler | Description |
36
+ |-----------|---------|-------------|
37
+ | `.ui` | UIHandler | SwissJS component syntax; compiled by `@swissjs/compiler` then TypeScript-stripped by esbuild |
38
+ | `.uix` | UIXHandler | JSX variant of SwissJS component syntax; same pipeline as `.ui` |
39
+ | `.ts` | TSHandler | Plain TypeScript; TypeScript-stripped by esbuild only |
40
+ | `.js` | JSHandler | Plain JavaScript; import-rewritten but not compiled |
41
+ | `.mjs` | MJSHandler | Same as `.js`; falls back to `.js` if `.mjs` does not exist |
42
+
43
+ SWITE handles extension negotiation: a request for `/src/Foo.js` will be served from `Foo.ts`, `Foo.ui`, or `Foo.uix` if the `.js` file does not exist on disk. Similarly, a request for `/src/Foo.ts` falls back to `.ui` or `.uix`.
44
+
45
+ ---
46
+
47
+ ## public/ directory
48
+
49
+ The `public/` directory is served as static files. SWITE's SPA fallback reads `public/index.html` and:
50
+
51
+ - Injects cache-busting query parameters on the entry point `<script>` tag
52
+ - Injects an `<script type="importmap">` block from `.swite/import-map.json` (merging with any existing importmap in HTML)
53
+ - Injects `<link rel="stylesheet">` tags for CSS files discovered in the entry point source
54
+
55
+ Files under `public/` are served with `no-cache` headers during development.
56
+
57
+ ---
58
+
59
+ ## Config file
60
+
61
+ SWITE looks for `swiss.config.ts` (preferred) or `swiss.config.js` at the project root. Both are optional. If neither exists, SWITE starts with defaults: port 3000, host `localhost`, no Python service.
62
+
63
+ See [Configuration](./configuration.md) for the full field reference.
64
+
65
+ ---
66
+
67
+ ## Workspace layout
68
+
69
+ When SWITE detects a monorepo (by walking up the directory tree looking for `pnpm-workspace.yaml` or a `package.json` with `workspaces`), it:
70
+
71
+ - Scans workspace `node_modules` for package resolution
72
+ - Serves `lib/`, `libraries/`, and `modules/` directories as static file prefixes
73
+ - Compiles source files under `packages/` through the handler pipeline
74
+
75
+ The workspace root is detected automatically; it can also be set explicitly via `SwiteConfig.rootDir`.
@@ -1,113 +1,113 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # Quickstart
8
-
9
- Get a SwissJS application running under SWITE in a few minutes.
10
-
11
- ---
12
-
13
- ## Prerequisites
14
-
15
- - Node.js 18.19.x or later
16
- - pnpm 10.x
17
- - A SwissJS application (`src/index.ui` as entry point)
18
-
19
- ---
20
-
21
- ## 1. Install SWITE
22
-
23
- In your application package:
24
-
25
- ```bash
26
- pnpm add -D @swissjs/swite
27
- ```
28
-
29
- Add the CLI commands to `package.json`:
30
-
31
- ```json
32
- {
33
- "scripts": {
34
- "dev": "swite dev",
35
- "build": "swite build",
36
- "start": "swite start"
37
- }
38
- }
39
- ```
40
-
41
- ---
42
-
43
- ## 2. Start the dev server
44
-
45
- ```bash
46
- pnpm dev
47
- ```
48
-
49
- SWITE starts on `http://localhost:3000` by default. Every `.ui`, `.uix`, `.ts`, and `.js` request is compiled and import-rewritten on demand. HMR is active on port 24678.
50
-
51
- ---
52
-
53
- ## 3. Project layout
54
-
55
- A minimal SWITE project:
56
-
57
- ```
58
- my-app/
59
- ├── src/
60
- │ ├── index.ui # entry point — mounted by public/index.html
61
- │ ├── App.ui # root component
62
- │ └── components/
63
- ├── public/
64
- │ └── index.html # shell HTML — SWITE injects import map here
65
- ├── package.json
66
- └── swiss.config.ts # optional
67
- ```
68
-
69
- SWITE resolves files relative to the directory where `swite dev` is invoked (the project root). The `src/` prefix in URLs maps directly to `<root>/src/` on disk.
70
-
71
- ---
72
-
73
- ## 4. Write a component
74
-
75
- ```typescript
76
- // src/components/Counter.ui
77
-
78
- component Counter {
79
- state {
80
- let count: number = 0;
81
- }
82
-
83
- render() {
84
- return html`
85
- <div>
86
- <p>Count: ${this.count}</p>
87
- <button onclick="${() => this.count++}">+1</button>
88
- </div>
89
- `;
90
- }
91
- }
92
- ```
93
-
94
- SWITE pipes `.ui` files through `@swissjs/compiler` (the SwissJS syntax transformer), then through esbuild's TypeScript stripper, rewrites all bare imports to browser-resolvable URLs, and serves the result as `application/javascript`.
95
-
96
- ---
97
-
98
- ## 5. Build for production
99
-
100
- ```bash
101
- pnpm build
102
- ```
103
-
104
- Output goes to `dist/`. The build entry is fixed to `src/index.ui`. See [`swite build`](../cli/build.md) for details.
105
-
106
- ---
107
-
108
- ## Next
109
-
110
- - [Project structure](./project-structure.md) — layout conventions
111
- - [Configuration](./configuration.md) — server port, Python service options
112
- - [CLI reference](../cli/index.md) — all commands
113
- - [Architecture](../architecture/index.md) — how SWITE works internally
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # Quickstart
8
+
9
+ Get a SwissJS application running under SWITE in a few minutes.
10
+
11
+ ---
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js 18.19.x or later
16
+ - pnpm 10.x
17
+ - A SwissJS application (`src/index.ui` as entry point)
18
+
19
+ ---
20
+
21
+ ## 1. Install SWITE
22
+
23
+ In your application package:
24
+
25
+ ```bash
26
+ pnpm add -D @swissjs/swite
27
+ ```
28
+
29
+ Add the CLI commands to `package.json`:
30
+
31
+ ```json
32
+ {
33
+ "scripts": {
34
+ "dev": "swite dev",
35
+ "build": "swite build",
36
+ "start": "swite start"
37
+ }
38
+ }
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 2. Start the dev server
44
+
45
+ ```bash
46
+ pnpm dev
47
+ ```
48
+
49
+ SWITE starts on `http://localhost:3000` by default. Every `.ui`, `.uix`, `.ts`, and `.js` request is compiled and import-rewritten on demand. HMR is active on port 24678.
50
+
51
+ ---
52
+
53
+ ## 3. Project layout
54
+
55
+ A minimal SWITE project:
56
+
57
+ ```
58
+ my-app/
59
+ ├── src/
60
+ │ ├── index.ui # entry point — mounted by public/index.html
61
+ │ ├── App.ui # root component
62
+ │ └── components/
63
+ ├── public/
64
+ │ └── index.html # shell HTML — SWITE injects import map here
65
+ ├── package.json
66
+ └── swiss.config.ts # optional
67
+ ```
68
+
69
+ SWITE resolves files relative to the directory where `swite dev` is invoked (the project root). The `src/` prefix in URLs maps directly to `<root>/src/` on disk.
70
+
71
+ ---
72
+
73
+ ## 4. Write a component
74
+
75
+ ```typescript
76
+ // src/components/Counter.ui
77
+
78
+ component Counter {
79
+ state {
80
+ let count: number = 0;
81
+ }
82
+
83
+ render() {
84
+ return html`
85
+ <div>
86
+ <p>Count: ${this.count}</p>
87
+ <button onclick="${() => this.count++}">+1</button>
88
+ </div>
89
+ `;
90
+ }
91
+ }
92
+ ```
93
+
94
+ SWITE pipes `.ui` files through `@swissjs/compiler` (the SwissJS syntax transformer), then through esbuild's TypeScript stripper, rewrites all bare imports to browser-resolvable URLs, and serves the result as `application/javascript`.
95
+
96
+ ---
97
+
98
+ ## 5. Build for production
99
+
100
+ ```bash
101
+ pnpm build
102
+ ```
103
+
104
+ Output goes to `dist/`. The build entry is fixed to `src/index.ui`. See [`swite build`](../cli/build.md) for details.
105
+
106
+ ---
107
+
108
+ ## Next
109
+
110
+ - [Project structure](./project-structure.md) — layout conventions
111
+ - [Configuration](./configuration.md) — server port, Python service options
112
+ - [CLI reference](../cli/index.md) — all commands
113
+ - [Architecture](../architecture/index.md) — how SWITE works internally
package/docs/index.md CHANGED
@@ -1,16 +1,16 @@
1
- <!--
2
- Copyright (c) 2024 Themba Mzumara
3
- SWITE - SWISS Development Server
4
- Licensed under the MIT License.
5
- -->
6
-
7
- # SWITE Documentation
8
-
9
- SWITE (`@swissjs/swite`) is the development server and production build tool for SwissJS applications. It provides on-demand TypeScript and SwissJS source compilation, bare-import rewriting, HMR, Python service lifecycle management, and an esbuild-powered production pipeline.
10
-
11
- ## Sections
12
-
13
- - [Guide](./guide/index.md) — Installation, project layout, configuration reference, and first steps
14
- - [CLI](./cli/index.md) — `swite dev`, `swite build`, and `swite start` command reference
15
- - [Architecture](./architecture/index.md) — Internals: dev server, import resolution, HMR, build pipeline, Python integration
16
- - [Development](./development/index.md) — Contributing, local build, and internal implementation notes
1
+ <!--
2
+ Copyright (c) 2024 Themba Mzumara
3
+ SWITE - SWISS Development Server
4
+ Licensed under the MIT License.
5
+ -->
6
+
7
+ # SWITE Documentation
8
+
9
+ SWITE (`@swissjs/swite`) is the development server and production build tool for SwissJS applications. It provides on-demand TypeScript and SwissJS source compilation, bare-import rewriting, HMR, Python service lifecycle management, and an esbuild-powered production pipeline.
10
+
11
+ ## Sections
12
+
13
+ - [Guide](./guide/index.md) — Installation, project layout, configuration reference, and first steps
14
+ - [CLI](./cli/index.md) — `swite dev`, `swite build`, and `swite start` command reference
15
+ - [Architecture](./architecture/index.md) — Internals: dev server, import resolution, HMR, build pipeline, Python integration
16
+ - [Development](./development/index.md) — Contributing, local build, and internal implementation notes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swissjs/swite",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "SWITE - SWISS Development Server (Vite replacement)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -9,8 +9,8 @@
9
9
  "swite": "dist/cli.js"
10
10
  },
11
11
  "scripts": {
12
- "build": "tsc -b",
13
- "dev": "tsc -b --watch",
12
+ "build": "tsc --noEmitOnError false",
13
+ "dev": "tsc --noEmitOnError false --watch",
14
14
  "clean": "rm -rf dist node_modules && rm -f tsconfig.tsbuildinfo",
15
15
  "generate-import-map": "tsx src/internal/generate-import-map-cli.ts",
16
16
  "test": "node --import tsx --test __tests__/import-rewriter-bug.test.ts",
@@ -19,15 +19,15 @@
19
19
  "release:publish": "changeset publish"
20
20
  },
21
21
  "dependencies": {
22
- "@swissjs/core": "0.2.0",
23
- "@swissjs/compiler": "0.2.0",
24
- "@swissjs/plugin-file-router": "1.0.2",
22
+ "@swissjs/core": "1.2.1",
23
+ "@swissjs/compiler": "1.2.1",
24
+ "@swissjs/plugin-file-router": "1.2.3",
25
25
  "chalk": "^5.3.0",
26
26
  "chokidar": "^3.5.3",
27
27
  "es-module-lexer": "^1.3.1",
28
28
  "esbuild": "^0.25.0",
29
29
  "express": "^4.18.2",
30
- "ws": "^8.20.1"
30
+ "ws": "^8.21.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/express": "^4.17.21",
@@ -50,8 +50,9 @@
50
50
  "path-to-regexp": "^0.1.13",
51
51
  "picomatch": "^2.3.2",
52
52
  "qs": ">=6.15.2",
53
- "esbuild": ">=0.25.0",
54
- "vite": ">=6.4.2"
53
+ "esbuild": ">=0.28.1",
54
+ "vite": ">=6.4.3",
55
+ "js-yaml": "^3.15.0"
55
56
  }
56
57
  },
57
58
  "repository": {
package/src/config/env.ts CHANGED
@@ -1,98 +1,98 @@
1
- /*
2
- * Environment Variable Support for SWITE
3
- * Provides import.meta.env replacement for SWITE
4
- */
5
-
6
- import { readFileSync, existsSync } from "node:fs";
7
- import { join } from "node:path";
8
-
9
- export interface EnvConfig {
10
- mode?: "development" | "production";
11
- prefix?: string; // Default: "SWITE_"
12
- }
13
-
14
- /**
15
- * Load environment variables from .env files.
16
- * Supports .env, .env.local, .env.[mode], .env.[mode].local
17
- */
18
- export function loadEnv(
19
- root: string,
20
- mode: string = "development",
21
- prefix: string = "SWITE_",
22
- ): Record<string, string> {
23
- const env: Record<string, string> = {};
24
- const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];
25
-
26
- for (const file of envFiles) {
27
- const envPath = join(root, file);
28
- if (!existsSync(envPath)) continue;
29
- const content = readFileSync(envPath, "utf-8");
30
- for (const line of content.split("\n")) {
31
- const trimmed = line.trim();
32
- if (!trimmed || trimmed.startsWith("#")) continue;
33
- const match = trimmed.match(/^([^=]+)=(.*)$/);
34
- if (!match) continue;
35
- const key = match[1].trim();
36
- const value = match[2].replace(/^["']|["']$/g, "");
37
- if (key.startsWith(prefix) || key.startsWith("PUBLIC_")) {
38
- env[key] = value;
39
- }
40
- }
41
- }
42
-
43
- return env;
44
- }
45
-
46
- /**
47
- * Replace all import.meta.env references in compiled code with their literal values.
48
- *
49
- * This is the only correct approach for ES modules — import.meta is sealed and
50
- * import.meta.env cannot be assigned at runtime. All substitution must happen at
51
- * transform time (here, after esbuild strips TypeScript).
52
- *
53
- * Handles:
54
- * - import.meta.env.KEY → JSON.stringify(env[KEY]) or "undefined"
55
- * - import.meta.env.DEV → true/false literal
56
- * - import.meta.env.PROD → true/false literal
57
- * - import.meta.env.MODE → "development"/"production" literal
58
- * - bare import.meta.env → serialized object literal (for spread, typeof, etc.)
59
- */
60
- export function inlineEnvReferences(
61
- code: string,
62
- env: Record<string, string>,
63
- mode: string = "development",
64
- ): string {
65
- if (!code.includes("import.meta.env")) return code;
66
-
67
- const isDev = mode !== "production";
68
-
69
- // Named key access first (most specific)
70
- code = code.replace(/\bimport\.meta\.env\.([A-Z_][A-Z0-9_]*)\b/g, (_, key: string) => {
71
- if (key === "DEV") return String(isDev);
72
- if (key === "PROD") return String(!isDev);
73
- if (key === "MODE") return JSON.stringify(mode);
74
- if (key === "SSR") return "false";
75
- if (key in env) return JSON.stringify(env[key]);
76
- return "undefined";
77
- });
78
-
79
- // Bare import.meta.env (spread/typeof patterns)
80
- if (code.includes("import.meta.env")) {
81
- const envLiteral = buildEnvLiteral(env, mode);
82
- code = code.replace(/\bimport\.meta\.env\b/g, envLiteral);
83
- }
84
-
85
- return code;
86
- }
87
-
88
- function buildEnvLiteral(env: Record<string, string>, mode: string): string {
89
- const isDev = mode !== "production";
90
- const entries: string[] = [
91
- `MODE:${JSON.stringify(mode)}`,
92
- `DEV:${isDev}`,
93
- `PROD:${!isDev}`,
94
- `SSR:false`,
95
- ...Object.entries(env).map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`),
96
- ];
97
- return `({${entries.join(",")}})`;
98
- }
1
+ /*
2
+ * Environment Variable Support for SWITE
3
+ * Provides import.meta.env replacement for SWITE
4
+ */
5
+
6
+ import { readFileSync, existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+
9
+ export interface EnvConfig {
10
+ mode?: "development" | "production";
11
+ prefix?: string; // Default: "SWITE_"
12
+ }
13
+
14
+ /**
15
+ * Load environment variables from .env files.
16
+ * Supports .env, .env.local, .env.[mode], .env.[mode].local
17
+ */
18
+ export function loadEnv(
19
+ root: string,
20
+ mode: string = "development",
21
+ prefix: string = "SWITE_",
22
+ ): Record<string, string> {
23
+ const env: Record<string, string> = {};
24
+ const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];
25
+
26
+ for (const file of envFiles) {
27
+ const envPath = join(root, file);
28
+ if (!existsSync(envPath)) continue;
29
+ const content = readFileSync(envPath, "utf-8");
30
+ for (const line of content.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#")) continue;
33
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
34
+ if (!match) continue;
35
+ const key = match[1].trim();
36
+ const value = match[2].replace(/^["']|["']$/g, "");
37
+ if (key.startsWith(prefix) || key.startsWith("PUBLIC_")) {
38
+ env[key] = value;
39
+ }
40
+ }
41
+ }
42
+
43
+ return env;
44
+ }
45
+
46
+ /**
47
+ * Replace all import.meta.env references in compiled code with their literal values.
48
+ *
49
+ * This is the only correct approach for ES modules — import.meta is sealed and
50
+ * import.meta.env cannot be assigned at runtime. All substitution must happen at
51
+ * transform time (here, after esbuild strips TypeScript).
52
+ *
53
+ * Handles:
54
+ * - import.meta.env.KEY → JSON.stringify(env[KEY]) or "undefined"
55
+ * - import.meta.env.DEV → true/false literal
56
+ * - import.meta.env.PROD → true/false literal
57
+ * - import.meta.env.MODE → "development"/"production" literal
58
+ * - bare import.meta.env → serialized object literal (for spread, typeof, etc.)
59
+ */
60
+ export function inlineEnvReferences(
61
+ code: string,
62
+ env: Record<string, string>,
63
+ mode: string = "development",
64
+ ): string {
65
+ if (!code.includes("import.meta.env")) return code;
66
+
67
+ const isDev = mode !== "production";
68
+
69
+ // Named key access first (most specific)
70
+ code = code.replace(/\bimport\.meta\.env\.([A-Z_][A-Z0-9_]*)\b/g, (_, key: string) => {
71
+ if (key === "DEV") return String(isDev);
72
+ if (key === "PROD") return String(!isDev);
73
+ if (key === "MODE") return JSON.stringify(mode);
74
+ if (key === "SSR") return "false";
75
+ if (key in env) return JSON.stringify(env[key]);
76
+ return "undefined";
77
+ });
78
+
79
+ // Bare import.meta.env (spread/typeof patterns)
80
+ if (code.includes("import.meta.env")) {
81
+ const envLiteral = buildEnvLiteral(env, mode);
82
+ code = code.replace(/\bimport\.meta\.env\b/g, envLiteral);
83
+ }
84
+
85
+ return code;
86
+ }
87
+
88
+ function buildEnvLiteral(env: Record<string, string>, mode: string): string {
89
+ const isDev = mode !== "production";
90
+ const entries: string[] = [
91
+ `MODE:${JSON.stringify(mode)}`,
92
+ `DEV:${isDev}`,
93
+ `PROD:${!isDev}`,
94
+ `SSR:false`,
95
+ ...Object.entries(env).map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`),
96
+ ];
97
+ return `({${entries.join(",")}})`;
98
+ }