@swissjs/swite 0.3.0

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 (163) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/workflows/ci.yml +59 -0
  3. package/.github/workflows/publish.yml +50 -0
  4. package/.github/workflows/release.yml +53 -0
  5. package/BUILD_ANALYSIS.md +89 -0
  6. package/BUILD_STRATEGY.md +75 -0
  7. package/CHANGELOG.md +53 -0
  8. package/DIRECTIVE.md +488 -0
  9. package/__tests__/css-extraction.test.ts +261 -0
  10. package/__tests__/css-injection-integration.test.ts +247 -0
  11. package/__tests__/css-middleware.test.ts +191 -0
  12. package/__tests__/import-rewriter-bug.test.ts +135 -0
  13. package/dist/builder.d.ts +36 -0
  14. package/dist/builder.d.ts.map +1 -0
  15. package/dist/builder.js +772 -0
  16. package/dist/cache/compilation-cache.d.ts +33 -0
  17. package/dist/cache/compilation-cache.d.ts.map +1 -0
  18. package/dist/cache/compilation-cache.js +130 -0
  19. package/dist/cli.d.ts +3 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +85 -0
  22. package/dist/config-loader.d.ts +8 -0
  23. package/dist/config-loader.d.ts.map +1 -0
  24. package/dist/config-loader.js +40 -0
  25. package/dist/config.d.ts +29 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +7 -0
  28. package/dist/dev/pythonDevManager.d.ts +12 -0
  29. package/dist/dev/pythonDevManager.d.ts.map +1 -0
  30. package/dist/dev/pythonDevManager.js +85 -0
  31. package/dist/env.d.ts +19 -0
  32. package/dist/env.d.ts.map +1 -0
  33. package/dist/env.js +112 -0
  34. package/dist/handlers/base-handler.d.ts +21 -0
  35. package/dist/handlers/base-handler.d.ts.map +1 -0
  36. package/dist/handlers/base-handler.js +38 -0
  37. package/dist/handlers/js-handler.d.ts +10 -0
  38. package/dist/handlers/js-handler.d.ts.map +1 -0
  39. package/dist/handlers/js-handler.js +87 -0
  40. package/dist/handlers/mjs-handler.d.ts +8 -0
  41. package/dist/handlers/mjs-handler.d.ts.map +1 -0
  42. package/dist/handlers/mjs-handler.js +44 -0
  43. package/dist/handlers/node-module-handler.d.ts +16 -0
  44. package/dist/handlers/node-module-handler.d.ts.map +1 -0
  45. package/dist/handlers/node-module-handler.js +267 -0
  46. package/dist/handlers/ts-handler.d.ts +11 -0
  47. package/dist/handlers/ts-handler.d.ts.map +1 -0
  48. package/dist/handlers/ts-handler.js +120 -0
  49. package/dist/handlers/ui-handler.d.ts +12 -0
  50. package/dist/handlers/ui-handler.d.ts.map +1 -0
  51. package/dist/handlers/ui-handler.js +182 -0
  52. package/dist/handlers/uix-handler.d.ts +12 -0
  53. package/dist/handlers/uix-handler.d.ts.map +1 -0
  54. package/dist/handlers/uix-handler.js +135 -0
  55. package/dist/hmr.d.ts +20 -0
  56. package/dist/hmr.d.ts.map +1 -0
  57. package/dist/hmr.js +265 -0
  58. package/dist/import-rewriter.d.ts +3 -0
  59. package/dist/import-rewriter.d.ts.map +1 -0
  60. package/dist/import-rewriter.js +351 -0
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/middleware/hmr-routes.d.ts +12 -0
  65. package/dist/middleware/hmr-routes.d.ts.map +1 -0
  66. package/dist/middleware/hmr-routes.js +97 -0
  67. package/dist/middleware/middleware-setup.d.ts +23 -0
  68. package/dist/middleware/middleware-setup.d.ts.map +1 -0
  69. package/dist/middleware/middleware-setup.js +596 -0
  70. package/dist/middleware/static-files.d.ts +15 -0
  71. package/dist/middleware/static-files.d.ts.map +1 -0
  72. package/dist/middleware/static-files.js +585 -0
  73. package/dist/proxy/SwiteProxyError.d.ts +6 -0
  74. package/dist/proxy/SwiteProxyError.d.ts.map +1 -0
  75. package/dist/proxy/SwiteProxyError.js +9 -0
  76. package/dist/proxy/proxyToPython.d.ts +28 -0
  77. package/dist/proxy/proxyToPython.d.ts.map +1 -0
  78. package/dist/proxy/proxyToPython.js +66 -0
  79. package/dist/resolver/bare-import-resolver.d.ts +9 -0
  80. package/dist/resolver/bare-import-resolver.d.ts.map +1 -0
  81. package/dist/resolver/bare-import-resolver.js +363 -0
  82. package/dist/resolver/symlink-registry.d.ts +13 -0
  83. package/dist/resolver/symlink-registry.d.ts.map +1 -0
  84. package/dist/resolver/symlink-registry.js +98 -0
  85. package/dist/resolver/url-resolver.d.ts +11 -0
  86. package/dist/resolver/url-resolver.d.ts.map +1 -0
  87. package/dist/resolver/url-resolver.js +268 -0
  88. package/dist/resolver/workspace-package-resolver.d.ts +10 -0
  89. package/dist/resolver/workspace-package-resolver.d.ts.map +1 -0
  90. package/dist/resolver/workspace-package-resolver.js +185 -0
  91. package/dist/resolver.d.ts +17 -0
  92. package/dist/resolver.d.ts.map +1 -0
  93. package/dist/resolver.js +191 -0
  94. package/dist/router/file-router.d.ts +19 -0
  95. package/dist/router/file-router.d.ts.map +1 -0
  96. package/dist/router/file-router.js +114 -0
  97. package/dist/server.d.ts +22 -0
  98. package/dist/server.d.ts.map +1 -0
  99. package/dist/server.js +122 -0
  100. package/dist/utils/cdn-fallback.d.ts +14 -0
  101. package/dist/utils/cdn-fallback.d.ts.map +1 -0
  102. package/dist/utils/cdn-fallback.js +36 -0
  103. package/dist/utils/file-path-resolver.d.ts +9 -0
  104. package/dist/utils/file-path-resolver.d.ts.map +1 -0
  105. package/dist/utils/file-path-resolver.js +187 -0
  106. package/dist/utils/generate-import-map-cli.d.ts +3 -0
  107. package/dist/utils/generate-import-map-cli.d.ts.map +1 -0
  108. package/dist/utils/generate-import-map-cli.js +32 -0
  109. package/dist/utils/generate-import-map.d.ts +21 -0
  110. package/dist/utils/generate-import-map.d.ts.map +1 -0
  111. package/dist/utils/generate-import-map.js +119 -0
  112. package/dist/utils/package-finder.d.ts +24 -0
  113. package/dist/utils/package-finder.d.ts.map +1 -0
  114. package/dist/utils/package-finder.js +161 -0
  115. package/dist/utils/package-registry.d.ts +36 -0
  116. package/dist/utils/package-registry.d.ts.map +1 -0
  117. package/dist/utils/package-registry.js +159 -0
  118. package/dist/utils/workspace.d.ts +6 -0
  119. package/dist/utils/workspace.d.ts.map +1 -0
  120. package/dist/utils/workspace.js +65 -0
  121. package/docs/IMPORT_REWRITING.md +164 -0
  122. package/docs/IMPORT_REWRITING_TROUBLESHOOTING.md +139 -0
  123. package/docs/PATH_RESOLUTION_GUIDE.md +221 -0
  124. package/package.json +49 -0
  125. package/src/adapters/proxy/SwiteProxyError.ts +12 -0
  126. package/src/adapters/proxy/proxyToPython.ts +88 -0
  127. package/src/build-engine/builder.ts +960 -0
  128. package/src/cli.ts +109 -0
  129. package/src/config/config-loader.ts +46 -0
  130. package/src/config/config.ts +34 -0
  131. package/src/config/env.ts +98 -0
  132. package/src/dev-engine/handlers/base-handler.ts +68 -0
  133. package/src/dev-engine/handlers/js-handler.ts +134 -0
  134. package/src/dev-engine/handlers/mjs-handler.ts +65 -0
  135. package/src/dev-engine/handlers/node-module-handler.ts +339 -0
  136. package/src/dev-engine/handlers/ts-handler.ts +143 -0
  137. package/src/dev-engine/handlers/ui-handler.ts +105 -0
  138. package/src/dev-engine/handlers/uix-handler.ts +90 -0
  139. package/src/dev-engine/hmr/hmr-client-template.ts +122 -0
  140. package/src/dev-engine/hmr/hmr.ts +173 -0
  141. package/src/dev-engine/middleware/hmr-routes.ts +120 -0
  142. package/src/dev-engine/middleware/middleware-setup.ts +351 -0
  143. package/src/dev-engine/middleware/static-files.ts +728 -0
  144. package/src/dev-engine/pythonDevManager.ts +116 -0
  145. package/src/dev-engine/router/file-router.ts +164 -0
  146. package/src/dev-engine/server.ts +152 -0
  147. package/src/index.ts +26 -0
  148. package/src/internal/cache/compilation-cache.ts +182 -0
  149. package/src/internal/generate-import-map-cli.ts +40 -0
  150. package/src/internal/generate-import-map.ts +154 -0
  151. package/src/kernel/package-finder.ts +164 -0
  152. package/src/kernel/package-registry.ts +198 -0
  153. package/src/kernel/workspace.ts +62 -0
  154. package/src/resolution/bare-import-resolver.ts +400 -0
  155. package/src/resolution/cdn/cdn-fallback.ts +37 -0
  156. package/src/resolution/path/file-path-resolver.ts +190 -0
  157. package/src/resolution/path/path-fixup.ts +19 -0
  158. package/src/resolution/resolver.ts +198 -0
  159. package/src/resolution/rewriting/import-rewriter.ts +237 -0
  160. package/src/resolution/symlink-registry.ts +114 -0
  161. package/src/resolution/url-resolver.ts +231 -0
  162. package/src/resolution/workspace-package-resolver.ts +94 -0
  163. package/tsconfig.json +37 -0
