@swissjs/swite 0.3.5 → 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 (78) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/DIRECTIVE.md +57 -2
  3. package/__tests__/import-rewriter-bug.test.ts +100 -113
  4. package/__tests__/security-r001-r002.test.ts +190 -0
  5. package/dist/build-engine/builder.js +9 -9
  6. package/dist/cli.js +0 -0
  7. package/dist/config/config.d.ts +0 -5
  8. package/dist/config/config.d.ts.map +1 -1
  9. package/dist/dev-engine/handlers/base-handler.d.ts +6 -0
  10. package/dist/dev-engine/handlers/base-handler.d.ts.map +1 -1
  11. package/dist/dev-engine/handlers/base-handler.js +91 -0
  12. package/dist/dev-engine/handlers/ui-handler.d.ts +0 -1
  13. package/dist/dev-engine/handlers/ui-handler.d.ts.map +1 -1
  14. package/dist/dev-engine/handlers/ui-handler.js +2 -64
  15. package/dist/dev-engine/handlers/uix-handler.d.ts +0 -1
  16. package/dist/dev-engine/handlers/uix-handler.d.ts.map +1 -1
  17. package/dist/dev-engine/handlers/uix-handler.js +2 -58
  18. package/dist/dev-engine/hmr/hmr-client-template.js +111 -111
  19. package/dist/dev-engine/hmr/hmr.d.ts +10 -1
  20. package/dist/dev-engine/hmr/hmr.d.ts.map +1 -1
  21. package/dist/dev-engine/hmr/hmr.js +40 -2
  22. package/dist/dev-engine/middleware/middleware-setup.js +4 -3
  23. package/dist/dev-engine/middleware/static-files.d.ts.map +1 -1
  24. package/dist/dev-engine/middleware/static-files.js +145 -62
  25. package/dist/dev-engine/pythonDevManager.js +1 -1
  26. package/dist/dev-engine/router/file-router.d.ts.map +1 -1
  27. package/dist/dev-engine/router/file-router.js +2 -29
  28. package/dist/dev-engine/server.d.ts +7 -0
  29. package/dist/dev-engine/server.d.ts.map +1 -1
  30. package/dist/dev-engine/server.js +31 -3
  31. package/dist/kernel/package-finder.d.ts +0 -8
  32. package/dist/kernel/package-finder.d.ts.map +1 -1
  33. package/dist/kernel/package-finder.js +2 -2
  34. package/dist/kernel/package-registry.d.ts +6 -0
  35. package/dist/kernel/package-registry.d.ts.map +1 -1
  36. package/dist/kernel/package-registry.js +8 -0
  37. package/dist/kernel/workspace.d.ts.map +1 -1
  38. package/dist/kernel/workspace.js +12 -9
  39. package/docs/architecture/build-pipeline.md +97 -97
  40. package/docs/architecture/dev-server.md +87 -87
  41. package/docs/architecture/hmr.md +78 -78
  42. package/docs/architecture/import-rewriting.md +101 -101
  43. package/docs/architecture/index.md +16 -16
  44. package/docs/architecture/python-integration.md +93 -93
  45. package/docs/architecture/resolution.md +92 -92
  46. package/docs/cli/build.md +78 -78
  47. package/docs/cli/dev.md +90 -90
  48. package/docs/cli/index.md +15 -15
  49. package/docs/cli/start.md +45 -45
  50. package/docs/development/contributing.md +74 -74
  51. package/docs/development/index.md +12 -12
  52. package/docs/development/internals.md +101 -101
  53. package/docs/guide/configuration.md +89 -89
  54. package/docs/guide/index.md +13 -13
  55. package/docs/guide/project-structure.md +75 -75
  56. package/docs/guide/quickstart.md +113 -113
  57. package/docs/index.md +16 -16
  58. package/package.json +29 -16
  59. package/src/build-engine/builder.ts +9 -9
  60. package/src/config/config.ts +0 -5
  61. package/src/config/env.ts +98 -98
  62. package/src/dev-engine/handlers/base-handler.ts +109 -0
  63. package/src/dev-engine/handlers/ui-handler.ts +30 -110
  64. package/src/dev-engine/handlers/uix-handler.ts +21 -95
  65. package/src/dev-engine/hmr/hmr-client-template.ts +122 -122
  66. package/src/dev-engine/hmr/hmr.ts +46 -1
  67. package/src/dev-engine/middleware/middleware-setup.ts +354 -354
  68. package/src/dev-engine/middleware/static-files.ts +203 -121
  69. package/src/dev-engine/pythonDevManager.ts +1 -1
  70. package/src/dev-engine/router/file-router.ts +2 -45
  71. package/src/dev-engine/server.ts +33 -3
  72. package/src/kernel/package-finder.ts +2 -2
  73. package/src/kernel/package-registry.ts +9 -0
  74. package/src/kernel/workspace.ts +8 -10
  75. package/src/resolution/cdn/cdn-fallback.ts +40 -40
  76. package/src/resolution/path/path-fixup.ts +27 -27
  77. package/src/resolution/rewriting/import-rewriter.ts +237 -237
  78. package/src/resolution/symlink-registry.ts +114 -114
