@noego/wood 0.1.2 → 0.2.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.
- package/README.md +56 -0
- package/bin/axe.js +3 -1
- package/bin/wood.js +7 -3
- package/dist/controller/controller_resolver.cjs +6 -2
- package/dist/controller/controller_resolver.cjs.map +1 -1
- package/dist/controller/controller_resolver.js +6 -2
- package/dist/controller/controller_resolver.js.map +1 -1
- package/dist/loader/loader_runner.cjs +5 -1
- package/dist/loader/loader_runner.cjs.map +1 -1
- package/dist/loader/loader_runner.js +5 -1
- package/dist/loader/loader_runner.js.map +1 -1
- package/dist/middleware/middleware_resolver.cjs +5 -1
- package/dist/middleware/middleware_resolver.cjs.map +1 -1
- package/dist/middleware/middleware_resolver.js +5 -1
- package/dist/middleware/middleware_resolver.js.map +1 -1
- package/dist/testing/browser.cjs +789 -0
- package/dist/testing/browser.cjs.map +1 -0
- package/dist/testing/browser.d.cts +183 -0
- package/dist/testing/browser.d.ts +183 -0
- package/dist/testing/browser.js +754 -0
- package/dist/testing/browser.js.map +1 -0
- package/dist/testing/index.cjs +5 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -1
- package/docs/browser-testing.md +212 -0
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -111,6 +111,62 @@ npm test
|
|
|
111
111
|
|
|
112
112
|
Publishing runs the same checks through `prepublishOnly`.
|
|
113
113
|
|
|
114
|
+
## Testing
|
|
115
|
+
|
|
116
|
+
Wood publishes browser-testing helpers from `@noego/wood/testing`.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { browser } from '@noego/wood/testing';
|
|
120
|
+
|
|
121
|
+
const harness = await browser.createHarness({
|
|
122
|
+
rootDir: process.cwd(),
|
|
123
|
+
componentDir: 'ui',
|
|
124
|
+
css: ['ui/app.css'],
|
|
125
|
+
bridge: {
|
|
126
|
+
operations: {
|
|
127
|
+
'settings.get': () => ({ readSpeed: 0 }),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const mounted = await harness.mountComponent('ui/components/StatusPanel.svelte');
|
|
133
|
+
await mounted.expect.visible('[data-testid="status-panel"]');
|
|
134
|
+
|
|
135
|
+
await harness.destroy();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The browser harness starts Vite with the Svelte plugin, launches Playwright,
|
|
139
|
+
installs a controlled Wood bridge fixture, and can mount individual Svelte
|
|
140
|
+
components, layered Wood trees, or Wood routes from a views config.
|
|
141
|
+
|
|
142
|
+
Use it for renderer behavior that needs real CSS, browser layout, animation
|
|
143
|
+
frames, z-index, clipping, or route/controller wiring without booting Electron.
|
|
144
|
+
Keep pure service and state tests in Node.
|
|
145
|
+
|
|
146
|
+
See [Browser Testing](./docs/browser-testing.md) for the full API shape and
|
|
147
|
+
boundary notes.
|
|
148
|
+
|
|
149
|
+
## Publishing
|
|
150
|
+
|
|
151
|
+
Publishing is handled by the `Publish` GitHub Actions workflow.
|
|
152
|
+
|
|
153
|
+
Prerequisites:
|
|
154
|
+
|
|
155
|
+
- Add an npm automation token as the repository secret `NPM_TOKEN`.
|
|
156
|
+
- Bump `package.json` and `package-lock.json` to a version that is not already published.
|
|
157
|
+
- Commit the version bump on `main`.
|
|
158
|
+
|
|
159
|
+
Release flow:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm version patch
|
|
163
|
+
git push origin main
|
|
164
|
+
git push origin main:release
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The workflow runs when the `release` branch is updated. It runs `prepublishOnly`
|
|
168
|
+
through `npm publish` and publishes the package to npm.
|
|
169
|
+
|
|
114
170
|
## Design Notes
|
|
115
171
|
|
|
116
172
|
High-level architecture and config format notes live in [DESIGN.md](./DESIGN.md).
|
package/bin/axe.js
CHANGED
|
@@ -9,7 +9,9 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
9
9
|
const __dirname = path.dirname(__filename);
|
|
10
10
|
const distDir = path.resolve(__dirname, '..', 'dist');
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Node's ESM loader requires file:// URLs for absolute paths on Windows;
|
|
13
|
+
// raw `C:\...` strings throw ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
14
|
+
const { readWoodDevMetadata } = await import(pathToFileURL(path.join(distDir, 'dev', 'dev_metadata.js')).href);
|
|
13
15
|
|
|
14
16
|
const INTERNAL_BRIDGE_KEY = '__NOEGO_WOOD_BRIDGE__';
|
|
15
17
|
|
package/bin/wood.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync, copyFileSync } from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
7
|
import yaml from 'js-yaml';
|
|
8
8
|
import { globSync } from 'glob';
|
|
9
9
|
import chokidar from 'chokidar';
|
|
@@ -15,6 +15,10 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
15
15
|
const __dirname = path.dirname(__filename);
|
|
16
16
|
const distDir = path.resolve(__dirname, '..', 'dist');
|
|
17
17
|
|
|
18
|
+
// Node's ESM loader requires file:// URLs for absolute paths on Windows;
|
|
19
|
+
// raw `C:\...` strings throw ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
20
|
+
const distUrl = (...segments) => pathToFileURL(path.join(distDir, ...segments)).href;
|
|
21
|
+
|
|
18
22
|
// Dynamic import from the built dist
|
|
19
23
|
const {
|
|
20
24
|
parseOperationsFile,
|
|
@@ -22,12 +26,12 @@ const {
|
|
|
22
26
|
generatePreload,
|
|
23
27
|
generateRuntime,
|
|
24
28
|
generateRendererApp,
|
|
25
|
-
} = await import(
|
|
29
|
+
} = await import(distUrl('index.js'));
|
|
26
30
|
const {
|
|
27
31
|
resolveWoodRemoteDebuggingPort,
|
|
28
32
|
writeWoodDevMetadata,
|
|
29
33
|
removeWoodDevMetadata,
|
|
30
|
-
} = await import(
|
|
34
|
+
} = await import(distUrl('dev', 'dev_metadata.js'));
|
|
31
35
|
|
|
32
36
|
// ---------------------------------------------------------------------------
|
|
33
37
|
// CLI entry
|
|
@@ -33,8 +33,12 @@ __export(controller_resolver_exports, {
|
|
|
33
33
|
});
|
|
34
34
|
module.exports = __toCommonJS(controller_resolver_exports);
|
|
35
35
|
var import_path = __toESM(require("path"), 1);
|
|
36
|
+
var import_node_url = require("node:url");
|
|
36
37
|
var import_logger = require("@noego/logger");
|
|
37
38
|
const logger = (0, import_logger.getLogger)("wood:controller");
|
|
39
|
+
function toImportSpecifier(spec) {
|
|
40
|
+
return import_path.default.isAbsolute(spec) ? (0, import_node_url.pathToFileURL)(spec).href : spec;
|
|
41
|
+
}
|
|
38
42
|
class ControllerResolver {
|
|
39
43
|
constructor(controllersDir, options) {
|
|
40
44
|
this.classCache = /* @__PURE__ */ new Map();
|
|
@@ -73,7 +77,7 @@ class ControllerResolver {
|
|
|
73
77
|
if (cached) return cached;
|
|
74
78
|
const importPath = this.pathCache.get(controllerName);
|
|
75
79
|
if (importPath) {
|
|
76
|
-
const mod = await import(importPath);
|
|
80
|
+
const mod = await import(toImportSpecifier(importPath));
|
|
77
81
|
const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];
|
|
78
82
|
if (!ControllerClass || typeof ControllerClass !== "function") {
|
|
79
83
|
throw new Error(
|
|
@@ -106,7 +110,7 @@ Registered controllers: [${Array.from(this.pathCache.keys()).join(", ")}]`
|
|
|
106
110
|
let loaded = false;
|
|
107
111
|
for (const candidate of candidates) {
|
|
108
112
|
try {
|
|
109
|
-
mod = await import(candidate);
|
|
113
|
+
mod = await import(toImportSpecifier(candidate));
|
|
110
114
|
loaded = true;
|
|
111
115
|
break;
|
|
112
116
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controller/controller_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { getLogger } from '@noego/logger';\nimport type { IpcContext, ControllerArgs } from '../types/context.cjs';\n\nconst logger = getLogger('wood:controller');\n\nexport class ControllerResolver {\n private baseDir: string;\n private classCache = new Map<string, any>();\n private pathCache = new Map<string, string>(); // name -> import path\n private mode: 'bundled' | 'dev';\n\n constructor(controllersDir: string, options?: { mode?: 'bundled' | 'dev' }) {\n this.baseDir = controllersDir;\n // Default to bundled mode for safety. Dev mode must be explicitly opted into.\n this.mode = options?.mode ?? (process.env.WOOD_MODE === 'development' ? 'dev' : 'bundled');\n }\n\n /**\n * Check whether a controller name has been pre-registered (class or path).\n */\n has(name: string): boolean {\n return this.classCache.has(name) || this.pathCache.has(name);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed. This avoids circular dependencies\n * at module load time.\n */\n registerPath(name: string, importPath: string): void {\n if (!importPath) {\n throw new Error(`registerPath(\"${name}\"): importPath cannot be empty`);\n }\n this.pathCache.set(name, importPath);\n }\n\n /**\n * Resolve a controller name to its class.\n *\n * 1. Check class cache (already imported)\n * 2. Check path cache (registered via registerPath) and lazily import\n * 3. In dev mode only: fall back to filesystem resolution\n *\n * Classes are cached -- same class reference returned for same name.\n */\n async resolveClass(controllerName: string): Promise<any> {\n const cached = this.classCache.get(controllerName);\n if (cached) return cached;\n\n // Check if import path is registered — lazily load it\n const importPath = this.pathCache.get(controllerName);\n if (importPath) {\n const mod = await import(importPath);\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\" at ${importPath}: module does not export a class`\n );\n }\n this.classCache.set(controllerName, ControllerClass);\n logger.debug(`Resolved controller \"${controllerName}\" from registered path`);\n return ControllerClass;\n }\n\n if (this.mode === 'bundled') {\n throw new Error(\n `Controller \"${controllerName}\" is not registered. ` +\n `In bundled mode, all controllers must be pre-registered via controllers.generated.ts. ` +\n `Run \"npx wood build\" to regenerate the registration file.\\n` +\n `Registered controllers: [${Array.from(this.pathCache.keys()).join(', ')}]`\n );\n }\n\n // Dev mode: attempt dynamic filesystem imports\n return this.resolveFromFilesystem(controllerName);\n }\n\n private async resolveFromFilesystem(controllerName: string): Promise<any> {\n const basePath = path.resolve(this.baseDir, controllerName);\n const candidates = [\n basePath + '.ts',\n basePath + '.controller.ts',\n basePath + '.js',\n basePath + '.controller.js',\n path.resolve(this.baseDir, controllerName, 'index.ts'),\n path.resolve(this.baseDir, controllerName, 'index.js'),\n ];\n\n let mod: any;\n let loaded = false;\n\n for (const candidate of candidates) {\n try {\n mod = await import(candidate);\n loaded = true;\n break;\n } catch {\n // Try next candidate\n }\n }\n\n if (!loaded) {\n throw new Error(\n `Controller \"${controllerName}\" not found. Searched:\\n` +\n candidates.map(c => ` - ${c}`).join('\\n')\n );\n }\n\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\": module does not export a class`\n );\n }\n\n logger.debug(`Resolved controller \"${controllerName}\" from ${this.baseDir}`);\n this.classCache.set(controllerName, ControllerClass);\n return ControllerClass;\n }\n\n /**\n * Create a controller instance using IoC container.\n * Per-request scoped: container.extend() creates an isolated scope.\n */\n async createInstance(ControllerClass: any, container: any): Promise<any> {\n const scopedContainer = container.extend();\n return scopedContainer.instance(ControllerClass);\n }\n\n /**\n * Pre-register a controller class by name so that resolveClass()\n * finds it in the cache without attempting a filesystem import.\n */\n register(name: string, ControllerClass: any): void {\n if (typeof ControllerClass !== 'function') {\n throw new Error(`register(\"${name}\"): expected a class/constructor, got ${typeof ControllerClass}`);\n }\n this.classCache.set(name, ControllerClass);\n }\n\n clearCache(): void {\n this.classCache.clear();\n }\n}\n\n/**\n * Build controller args from IPC context.\n * Creates a fake req/res that matches the Dinner web pattern so\n * existing controllers work without modification.\n */\nexport function buildControllerArgs(ipcContext: IpcContext, container: unknown): ControllerArgs {\n return {\n req: {\n body: ipcContext.body ?? {},\n params: ipcContext.params ?? {},\n query: ipcContext.query ?? {},\n context: ipcContext.context ?? {},\n headers: {},\n },\n res: {\n status: (_code: number) => {},\n cookie: (_name: string, _value: string, _opts?: unknown) => {},\n clearCookie: (_name: string, _opts?: unknown) => {},\n set: (_header: string, _value: string) => {},\n },\n container,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,oBAA0B;AAG1B,MAAM,aAAS,yBAAU,iBAAiB;
|
|
1
|
+
{"version":3,"sources":["../../src/controller/controller_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { getLogger } from '@noego/logger';\nimport type { IpcContext, ControllerArgs } from '../types/context.cjs';\n\nconst logger = getLogger('wood:controller');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\nexport class ControllerResolver {\n private baseDir: string;\n private classCache = new Map<string, any>();\n private pathCache = new Map<string, string>(); // name -> import path\n private mode: 'bundled' | 'dev';\n\n constructor(controllersDir: string, options?: { mode?: 'bundled' | 'dev' }) {\n this.baseDir = controllersDir;\n // Default to bundled mode for safety. Dev mode must be explicitly opted into.\n this.mode = options?.mode ?? (process.env.WOOD_MODE === 'development' ? 'dev' : 'bundled');\n }\n\n /**\n * Check whether a controller name has been pre-registered (class or path).\n */\n has(name: string): boolean {\n return this.classCache.has(name) || this.pathCache.has(name);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed. This avoids circular dependencies\n * at module load time.\n */\n registerPath(name: string, importPath: string): void {\n if (!importPath) {\n throw new Error(`registerPath(\"${name}\"): importPath cannot be empty`);\n }\n this.pathCache.set(name, importPath);\n }\n\n /**\n * Resolve a controller name to its class.\n *\n * 1. Check class cache (already imported)\n * 2. Check path cache (registered via registerPath) and lazily import\n * 3. In dev mode only: fall back to filesystem resolution\n *\n * Classes are cached -- same class reference returned for same name.\n */\n async resolveClass(controllerName: string): Promise<any> {\n const cached = this.classCache.get(controllerName);\n if (cached) return cached;\n\n // Check if import path is registered — lazily load it\n const importPath = this.pathCache.get(controllerName);\n if (importPath) {\n const mod = await import(toImportSpecifier(importPath));\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\" at ${importPath}: module does not export a class`\n );\n }\n this.classCache.set(controllerName, ControllerClass);\n logger.debug(`Resolved controller \"${controllerName}\" from registered path`);\n return ControllerClass;\n }\n\n if (this.mode === 'bundled') {\n throw new Error(\n `Controller \"${controllerName}\" is not registered. ` +\n `In bundled mode, all controllers must be pre-registered via controllers.generated.ts. ` +\n `Run \"npx wood build\" to regenerate the registration file.\\n` +\n `Registered controllers: [${Array.from(this.pathCache.keys()).join(', ')}]`\n );\n }\n\n // Dev mode: attempt dynamic filesystem imports\n return this.resolveFromFilesystem(controllerName);\n }\n\n private async resolveFromFilesystem(controllerName: string): Promise<any> {\n const basePath = path.resolve(this.baseDir, controllerName);\n const candidates = [\n basePath + '.ts',\n basePath + '.controller.ts',\n basePath + '.js',\n basePath + '.controller.js',\n path.resolve(this.baseDir, controllerName, 'index.ts'),\n path.resolve(this.baseDir, controllerName, 'index.js'),\n ];\n\n let mod: any;\n let loaded = false;\n\n for (const candidate of candidates) {\n try {\n mod = await import(toImportSpecifier(candidate));\n loaded = true;\n break;\n } catch {\n // Try next candidate\n }\n }\n\n if (!loaded) {\n throw new Error(\n `Controller \"${controllerName}\" not found. Searched:\\n` +\n candidates.map(c => ` - ${c}`).join('\\n')\n );\n }\n\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\": module does not export a class`\n );\n }\n\n logger.debug(`Resolved controller \"${controllerName}\" from ${this.baseDir}`);\n this.classCache.set(controllerName, ControllerClass);\n return ControllerClass;\n }\n\n /**\n * Create a controller instance using IoC container.\n * Per-request scoped: container.extend() creates an isolated scope.\n */\n async createInstance(ControllerClass: any, container: any): Promise<any> {\n const scopedContainer = container.extend();\n return scopedContainer.instance(ControllerClass);\n }\n\n /**\n * Pre-register a controller class by name so that resolveClass()\n * finds it in the cache without attempting a filesystem import.\n */\n register(name: string, ControllerClass: any): void {\n if (typeof ControllerClass !== 'function') {\n throw new Error(`register(\"${name}\"): expected a class/constructor, got ${typeof ControllerClass}`);\n }\n this.classCache.set(name, ControllerClass);\n }\n\n clearCache(): void {\n this.classCache.clear();\n }\n}\n\n/**\n * Build controller args from IPC context.\n * Creates a fake req/res that matches the Dinner web pattern so\n * existing controllers work without modification.\n */\nexport function buildControllerArgs(ipcContext: IpcContext, container: unknown): ControllerArgs {\n return {\n req: {\n body: ipcContext.body ?? {},\n params: ipcContext.params ?? {},\n query: ipcContext.query ?? {},\n context: ipcContext.context ?? {},\n headers: {},\n },\n res: {\n status: (_code: number) => {},\n cookie: (_name: string, _value: string, _opts?: unknown) => {},\n clearCookie: (_name: string, _opts?: unknown) => {},\n set: (_header: string, _value: string) => {},\n },\n container,\n };\n}\n"],"mappings":"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","names":["path"]}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
import { getLogger } from "@noego/logger";
|
|
3
4
|
const logger = getLogger("wood:controller");
|
|
5
|
+
function toImportSpecifier(spec) {
|
|
6
|
+
return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;
|
|
7
|
+
}
|
|
4
8
|
class ControllerResolver {
|
|
5
9
|
constructor(controllersDir, options) {
|
|
6
10
|
this.classCache = /* @__PURE__ */ new Map();
|
|
@@ -39,7 +43,7 @@ class ControllerResolver {
|
|
|
39
43
|
if (cached) return cached;
|
|
40
44
|
const importPath = this.pathCache.get(controllerName);
|
|
41
45
|
if (importPath) {
|
|
42
|
-
const mod = await import(importPath);
|
|
46
|
+
const mod = await import(toImportSpecifier(importPath));
|
|
43
47
|
const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];
|
|
44
48
|
if (!ControllerClass || typeof ControllerClass !== "function") {
|
|
45
49
|
throw new Error(
|
|
@@ -72,7 +76,7 @@ Registered controllers: [${Array.from(this.pathCache.keys()).join(", ")}]`
|
|
|
72
76
|
let loaded = false;
|
|
73
77
|
for (const candidate of candidates) {
|
|
74
78
|
try {
|
|
75
|
-
mod = await import(candidate);
|
|
79
|
+
mod = await import(toImportSpecifier(candidate));
|
|
76
80
|
loaded = true;
|
|
77
81
|
break;
|
|
78
82
|
} catch {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controller/controller_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { getLogger } from '@noego/logger';\nimport type { IpcContext, ControllerArgs } from '../types/context.js';\n\nconst logger = getLogger('wood:controller');\n\nexport class ControllerResolver {\n private baseDir: string;\n private classCache = new Map<string, any>();\n private pathCache = new Map<string, string>(); // name -> import path\n private mode: 'bundled' | 'dev';\n\n constructor(controllersDir: string, options?: { mode?: 'bundled' | 'dev' }) {\n this.baseDir = controllersDir;\n // Default to bundled mode for safety. Dev mode must be explicitly opted into.\n this.mode = options?.mode ?? (process.env.WOOD_MODE === 'development' ? 'dev' : 'bundled');\n }\n\n /**\n * Check whether a controller name has been pre-registered (class or path).\n */\n has(name: string): boolean {\n return this.classCache.has(name) || this.pathCache.has(name);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed. This avoids circular dependencies\n * at module load time.\n */\n registerPath(name: string, importPath: string): void {\n if (!importPath) {\n throw new Error(`registerPath(\"${name}\"): importPath cannot be empty`);\n }\n this.pathCache.set(name, importPath);\n }\n\n /**\n * Resolve a controller name to its class.\n *\n * 1. Check class cache (already imported)\n * 2. Check path cache (registered via registerPath) and lazily import\n * 3. In dev mode only: fall back to filesystem resolution\n *\n * Classes are cached -- same class reference returned for same name.\n */\n async resolveClass(controllerName: string): Promise<any> {\n const cached = this.classCache.get(controllerName);\n if (cached) return cached;\n\n // Check if import path is registered — lazily load it\n const importPath = this.pathCache.get(controllerName);\n if (importPath) {\n const mod = await import(importPath);\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\" at ${importPath}: module does not export a class`\n );\n }\n this.classCache.set(controllerName, ControllerClass);\n logger.debug(`Resolved controller \"${controllerName}\" from registered path`);\n return ControllerClass;\n }\n\n if (this.mode === 'bundled') {\n throw new Error(\n `Controller \"${controllerName}\" is not registered. ` +\n `In bundled mode, all controllers must be pre-registered via controllers.generated.ts. ` +\n `Run \"npx wood build\" to regenerate the registration file.\\n` +\n `Registered controllers: [${Array.from(this.pathCache.keys()).join(', ')}]`\n );\n }\n\n // Dev mode: attempt dynamic filesystem imports\n return this.resolveFromFilesystem(controllerName);\n }\n\n private async resolveFromFilesystem(controllerName: string): Promise<any> {\n const basePath = path.resolve(this.baseDir, controllerName);\n const candidates = [\n basePath + '.ts',\n basePath + '.controller.ts',\n basePath + '.js',\n basePath + '.controller.js',\n path.resolve(this.baseDir, controllerName, 'index.ts'),\n path.resolve(this.baseDir, controllerName, 'index.js'),\n ];\n\n let mod: any;\n let loaded = false;\n\n for (const candidate of candidates) {\n try {\n mod = await import(candidate);\n loaded = true;\n break;\n } catch {\n // Try next candidate\n }\n }\n\n if (!loaded) {\n throw new Error(\n `Controller \"${controllerName}\" not found. Searched:\\n` +\n candidates.map(c => ` - ${c}`).join('\\n')\n );\n }\n\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\": module does not export a class`\n );\n }\n\n logger.debug(`Resolved controller \"${controllerName}\" from ${this.baseDir}`);\n this.classCache.set(controllerName, ControllerClass);\n return ControllerClass;\n }\n\n /**\n * Create a controller instance using IoC container.\n * Per-request scoped: container.extend() creates an isolated scope.\n */\n async createInstance(ControllerClass: any, container: any): Promise<any> {\n const scopedContainer = container.extend();\n return scopedContainer.instance(ControllerClass);\n }\n\n /**\n * Pre-register a controller class by name so that resolveClass()\n * finds it in the cache without attempting a filesystem import.\n */\n register(name: string, ControllerClass: any): void {\n if (typeof ControllerClass !== 'function') {\n throw new Error(`register(\"${name}\"): expected a class/constructor, got ${typeof ControllerClass}`);\n }\n this.classCache.set(name, ControllerClass);\n }\n\n clearCache(): void {\n this.classCache.clear();\n }\n}\n\n/**\n * Build controller args from IPC context.\n * Creates a fake req/res that matches the Dinner web pattern so\n * existing controllers work without modification.\n */\nexport function buildControllerArgs(ipcContext: IpcContext, container: unknown): ControllerArgs {\n return {\n req: {\n body: ipcContext.body ?? {},\n params: ipcContext.params ?? {},\n query: ipcContext.query ?? {},\n context: ipcContext.context ?? {},\n headers: {},\n },\n res: {\n status: (_code: number) => {},\n cookie: (_name: string, _value: string, _opts?: unknown) => {},\n clearCookie: (_name: string, _opts?: unknown) => {},\n set: (_header: string, _value: string) => {},\n },\n container,\n };\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,iBAAiB;AAG1B,MAAM,SAAS,UAAU,iBAAiB;
|
|
1
|
+
{"version":3,"sources":["../../src/controller/controller_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { getLogger } from '@noego/logger';\nimport type { IpcContext, ControllerArgs } from '../types/context.js';\n\nconst logger = getLogger('wood:controller');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\nexport class ControllerResolver {\n private baseDir: string;\n private classCache = new Map<string, any>();\n private pathCache = new Map<string, string>(); // name -> import path\n private mode: 'bundled' | 'dev';\n\n constructor(controllersDir: string, options?: { mode?: 'bundled' | 'dev' }) {\n this.baseDir = controllersDir;\n // Default to bundled mode for safety. Dev mode must be explicitly opted into.\n this.mode = options?.mode ?? (process.env.WOOD_MODE === 'development' ? 'dev' : 'bundled');\n }\n\n /**\n * Check whether a controller name has been pre-registered (class or path).\n */\n has(name: string): boolean {\n return this.classCache.has(name) || this.pathCache.has(name);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed. This avoids circular dependencies\n * at module load time.\n */\n registerPath(name: string, importPath: string): void {\n if (!importPath) {\n throw new Error(`registerPath(\"${name}\"): importPath cannot be empty`);\n }\n this.pathCache.set(name, importPath);\n }\n\n /**\n * Resolve a controller name to its class.\n *\n * 1. Check class cache (already imported)\n * 2. Check path cache (registered via registerPath) and lazily import\n * 3. In dev mode only: fall back to filesystem resolution\n *\n * Classes are cached -- same class reference returned for same name.\n */\n async resolveClass(controllerName: string): Promise<any> {\n const cached = this.classCache.get(controllerName);\n if (cached) return cached;\n\n // Check if import path is registered — lazily load it\n const importPath = this.pathCache.get(controllerName);\n if (importPath) {\n const mod = await import(toImportSpecifier(importPath));\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\" at ${importPath}: module does not export a class`\n );\n }\n this.classCache.set(controllerName, ControllerClass);\n logger.debug(`Resolved controller \"${controllerName}\" from registered path`);\n return ControllerClass;\n }\n\n if (this.mode === 'bundled') {\n throw new Error(\n `Controller \"${controllerName}\" is not registered. ` +\n `In bundled mode, all controllers must be pre-registered via controllers.generated.ts. ` +\n `Run \"npx wood build\" to regenerate the registration file.\\n` +\n `Registered controllers: [${Array.from(this.pathCache.keys()).join(', ')}]`\n );\n }\n\n // Dev mode: attempt dynamic filesystem imports\n return this.resolveFromFilesystem(controllerName);\n }\n\n private async resolveFromFilesystem(controllerName: string): Promise<any> {\n const basePath = path.resolve(this.baseDir, controllerName);\n const candidates = [\n basePath + '.ts',\n basePath + '.controller.ts',\n basePath + '.js',\n basePath + '.controller.js',\n path.resolve(this.baseDir, controllerName, 'index.ts'),\n path.resolve(this.baseDir, controllerName, 'index.js'),\n ];\n\n let mod: any;\n let loaded = false;\n\n for (const candidate of candidates) {\n try {\n mod = await import(toImportSpecifier(candidate));\n loaded = true;\n break;\n } catch {\n // Try next candidate\n }\n }\n\n if (!loaded) {\n throw new Error(\n `Controller \"${controllerName}\" not found. Searched:\\n` +\n candidates.map(c => ` - ${c}`).join('\\n')\n );\n }\n\n const ControllerClass = mod.default ?? mod[Object.keys(mod)[0]];\n if (!ControllerClass || typeof ControllerClass !== 'function') {\n throw new Error(\n `Controller \"${controllerName}\": module does not export a class`\n );\n }\n\n logger.debug(`Resolved controller \"${controllerName}\" from ${this.baseDir}`);\n this.classCache.set(controllerName, ControllerClass);\n return ControllerClass;\n }\n\n /**\n * Create a controller instance using IoC container.\n * Per-request scoped: container.extend() creates an isolated scope.\n */\n async createInstance(ControllerClass: any, container: any): Promise<any> {\n const scopedContainer = container.extend();\n return scopedContainer.instance(ControllerClass);\n }\n\n /**\n * Pre-register a controller class by name so that resolveClass()\n * finds it in the cache without attempting a filesystem import.\n */\n register(name: string, ControllerClass: any): void {\n if (typeof ControllerClass !== 'function') {\n throw new Error(`register(\"${name}\"): expected a class/constructor, got ${typeof ControllerClass}`);\n }\n this.classCache.set(name, ControllerClass);\n }\n\n clearCache(): void {\n this.classCache.clear();\n }\n}\n\n/**\n * Build controller args from IPC context.\n * Creates a fake req/res that matches the Dinner web pattern so\n * existing controllers work without modification.\n */\nexport function buildControllerArgs(ipcContext: IpcContext, container: unknown): ControllerArgs {\n return {\n req: {\n body: ipcContext.body ?? {},\n params: ipcContext.params ?? {},\n query: ipcContext.query ?? {},\n context: ipcContext.context ?? {},\n headers: {},\n },\n res: {\n status: (_code: number) => {},\n cookie: (_name: string, _value: string, _opts?: unknown) => {},\n clearCookie: (_name: string, _opts?: unknown) => {},\n set: (_header: string, _value: string) => {},\n },\n container,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -32,9 +32,13 @@ __export(loader_runner_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(loader_runner_exports);
|
|
34
34
|
var import_path = __toESM(require("path"), 1);
|
|
35
|
+
var import_node_url = require("node:url");
|
|
35
36
|
var import_fs = require("fs");
|
|
36
37
|
var import_logger = require("@noego/logger");
|
|
37
38
|
const logger = (0, import_logger.getLogger)("wood:loader");
|
|
39
|
+
function toImportSpecifier(spec) {
|
|
40
|
+
return import_path.default.isAbsolute(spec) ? (0, import_node_url.pathToFileURL)(spec).href : spec;
|
|
41
|
+
}
|
|
38
42
|
class LoaderRunner {
|
|
39
43
|
constructor(componentDir) {
|
|
40
44
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -60,7 +64,7 @@ class LoaderRunner {
|
|
|
60
64
|
if (cached && cached.mtime === mtime) {
|
|
61
65
|
return cached.fn;
|
|
62
66
|
}
|
|
63
|
-
const mod = await import(`${loaderPath}?t=${Date.now()}`);
|
|
67
|
+
const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);
|
|
64
68
|
const fn = mod.default;
|
|
65
69
|
if (typeof fn !== "function") {
|
|
66
70
|
logger.warn(`Loader at "${loaderPath}" does not export a default function`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.cjs';\nimport type { ViewDef } from '../types/views.cjs';\n\nconst logger = getLogger('wood:loader');\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${loaderPath}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,gBAAyB;AACzB,oBAA0B;AAI1B,MAAM,aAAS,yBAAU,aAAa;
|
|
1
|
+
{"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.cjs';\nimport type { ViewDef } from '../types/views.cjs';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,sBAA8B;AAC9B,gBAAyB;AACzB,oBAA0B;AAI1B,MAAM,aAAS,yBAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,YAAAA,QAAK,WAAW,IAAI,QAAI,+BAAc,IAAI,EAAE,OAAO;AAC5D;AAQO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA0B;AAC9C,UAAM,WAAW,SAAS,QAAQ,aAAa,UAAU;AACzD,WAAO,YAAAA,QAAK,QAAQ,KAAK,cAAc,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,WAAW,YAAkF;AACzG,QAAI;AACF,YAAM,WAAO,oBAAS,UAAU;AAChC,YAAM,QAAQ,KAAK;AAEnB,YAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,UAAI,UAAU,OAAO,UAAU,OAAO;AACpC,eAAO,OAAO;AAAA,MAChB;AAGA,YAAM,MAAM,MAAM,OAAO,GAAG,kBAAkB,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC;AACzE,YAAM,KAAK,IAAI;AAEf,UAAI,OAAO,OAAO,YAAY;AAC5B,eAAO,KAAK,cAAc,UAAU,sCAAsC;AAC1E,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,kBAAkB,UAAU,EAAE;AAC3C,WAAK,MAAM,IAAI,YAAY,EAAE,IAAI,OAAO,UAAU,WAAW,CAAC;AAC9D,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AAEvB,UAAM,uBAAuB,QAAQ,QAAQ,IAAI,OAAO,eAAe;AACrE,YAAM,aAAa,KAAK,cAAc,UAAU;AAChD,YAAM,SAAS,MAAM,KAAK,WAAW,UAAU;AAC/C,UAAI,QAAQ;AACV,eAAO,OAAO,WAAW;AAAA,MAC3B;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,aAAa,MAAM,QAAQ,IAAI,oBAAoB;AAGzD,UAAM,iBAAiB,KAAK,cAAc,QAAQ,QAAQ;AAC1D,UAAM,aAAa,MAAM,KAAK,WAAW,cAAc;AACvD,UAAM,WAAW,aAAa,MAAM,WAAW,WAAW,IAAI,CAAC;AAE/D,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,WAAW,OAAO,CAAC,MAAM,MAAM,MAAS;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":["path"]}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
2
3
|
import { statSync } from "fs";
|
|
3
4
|
import { getLogger } from "@noego/logger";
|
|
4
5
|
const logger = getLogger("wood:loader");
|
|
6
|
+
function toImportSpecifier(spec) {
|
|
7
|
+
return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;
|
|
8
|
+
}
|
|
5
9
|
class LoaderRunner {
|
|
6
10
|
constructor(componentDir) {
|
|
7
11
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -27,7 +31,7 @@ class LoaderRunner {
|
|
|
27
31
|
if (cached && cached.mtime === mtime) {
|
|
28
32
|
return cached.fn;
|
|
29
33
|
}
|
|
30
|
-
const mod = await import(`${loaderPath}?t=${Date.now()}`);
|
|
34
|
+
const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);
|
|
31
35
|
const fn = mod.default;
|
|
32
36
|
if (typeof fn !== "function") {
|
|
33
37
|
logger.warn(`Loader at "${loaderPath}" does not export a default function`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.js';\nimport type { ViewDef } from '../types/views.js';\n\nconst logger = getLogger('wood:loader');\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${loaderPath}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,MAAM,SAAS,UAAU,aAAa;
|
|
1
|
+
{"version":3,"sources":["../../src/loader/loader_runner.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport { statSync } from 'fs';\nimport { getLogger } from '@noego/logger';\nimport type { RequestData, LoaderResult } from '../types/context.js';\nimport type { ViewDef } from '../types/views.js';\n\nconst logger = getLogger('wood:loader');\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\ninterface LoaderEntry {\n fn: (request: RequestData) => Promise<unknown>;\n mtime: number;\n filePath: string;\n}\n\nexport class LoaderRunner {\n private cache = new Map<string, LoaderEntry>();\n private componentDir: string;\n\n constructor(componentDir: string) {\n this.componentDir = componentDir;\n }\n\n /**\n * Discover the loader file for a view component.\n * Convention: for `pages/chat/main.svelte`, look for `pages/chat/main.load.ts`\n */\n private getLoaderPath(viewPath: string): string {\n const basePath = viewPath.replace(/\\.svelte$/, '.load.ts');\n return path.resolve(this.componentDir, basePath);\n }\n\n /**\n * Load a loader function with mtime-based cache invalidation.\n * Returns null when no loader file exists (not an error).\n */\n private async loadLoader(loaderPath: string): Promise<((request: RequestData) => Promise<unknown>) | null> {\n try {\n const stat = statSync(loaderPath);\n const mtime = stat.mtimeMs;\n\n const cached = this.cache.get(loaderPath);\n if (cached && cached.mtime === mtime) {\n return cached.fn;\n }\n\n // Dynamic import with cache-bust query for dev reloading\n const mod = await import(`${toImportSpecifier(loaderPath)}?t=${Date.now()}`);\n const fn = mod.default;\n\n if (typeof fn !== 'function') {\n logger.warn(`Loader at \"${loaderPath}\" does not export a default function`);\n return null;\n }\n\n logger.debug(`Loaded loader: ${loaderPath}`);\n this.cache.set(loaderPath, { fn, mtime, filePath: loaderPath });\n return fn;\n } catch {\n return null;\n }\n }\n\n /**\n * Execute all loaders for a matched route.\n * Layout loaders run in parallel, view loader runs after.\n */\n async executeLoaders(\n viewDef: ViewDef,\n requestData: RequestData,\n ): Promise<LoaderResult> {\n // Load layout loaders in parallel\n const layoutLoaderPromises = viewDef.layouts.map(async (layoutPath) => {\n const loaderPath = this.getLoaderPath(layoutPath);\n const loader = await this.loadLoader(loaderPath);\n if (loader) {\n return loader(requestData);\n }\n return undefined;\n });\n\n const layoutData = await Promise.all(layoutLoaderPromises);\n\n // Load view loader\n const viewLoaderPath = this.getLoaderPath(viewDef.viewPath);\n const viewLoader = await this.loadLoader(viewLoaderPath);\n const viewData = viewLoader ? await viewLoader(requestData) : {};\n\n logger.debug(`Executed loaders for view \"${viewDef.pageName}\": ${viewDef.layouts.length} layout(s)`);\n\n return {\n layoutData: layoutData.filter((d) => d !== undefined),\n viewData,\n };\n }\n\n /** Clear the loader cache. */\n clearCache(): void {\n this.cache.clear();\n logger.debug('Loader cache cleared');\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAI1B,MAAM,SAAS,UAAU,aAAa;AAItC,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,KAAK,WAAW,IAAI,IAAI,cAAc,IAAI,EAAE,OAAO;AAC5D;AAQO,MAAM,aAAa;AAAA,EAIxB,YAAY,cAAsB;AAHlC,SAAQ,QAAQ,oBAAI,IAAyB;AAI3C,SAAK,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,cAAc,UAA0B;AAC9C,UAAM,WAAW,SAAS,QAAQ,aAAa,UAAU;AACzD,WAAO,KAAK,QAAQ,KAAK,cAAc,QAAQ;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,WAAW,YAAkF;AACzG,QAAI;AACF,YAAM,OAAO,SAAS,UAAU;AAChC,YAAM,QAAQ,KAAK;AAEnB,YAAM,SAAS,KAAK,MAAM,IAAI,UAAU;AACxC,UAAI,UAAU,OAAO,UAAU,OAAO;AACpC,eAAO,OAAO;AAAA,MAChB;AAGA,YAAM,MAAM,MAAM,OAAO,GAAG,kBAAkB,UAAU,CAAC,MAAM,KAAK,IAAI,CAAC;AACzE,YAAM,KAAK,IAAI;AAEf,UAAI,OAAO,OAAO,YAAY;AAC5B,eAAO,KAAK,cAAc,UAAU,sCAAsC;AAC1E,eAAO;AAAA,MACT;AAEA,aAAO,MAAM,kBAAkB,UAAU,EAAE;AAC3C,WAAK,MAAM,IAAI,YAAY,EAAE,IAAI,OAAO,UAAU,WAAW,CAAC;AAC9D,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eACJ,SACA,aACuB;AAEvB,UAAM,uBAAuB,QAAQ,QAAQ,IAAI,OAAO,eAAe;AACrE,YAAM,aAAa,KAAK,cAAc,UAAU;AAChD,YAAM,SAAS,MAAM,KAAK,WAAW,UAAU;AAC/C,UAAI,QAAQ;AACV,eAAO,OAAO,WAAW;AAAA,MAC3B;AACA,aAAO;AAAA,IACT,CAAC;AAED,UAAM,aAAa,MAAM,QAAQ,IAAI,oBAAoB;AAGzD,UAAM,iBAAiB,KAAK,cAAc,QAAQ,QAAQ;AAC1D,UAAM,aAAa,MAAM,KAAK,WAAW,cAAc;AACvD,UAAM,WAAW,aAAa,MAAM,WAAW,WAAW,IAAI,CAAC;AAE/D,WAAO,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,QAAQ,QAAQ,MAAM,YAAY;AAEnG,WAAO;AAAA,MACL,YAAY,WAAW,OAAO,CAAC,MAAM,MAAM,MAAS;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,aAAmB;AACjB,SAAK,MAAM,MAAM;AACjB,WAAO,MAAM,sBAAsB;AAAA,EACrC;AACF;","names":[]}
|
|
@@ -32,6 +32,10 @@ __export(middleware_resolver_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(middleware_resolver_exports);
|
|
34
34
|
var import_path = __toESM(require("path"), 1);
|
|
35
|
+
var import_node_url = require("node:url");
|
|
36
|
+
function toImportSpecifier(spec) {
|
|
37
|
+
return import_path.default.isAbsolute(spec) ? (0, import_node_url.pathToFileURL)(spec).href : spec;
|
|
38
|
+
}
|
|
35
39
|
class MiddlewareResolver {
|
|
36
40
|
constructor(middlewareDir) {
|
|
37
41
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -51,7 +55,7 @@ class MiddlewareResolver {
|
|
|
51
55
|
const [filePart, selectorPart] = spec.includes(":") ? spec.split(":", 2) : [spec, "default"];
|
|
52
56
|
const relPath = filePart.replace(/\./g, import_path.default.sep);
|
|
53
57
|
const fullPath = import_path.default.resolve(this.baseDir, relPath);
|
|
54
|
-
const mod = await import(fullPath);
|
|
58
|
+
const mod = await import(toImportSpecifier(fullPath));
|
|
55
59
|
let fn;
|
|
56
60
|
if (selectorPart === "default") {
|
|
57
61
|
fn = mod.default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middleware/middleware_resolver.ts"],"sourcesContent":["import path from 'path';\nimport type { WoodMiddlewareFn } from '../types/index.cjs';\n\nexport class MiddlewareResolver {\n private baseDir: string;\n private cache = new Map<string, WoodMiddlewareFn>();\n\n constructor(middlewareDir: string) {\n this.baseDir = middlewareDir;\n }\n\n /**\n * Resolve a middleware spec like \"auth.admin:is_admin\" into a function.\n *\n * Format: \"file.path:function_selector\"\n * - Dots in file path become path separators: auth.admin -> middleware/auth/admin.ts\n * - function_selector: named export, \"*\" for all, comma-separated for multiple\n * - If no colon, use default export\n */\n async resolve(spec: string): Promise<WoodMiddlewareFn> {\n const cached = this.cache.get(spec);\n if (cached) return cached;\n\n const [filePart, selectorPart] = spec.includes(':')\n ? spec.split(':', 2) as [string, string]\n : [spec, 'default'];\n\n const relPath = filePart.replace(/\\./g, path.sep);\n const fullPath = path.resolve(this.baseDir, relPath);\n\n const mod = await import(fullPath);\n\n let fn: WoodMiddlewareFn;\n if (selectorPart === 'default') {\n fn = mod.default;\n } else if (selectorPart === '*') {\n const fns = Object.values(mod).filter(v => typeof v === 'function') as WoodMiddlewareFn[];\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else if (selectorPart.includes(',')) {\n const names = selectorPart.split(',').map(s => s.trim());\n const fns = names.map(name => {\n if (typeof mod[name] !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${name}\" not found or not a function`);\n }\n return mod[name] as WoodMiddlewareFn;\n });\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else {\n fn = mod[selectorPart];\n if (typeof fn !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${selectorPart}\" not found or not a function`);\n }\n }\n\n this.cache.set(spec, fn);\n return fn;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/middleware_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport type { WoodMiddlewareFn } from '../types/index.cjs';\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\nexport class MiddlewareResolver {\n private baseDir: string;\n private cache = new Map<string, WoodMiddlewareFn>();\n\n constructor(middlewareDir: string) {\n this.baseDir = middlewareDir;\n }\n\n /**\n * Resolve a middleware spec like \"auth.admin:is_admin\" into a function.\n *\n * Format: \"file.path:function_selector\"\n * - Dots in file path become path separators: auth.admin -> middleware/auth/admin.ts\n * - function_selector: named export, \"*\" for all, comma-separated for multiple\n * - If no colon, use default export\n */\n async resolve(spec: string): Promise<WoodMiddlewareFn> {\n const cached = this.cache.get(spec);\n if (cached) return cached;\n\n const [filePart, selectorPart] = spec.includes(':')\n ? spec.split(':', 2) as [string, string]\n : [spec, 'default'];\n\n const relPath = filePart.replace(/\\./g, path.sep);\n const fullPath = path.resolve(this.baseDir, relPath);\n\n const mod = await import(toImportSpecifier(fullPath));\n\n let fn: WoodMiddlewareFn;\n if (selectorPart === 'default') {\n fn = mod.default;\n } else if (selectorPart === '*') {\n const fns = Object.values(mod).filter(v => typeof v === 'function') as WoodMiddlewareFn[];\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else if (selectorPart.includes(',')) {\n const names = selectorPart.split(',').map(s => s.trim());\n const fns = names.map(name => {\n if (typeof mod[name] !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${name}\" not found or not a function`);\n }\n return mod[name] as WoodMiddlewareFn;\n });\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else {\n fn = mod[selectorPart];\n if (typeof fn !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${selectorPart}\" not found or not a function`);\n }\n }\n\n this.cache.set(spec, fn);\n return fn;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAiB;AACjB,sBAA8B;AAK9B,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,YAAAA,QAAK,WAAW,IAAI,QAAI,+BAAc,IAAI,EAAE,OAAO;AAC5D;AAEO,MAAM,mBAAmB;AAAA,EAI9B,YAAY,eAAuB;AAFnC,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAQ,MAAyC;AACrD,UAAM,SAAS,KAAK,MAAM,IAAI,IAAI;AAClC,QAAI,OAAQ,QAAO;AAEnB,UAAM,CAAC,UAAU,YAAY,IAAI,KAAK,SAAS,GAAG,IAC9C,KAAK,MAAM,KAAK,CAAC,IACjB,CAAC,MAAM,SAAS;AAEpB,UAAM,UAAU,SAAS,QAAQ,OAAO,YAAAA,QAAK,GAAG;AAChD,UAAM,WAAW,YAAAA,QAAK,QAAQ,KAAK,SAAS,OAAO;AAEnD,UAAM,MAAM,MAAM,OAAO,kBAAkB,QAAQ;AAEnD,QAAI;AACJ,QAAI,iBAAiB,WAAW;AAC9B,WAAK,IAAI;AAAA,IACX,WAAW,iBAAiB,KAAK;AAC/B,YAAM,MAAM,OAAO,OAAO,GAAG,EAAE,OAAO,OAAK,OAAO,MAAM,UAAU;AAClE,WAAK,OAAO,QAAQ;AAClB,mBAAW,KAAK,IAAK,OAAM,EAAE,GAAG;AAAA,MAClC;AAAA,IACF,WAAW,aAAa,SAAS,GAAG,GAAG;AACrC,YAAM,QAAQ,aAAa,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACvD,YAAM,MAAM,MAAM,IAAI,UAAQ;AAC5B,YAAI,OAAO,IAAI,IAAI,MAAM,YAAY;AACnC,gBAAM,IAAI,MAAM,eAAe,IAAI,cAAc,IAAI,+BAA+B;AAAA,QACtF;AACA,eAAO,IAAI,IAAI;AAAA,MACjB,CAAC;AACD,WAAK,OAAO,QAAQ;AAClB,mBAAW,KAAK,IAAK,OAAM,EAAE,GAAG;AAAA,MAClC;AAAA,IACF,OAAO;AACL,WAAK,IAAI,YAAY;AACrB,UAAI,OAAO,OAAO,YAAY;AAC5B,cAAM,IAAI,MAAM,eAAe,IAAI,cAAc,YAAY,+BAA+B;AAAA,MAC9F;AAAA,IACF;AAEA,SAAK,MAAM,IAAI,MAAM,EAAE;AACvB,WAAO;AAAA,EACT;AAAA,EAEA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":["path"]}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
function toImportSpecifier(spec) {
|
|
4
|
+
return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;
|
|
5
|
+
}
|
|
2
6
|
class MiddlewareResolver {
|
|
3
7
|
constructor(middlewareDir) {
|
|
4
8
|
this.cache = /* @__PURE__ */ new Map();
|
|
@@ -18,7 +22,7 @@ class MiddlewareResolver {
|
|
|
18
22
|
const [filePart, selectorPart] = spec.includes(":") ? spec.split(":", 2) : [spec, "default"];
|
|
19
23
|
const relPath = filePart.replace(/\./g, path.sep);
|
|
20
24
|
const fullPath = path.resolve(this.baseDir, relPath);
|
|
21
|
-
const mod = await import(fullPath);
|
|
25
|
+
const mod = await import(toImportSpecifier(fullPath));
|
|
22
26
|
let fn;
|
|
23
27
|
if (selectorPart === "default") {
|
|
24
28
|
fn = mod.default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/middleware/middleware_resolver.ts"],"sourcesContent":["import path from 'path';\nimport type { WoodMiddlewareFn } from '../types/index.js';\n\nexport class MiddlewareResolver {\n private baseDir: string;\n private cache = new Map<string, WoodMiddlewareFn>();\n\n constructor(middlewareDir: string) {\n this.baseDir = middlewareDir;\n }\n\n /**\n * Resolve a middleware spec like \"auth.admin:is_admin\" into a function.\n *\n * Format: \"file.path:function_selector\"\n * - Dots in file path become path separators: auth.admin -> middleware/auth/admin.ts\n * - function_selector: named export, \"*\" for all, comma-separated for multiple\n * - If no colon, use default export\n */\n async resolve(spec: string): Promise<WoodMiddlewareFn> {\n const cached = this.cache.get(spec);\n if (cached) return cached;\n\n const [filePart, selectorPart] = spec.includes(':')\n ? spec.split(':', 2) as [string, string]\n : [spec, 'default'];\n\n const relPath = filePart.replace(/\\./g, path.sep);\n const fullPath = path.resolve(this.baseDir, relPath);\n\n const mod = await import(fullPath);\n\n let fn: WoodMiddlewareFn;\n if (selectorPart === 'default') {\n fn = mod.default;\n } else if (selectorPart === '*') {\n const fns = Object.values(mod).filter(v => typeof v === 'function') as WoodMiddlewareFn[];\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else if (selectorPart.includes(',')) {\n const names = selectorPart.split(',').map(s => s.trim());\n const fns = names.map(name => {\n if (typeof mod[name] !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${name}\" not found or not a function`);\n }\n return mod[name] as WoodMiddlewareFn;\n });\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else {\n fn = mod[selectorPart];\n if (typeof fn !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${selectorPart}\" not found or not a function`);\n }\n }\n\n this.cache.set(spec, fn);\n return fn;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/middleware_resolver.ts"],"sourcesContent":["import path from 'path';\nimport { pathToFileURL } from 'node:url';\nimport type { WoodMiddlewareFn } from '../types/index.js';\n\n// Node's ESM loader rejects raw absolute Windows paths (`C:\\...`) — they must\n// be file:// URLs. Bare specifiers and relative paths pass through unchanged.\nfunction toImportSpecifier(spec: string): string {\n return path.isAbsolute(spec) ? pathToFileURL(spec).href : spec;\n}\n\nexport class MiddlewareResolver {\n private baseDir: string;\n private cache = new Map<string, WoodMiddlewareFn>();\n\n constructor(middlewareDir: string) {\n this.baseDir = middlewareDir;\n }\n\n /**\n * Resolve a middleware spec like \"auth.admin:is_admin\" into a function.\n *\n * Format: \"file.path:function_selector\"\n * - Dots in file path become path separators: auth.admin -> middleware/auth/admin.ts\n * - function_selector: named export, \"*\" for all, comma-separated for multiple\n * - If no colon, use default export\n */\n async resolve(spec: string): Promise<WoodMiddlewareFn> {\n const cached = this.cache.get(spec);\n if (cached) return cached;\n\n const [filePart, selectorPart] = spec.includes(':')\n ? spec.split(':', 2) as [string, string]\n : [spec, 'default'];\n\n const relPath = filePart.replace(/\\./g, path.sep);\n const fullPath = path.resolve(this.baseDir, relPath);\n\n const mod = await import(toImportSpecifier(fullPath));\n\n let fn: WoodMiddlewareFn;\n if (selectorPart === 'default') {\n fn = mod.default;\n } else if (selectorPart === '*') {\n const fns = Object.values(mod).filter(v => typeof v === 'function') as WoodMiddlewareFn[];\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else if (selectorPart.includes(',')) {\n const names = selectorPart.split(',').map(s => s.trim());\n const fns = names.map(name => {\n if (typeof mod[name] !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${name}\" not found or not a function`);\n }\n return mod[name] as WoodMiddlewareFn;\n });\n fn = async (ctx) => {\n for (const f of fns) await f(ctx);\n };\n } else {\n fn = mod[selectorPart];\n if (typeof fn !== 'function') {\n throw new Error(`Middleware \"${spec}\": export \"${selectorPart}\" not found or not a function`);\n }\n }\n\n this.cache.set(spec, fn);\n return fn;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAK9B,SAAS,kBAAkB,MAAsB;AAC/C,SAAO,KAAK,WAAW,IAAI,IAAI,cAAc,IAAI,EAAE,OAAO;AAC5D;AAEO,MAAM,mBAAmB;AAAA,EAI9B,YAAY,eAAuB;AAFnC,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,QAAQ,MAAyC;AACrD,UAAM,SAAS,KAAK,MAAM,IAAI,IAAI;AAClC,QAAI,OAAQ,QAAO;AAEnB,UAAM,CAAC,UAAU,YAAY,IAAI,KAAK,SAAS,GAAG,IAC9C,KAAK,MAAM,KAAK,CAAC,IACjB,CAAC,MAAM,SAAS;AAEpB,UAAM,UAAU,SAAS,QAAQ,OAAO,KAAK,GAAG;AAChD,UAAM,WAAW,KAAK,QAAQ,KAAK,SAAS,OAAO;AAEnD,UAAM,MAAM,MAAM,OAAO,kBAAkB,QAAQ;AAEnD,QAAI;AACJ,QAAI,iBAAiB,WAAW;AAC9B,WAAK,IAAI;AAAA,IACX,WAAW,iBAAiB,KAAK;AAC/B,YAAM,MAAM,OAAO,OAAO,GAAG,EAAE,OAAO,OAAK,OAAO,MAAM,UAAU;AAClE,WAAK,OAAO,QAAQ;AAClB,mBAAW,KAAK,IAAK,OAAM,EAAE,GAAG;AAAA,MAClC;AAAA,IACF,WAAW,aAAa,SAAS,GAAG,GAAG;AACrC,YAAM,QAAQ,aAAa,MAAM,GAAG,EAAE,IAAI,OAAK,EAAE,KAAK,CAAC;AACvD,YAAM,MAAM,MAAM,IAAI,UAAQ;AAC5B,YAAI,OAAO,IAAI,IAAI,MAAM,YAAY;AACnC,gBAAM,IAAI,MAAM,eAAe,IAAI,cAAc,IAAI,+BAA+B;AAAA,QACtF;AACA,eAAO,IAAI,IAAI;AAAA,MACjB,CAAC;AACD,WAAK,OAAO,QAAQ;AAClB,mBAAW,KAAK,IAAK,OAAM,EAAE,GAAG;AAAA,MAClC;AAAA,IACF,OAAO;AACL,WAAK,IAAI,YAAY;AACrB,UAAI,OAAO,OAAO,YAAY;AAC5B,cAAM,IAAI,MAAM,eAAe,IAAI,cAAc,YAAY,+BAA+B;AAAA,MAC9F;AAAA,IACF;AAEA,SAAK,MAAM,IAAI,MAAM,EAAE;AACvB,WAAO;AAAA,EACT;AAAA,EAEA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":[]}
|