@@ -0,0 +1,960 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Production Builder
4
+ * Licensed under the MIT License.
5
+ */
6
+
7
+ import { build as esbuild, type Plugin } from "esbuild";
8
+ import type { BuildOptions } from "esbuild";
9
+ import { UiCompiler } from "@swissjs/compiler";
10
+ import { promises as fs } from "node:fs";
11
+ import path from "node:path";
12
+ import chalk from "chalk";
13
+ import { ModuleResolver } from "../resolution/resolver.js";
14
+
15
+ export interface BuildConfig {
16
+ root: string;
17
+ entry: string;
18
+ outDir: string;
19
+ publicDir?: string;
20
+ minify?: boolean;
21
+ sourcemap?: boolean;
22
+ format?: "esm" | "cjs" | "iife";
23
+ target?: string;
24
+ external?: string[];
25
+ }
26
+
27
+ export class SwiteBuilder {
28
+ private compiler = new UiCompiler();
29
+ private config: Required<BuildConfig>;
30
+ private resolver: ModuleResolver;
31
+
32
+ constructor(config: BuildConfig) {
33
+ this.config = {
34
+ root: config.root,
35
+ entry: config.entry,
36
+ outDir: config.outDir,
37
+ publicDir: config.publicDir || "public",
38
+ minify: config.minify ?? true,
39
+ sourcemap: config.sourcemap ?? false,
40
+ format: config.format || "esm",
41
+ target: config.target || "es2020",
42
+ external: config.external || [],
43
+ };
44
+ this.resolver = new ModuleResolver(config.root);
45
+ }
46
+
47
+ async build(): Promise<void> {
48
+ const startTime = Date.now();
49
+ console.log(chalk.cyan("\n⚡ SWITE - Production Build\n"));
50
+
51
+ const tempDir = path.join(this.config.root, ".swite-build");
52
+ try {
53
+ await this.cleanOutputDir();
54
+ await this.compileSwissFiles(tempDir);
55
+ await this.bundle(tempDir);
56
+ await this.copyPublicAssets();
57
+
58
+ const duration = Date.now() - startTime;
59
+ console.log(chalk.green(`\n✅ Build completed in ${duration}ms\n`));
60
+ } catch (error) {
61
+ console.error(chalk.red("\n❌ Build failed:"), error);
62
+ throw error;
63
+ } finally {
64
+ await fs.rm(tempDir, { recursive: true, force: true });
65
+ }
66
+ }
67
+
68
+ private async cleanOutputDir(): Promise<void> {
69
+ console.log(chalk.blue("🧹 Cleaning output directory..."));
70
+ await fs.rm(this.config.outDir, { recursive: true, force: true });
71
+ await fs.mkdir(this.config.outDir, { recursive: true });
72
+ }
73
+
74
+ private async compileSwissFiles(tempDir: string): Promise<void> {
75
+ console.log(chalk.blue("🔨 Compiling Swiss files..."));
76
+ await fs.mkdir(tempDir, { recursive: true });
77
+
78
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
79
+ const appRelativeToWorkspace = workspaceRoot
80
+ ? path.relative(workspaceRoot, this.config.root)
81
+ : "";
82
+
83
+ // Step 1: Compile app's own files
84
+ const srcDir = path.join(this.config.root, "src");
85
+ const appTempDir = appRelativeToWorkspace
86
+ ? path.join(tempDir, appRelativeToWorkspace, "src")
87
+ : path.join(tempDir, "src");
88
+ await this.compileDirectory(srcDir, appTempDir, "app");
89
+
90
+ // Step 2: Discover and compile workspace dependencies
91
+ const workspaceDeps = await this.discoverWorkspaceDependencies();
92
+ for (const dep of workspaceDeps) {
93
+ console.log(chalk.blue(`📦 Compiling dependency: ${dep.name}`));
94
+ // Preserve workspace structure: libraries/skltn/src or packages/cart/src or modules/cart/src
95
+ const depRelativeToWorkspace = workspaceRoot
96
+ ? path.relative(workspaceRoot, dep.pkgDir)
97
+ : "";
98
+ const depTempDir = depRelativeToWorkspace
99
+ ? path.join(tempDir, depRelativeToWorkspace, "src")
100
+ : path.join(tempDir, "src");
101
+ await this.compileDirectory(dep.srcDir, depTempDir, dep.name);
102
+ }
103
+ }
104
+
105
+ private async compileDirectory(
106
+ srcDir: string,
107
+ tempDir: string,
108
+ label: string,
109
+ ): Promise<void> {
110
+ // Find all .ui and .uix files
111
+ const files = await this.findSwissFiles(srcDir);
112
+
113
+ for (const file of files) {
114
+ const relativePath = path.relative(srcDir, file);
115
+ const outputPath = path.join(
116
+ tempDir,
117
+ relativePath.replace(/\.(ui|uix)$/, ".tsx"),
118
+ );
119
+
120
+ // Ensure output directory exists
121
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
122
+
123
+ // Compile file
124
+ const source = await fs.readFile(file, "utf-8");
125
+ let compiled = await this.compiler.compileAsync(source, file);
126
+
127
+ // Rewrite .ui/.uix imports to .tsx in compiled output (esbuild needs .tsx for JSX)
128
+ compiled = compiled.replace(
129
+ /from\s+['"]([^'"]*\.)(ui|uix)['"]/g,
130
+ "from '$1tsx'",
131
+ );
132
+ compiled = compiled.replace(
133
+ /import\s+['"]([^'"]*\.)(ui|uix)['"]/g,
134
+ "import '$1tsx'",
135
+ );
136
+
137
+ // Add export default for the named export so default imports resolve at bundle time
138
+ const namedExportMatch = compiled.match(/export\s*\{\s*(\w+)\s*\}\s*;?\s*$/);
139
+ if (namedExportMatch?.[1]) {
140
+ compiled += `\nexport default ${namedExportMatch[1]};\n`;
141
+ }
142
+
143
+ await fs.writeFile(outputPath, compiled, "utf-8");
144
+
145
+ console.log(chalk.gray(` ✓ [${label}] ${relativePath}`));
146
+ }
147
+
148
+ // Copy .ts files and rewrite .ui/.uix imports to .tsx
149
+ const tsFiles = await this.findFiles(srcDir, /\.ts$/);
150
+ for (const file of tsFiles) {
151
+ const relativePath = path.relative(srcDir, file);
152
+ const outputPath = path.join(tempDir, relativePath);
153
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
154
+
155
+ // Read source and rewrite imports
156
+ const source = await fs.readFile(file, "utf-8");
157
+ // Replace .ui and .uix imports with .tsx (compiled Swiss files are emitted as .tsx)
158
+ const rewritten = source.replace(
159
+ /from\s+['"](\.\/[^'"]*\.)(ui|uix)['"]/g,
160
+ "from '$1tsx'",
161
+ );
162
+ await fs.writeFile(outputPath, rewritten, "utf-8");
163
+ }
164
+
165
+ // Copy .css and other static assets so imports resolve
166
+ const cssFiles = await this.findFiles(srcDir, /\.css$/);
167
+ for (const file of cssFiles) {
168
+ const relativePath = path.relative(srcDir, file);
169
+ const outputPath = path.join(tempDir, relativePath);
170
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
171
+ await fs.copyFile(file, outputPath);
172
+ }
173
+ }
174
+
175
+ private async discoverWorkspaceDependencies(): Promise<
176
+ Array<{ name: string; srcDir: string; pkgDir: string }>
177
+ > {
178
+ const deps: Array<{ name: string; srcDir: string; pkgDir: string }> = [];
179
+
180
+ try {
181
+ const packageJsonPath = path.join(this.config.root, "package.json");
182
+ const packageJson = JSON.parse(
183
+ await fs.readFile(packageJsonPath, "utf-8"),
184
+ );
185
+
186
+ const allDeps = {
187
+ ...packageJson.dependencies,
188
+ ...packageJson.peerDependencies,
189
+ ...packageJson.devDependencies,
190
+ };
191
+
192
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
193
+ if (!workspaceRoot) {
194
+ return deps;
195
+ }
196
+
197
+ // Also discover packages imported in source files (for transitive dependencies)
198
+ const discoveredPackages = new Set<string>();
199
+ const srcDir = path.join(this.config.root, "src");
200
+
201
+ // Scan source files for workspace package imports
202
+ const scanForImports = async (dir: string) => {
203
+ try {
204
+ const entries = await fs.readdir(dir, { withFileTypes: true });
205
+ for (const entry of entries) {
206
+ const fullPath = path.join(dir, entry.name);
207
+ if (entry.isDirectory()) {
208
+ await scanForImports(fullPath);
209
+ } else if (
210
+ entry.name.endsWith(".ts") ||
211
+ entry.name.endsWith(".ui") ||
212
+ entry.name.endsWith(".uix")
213
+ ) {
214
+ const content = await fs.readFile(fullPath, "utf-8");
215
+ // Match imports like: import ... from '@swiss-enterprise/cart/...' or '@alpine/skltn/...'
216
+ const importMatches = content.matchAll(
217
+ /from\s+['"](@[\w-]+\/[\w-]+)/g,
218
+ );
219
+ for (const match of importMatches) {
220
+ const pkgName = match[1];
221
+ if (pkgName && !discoveredPackages.has(pkgName)) {
222
+ discoveredPackages.add(pkgName);
223
+ }
224
+ }
225
+ }
226
+ }
227
+ } catch {
228
+ // Ignore errors
229
+ }
230
+ };
231
+
232
+ if (await this.fileExists(srcDir)) {
233
+ await scanForImports(srcDir);
234
+ }
235
+
236
+ for (const [depName, depVersion] of Object.entries(allDeps)) {
237
+ // Only process workspace dependencies
238
+ if (
239
+ typeof depVersion === "string" &&
240
+ depVersion.startsWith("workspace:")
241
+ ) {
242
+ // Extract package name (handle scoped packages)
243
+ const pkgName = depName.startsWith("@")
244
+ ? depName.split("/")[1]
245
+ : depName;
246
+
247
+ // Try common package locations
248
+ const possibleDirs = [
249
+ path.join(workspaceRoot, "lib", pkgName),
250
+ path.join(workspaceRoot, "packages", pkgName),
251
+ path.join(workspaceRoot, "packages", "runtime", pkgName),
252
+ path.join(workspaceRoot, "packages", "plugins", pkgName),
253
+ path.join(workspaceRoot, "packages", "domain", pkgName),
254
+ ];
255
+
256
+ for (const pkgDir of possibleDirs) {
257
+ const pkgJsonPath = path.join(pkgDir, "package.json");
258
+ if (await this.fileExists(pkgJsonPath)) {
259
+ const pkgJson = JSON.parse(
260
+ await fs.readFile(pkgJsonPath, "utf-8"),
261
+ );
262
+ // Verify it's the right package
263
+ if (pkgJson.name === depName) {
264
+ const srcDir = path.join(pkgDir, "src");
265
+ if (await this.fileExists(srcDir)) {
266
+ deps.push({
267
+ name: depName,
268
+ srcDir,
269
+ pkgDir,
270
+ });
271
+ console.log(
272
+ chalk.gray(` 📦 Found workspace dependency: ${depName}`),
273
+ );
274
+ break;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ // Also process discovered packages from source files
283
+ for (const pkgName of discoveredPackages) {
284
+ // Skip if already in deps
285
+ if (deps.some((d) => d.name === pkgName)) {
286
+ continue;
287
+ }
288
+
289
+ // Extract package name (handle scoped packages)
290
+ const pkgNameOnly = pkgName.startsWith("@")
291
+ ? pkgName.split("/")[1]
292
+ : pkgName;
293
+
294
+ // Try common package locations
295
+ const possibleDirs = [
296
+ path.join(workspaceRoot, "lib", pkgNameOnly),
297
+ path.join(workspaceRoot, "packages", pkgNameOnly),
298
+ path.join(workspaceRoot, "packages", "runtime", pkgNameOnly),
299
+ path.join(workspaceRoot, "packages", "plugins", pkgNameOnly),
300
+ path.join(workspaceRoot, "packages", "domain", pkgNameOnly),
301
+ ];
302
+
303
+ for (const pkgDir of possibleDirs) {
304
+ const pkgJsonPath = path.join(pkgDir, "package.json");
305
+ if (await this.fileExists(pkgJsonPath)) {
306
+ const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
307
+ // Verify it's the right package
308
+ if (pkgJson.name === pkgName) {
309
+ const srcDir = path.join(pkgDir, "src");
310
+ if (await this.fileExists(srcDir)) {
311
+ deps.push({
312
+ name: pkgName,
313
+ srcDir,
314
+ pkgDir,
315
+ });
316
+ console.log(
317
+ chalk.gray(
318
+ ` 📦 Discovered transitive dependency: ${pkgName}`,
319
+ ),
320
+ );
321
+ break;
322
+ }
323
+ }
324
+ }
325
+ }
326
+ }
327
+ } catch (error) {
328
+ console.warn(chalk.yellow("⚠️ Could not discover dependencies:"), error);
329
+ }
330
+
331
+ return deps;
332
+ }
333
+
334
+ private async bundle(tempDir: string): Promise<void> {
335
+ console.log(chalk.blue("📦 Bundling with esbuild..."));
336
+
337
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
338
+ const appRelativeToWorkspace = workspaceRoot
339
+ ? path.relative(workspaceRoot, this.config.root)
340
+ : "";
341
+
342
+ // Determine entry point - look for compiled version in temp
343
+ // const entryBasename = path.basename(this.config.entry, path.extname(this.config.entry)); // Unused
344
+ const entryExt = path.extname(this.config.entry);
345
+
346
+ let entryPoint: string;
347
+ // Entry point is always relative to src directory
348
+ const entryRelativeToSrc = path.relative(
349
+ path.join(this.config.root, "src"),
350
+ this.config.entry,
351
+ );
352
+
353
+ if (entryExt === ".ui" || entryExt === ".uix") {
354
+ // Entry was a Swiss file, use compiled .tsx version
355
+ const entryTsx = entryRelativeToSrc.replace(/\.(ui|uix)$/, ".tsx");
356
+ entryPoint = appRelativeToWorkspace
357
+ ? path.join(tempDir, appRelativeToWorkspace, "src", entryTsx)
358
+ : path.join(tempDir, "src", entryTsx);
359
+ } else {
360
+ // Entry is .ts or .js, use from temp
361
+ entryPoint = appRelativeToWorkspace
362
+ ? path.join(tempDir, appRelativeToWorkspace, "src", entryRelativeToSrc)
363
+ : path.join(tempDir, "src", entryRelativeToSrc);
364
+ }
365
+
366
+ // Verify entry point exists
367
+ if (!(await this.fileExists(entryPoint))) {
368
+ throw new Error(
369
+ `Entry point not found: ${entryPoint} (from ${this.config.entry})`,
370
+ );
371
+ }
372
+
373
+ // Configure esbuild to resolve workspace packages from temp directory
374
+ const absWorkingDir = workspaceRoot || this.config.root;
375
+ // const aliases = workspaceRoot ? await this.createAliases(workspaceRoot, tempDir) : {}; // Unused
376
+
377
+ // Mark Node.js built-ins as external; user-facing framework packages are
378
+ // resolved at runtime — do not hardcode package scopes here
379
+ const nodeBuiltins = [
380
+ "fs",
381
+ "path",
382
+ "os",
383
+ "crypto",
384
+ "http",
385
+ "https",
386
+ "net",
387
+ "stream",
388
+ "util",
389
+ "events",
390
+ "child_process",
391
+ "url",
392
+ "querystring",
393
+ "zlib",
394
+ "assert",
395
+ "constants",
396
+ "tty",
397
+ "node:fs",
398
+ "node:path",
399
+ "node:os",
400
+ "node:crypto",
401
+ "node:http",
402
+ "node:https",
403
+ "node:net",
404
+ "node:stream",
405
+ "node:util",
406
+ "node:events",
407
+ "node:child_process",
408
+ "node:url",
409
+ "node:querystring",
410
+ "node:zlib",
411
+ "node:assert",
412
+ "node:constants",
413
+ "node:tty",
414
+ "fs/promises",
415
+ "node:fs/promises",
416
+ ];
417
+
418
+ // Resolve relative .js imports to .tsx when UiCompiler rewrites .ui→.js but emits .tsx files
419
+ const jsTsxFallbackPlugin: Plugin = {
420
+ name: "js-tsx-fallback",
421
+ setup(build) {
422
+ build.onResolve({ filter: /\.js$/ }, async (args) => {
423
+ if (!args.path.startsWith(".")) return undefined;
424
+ const jsPath = path.resolve(path.dirname(args.resolveDir), args.path);
425
+ const tsxPath = jsPath.replace(/\.js$/, ".tsx");
426
+ try {
427
+ await fs.access(tsxPath);
428
+ return { path: tsxPath };
429
+ } catch {
430
+ return undefined;
431
+ }
432
+ });
433
+ },
434
+ };
435
+
436
+ // Stub .css imports so they resolve (build output may copy CSS separately)
437
+ const cssStubPlugin: Plugin = {
438
+ name: "css-stub",
439
+ setup(build) {
440
+ build.onLoad({ filter: /\.css$/ }, () => ({
441
+ contents: "export {};",
442
+ loader: "js",
443
+ }));
444
+ },
445
+ };
446
+
447
+ // Create plugin to resolve workspace packages to compiled files
448
+ const workspaceDeps = await this.discoverWorkspaceDependencies();
449
+ const fileExists = this.fileExists.bind(this);
450
+ const findWorkspaceRoot = this.findWorkspaceRoot.bind(this);
451
+ const appRoot = this.config.root;
452
+ const wsRoot = await findWorkspaceRoot(appRoot);
453
+ const tempDirForPlugin = tempDir; // Capture tempDir for plugin
454
+
455
+ // Helper function to safely join paths - filters out invalid values
456
+ const safePathJoin = (
457
+ ...parts: (string | undefined | null)[]
458
+ ): string | null => {
459
+ const validParts = parts.filter(
460
+ (p): p is string => p != null && typeof p === "string" && p.length > 0,
461
+ );
462
+ if (validParts.length === 0) return null;
463
+ try {
464
+ return path.join(...validParts);
465
+ } catch {
466
+ return null;
467
+ }
468
+ };
469
+
470
+ const workspaceResolverPlugin: Plugin = {
471
+ name: "workspace-resolver",
472
+ setup(build) {
473
+ // Resolve workspace packages
474
+ build.onResolve({ filter: /^@/ }, async (args) => {
475
+ // Early return if tempDirForPlugin is invalid
476
+ if (
477
+ !tempDirForPlugin ||
478
+ typeof tempDirForPlugin !== "string" ||
479
+ tempDirForPlugin.length === 0
480
+ ) {
481
+ return undefined;
482
+ }
483
+ // Check if this is a workspace package (from dependencies or try to find it)
484
+ let matchingDep = workspaceDeps.find((d: { name: string }) =>
485
+ args.path.startsWith(d.name),
486
+ );
487
+
488
+ // If not in dependencies, try to find it in workspace
489
+ if (!matchingDep && wsRoot) {
490
+ const pkgName = args.path.split("/")[0];
491
+ const pkgNameOnly = pkgName.startsWith("@")
492
+ ? pkgName.split("/")[1]
493
+ : pkgName;
494
+ if (
495
+ !wsRoot ||
496
+ typeof wsRoot !== "string" ||
497
+ !pkgNameOnly ||
498
+ typeof pkgNameOnly !== "string"
499
+ ) {
500
+ return undefined;
501
+ }
502
+
503
+ const possibleDirs = [
504
+ safePathJoin(wsRoot, "lib", pkgNameOnly),
505
+ safePathJoin(wsRoot, "packages", pkgNameOnly),
506
+ ].filter((p): p is string => p !== null);
507
+
508
+ for (const pkgDir of possibleDirs) {
509
+ const pkgJsonPath = safePathJoin(pkgDir, "package.json");
510
+ if (pkgJsonPath && (await fileExists(pkgJsonPath))) {
511
+ const pkgJson = JSON.parse(
512
+ await fs.readFile(pkgJsonPath, "utf-8"),
513
+ );
514
+ if (pkgJson.name === pkgName) {
515
+ const srcDir = safePathJoin(pkgDir, "src");
516
+ if (srcDir && (await fileExists(srcDir))) {
517
+ matchingDep = {
518
+ name: pkgName,
519
+ srcDir: srcDir,
520
+ pkgDir: pkgDir,
521
+ };
522
+ break;
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ if (matchingDep && wsRoot && matchingDep.pkgDir) {
530
+ try {
531
+ // Resolve to compiled file in temp directory
532
+ let depRelativeToWorkspace: string = "";
533
+ try {
534
+ if (
535
+ wsRoot &&
536
+ matchingDep.pkgDir &&
537
+ typeof wsRoot === "string" &&
538
+ typeof matchingDep.pkgDir === "string"
539
+ ) {
540
+ const rel = path.relative(wsRoot, matchingDep.pkgDir);
541
+ if (
542
+ rel &&
543
+ typeof rel === "string" &&
544
+ rel !== "." &&
545
+ rel.length > 0
546
+ ) {
547
+ depRelativeToWorkspace = rel;
548
+ }
549
+ }
550
+ } catch (err) {
551
+ console.warn(
552
+ `[SWITE] Error calculating relative path for ${matchingDep.name}:`,
553
+ err,
554
+ );
555
+ depRelativeToWorkspace = "";
556
+ }
557
+
558
+ // Extract subpath (e.g., "@alpine/skltn/shell" -> "shell")
559
+ const subPath = args.path.replace(matchingDep.name + "/", "");
560
+
561
+ // Log for debugging
562
+ console.log(
563
+ `[SWITE] Resolving ${args.path} -> subPath: ${subPath} from ${matchingDep.name} (${depRelativeToWorkspace || "root"})`,
564
+ );
565
+
566
+ // Try to resolve the subpath
567
+ let resolvedPath: string | null = null;
568
+ if (subPath) {
569
+ // Check package.json exports
570
+ if (
571
+ !matchingDep.pkgDir ||
572
+ typeof matchingDep.pkgDir !== "string"
573
+ ) {
574
+ return undefined;
575
+ }
576
+ const pkgJsonPath = safePathJoin(
577
+ matchingDep.pkgDir,
578
+ "package.json",
579
+ );
580
+ if (!pkgJsonPath) {
581
+ return undefined;
582
+ }
583
+ if (await fileExists(pkgJsonPath)) {
584
+ try {
585
+ const pkgJson = JSON.parse(
586
+ await fs.readFile(pkgJsonPath, "utf-8"),
587
+ );
588
+ if (pkgJson.exports) {
589
+ const exportKey = `./${subPath}`;
590
+ let exportValue = pkgJson.exports[exportKey];
591
+
592
+ // Try directory-based matching
593
+ if (!exportValue && subPath.includes("/")) {
594
+ const dirPath = subPath.split("/")[0];
595
+ exportValue = pkgJson.exports[`./${dirPath}`];
596
+ }
597
+
598
+ if (exportValue) {
599
+ const exportPath =
600
+ typeof exportValue === "string"
601
+ ? exportValue
602
+ : exportValue.import || exportValue.default;
603
+
604
+ if (exportPath && typeof exportPath === "string") {
605
+ // Convert export path to compiled path
606
+ const srcRelative = exportPath.replace(
607
+ /^\.\/src\//,
608
+ "",
609
+ );
610
+ const compiledPath = srcRelative.replace(
611
+ /\.(ui|uix)$/,
612
+ ".tsx",
613
+ );
614
+
615
+ // Validate compiledPath is a valid string
616
+ if (
617
+ !compiledPath ||
618
+ typeof compiledPath !== "string" ||
619
+ compiledPath.length === 0
620
+ ) {
621
+ return undefined;
622
+ }
623
+
624
+ const parts: string[] = [];
625
+ // Validate tempDirForPlugin is a valid string
626
+ if (
627
+ tempDirForPlugin &&
628
+ typeof tempDirForPlugin === "string" &&
629
+ tempDirForPlugin.length > 0
630
+ ) {
631
+ parts.push(tempDirForPlugin);
632
+ } else {
633
+ // Skip this resolution if tempDir is invalid
634
+ return undefined;
635
+ }
636
+ if (
637
+ depRelativeToWorkspace &&
638
+ typeof depRelativeToWorkspace === "string" &&
639
+ depRelativeToWorkspace.length > 0
640
+ ) {
641
+ parts.push(depRelativeToWorkspace);
642
+ }
643
+ // Validate compiledPath before adding
644
+ if (
645
+ compiledPath &&
646
+ typeof compiledPath === "string" &&
647
+ compiledPath.length > 0
648
+ ) {
649
+ const joined = safePathJoin(
650
+ tempDirForPlugin,
651
+ depRelativeToWorkspace || undefined,
652
+ "src",
653
+ compiledPath,
654
+ );
655
+ if (joined) {
656
+ resolvedPath = joined;
657
+ }
658
+ }
659
+ }
660
+ }
661
+ }
662
+ } catch (err) {
663
+ // Fallback to index
664
+ console.warn(
665
+ `[SWITE] Error reading exports for ${matchingDep.name}:`,
666
+ err,
667
+ );
668
+ }
669
+ }
670
+ }
671
+
672
+ // Fallback: try index.js
673
+ if (!resolvedPath || !(await fileExists(resolvedPath))) {
674
+ const fallbackParts: string[] = [];
675
+ // Validate tempDirForPlugin is a valid string
676
+ if (
677
+ tempDirForPlugin &&
678
+ typeof tempDirForPlugin === "string" &&
679
+ tempDirForPlugin.length > 0
680
+ ) {
681
+ fallbackParts.push(tempDirForPlugin);
682
+ } else {
683
+ // Cannot build without valid tempDir
684
+ return undefined;
685
+ }
686
+ if (
687
+ depRelativeToWorkspace &&
688
+ typeof depRelativeToWorkspace === "string" &&
689
+ depRelativeToWorkspace.length > 0
690
+ ) {
691
+ fallbackParts.push(depRelativeToWorkspace);
692
+ }
693
+ // Use safe path join helper
694
+ const joined = safePathJoin(
695
+ tempDirForPlugin,
696
+ depRelativeToWorkspace || undefined,
697
+ "src",
698
+ "index.js",
699
+ );
700
+ if (joined) {
701
+ resolvedPath = joined;
702
+ }
703
+ }
704
+
705
+ // Final check - ensure path is valid string and exists
706
+ if (
707
+ resolvedPath &&
708
+ typeof resolvedPath === "string" &&
709
+ resolvedPath.length > 0
710
+ ) {
711
+ try {
712
+ if (await fileExists(resolvedPath)) {
713
+ return { path: resolvedPath };
714
+ }
715
+ } catch {
716
+ // Ignore file check errors
717
+ }
718
+ }
719
+ } catch (err) {
720
+ console.warn(`[SWITE] Error resolving ${args.path}:`, err);
721
+ }
722
+ }
723
+
724
+ // Let esbuild handle it normally (return undefined, not null)
725
+ return undefined;
726
+ });
727
+
728
+ // Also resolve relative imports in compiled workspace packages
729
+ build.onResolve(
730
+ { filter: /^\.\.?\/.*\.(js|ts|ui|uix)$/ },
731
+ async (args) => {
732
+ // Only handle if it's in a workspace package directory
733
+ if (args.importer && args.importer.includes(".swite-build")) {
734
+ // Check if the importer is in a workspace package
735
+ const importerPath = args.importer;
736
+ const match = importerPath.match(
737
+ /\.swite-build[/\\]([^/\\]+[/\\][^/\\]+)[/\\]src/,
738
+ );
739
+ if (match) {
740
+ // Try to resolve relative to the importer
741
+ const importerDir = path.dirname(importerPath);
742
+ const resolved = safePathJoin(importerDir, args.path);
743
+ if (resolved && (await fileExists(resolved))) {
744
+ return { path: resolved };
745
+ }
746
+
747
+ // Try with .tsx extension if original had .ui/.uix
748
+ const pathWithoutExt = args.path.replace(/\.(ui|uix)$/, "");
749
+ const resolvedTsx = safePathJoin(
750
+ importerDir,
751
+ pathWithoutExt + ".tsx",
752
+ );
753
+ if (resolvedTsx && (await fileExists(resolvedTsx))) {
754
+ return { path: resolvedTsx };
755
+ }
756
+ }
757
+ }
758
+ return undefined;
759
+ },
760
+ );
761
+ },
762
+ };
763
+
764
+ const buildOptions: BuildOptions = {
765
+ entryPoints: [entryPoint],
766
+ bundle: true,
767
+ outdir: this.config.outDir,
768
+ format: this.config.format,
769
+ target: this.config.target,
770
+ minify: this.config.minify,
771
+ sourcemap: this.config.sourcemap,
772
+ external: [...this.config.external, ...nodeBuiltins],
773
+ platform: "node", // Keep as node for now - browser apps will need different strategy
774
+ splitting: this.config.format === "esm",
775
+ metafile: true,
776
+ logLevel: "info",
777
+ absWorkingDir, // Help esbuild resolve modules from workspace root
778
+ plugins: [jsTsxFallbackPlugin, cssStubPlugin, workspaceResolverPlugin],
779
+ };
780
+
781
+ // Add aliases via plugins if esbuild version supports it
782
+ // For now, we'll rely on the compiled files being in the right structure
783
+
784
+ const result = await esbuild(buildOptions);
785
+
786
+ // Log bundle stats (metafile paths can be relative to absWorkingDir)
787
+ if (result.metafile) {
788
+ const outputs = Object.keys(result.metafile.outputs);
789
+ console.log(chalk.green(`\n Generated ${outputs.length} file(s):`));
790
+ for (const output of outputs) {
791
+ const resolvedPath = path.isAbsolute(output)
792
+ ? output
793
+ : path.join(absWorkingDir, output);
794
+ const stats = await fs.stat(resolvedPath);
795
+ const size = this.formatBytes(stats.size);
796
+ console.log(chalk.gray(` ${path.basename(output)}: ${size}`));
797
+ }
798
+ }
799
+ }
800
+
801
+ private async copyPublicAssets(): Promise<void> {
802
+ const publicPath = path.join(this.config.root, this.config.publicDir);
803
+
804
+ try {
805
+ await fs.access(publicPath);
806
+ } catch {
807
+ // Public directory doesn't exist, skip
808
+ return;
809
+ }
810
+
811
+ console.log(chalk.blue("📁 Copying public assets..."));
812
+ await this.copyDir(publicPath, this.config.outDir);
813
+ }
814
+
815
+ private async findSwissFiles(dir: string): Promise<string[]> {
816
+ const files: string[] = [];
817
+
818
+ try {
819
+ const entries = await fs.readdir(dir, { withFileTypes: true });
820
+
821
+ for (const entry of entries) {
822
+ if (entry.name === "node_modules") continue;
823
+ const fullPath = path.join(dir, entry.name);
824
+
825
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
826
+ files.push(...(await this.findSwissFiles(fullPath)));
827
+ } else if (entry.name.endsWith(".ui") || entry.name.endsWith(".uix")) {
828
+ files.push(fullPath);
829
+ }
830
+ }
831
+ } catch {
832
+ // Directory doesn't exist or can't be read
833
+ }
834
+
835
+ return files;
836
+ }
837
+
838
+ private async findFiles(dir: string, pattern: RegExp): Promise<string[]> {
839
+ const files: string[] = [];
840
+
841
+ try {
842
+ const entries = await fs.readdir(dir, { withFileTypes: true });
843
+
844
+ for (const entry of entries) {
845
+ if (entry.name === "node_modules") continue;
846
+ const fullPath = path.join(dir, entry.name);
847
+
848
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
849
+ files.push(...(await this.findFiles(fullPath, pattern)));
850
+ } else if (pattern.test(entry.name)) {
851
+ files.push(fullPath);
852
+ }
853
+ }
854
+ } catch {
855
+ // Directory doesn't exist or can't be read
856
+ }
857
+
858
+ return files;
859
+ }
860
+
861
+ private async copyDir(src: string, dest: string): Promise<void> {
862
+ await fs.mkdir(dest, { recursive: true });
863
+ const entries = await fs.readdir(src, { withFileTypes: true });
864
+
865
+ for (const entry of entries) {
866
+ const srcPath = path.join(src, entry.name);
867
+ const destPath = path.join(dest, entry.name);
868
+
869
+ if (entry.isDirectory()) {
870
+ await this.copyDir(srcPath, destPath);
871
+ } else {
872
+ await fs.copyFile(srcPath, destPath);
873
+ }
874
+ }
875
+ }
876
+
877
+ private formatBytes(bytes: number): string {
878
+ if (bytes === 0) return "0 B";
879
+ const k = 1024;
880
+ const sizes = ["B", "KB", "MB", "GB"];
881
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
882
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
883
+ }
884
+
885
+ private async findWorkspaceRoot(startDir: string): Promise<string | null> {
886
+ let current = startDir;
887
+ for (let i = 0; i < 5; i++) {
888
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
889
+ const packageJson = path.join(current, "package.json");
890
+ try {
891
+ if (await this.fileExists(workspaceFile)) {
892
+ return current;
893
+ }
894
+ if (await this.fileExists(packageJson)) {
895
+ const pkg = JSON.parse(await fs.readFile(packageJson, "utf-8"));
896
+ if (pkg.workspaces) {
897
+ return current;
898
+ }
899
+ }
900
+ } catch {
901
+ // Continue searching
902
+ }
903
+ const parent = path.dirname(current);
904
+ if (parent === current) break;
905
+ current = parent;
906
+ }
907
+ return null;
908
+ }
909
+
910
+ private async fileExists(filePath: string): Promise<boolean> {
911
+ try {
912
+ await fs.access(filePath);
913
+ return true;
914
+ } catch {
915
+ return false;
916
+ }
917
+ }
918
+
919
+ private async createAliases(
920
+ workspaceRoot: string,
921
+ tempDir: string,
922
+ ): Promise<Record<string, string>> {
923
+ const aliases: Record<string, string> = {};
924
+ const deps = await this.discoverWorkspaceDependencies();
925
+
926
+ for (const dep of deps) {
927
+ const depRelativeToWorkspace = path.relative(workspaceRoot, dep.pkgDir);
928
+ const aliasPath = path.join(tempDir, depRelativeToWorkspace, "src");
929
+ aliases[dep.name] = aliasPath;
930
+ // Also add subpath exports
931
+ const pkgJsonPath = path.join(dep.pkgDir, "package.json");
932
+ try {
933
+ const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
934
+ if (pkgJson.exports) {
935
+ for (const [exportKey, exportValue] of Object.entries(
936
+ pkgJson.exports,
937
+ )) {
938
+ if (exportKey !== "." && typeof exportValue === "string") {
939
+ const fullAlias = `${dep.name}${exportKey}`;
940
+ const exportPath = exportValue.replace(/^\.\/src\//, "");
941
+ aliases[fullAlias] = path.join(aliasPath, exportPath);
942
+ }
943
+ }
944
+ }
945
+ } catch {
946
+ // Skip if can't read package.json
947
+ }
948
+ }
949
+
950
+ return aliases;
951
+ }
952
+ }
953
+
954
+ /**
955
+ * Convenience function to build a project
956
+ */
957
+ export async function build(config: BuildConfig): Promise<void> {
958
+ const builder = new SwiteBuilder(config);
959
+ await builder.build();
960
+ }