@@ -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.3.5",
3
+ "version": "0.4.2",
4
4
  "description": "SWITE - SWISS Development Server (Vite replacement)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,16 +8,26 @@
8
8
  "bin": {
9
9
  "swite": "dist/cli.js"
10
10
  },
11
+ "scripts": {
12
+ "build": "tsc --noEmitOnError false",
13
+ "dev": "tsc --noEmitOnError false --watch",
14
+ "clean": "rm -rf dist node_modules && rm -f tsconfig.tsbuildinfo",
15
+ "generate-import-map": "tsx src/internal/generate-import-map-cli.ts",
16
+ "test": "node --import tsx --test __tests__/import-rewriter-bug.test.ts",
17
+ "changeset": "changeset",
18
+ "release:version": "changeset version",
19
+ "release:publish": "changeset publish"
20
+ },
11
21
  "dependencies": {
12
- "@swissjs/core": "0.1.11",
13
- "@swissjs/compiler": "0.1.5",
14
- "@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",
15
25
  "chalk": "^5.3.0",
16
26
  "chokidar": "^3.5.3",
17
27
  "es-module-lexer": "^1.3.1",
18
28
  "esbuild": "^0.25.0",
19
29
  "express": "^4.18.2",
20
- "ws": "^8.20.1"
30
+ "ws": "^8.21.0"
21
31
  },
22
32
  "devDependencies": {
23
33
  "@types/express": "^4.17.21",
@@ -32,18 +42,21 @@
32
42
  "access": "public"
33
43
  },
34
44
  "license": "MIT",
45
+ "pnpm": {
46
+ "onlyBuiltDependencies": [
47
+ "esbuild"
48
+ ],
49
+ "overrides": {
50
+ "path-to-regexp": "^0.1.13",
51
+ "picomatch": "^2.3.2",
52
+ "qs": ">=6.15.2",
53
+ "esbuild": ">=0.28.1",
54
+ "vite": ">=6.4.3",
55
+ "js-yaml": "^3.15.0"
56
+ }
57
+ },
35
58
  "repository": {
36
59
  "type": "git",
37
60
  "url": "git+https://github.com/kibologic/swite.git"
38
- },
39
- "scripts": {
40
- "build": "tsc -b",
41
- "dev": "tsc -b --watch",
42
- "clean": "rm -rf dist node_modules && rm -f tsconfig.tsbuildinfo",
43
- "generate-import-map": "tsx src/internal/generate-import-map-cli.ts",
44
- "test": "node --import tsx --test __tests__/import-rewriter-bug.test.ts",
45
- "changeset": "changeset",
46
- "release:version": "changeset version",
47
- "release:publish": "changeset publish"
48
61
  }
