@karmaniverous/smoz 0.2.4 → 0.2.6
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/dist/cli/index.cjs +65 -31
- package/dist/mjs/cli/inline-server.js +296 -0
- package/package.json +2 -3
- package/templates/default/app/config/openapi.ts +10 -9
- package/templates/default/eslint.config.ts +12 -0
- package/templates/default/package.json +2 -2
- package/templates/default/serverless.ts +0 -3
- package/templates/default/tsconfig.downstream.json +1 -6
package/dist/cli/index.cjs
CHANGED
|
@@ -495,13 +495,17 @@ const collect = async (root) => {
|
|
|
495
495
|
buckets.serverless.sort();
|
|
496
496
|
return buckets;
|
|
497
497
|
};
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Build ESM import lines using POSIX-relative specifiers from the generated
|
|
500
|
+
* file directory (app/generated) to each target file. Avoids "@/" alias so
|
|
501
|
+
* dynamic import in the inline server works reliably under tsx.
|
|
502
|
+
*/
|
|
503
|
+
const makeImports = (root, outDir, files) => files.map((abs) => {
|
|
504
|
+
const relFromOut = toPosix$1(path.relative(outDir, abs));
|
|
505
|
+
const noExt = withoutTsExt(relFromOut);
|
|
506
|
+
const spec = noExt.startsWith('.') ? noExt : `./${noExt}`;
|
|
507
|
+
return `import '${spec}';`;
|
|
508
|
+
});
|
|
505
509
|
const buildFile = (imports) => {
|
|
506
510
|
if (imports.length === 0)
|
|
507
511
|
return `${HEADER}\n`;
|
|
@@ -509,10 +513,10 @@ const buildFile = (imports) => {
|
|
|
509
513
|
};
|
|
510
514
|
const runRegister = async (root) => {
|
|
511
515
|
const buckets = await collect(root);
|
|
512
|
-
const lambdaImports = makeImports(root, buckets.lambda);
|
|
513
|
-
const openapiImports = makeImports(root, buckets.openapi);
|
|
514
|
-
const serverlessImports = makeImports(root, buckets.serverless);
|
|
515
516
|
const outDir = path.join(root, 'app', 'generated');
|
|
517
|
+
const lambdaImports = makeImports(root, outDir, buckets.lambda);
|
|
518
|
+
const openapiImports = makeImports(root, outDir, buckets.openapi);
|
|
519
|
+
const serverlessImports = makeImports(root, outDir, buckets.serverless);
|
|
516
520
|
const fnsPath = path.join(outDir, 'register.functions.ts');
|
|
517
521
|
const oaiPath = path.join(outDir, 'register.openapi.ts');
|
|
518
522
|
const srvPath = path.join(outDir, 'register.serverless.ts');
|
|
@@ -529,11 +533,6 @@ const runRegister = async (root) => {
|
|
|
529
533
|
return { wrote };
|
|
530
534
|
};
|
|
531
535
|
|
|
532
|
-
/* Dev loop orchestrator
|
|
533
|
-
* - Watches author sources; debounces bursts; runs tasks in order: register → openapi.
|
|
534
|
-
* - Optional local serving (--local inline|offline).
|
|
535
|
-
* - Stage/env: seeds process.env with concrete values for the selected stage.
|
|
536
|
-
*/
|
|
537
536
|
const runDev = async (root, opts) => {
|
|
538
537
|
const verbose = !!opts.verbose;
|
|
539
538
|
const stage = typeof opts.stage === 'string'
|
|
@@ -547,7 +546,8 @@ const runDev = async (root, opts) => {
|
|
|
547
546
|
if (verbose)
|
|
548
547
|
console.warn('[dev] env seeding warning:', e.message);
|
|
549
548
|
}
|
|
550
|
-
const
|
|
549
|
+
const modeInitial = opts.local;
|
|
550
|
+
let mode = modeInitial;
|
|
551
551
|
const port = typeof opts.port === 'number' ? opts.port : 0;
|
|
552
552
|
if (verbose) {
|
|
553
553
|
console.log(`[dev] options: register=${String(opts.register)} ` +
|
|
@@ -633,8 +633,18 @@ const runDev = async (root, opts) => {
|
|
|
633
633
|
offline = await launchOffline(root, { stage, port, verbose });
|
|
634
634
|
}
|
|
635
635
|
else if (mode === 'inline') {
|
|
636
|
-
|
|
637
|
-
|
|
636
|
+
try {
|
|
637
|
+
inlineChild = await launchInline(root, { stage, port, verbose });
|
|
638
|
+
}
|
|
639
|
+
catch (e) {
|
|
640
|
+
const msg = e.message;
|
|
641
|
+
console.warn('[dev] inline unavailable:', msg);
|
|
642
|
+
console.warn('[dev] Falling back to serverless-offline.');
|
|
643
|
+
mode = 'offline';
|
|
644
|
+
offline = await launchOffline(root, { stage, port, verbose });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
// Watch sources
|
|
638
648
|
const globs = [
|
|
639
649
|
path.join(root, 'app', 'functions', '**', 'lambda.ts'),
|
|
640
650
|
path.join(root, 'app', 'functions', '**', 'openapi.ts'),
|
|
@@ -742,29 +752,53 @@ const seedEnvForStage = async (root, stage, verbose) => {
|
|
|
742
752
|
};
|
|
743
753
|
// Inline local runner: spawn tsx to run a TS server script.
|
|
744
754
|
const launchInline = async (root, opts) => {
|
|
745
|
-
const { spawn } = await import('node:child_process');
|
|
755
|
+
const { spawn, spawnSync } = await import('node:child_process');
|
|
746
756
|
const path = await import('node:path');
|
|
747
757
|
const fs = await import('node:fs');
|
|
748
|
-
|
|
749
|
-
|
|
758
|
+
// Resolve packaged inline server from the project root so this works
|
|
759
|
+
// both when running the built CLI and via tsx on sources.
|
|
760
|
+
const entry = path.resolve(root, 'dist', 'mjs', 'cli', 'inline-server.js');
|
|
750
761
|
if (!fs.existsSync(entry)) {
|
|
751
|
-
throw new Error('inline server entry missing
|
|
762
|
+
throw new Error('inline server entry missing in package (dist/mjs/cli/inline-server.js)');
|
|
752
763
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
764
|
+
// Prefer project-local tsx; otherwise probe PATH for tsx availability
|
|
765
|
+
const tsxCli = path.resolve(root, 'node_modules', 'tsx', 'dist', 'cli.js');
|
|
766
|
+
let cmd;
|
|
767
|
+
let args;
|
|
768
|
+
let useShell = false;
|
|
769
|
+
let tsxAvailable = false;
|
|
770
|
+
if (fs.existsSync(tsxCli)) {
|
|
771
|
+
tsxAvailable = true;
|
|
772
|
+
cmd = process.execPath;
|
|
773
|
+
// Pass the TSX CLI script followed by our entry. On Windows, avoid
|
|
774
|
+
// putting unknown flags before the script to prevent Node from treating
|
|
775
|
+
// them as its own options. Enable tsconfig-paths via env only.
|
|
776
|
+
args = [tsxCli, entry];
|
|
758
777
|
}
|
|
759
778
|
else {
|
|
760
|
-
|
|
779
|
+
// Probe PATH: tsx --version
|
|
780
|
+
const probe = spawnSync(process.platform === 'win32' ? 'tsx.cmd' : 'tsx', ['--version'], { shell: true });
|
|
781
|
+
if (typeof probe.status === 'number' && probe.status === 0)
|
|
782
|
+
tsxAvailable = true;
|
|
783
|
+
cmd = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
|
|
784
|
+
// Invoke tsx from PATH with just the script entry; rely on
|
|
785
|
+
// TSX_TSCONFIG_PATHS=1 in the environment for path alias resolution.
|
|
786
|
+
args = [entry];
|
|
787
|
+
useShell = true;
|
|
788
|
+
}
|
|
789
|
+
if (!tsxAvailable) {
|
|
790
|
+
throw new Error('tsx not found (required for inline). Install "tsx" as a devDependency or use --local offline.');
|
|
761
791
|
}
|
|
762
|
-
const spawnChild = () => spawn(
|
|
792
|
+
const spawnChild = () => spawn(cmd, args, {
|
|
763
793
|
cwd: root,
|
|
764
794
|
stdio: 'inherit',
|
|
765
|
-
shell:
|
|
795
|
+
shell: useShell,
|
|
796
|
+
// Enable tsconfig paths resolution for "@/..." via environment;
|
|
797
|
+
// avoids passing CLI flags that can confuse Node on Windows.
|
|
798
|
+
// Keep additional SMOZ_* env for server configuration.
|
|
766
799
|
env: {
|
|
767
800
|
...process.env,
|
|
801
|
+
TSX_TSCONFIG_PATHS: '1',
|
|
768
802
|
SMOZ_STAGE: opts.stage,
|
|
769
803
|
SMOZ_PORT: String(opts.port),
|
|
770
804
|
SMOZ_VERBOSE: opts.verbose ? '1' : '',
|
|
@@ -1426,7 +1460,7 @@ const main = () => {
|
|
|
1426
1460
|
.option('-l, --local [mode]', 'Local server mode: inline|offline', 'inline')
|
|
1427
1461
|
.option('-s, --stage <name>', 'Stage name (default inferred)')
|
|
1428
1462
|
.option('-p, --port <n>', 'Port (0=random)', (v) => Number(v), 0)
|
|
1429
|
-
.option('-
|
|
1463
|
+
.option('-V, --verbose', 'Verbose logging', false)
|
|
1430
1464
|
.action(async (opts) => {
|
|
1431
1465
|
try {
|
|
1432
1466
|
const cfg = readSmozConfig(root).cliDefaults?.dev ?? {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { URL, pathToFileURL } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const firstVal = (v) => Array.isArray(v) ? v[0] : v;
|
|
6
|
+
const arrVal = (v) => typeof v === 'string' ? [v] : Array.isArray(v) ? v : undefined;
|
|
7
|
+
const toHeaders = (raw) => {
|
|
8
|
+
const single = {};
|
|
9
|
+
const multi = {};
|
|
10
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
11
|
+
const fv = firstVal(v);
|
|
12
|
+
const av = arrVal(v);
|
|
13
|
+
if (typeof fv === 'string')
|
|
14
|
+
single[k] = fv;
|
|
15
|
+
if (Array.isArray(av))
|
|
16
|
+
multi[k] = av;
|
|
17
|
+
}
|
|
18
|
+
return { single, multi };
|
|
19
|
+
};
|
|
20
|
+
const readBody = (req) => new Promise((resolve) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
req.on('data', (c) => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(String(c))));
|
|
23
|
+
req.on('end', () => {
|
|
24
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
25
|
+
});
|
|
26
|
+
req.on('error', () => {
|
|
27
|
+
resolve('');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
const match = (segs, pathName) => {
|
|
31
|
+
const parts = pathName
|
|
32
|
+
.replace(/^\/+|\/+$/g, '')
|
|
33
|
+
.split('/')
|
|
34
|
+
.filter(Boolean);
|
|
35
|
+
if (parts.length !== segs.length)
|
|
36
|
+
return { ok: false, params: {} };
|
|
37
|
+
const params = {};
|
|
38
|
+
for (let i = 0; i < segs.length; i += 1) {
|
|
39
|
+
const seg = segs[i];
|
|
40
|
+
const p = parts[i];
|
|
41
|
+
if (seg.literal) {
|
|
42
|
+
if (seg.literal !== p)
|
|
43
|
+
return { ok: false, params: {} };
|
|
44
|
+
}
|
|
45
|
+
else if (seg.key) {
|
|
46
|
+
params[seg.key] = p;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, params };
|
|
50
|
+
};
|
|
51
|
+
const toEvent = async (req, route, params) => {
|
|
52
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
53
|
+
const { single, multi } = toHeaders(req.headers);
|
|
54
|
+
const method = (req.method ?? '').toUpperCase();
|
|
55
|
+
const stage = process.env.SMOZ_STAGE ?? 'dev';
|
|
56
|
+
const search = new URLSearchParams(url.search);
|
|
57
|
+
const query = {};
|
|
58
|
+
const mquery = {};
|
|
59
|
+
for (const key of Array.from(new Set(search.keys()))) {
|
|
60
|
+
const vals = search.getAll(key);
|
|
61
|
+
if (vals.length > 0) {
|
|
62
|
+
query[key] = vals[0];
|
|
63
|
+
mquery[key] = vals;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
let body = '';
|
|
67
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
68
|
+
body = await readBody(req);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
httpMethod: method,
|
|
72
|
+
headers: single,
|
|
73
|
+
multiValueHeaders: multi,
|
|
74
|
+
body,
|
|
75
|
+
isBase64Encoded: false,
|
|
76
|
+
path: url.pathname,
|
|
77
|
+
queryStringParameters: Object.keys(query).length ? query : {},
|
|
78
|
+
multiValueQueryStringParameters: Object.keys(mquery).length ? mquery : {},
|
|
79
|
+
pathParameters: Object.keys(params).length ? params : {},
|
|
80
|
+
stageVariables: null,
|
|
81
|
+
resource: route.pattern,
|
|
82
|
+
requestContext: {
|
|
83
|
+
accountId: 'acc',
|
|
84
|
+
apiId: 'inline',
|
|
85
|
+
httpMethod: method,
|
|
86
|
+
identity: {},
|
|
87
|
+
path: url.pathname,
|
|
88
|
+
stage,
|
|
89
|
+
requestId: String(Date.now()),
|
|
90
|
+
requestTimeEpoch: Date.now(),
|
|
91
|
+
resourceId: 'res',
|
|
92
|
+
resourcePath: route.pattern,
|
|
93
|
+
authorizer: {},
|
|
94
|
+
protocol: 'HTTP/1.1',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const writeResult = (res, result) => {
|
|
99
|
+
const status = typeof result.statusCode === 'number' ? result.statusCode : 200;
|
|
100
|
+
const headers = result.headers ?? {};
|
|
101
|
+
const body = typeof result.body === 'string' ? result.body : '';
|
|
102
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
103
|
+
if (typeof v === 'string')
|
|
104
|
+
res.setHeader(k, v);
|
|
105
|
+
}
|
|
106
|
+
res.statusCode = status;
|
|
107
|
+
res.end(body);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Load downstream registers to populate the app registry.
|
|
112
|
+
* Tries TS/JS candidates and logs which one was loaded when SMOZ_VERBOSE is set.
|
|
113
|
+
*/
|
|
114
|
+
const loadRegisters = async (root) => {
|
|
115
|
+
const candidates = [
|
|
116
|
+
path.resolve(root, 'app', 'generated', 'register.functions.ts'),
|
|
117
|
+
path.resolve(root, 'app', 'generated', 'register.functions.mts'),
|
|
118
|
+
path.resolve(root, 'app', 'generated', 'register.functions.js'),
|
|
119
|
+
path.resolve(root, 'app', 'generated', 'register.functions.mjs'),
|
|
120
|
+
];
|
|
121
|
+
for (const p of candidates) {
|
|
122
|
+
try {
|
|
123
|
+
const url = pathToFileURL(p).href;
|
|
124
|
+
await import(url);
|
|
125
|
+
if (process.env.SMOZ_VERBOSE) {
|
|
126
|
+
const rel = path.relative(root, p).split(path.sep).join('/');
|
|
127
|
+
console.log('[inline] registers loaded:', rel);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// try next candidate
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.warn('[inline] Could not load app/generated/register.functions.*. Run "npx smoz register" before inline dev.');
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Load the App instance (TS source) from the downstream project.
|
|
139
|
+
* Returns only the surface needed by the inline server.
|
|
140
|
+
*/
|
|
141
|
+
const loadApp = async (root) => {
|
|
142
|
+
const p = path.resolve(root, 'app', 'config', 'app.config.ts');
|
|
143
|
+
const url = pathToFileURL(p).href;
|
|
144
|
+
const mod = (await import(url));
|
|
145
|
+
const app = mod.app;
|
|
146
|
+
if (!app || typeof app.buildAllServerlessFunctions !== 'function') {
|
|
147
|
+
throw new Error('Failed to load app/config/app.config.ts (missing export "app").');
|
|
148
|
+
}
|
|
149
|
+
return app;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const splitPattern = (p) => p
|
|
153
|
+
.replace(/\\/g, '/')
|
|
154
|
+
.replace(/^\/+|\/+$/g, '')
|
|
155
|
+
.split('/')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((s) => s.startsWith('{') && s.endsWith('}')
|
|
158
|
+
? { key: s.slice(1, -1) }
|
|
159
|
+
: { literal: s });
|
|
160
|
+
/**
|
|
161
|
+
* Build route table from the downstream app's aggregated serverless functions.
|
|
162
|
+
*/
|
|
163
|
+
const loadHandlers = async (root, app) => {
|
|
164
|
+
const fns = app.buildAllServerlessFunctions();
|
|
165
|
+
const routes = [];
|
|
166
|
+
for (const [, defUnknown] of Object.entries(fns)) {
|
|
167
|
+
const def = defUnknown;
|
|
168
|
+
if (typeof def.handler !== 'string' || !Array.isArray(def.events))
|
|
169
|
+
continue;
|
|
170
|
+
const [moduleRel, exportName] = (() => {
|
|
171
|
+
const lastDot = def.handler.lastIndexOf('.');
|
|
172
|
+
if (lastDot < 0)
|
|
173
|
+
return [def.handler, 'handler'];
|
|
174
|
+
return [
|
|
175
|
+
def.handler.slice(0, lastDot),
|
|
176
|
+
def.handler.slice(lastDot + 1),
|
|
177
|
+
];
|
|
178
|
+
})();
|
|
179
|
+
// Resolve TS source module; dev runs via tsx so TS imports are OK
|
|
180
|
+
const candidates = [
|
|
181
|
+
path.resolve(root, `${moduleRel}.ts`),
|
|
182
|
+
path.resolve(root, `${moduleRel}.mts`),
|
|
183
|
+
path.resolve(root, `${moduleRel}.js`),
|
|
184
|
+
path.resolve(root, `${moduleRel}.mjs`),
|
|
185
|
+
];
|
|
186
|
+
// Always try the first candidate (TS is expected in authoring).
|
|
187
|
+
const modUrl = pathToFileURL(candidates[0]).href;
|
|
188
|
+
const mod = (await import(modUrl));
|
|
189
|
+
const handler = mod[exportName];
|
|
190
|
+
if (typeof handler !== 'function')
|
|
191
|
+
continue;
|
|
192
|
+
for (const evt of def.events) {
|
|
193
|
+
const httpEvt = evt
|
|
194
|
+
.http;
|
|
195
|
+
const method = (httpEvt?.method ?? '').toUpperCase();
|
|
196
|
+
const pattern = '/' + (httpEvt?.path ?? '').replace(/^\/+/, '');
|
|
197
|
+
if (!method || !pattern)
|
|
198
|
+
continue;
|
|
199
|
+
routes.push({
|
|
200
|
+
method,
|
|
201
|
+
pattern,
|
|
202
|
+
segs: splitPattern(pattern),
|
|
203
|
+
handlerRef: `${moduleRel}.${exportName}`,
|
|
204
|
+
handler: handler,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return routes;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Inline HTTP dev server (module entry).
|
|
213
|
+
*
|
|
214
|
+
* Decomposed from the original single-file implementation to improve
|
|
215
|
+
* readability and maintainability:
|
|
216
|
+
* - loaders.ts: register/app loaders
|
|
217
|
+
* - routes.ts: route table construction
|
|
218
|
+
* - http.ts: request/event mapping helpers
|
|
219
|
+
*/
|
|
220
|
+
const start = async () => {
|
|
221
|
+
const root = process.cwd();
|
|
222
|
+
// Ensure register side-effects are loaded so the registry has routes
|
|
223
|
+
await loadRegisters(root);
|
|
224
|
+
// Load the App instance from the same TS source as the registers
|
|
225
|
+
const app = await loadApp(root);
|
|
226
|
+
const routes = await loadHandlers(root, app);
|
|
227
|
+
const portEnv = process.env.SMOZ_PORT;
|
|
228
|
+
const port = typeof portEnv === 'string' && portEnv.length > 0 ? Number(portEnv) : 0;
|
|
229
|
+
const server = http.createServer((req, res) => {
|
|
230
|
+
void (async () => {
|
|
231
|
+
try {
|
|
232
|
+
const method = (req.method ?? '').toUpperCase();
|
|
233
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
234
|
+
// Allow HEAD to match GET routes; the wrapped handler/middleware will
|
|
235
|
+
// short-circuit HEAD requests to 200 {} and set Content-Type.
|
|
236
|
+
const searchMethod = method === 'HEAD' ? 'GET' : method;
|
|
237
|
+
const route = routes.find((r) => r.method === searchMethod && match(r.segs, url.pathname).ok);
|
|
238
|
+
if (!route) {
|
|
239
|
+
res.statusCode = 404;
|
|
240
|
+
res.setHeader('Content-Type', 'application/json');
|
|
241
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const { params } = match(route.segs, url.pathname);
|
|
245
|
+
const evt = await toEvent(req, route, params);
|
|
246
|
+
const result = await route.handler(evt, {
|
|
247
|
+
awsRequestId: String(Date.now()),
|
|
248
|
+
callbackWaitsForEmptyEventLoop: false,
|
|
249
|
+
functionName: 'inline',
|
|
250
|
+
functionVersion: '$LATEST',
|
|
251
|
+
invokedFunctionArn: 'arn:inline',
|
|
252
|
+
logGroupName: 'lg',
|
|
253
|
+
logStreamName: 'ls',
|
|
254
|
+
memoryLimitInMB: '256',
|
|
255
|
+
getRemainingTimeInMillis: () => 30000,
|
|
256
|
+
done: () => undefined,
|
|
257
|
+
fail: () => undefined,
|
|
258
|
+
succeed: () => undefined,
|
|
259
|
+
});
|
|
260
|
+
writeResult(res, result);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
res.statusCode = 500;
|
|
264
|
+
res.setHeader('Content-Type', 'application/json');
|
|
265
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
266
|
+
}
|
|
267
|
+
})();
|
|
268
|
+
});
|
|
269
|
+
server.listen(port, () => {
|
|
270
|
+
const addr = server.address();
|
|
271
|
+
const resolved = typeof addr === 'object' && addr && 'port' in addr
|
|
272
|
+
? addr.port
|
|
273
|
+
: port;
|
|
274
|
+
// Print route table
|
|
275
|
+
// Keep output consistent with existing tests:
|
|
276
|
+
// "[inline] listening on http://localhost:<port>"
|
|
277
|
+
console.log('[inline] listening on http://localhost:' + String(resolved));
|
|
278
|
+
const header = '[inline] routes:\n';
|
|
279
|
+
if (routes.length === 0) {
|
|
280
|
+
console.log(header + ' (none found)');
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const body = routes
|
|
284
|
+
.map((r) => ' ' +
|
|
285
|
+
r.method.padEnd(6) +
|
|
286
|
+
' ' +
|
|
287
|
+
r.pattern +
|
|
288
|
+
' -> ' +
|
|
289
|
+
r.handlerRef)
|
|
290
|
+
.join('\n');
|
|
291
|
+
console.log(header + body);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
// Start immediately when run via tsx
|
|
296
|
+
void start();
|
package/package.json
CHANGED
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
},
|
|
32
32
|
"description": "John Galt Services back end.",
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@rollup/plugin-alias": "^5.1.1",
|
|
35
34
|
"@dotenvx/dotenvx": "^1.49.0",
|
|
36
35
|
"@eslint/js": "^9.35.0",
|
|
36
|
+
"@rollup/plugin-alias": "^5.1.1",
|
|
37
37
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
38
38
|
"@serverless/typescript": "^4.18.2",
|
|
39
39
|
"@types/aws-lambda": "^8.10.152",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"fs-extra": "^11.3.1",
|
|
50
50
|
"knip": "^5.63.1",
|
|
51
51
|
"lefthook": "^1.13.0",
|
|
52
|
-
"pkg-dir": "^9.0.0",
|
|
53
52
|
"prettier": "^3.6.2",
|
|
54
53
|
"release-it": "^19.0.4",
|
|
55
54
|
"rimraf": "^6.0.1",
|
|
@@ -182,5 +181,5 @@
|
|
|
182
181
|
"templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
|
|
183
182
|
},
|
|
184
183
|
"type": "module",
|
|
185
|
-
"version": "0.2.
|
|
184
|
+
"version": "0.2.6"
|
|
186
185
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import * as fs from 'fs-extra';
|
|
1
|
+
import fs from 'fs-extra';
|
|
4
2
|
import { packageDirectorySync } from 'package-directory';
|
|
3
|
+
import path from 'path';
|
|
5
4
|
import { createDocument } from 'zod-openapi';
|
|
6
5
|
|
|
7
6
|
import { app } from '@/app/config/app.config';
|
|
@@ -22,18 +21,20 @@ console.log('Generating OpenAPI document...');
|
|
|
22
21
|
const paths = app.buildAllOpenApiPaths();
|
|
23
22
|
export const doc = createDocument({
|
|
24
23
|
openapi: '3.1.0',
|
|
25
|
-
servers: [
|
|
24
|
+
servers: [
|
|
25
|
+
{ description: 'Production', url: 'https://api.example.test' },
|
|
26
|
+
{ description: 'Dev', url: 'https://api.dev.example.test' },
|
|
27
|
+
],
|
|
26
28
|
info: {
|
|
27
|
-
title: process.env.npm_package_name ?? '
|
|
28
|
-
version: process.env.npm_package_version ?? '
|
|
29
|
+
title: process.env.npm_package_name ?? '',
|
|
30
|
+
version: process.env.npm_package_version ?? '',
|
|
29
31
|
},
|
|
30
32
|
paths,
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
const pkgDir = packageDirectorySync();
|
|
34
|
-
if (!pkgDir)
|
|
35
|
-
|
|
36
|
-
}
|
|
36
|
+
if (!pkgDir) throw new Error('Could not find package root directory');
|
|
37
|
+
|
|
37
38
|
const outDir = path.join(pkgDir, 'app', 'generated');
|
|
38
39
|
fs.ensureDirSync(outDir);
|
|
39
40
|
fs.writeFileSync(
|
|
@@ -9,6 +9,18 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
const tsconfigRootDir = dirname(fileURLToPath(import.meta.url));
|
|
10
10
|
|
|
11
11
|
export default [
|
|
12
|
+
{
|
|
13
|
+
ignores: [
|
|
14
|
+
'.serverless/**',
|
|
15
|
+
'.stan/**',
|
|
16
|
+
'**/.tsbuild/**',
|
|
17
|
+
'**/generated/**',
|
|
18
|
+
'coverage/**',
|
|
19
|
+
'dist/**',
|
|
20
|
+
'docs/**',
|
|
21
|
+
'node_modules/**',
|
|
22
|
+
],
|
|
23
|
+
},
|
|
12
24
|
eslint.configs.recommended,
|
|
13
25
|
...tseslint.configs.strictTypeChecked,
|
|
14
26
|
prettierConfig,
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"version": "0.0.0",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@middy/core": "^6.4.5",
|
|
8
|
-
"zod": "^4.1.6",
|
|
9
8
|
"fs-extra": "^11.3.1",
|
|
10
|
-
"package-directory": "^8.1.0"
|
|
9
|
+
"package-directory": "^8.1.0",
|
|
10
|
+
"zod": "^4.1.6"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"@serverless/typescript": "^4.18.2",
|
|
@@ -107,9 +107,6 @@ const config: AWS = {
|
|
|
107
107
|
versionFunctions: false,
|
|
108
108
|
},
|
|
109
109
|
functions: app.buildAllServerlessFunctions() as NonNullable<AWS['functions']>,
|
|
110
|
-
// Optional esbuild configuration (parity with /app fixture):
|
|
111
|
-
// These booleans are driven by global params in app.config.ts:
|
|
112
|
-
// ESB_MINIFY, ESB_SOURCEMAP
|
|
113
110
|
build: {
|
|
114
111
|
esbuild: {
|
|
115
112
|
bundle: true,
|
|
@@ -7,12 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"tsBuildInfoFile": ".tsbuild/tsconfig.tsbuildinfo"
|
|
9
9
|
},
|
|
10
|
-
"exclude": [
|
|
11
|
-
".serverless/**",
|
|
12
|
-
"node_modules/**",
|
|
13
|
-
"app/generated/**",
|
|
14
|
-
"dist/**"
|
|
15
|
-
],
|
|
10
|
+
"exclude": [".serverless/**", "node_modules/**", "app/generated/**"],
|
|
16
11
|
"extends": "./tsconfig.base.json",
|
|
17
12
|
"include": ["**/*", "**/*.json"]
|
|
18
13
|
}
|