@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,191 @@
1
+ /*
2
+ * Module Resolver for SWITE
3
+ */
4
+ import path from "node:path";
5
+ import { promises as fs } from "node:fs";
6
+ import chalk from "chalk";
7
+ import { findSwissLibMonorepo } from "./utils/package-finder.js";
8
+ import { toUrl } from "./resolver/url-resolver.js";
9
+ import { resolveWorkspacePackage } from "./resolver/workspace-package-resolver.js";
10
+ import { resolveBareImport } from "./resolver/bare-import-resolver.js";
11
+ export class ModuleResolver {
12
+ constructor(root) {
13
+ this.root = root;
14
+ this.workspaceRoot = null;
15
+ this.importMap = null;
16
+ }
17
+ /**
18
+ * Set pre-resolved import map (from build-time generation)
19
+ */
20
+ setImportMap(importMap) {
21
+ this.importMap = importMap;
22
+ if (importMap) {
23
+ console.log(chalk.green(`[Resolver] Loaded import map with ${Object.keys(importMap).length - 2} entries`));
24
+ }
25
+ }
26
+ async getWorkspaceRoot() {
27
+ if (this.workspaceRoot)
28
+ return this.workspaceRoot;
29
+ const findWorkspaceRoot = async (startDir) => {
30
+ let current = startDir;
31
+ for (let i = 0; i < 5; i++) {
32
+ const workspaceFile = path.join(current, "pnpm-workspace.yaml");
33
+ const packageJson = path.join(current, "package.json");
34
+ if ((await this.fileExists(workspaceFile)) ||
35
+ ((await this.fileExists(packageJson)) &&
36
+ JSON.parse(await fs.readFile(packageJson, "utf-8")).workspaces)) {
37
+ return current;
38
+ }
39
+ const parent = path.dirname(current);
40
+ if (parent === current)
41
+ break;
42
+ current = parent;
43
+ }
44
+ return null;
45
+ };
46
+ this.workspaceRoot = await findWorkspaceRoot(this.root);
47
+ return this.workspaceRoot;
48
+ }
49
+ async resolve(specifier, importer) {
50
+ console.log(`[SWITE] resolve CALLED: specifier: ${specifier}, importer: ${importer}`);
51
+ // Check import map first (fast path)
52
+ if (this.importMap && !specifier.startsWith(".") && !specifier.startsWith("/")) {
53
+ const mapped = this.importMap.imports[specifier];
54
+ if (mapped) {
55
+ console.log(chalk.green(`[Resolver] ✅ Import map hit: ${specifier} -> ${mapped}`));
56
+ return mapped;
57
+ }
58
+ }
59
+ // CRITICAL: Skip variable references - they should never be resolved as modules
60
+ // Variables like def.componentUrl, someVar, etc. should be left as-is
61
+ // Only resolve actual module specifiers (bare imports starting with @ or valid package names)
62
+ if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
63
+ // Check if this looks like a variable reference (property access, camelCase without @, etc.)
64
+ // Valid module specifiers: @scope/name, package-name, ./relative, /absolute
65
+ // Variable references: def.componentUrl, someVar, obj.prop, etc.
66
+ if (specifier.includes(".") && !specifier.startsWith("@")) {
67
+ // Property access pattern (def.componentUrl) - this is a variable, not a module
68
+ console.warn(`[SWITE] resolve: Skipping variable reference: ${specifier}`);
69
+ return specifier; // Return as-is, don't try to resolve
70
+ }
71
+ // Additional check: if it doesn't look like a valid package name, it might be a variable
72
+ // Valid package names: start with letter or @, contain only alphanumeric, -, _, /
73
+ // Also allow file extensions at the end: .js, .ts, .ui, .uix, etc.
74
+ if (!/^[@a-zA-Z][a-zA-Z0-9_/@-]*(\.(js|ts|ui|uix|mjs|cjs|jsx|tsx))?$/.test(specifier)) {
75
+ console.warn(`[SWITE] resolve: Invalid module specifier (likely variable): ${specifier}`);
76
+ return specifier; // Return as-is
77
+ }
78
+ const context = {
79
+ root: this.root,
80
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
81
+ fileExists: (p) => this.fileExists(p),
82
+ resolveWorkspacePackage: (pkgName) => this.resolveWorkspacePackage(pkgName),
83
+ };
84
+ const result = await resolveBareImport(specifier, context);
85
+ console.log(`[SWITE] resolve RESULT: ${specifier} -> ${result}`);
86
+ return result;
87
+ }
88
+ // Handle absolute paths (already URLs)
89
+ if (specifier.startsWith("/")) {
90
+ return specifier;
91
+ }
92
+ // Handle relative imports
93
+ // importer might be a URL path (/src/modules/index.ui) or file path
94
+ // Convert URL to file path if needed
95
+ let importerPath = importer;
96
+ if (importer.startsWith("/")) {
97
+ // URL path - convert to file path
98
+ // Prioritize app root for app files (src/, public/, assets/)
99
+ if (importer.startsWith("/src/") ||
100
+ importer.startsWith("/public/") ||
101
+ importer.startsWith("/assets/")) {
102
+ // App-specific paths - resolve from app root
103
+ importerPath = path.join(this.root, importer);
104
+ }
105
+ else {
106
+ // Other paths - try workspace root first (for libraries/, packages/)
107
+ const workspaceRoot = await this.getWorkspaceRoot();
108
+ if (workspaceRoot) {
109
+ const workspacePath = path.join(workspaceRoot, importer);
110
+ if (await this.fileExists(workspacePath)) {
111
+ importerPath = workspacePath;
112
+ }
113
+ else {
114
+ // Fallback to app root
115
+ importerPath = path.join(this.root, importer);
116
+ }
117
+ }
118
+ else {
119
+ // No workspace root, use app root
120
+ importerPath = path.join(this.root, importer);
121
+ }
122
+ }
123
+ }
124
+ const importerDir = path.dirname(importerPath);
125
+ // If specifier already has an extension (.ui, .uix, .ts, .js, etc.), try it first
126
+ // This preserves .ui/.uix extensions for SWISS files
127
+ const hasExtension = /\.(ui|uix|ts|tsx|js|jsx|mjs)$/.test(specifier);
128
+ if (hasExtension) {
129
+ // Specifier has extension, resolve it directly
130
+ const resolved = path.resolve(importerDir, specifier);
131
+ console.log(`[SWITE] resolve relative (hasExt): ${specifier}, importerDir: ${importerDir}, resolved: ${resolved}, exists: ${await this.fileExists(resolved)}`);
132
+ if (await this.fileExists(resolved)) {
133
+ const url = await this.toUrl(resolved);
134
+ console.log(`[SWITE] resolve relative: ${specifier} -> ${resolved} -> ${url}`);
135
+ return url;
136
+ }
137
+ }
138
+ // If no extension or file not found, try adding extensions
139
+ // Strip any existing extension from specifier (but preserve .ui/.uix if present)
140
+ const specifierWithoutExt = specifier.replace(/\.(js|ts|jsx|tsx|mjs)$/, "");
141
+ const resolved = path.resolve(importerDir, specifierWithoutExt);
142
+ console.log(`[SWITE] resolve relative (trying extensions): specifierWithoutExt: ${specifierWithoutExt}, resolved: ${resolved}`);
143
+ // Try adding extensions (prioritize .ui and .uix for SWISS files)
144
+ const extensions = [".ui", ".uix", ".ts", ".tsx", ".js", ".jsx", ".mjs"];
145
+ for (const ext of extensions) {
146
+ const withExt = resolved + ext;
147
+ const exists = await this.fileExists(withExt);
148
+ console.log(`[SWITE] trying extension ${ext}: ${withExt}, exists: ${exists}`);
149
+ if (exists) {
150
+ const url = await this.toUrl(withExt);
151
+ console.log(`[SWITE] found with ${ext}: ${url}`);
152
+ return url;
153
+ }
154
+ }
155
+ // Try index files
156
+ for (const ext of extensions) {
157
+ const indexFile = path.join(resolved, `index${ext}`);
158
+ if (await this.fileExists(indexFile)) {
159
+ return await this.toUrl(indexFile);
160
+ }
161
+ }
162
+ // Return as-is if nothing found
163
+ return await this.toUrl(resolved);
164
+ }
165
+ async fileExists(filePath) {
166
+ try {
167
+ await fs.access(filePath);
168
+ return true;
169
+ }
170
+ catch {
171
+ return false;
172
+ }
173
+ }
174
+ async resolveWorkspacePackage(pkgName) {
175
+ const context = {
176
+ root: this.root,
177
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
178
+ fileExists: (p) => this.fileExists(p),
179
+ };
180
+ return resolveWorkspacePackage(pkgName, context);
181
+ }
182
+ // OLD IMPLEMENTATION REMOVED - now uses resolver/workspace-package-resolver.ts
183
+ async toUrl(filePath) {
184
+ const context = {
185
+ root: this.root,
186
+ getWorkspaceRoot: () => this.getWorkspaceRoot(),
187
+ fileExists: (p) => this.fileExists(p),
188
+ };
189
+ return toUrl(filePath, context);
190
+ }
191
+ }
@@ -0,0 +1,19 @@
1
+ import type { RouteDefinition } from "@kibologic/core";
2
+ import { RouteScanner } from "@kibologic/plugin-file-router/core";
3
+ import { createFileWatcher } from "@kibologic/plugin-file-router/dev";
4
+ import { HMREngine } from "../hmr.js";
5
+ export interface FileRouterConfig {
6
+ root: string;
7
+ hmr: HMREngine;
8
+ }
9
+ export interface FileRouterResult {
10
+ routeScanner: RouteScanner | null;
11
+ routeWatcher: Awaited<ReturnType<typeof createFileWatcher>> | null;
12
+ routes: RouteDefinition[];
13
+ }
14
+ /**
15
+ * Setup file-based routing
16
+ * Scans for route files in pages/ directories and watches for changes
17
+ */
18
+ export declare function setupFileRouter(config: FileRouterConfig): Promise<FileRouterResult>;
19
+ //# sourceMappingURL=file-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-router.d.ts","sourceRoot":"","sources":["../../src/router/file-router.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,SAAS,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,GAAG,IAAI,CAAC;IACnE,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAAC,gBAAgB,CAAC,CAmI3B"}
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+ import { promises as fs } from "node:fs";
7
+ import path from "node:path";
8
+ import chalk from "chalk";
9
+ import { RouteScanner } from "@kibologic/plugin-file-router/core";
10
+ import { createFileWatcher } from "@kibologic/plugin-file-router/dev";
11
+ import { HMREngine } from "../hmr.js";
12
+ import { findWorkspaceRoot } from "../utils/workspace.js";
13
+ /**
14
+ * Setup file-based routing
15
+ * Scans for route files in pages/ directories and watches for changes
16
+ */
17
+ export async function setupFileRouter(config) {
18
+ const result = {
19
+ routeScanner: null,
20
+ routeWatcher: null,
21
+ routes: [],
22
+ };
23
+ try {
24
+ const workspaceRoot = await findWorkspaceRoot(config.root);
25
+ const appRoot = config.root;
26
+ // Initialize route scanner
27
+ result.routeScanner = new RouteScanner({
28
+ routesDir: "./src/pages",
29
+ extensions: [".ui", ".uix"],
30
+ layouts: true,
31
+ lazyLoading: true,
32
+ });
33
+ // Scan routes from multiple locations:
34
+ // 1. App's pages directory (apps/alpine/src/pages)
35
+ // 2. SKLTN's pages directory (framework/skltn/src/pages) - for reusable auth pages
36
+ const routesToScan = [];
37
+ // App pages
38
+ const appPagesDir = path.join(appRoot, "src", "pages");
39
+ try {
40
+ await fs.access(appPagesDir);
41
+ routesToScan.push(appPagesDir);
42
+ console.log(chalk.gray(` 📄 Scanning app routes from ${appPagesDir}`));
43
+ }
44
+ catch {
45
+ // pages directory doesn't exist, skip
46
+ }
47
+ // SKLTN pages (if workspace root exists)
48
+ if (workspaceRoot && workspaceRoot !== appRoot) {
49
+ // Try framework/skltn first (new location), then fallback to lib/skltn (legacy)
50
+ const skltnPagesDir = path.join(workspaceRoot, "framework", "skltn", "src", "pages");
51
+ const legacySkltnPagesDir = path.join(workspaceRoot, "lib", "skltn", "src", "pages");
52
+ try {
53
+ await fs.access(skltnPagesDir);
54
+ routesToScan.push(skltnPagesDir);
55
+ console.log(chalk.gray(` 📄 Scanning SKLTN routes from ${skltnPagesDir}`));
56
+ }
57
+ catch {
58
+ // Try legacy location
59
+ try {
60
+ await fs.access(legacySkltnPagesDir);
61
+ routesToScan.push(legacySkltnPagesDir);
62
+ console.log(chalk.gray(` 📄 Scanning SKLTN routes from ${legacySkltnPagesDir} (legacy)`));
63
+ }
64
+ catch {
65
+ // pages directory doesn't exist, skip
66
+ }
67
+ }
68
+ }
69
+ // Scan all route directories
70
+ for (const pagesDir of routesToScan) {
71
+ try {
72
+ const scannedRoutes = await result.routeScanner.scanRoutes(pagesDir);
73
+ result.routes.push(...scannedRoutes);
74
+ console.log(chalk.green(` ✓ Found ${scannedRoutes.length} routes in ${pagesDir}`));
75
+ }
76
+ catch (error) {
77
+ console.warn(chalk.yellow(` ⚠ Failed to scan routes from ${pagesDir}:`), error);
78
+ }
79
+ }
80
+ // Setup file watcher for route changes
81
+ if (routesToScan.length > 0) {
82
+ // Watch the first pages directory (can be extended to watch multiple)
83
+ result.routeWatcher = await createFileWatcher({
84
+ directory: routesToScan[0],
85
+ extensions: [".ui", ".uix"],
86
+ });
87
+ result.routeWatcher.on("change", async (filePath) => {
88
+ console.log(chalk.yellow(` 🔄 Route file changed: ${filePath}`));
89
+ // Rescan routes
90
+ result.routes = [];
91
+ for (const pagesDir of routesToScan) {
92
+ try {
93
+ const scannedRoutes = await result.routeScanner.scanRoutes(pagesDir);
94
+ result.routes.push(...scannedRoutes);
95
+ }
96
+ catch (error) {
97
+ console.warn(`Failed to rescan routes:`, error);
98
+ }
99
+ }
100
+ // Notify HMR about route changes
101
+ config.hmr.notifyChange(filePath);
102
+ });
103
+ console.log(chalk.green(` ✓ File router initialized with ${result.routes.length} routes`));
104
+ }
105
+ else {
106
+ console.log(chalk.gray(` ⚠ No pages directories found, file router disabled`));
107
+ }
108
+ }
109
+ catch (error) {
110
+ console.warn(chalk.yellow(` ⚠ File router setup failed:`), error);
111
+ // Continue without file router
112
+ }
113
+ return result;
114
+ }
@@ -0,0 +1,22 @@
1
+ export interface SwiteConfig {
2
+ root: string;
3
+ rootDir?: string;
4
+ publicDir: string;
5
+ port: number;
6
+ host: string;
7
+ open: boolean;
8
+ hmrPort?: number;
9
+ }
10
+ export declare class SwiteServer {
11
+ private app;
12
+ private resolver;
13
+ private hmr;
14
+ private config;
15
+ private routeScanner;
16
+ private routeWatcher;
17
+ private routes;
18
+ constructor(config?: Partial<SwiteConfig>);
19
+ private findWorkspaceRoot;
20
+ start(): Promise<void>;
21
+ }
22
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IAKb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,GAAG,CAAa;IACxB,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,YAAY,CACb;IACP,OAAO,CAAC,MAAM,CAAyB;gBAE3B,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM;YAe/B,iBAAiB;IAuBzB,KAAK;CAsEZ"}
package/dist/server.js ADDED
@@ -0,0 +1,122 @@
1
+ /*
2
+ * Copyright (c) 2024 Themba Mzumara
3
+ * SWITE - SWISS Development Server
4
+ * Licensed under the MIT License.
5
+ */
6
+ import { RouteScanner } from "@kibologic/plugin-file-router/core";
7
+ import { createFileWatcher } from "@kibologic/plugin-file-router/dev";
8
+ import express from "express";
9
+ import path from "node:path";
10
+ import { promises as fs } from "node:fs";
11
+ import { ModuleResolver } from "./resolver.js";
12
+ import { HMREngine } from "./hmr.js";
13
+ import chalk from "chalk";
14
+ import { setupMiddleware } from "./middleware/middleware-setup.js";
15
+ import { buildSymlinkRegistry } from "./resolver/symlink-registry.js";
16
+ import { findSwissLibMonorepo } from "./utils/package-finder.js";
17
+ export class SwiteServer {
18
+ constructor(config = {}) {
19
+ this.app = express();
20
+ this.routeScanner = null;
21
+ this.routeWatcher = null;
22
+ this.routes = [];
23
+ this.config = {
24
+ root: process.cwd(),
25
+ publicDir: "public",
26
+ port: 3000,
27
+ host: "localhost",
28
+ open: true,
29
+ ...config,
30
+ };
31
+ this.resolver = new ModuleResolver(this.config.root);
32
+ this.hmr = new HMREngine(this.config.root, this.config.hmrPort);
33
+ }
34
+ // CG-03: find workspace root by walking up from startDir
35
+ async findWorkspaceRoot(startDir) {
36
+ if (this.config.rootDir) {
37
+ return path.resolve(this.config.rootDir);
38
+ }
39
+ let current = startDir;
40
+ for (let i = 0; i < 6; i++) {
41
+ try {
42
+ await fs.access(path.join(current, "pnpm-workspace.yaml"));
43
+ return current;
44
+ }
45
+ catch { }
46
+ try {
47
+ const pkgJson = JSON.parse(await fs.readFile(path.join(current, "package.json"), "utf-8"));
48
+ if (pkgJson.workspaces)
49
+ return current;
50
+ }
51
+ catch { }
52
+ const parent = path.dirname(current);
53
+ if (parent === current)
54
+ break;
55
+ current = parent;
56
+ }
57
+ return null;
58
+ }
59
+ async start() {
60
+ const startTime = Date.now();
61
+ console.log(chalk.cyan("\n⚡ SWITE - SWISS Development Server\n"));
62
+ console.time("Startup");
63
+ // CG-03: Build symlink registry before serving any requests.
64
+ // Maps realpath(node_modules/pkg symlink) → /node_modules/pkg browser URL
65
+ // so toUrl() can map absolute filesystem paths back to browser URLs.
66
+ console.time("Symlink Registry");
67
+ try {
68
+ const nodeModulesDirs = [
69
+ path.join(this.config.root, "node_modules"),
70
+ // Also scan the server package's own node_modules (one level up from the
71
+ // app root, e.g. apps/server/node_modules) — pnpm places workspace package
72
+ // symlinks there, not in the app root's node_modules subfolder.
73
+ path.join(path.dirname(this.config.root), "node_modules"),
74
+ ];
75
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
76
+ if (workspaceRoot) {
77
+ nodeModulesDirs.push(path.join(workspaceRoot, "node_modules"));
78
+ }
79
+ const swissLib = await findSwissLibMonorepo(this.config.root);
80
+ if (swissLib) {
81
+ nodeModulesDirs.push(path.join(swissLib, "node_modules"));
82
+ }
83
+ await buildSymlinkRegistry(nodeModulesDirs);
84
+ }
85
+ catch (err) {
86
+ console.warn(`[SWITE] Symlink registry build failed: ${err.message}`);
87
+ }
88
+ console.timeEnd("Symlink Registry");
89
+ // Setup middleware
90
+ console.time("Middleware Setup");
91
+ const workspaceRoot = await this.findWorkspaceRoot(this.config.root);
92
+ const middlewareResult = await setupMiddleware(this.app, {
93
+ root: this.config.root,
94
+ workspaceRoot,
95
+ publicDir: this.config.publicDir,
96
+ resolver: this.resolver,
97
+ hmr: this.hmr,
98
+ });
99
+ this.routes = middlewareResult.routes;
100
+ this.routeScanner = middlewareResult.routeScanner;
101
+ this.routeWatcher = middlewareResult.routeWatcher;
102
+ console.timeEnd("Middleware Setup");
103
+ // Start HMR
104
+ console.time("HMR Start");
105
+ await this.hmr.initialize();
106
+ await this.hmr.start();
107
+ console.timeEnd("HMR Start");
108
+ // Start HTTP server
109
+ // Use 0.0.0.0 to bind to all interfaces (IPv4 and IPv6)
110
+ const bindHost = this.config.host === "localhost" ? "0.0.0.0" : this.config.host;
111
+ console.time("HTTP Listen");
112
+ await new Promise((resolve) => {
113
+ this.app.listen(this.config.port, bindHost, () => {
114
+ console.timeEnd("HTTP Listen");
115
+ console.timeEnd("Startup");
116
+ console.log(chalk.green(` ➜ Local: http://localhost:${this.config.port}/`));
117
+ console.log(chalk.gray(` ➜ Ready in ${Date.now() - startTime}ms\n`));
118
+ resolve();
119
+ });
120
+ });
121
+ }
122
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * CDN fallback policy.
3
+ *
4
+ * Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
5
+ * This must be safe and project-agnostic:
6
+ * - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
7
+ * - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
8
+ *
9
+ * Opt-in:
10
+ * - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
11
+ * e.g. "@types,@tanstack".
12
+ */
13
+ export declare function shouldUseCdnFallback(specifierOrPkg: string): boolean;
14
+ //# sourceMappingURL=cdn-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cdn-fallback.d.ts","sourceRoot":"","sources":["../../src/utils/cdn-fallback.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmBH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAKpE"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * CDN fallback policy.
3
+ *
4
+ * Swite can fall back to jsDelivr (+esm) for packages it can't resolve locally.
5
+ * This must be safe and project-agnostic:
6
+ * - Unscoped packages (e.g. "react") are usually public on npm; allow by default.
7
+ * - Scoped packages (e.g. "@scope/pkg") may be private; do NOT CDN-fallback by default.
8
+ *
9
+ * Opt-in:
10
+ * - Set `SWITE_CDN_FALLBACK_SCOPES` to a comma-separated list of scopes to allow,
11
+ * e.g. "@types,@tanstack".
12
+ */
13
+ function getScope(specifierOrPkg) {
14
+ if (!specifierOrPkg.startsWith("@"))
15
+ return null;
16
+ const firstSlash = specifierOrPkg.indexOf("/");
17
+ if (firstSlash === -1)
18
+ return null;
19
+ return specifierOrPkg.slice(0, firstSlash); // "@scope"
20
+ }
21
+ function parseAllowList() {
22
+ const raw = process.env.SWITE_CDN_FALLBACK_SCOPES || "";
23
+ const scopes = raw
24
+ .split(",")
25
+ .map((s) => s.trim())
26
+ .filter(Boolean)
27
+ .map((s) => (s.startsWith("@") ? s : `@${s}`));
28
+ return new Set(scopes);
29
+ }
30
+ export function shouldUseCdnFallback(specifierOrPkg) {
31
+ const scope = getScope(specifierOrPkg);
32
+ if (!scope)
33
+ return true; // unscoped: allow by default
34
+ const allow = parseAllowList();
35
+ return allow.has(scope);
36
+ }
@@ -0,0 +1,9 @@
1
+ export interface PathResolverContext {
2
+ root: string;
3
+ workspaceRoot: string | null;
4
+ }
5
+ /**
6
+ * Resolve file path from URL, handling SWISS packages, workspace packages, and app files
7
+ */
8
+ export declare function resolveFilePath(url: string, root: string, workspaceRoot?: string | null): Promise<string>;
9
+ //# sourceMappingURL=file-path-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-path-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/file-path-resolver.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,aAAa,GAAE,MAAM,GAAG,IAAW,GAClC,OAAO,CAAC,MAAM,CAAC,CAmLjB"}