49
- }
62
+ }
@@ -56,9 +56,9 @@ export class SwiteBuilder {
56
56
  await this.copyPublicAssets();
57
57
 
58
58
  const duration = Date.now() - startTime;
59
- console.log(chalk.green(`\n Build completed in ${duration}ms\n`));
59
+ console.log(chalk.green(`\n[OK] Build completed in ${duration}ms\n`));
60
60
  } catch (error) {
61
- console.error(chalk.red("\n Build failed:"), error);
61
+ console.error(chalk.red("\n[FAIL] Build failed:"), error);
62
62
  throw error;
63
63
  } finally {
64
64
  await fs.rm(tempDir, { recursive: true, force: true });
@@ -66,13 +66,13 @@ export class SwiteBuilder {
66
66
  }
67
67
 
68
68
  private async cleanOutputDir(): Promise<void> {
69
- console.log(chalk.blue("🧹 Cleaning output directory..."));
69
+ console.log(chalk.blue("[clean] Cleaning output directory..."));
70
70
  await fs.rm(this.config.outDir, { recursive: true, force: true });
71
71
  await fs.mkdir(this.config.outDir, { recursive: true });
72
72
  }
73
73
 
74
74
  private async compileSwissFiles(tempDir: string): Promise<void> {
75
- console.log(chalk.blue("🔨 Compiling Swiss files..."));
75
+ console.log(chalk.blue("[compile] Compiling Swiss files..."));
76
76
  await fs.mkdir(tempDir, { recursive: true });
77
77
 
78
78
  const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
@@ -90,7 +90,7 @@ export class SwiteBuilder {
90
90
  // Step 2: Discover and compile workspace dependencies
91
91
  const workspaceDeps = await this.discoverWorkspaceDependencies();
92
92
  for (const dep of workspaceDeps) {
93
- console.log(chalk.blue(`📦 Compiling dependency: ${dep.name}`));
93
+ console.log(chalk.blue(`[bundle] Compiling dependency: ${dep.name}`));
94
94
  // Preserve workspace structure: libraries/skltn/src or packages/cart/src or modules/cart/src
95
95
  const depRelativeToWorkspace = workspaceRoot
96
96
  ? path.relative(workspaceRoot, dep.pkgDir)
@@ -269,7 +269,7 @@ export class SwiteBuilder {
269
269
  pkgDir,
270
270
  });
271
271
  console.log(
272
- chalk.gray(` 📦 Found workspace dependency: ${depName}`),
272
+ chalk.gray(` [dep] Found workspace dependency: ${depName}`),
273
273
  );
274
274
  break;
275
275
  }
@@ -315,7 +315,7 @@ export class SwiteBuilder {
315
315
  });
316
316
  console.log(
317
317
  chalk.gray(
318
- ` 📦 Discovered transitive dependency: ${pkgName}`,
318
+ ` [dep] Discovered transitive dependency: ${pkgName}`,
319
319
  ),
320
320
  );
321
321
  break;
@@ -325,14 +325,14 @@ export class SwiteBuilder {
325
325
  }
326
326
  }
327
327
  } catch (error) {
328
- console.warn(chalk.yellow("⚠️ Could not discover dependencies:"), error);
328
+ console.warn(chalk.yellow("[warn] Could not discover dependencies:"), error);
329
329
  }
330
330
 
331
331
  return deps;
332
332
  }
333
333
 
334
334
  private async bundle(tempDir: string): Promise<void> {
335
- console.log(chalk.blue("📦 Bundling with esbuild..."));
335
+ console.log(chalk.blue("[bundle] Bundling with esbuild..."));
336
336
 
337
337
  const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
338
338
  const appRelativeToWorkspace = workspaceRoot
@@ -37,11 +37,6 @@ export interface SwiteUserConfig {
37
37
  * Path is relative to the project root.
38
38
  */
39
39
  entry?: string;
40
- /**
41
- * Module aliases resolved during bare import resolution.
42
- * e.g. { "@/": "src/" } maps @/ imports to the src/ directory.
43
- */
44
- aliases?: Record<string, string>;
45
40
  /**
46
41
  * Glob patterns to exclude from HMR watching in addition to the defaults
47
42
  * (node_modules, .git, dist). Useful for generated files or large assets.
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
+ }