@karmaniverous/smoz 0.2.5 → 0.2.7

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.
@@ -5,10 +5,10 @@ var fs = require('node:fs');
5
5
  var path = require('node:path');
6
6
  var commander = require('commander');
7
7
  var packageDirectory = require('package-directory');
8
- var node_url = require('node:url');
9
8
  var chokidar = require('chokidar');
10
9
  var node_child_process = require('node:child_process');
11
10
  var os = require('node:os');
11
+ var node_url = require('node:url');
12
12
  var node_process = require('node:process');
13
13
  var promises = require('node:readline/promises');
14
14
 
@@ -348,10 +348,19 @@ const spawnOffline = (root, cmd, verbose) => {
348
348
  env: baseEnv,
349
349
  });
350
350
  const prefix = '[offline] ';
351
+ let announced = false;
351
352
  const emit = (buf) => {
352
353
  const text = buf.toString('utf8');
353
354
  if (verbose)
354
355
  process.stdout.write(prefix + text);
356
+ // Detect the actual listening URL and announce it once to avoid "0" confusion.
357
+ const m = text.match(/listening on (https?:\/\/(?:localhost|127\.0\.0\.1):\d+)/i);
358
+ const url = m?.[1];
359
+ if (url && !announced) {
360
+ announced = true;
361
+ // Print a concise, canonical URL line.
362
+ process.stdout.write(`[dev] offline: ${url}\n`);
363
+ }
355
364
  };
356
365
  const emitErr = (buf) => {
357
366
  const text = buf.toString('utf8');
@@ -495,13 +504,17 @@ const collect = async (root) => {
495
504
  buckets.serverless.sort();
496
505
  return buckets;
497
506
  };
498
- const makeImports = (root, files) => {
499
- return files.map((abs) => {
500
- const rel = toPosix$1(path.relative(root, abs));
501
- const noExt = withoutTsExt(rel);
502
- return `import '@/${noExt}';`;
503
- });
504
- };
507
+ /**
508
+ * Build ESM import lines using POSIX-relative specifiers from the generated
509
+ * file directory (app/generated) to each target file. Avoids "@/" alias so
510
+ * dynamic import in the inline server works reliably under tsx.
511
+ */
512
+ const makeImports = (root, outDir, files) => files.map((abs) => {
513
+ const relFromOut = toPosix$1(path.relative(outDir, abs));
514
+ const noExt = withoutTsExt(relFromOut);
515
+ const spec = noExt.startsWith('.') ? noExt : `./${noExt}`;
516
+ return `import '${spec}';`;
517
+ });
505
518
  const buildFile = (imports) => {
506
519
  if (imports.length === 0)
507
520
  return `${HEADER}\n`;
@@ -509,10 +522,10 @@ const buildFile = (imports) => {
509
522
  };
510
523
  const runRegister = async (root) => {
511
524
  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
525
  const outDir = path.join(root, 'app', 'generated');
526
+ const lambdaImports = makeImports(root, outDir, buckets.lambda);
527
+ const openapiImports = makeImports(root, outDir, buckets.openapi);
528
+ const serverlessImports = makeImports(root, outDir, buckets.serverless);
516
529
  const fnsPath = path.join(outDir, 'register.functions.ts');
517
530
  const oaiPath = path.join(outDir, 'register.openapi.ts');
518
531
  const srvPath = path.join(outDir, 'register.serverless.ts');
@@ -529,11 +542,146 @@ const runRegister = async (root) => {
529
542
  return { wrote };
530
543
  };
531
544
 
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
- */
545
+ const inferDefaultStage = (root, verbose) => {
546
+ // Prefer “dev”; explicit --stage overrides remain available.
547
+ if (verbose)
548
+ console.log('[dev] inferring stage: dev (explicit --stage overrides)');
549
+ return 'dev';
550
+ };
551
+ const seedEnvForStage = async (root, stage, verbose) => {
552
+ // Best effort: import the app config to read declared env keys and concrete values.
553
+ // Preserve existing process.env values; only seed when unset.
554
+ try {
555
+ const appConfigUrl = node_url.pathToFileURL(path.resolve(root, 'app', 'config', 'app.config.ts')).href;
556
+ // Dynamically import the TS module under tsx
557
+ const mod = (await import(appConfigUrl));
558
+ const app = mod.app;
559
+ const stages = mod.stages;
560
+ const globalKeys = Array.isArray(app?.global?.envKeys)
561
+ ? app.global.envKeys
562
+ : [];
563
+ const stageKeys = Array.isArray(app?.stage?.envKeys)
564
+ ? app.stage.envKeys
565
+ : [];
566
+ const globalParams = (stages?.default).params ?? {};
567
+ const stageParams = (stages?.[stage]).params ?? {};
568
+ const seedPair = (key, from) => {
569
+ if (key in process.env)
570
+ return;
571
+ const val = from[key];
572
+ if (val === undefined)
573
+ return;
574
+ if (typeof val === 'string') {
575
+ process.env[key] = val;
576
+ if (verbose)
577
+ console.log(`[dev] env: ${key}=${val}`);
578
+ return;
579
+ }
580
+ if (typeof val === 'number' || typeof val === 'boolean') {
581
+ const v = String(val);
582
+ process.env[key] = v;
583
+ if (verbose)
584
+ console.log(`[dev] env: ${key}=${v}`);
585
+ return;
586
+ }
587
+ // Non-primitive; skip to avoid [object Object] surprise.
588
+ if (verbose)
589
+ console.log(`[dev] env: skip ${key} (non-primitive)`);
590
+ };
591
+ for (const k of globalKeys) {
592
+ if (typeof k === 'string') {
593
+ seedPair(k, globalParams);
594
+ }
595
+ }
596
+ for (const k of stageKeys) {
597
+ if (typeof k === 'string') {
598
+ seedPair(k, stageParams);
599
+ }
600
+ }
601
+ // Ensure STAGE itself is present as a last resort
602
+ if (!process.env.STAGE) {
603
+ process.env.STAGE = stage;
604
+ if (verbose)
605
+ console.log(`[dev] env: STAGE=${stage}`);
606
+ }
607
+ }
608
+ catch {
609
+ // Fallback: seed STAGE only
610
+ if (!process.env.STAGE) {
611
+ process.env.STAGE = stage;
612
+ if (verbose)
613
+ console.log(`[dev] env: STAGE=${stage}`);
614
+ }
615
+ }
616
+ };
617
+
618
+ const resolveTsxCommand = (root, tsEntry) => {
619
+ const localCli = path.resolve(root, 'node_modules', 'tsx', 'dist', 'cli.js');
620
+ if (fs.existsSync(localCli)) {
621
+ // Normalize to POSIX separators for cross-platform comparisons (tests)
622
+ const localCliPosix = localCli.split(path.sep).join('/');
623
+ return {
624
+ cmd: process.execPath,
625
+ args: [localCliPosix, tsEntry],
626
+ shell: false,
627
+ };
628
+ }
629
+ const probe = node_child_process.spawnSync(process.platform === 'win32' ? 'tsx.cmd' : 'tsx', ['--version'], { shell: true });
630
+ if (typeof probe.status === 'number' && probe.status === 0) {
631
+ return {
632
+ cmd: process.platform === 'win32' ? 'tsx.cmd' : 'tsx',
633
+ args: [tsEntry],
634
+ shell: true,
635
+ };
636
+ }
637
+ throw new Error('Inline requires tsx. Install it with "npm i -D tsx", or run "smoz dev -l offline".');
638
+ };
639
+ const launchInline = async (root, opts) => {
640
+ // Resolve the installed smoz package root robustly (works from compiled CLI and tsx ESM):
641
+ // Prefer __dirname when available (CJS), otherwise derive from import.meta.url (ESM).
642
+ const here = typeof __dirname === 'string'
643
+ ? __dirname
644
+ : path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
645
+ const pkgRoot = packageDirectory.packageDirectorySync({ cwd: here }) ?? process.cwd();
646
+ const tsEntry = path.resolve(pkgRoot, 'src', 'cli', 'local', 'inline.server', 'index.ts');
647
+ const makeTsx = () => {
648
+ const { cmd, args, shell } = resolveTsxCommand(root, tsEntry);
649
+ return node_child_process.spawn(cmd, args, {
650
+ cwd: root,
651
+ stdio: 'inherit',
652
+ shell,
653
+ // Enable tsconfig paths for "@/..." during TS fallback.
654
+ env: {
655
+ ...process.env,
656
+ TSX_TSCONFIG_PATHS: '1',
657
+ SMOZ_STAGE: opts.stage,
658
+ SMOZ_PORT: String(opts.port),
659
+ SMOZ_VERBOSE: opts.verbose ? '1' : '',
660
+ },
661
+ });
662
+ };
663
+ let child = makeTsx();
664
+ const close = async () => new Promise((resolve) => {
665
+ // If the process has already exited (exitCode set), resolve immediately.
666
+ if (child.exitCode !== null) {
667
+ resolve();
668
+ return;
669
+ }
670
+ child.once('exit', () => {
671
+ resolve();
672
+ });
673
+ child.kill('SIGTERM');
674
+ setTimeout(() => {
675
+ resolve();
676
+ }, 1500);
677
+ });
678
+ const restart = async () => {
679
+ await close();
680
+ child = makeTsx();
681
+ };
682
+ return { close, restart };
683
+ };
684
+
537
685
  const runDev = async (root, opts) => {
538
686
  const verbose = !!opts.verbose;
539
687
  const stage = typeof opts.stage === 'string'
@@ -547,7 +695,8 @@ const runDev = async (root, opts) => {
547
695
  if (verbose)
548
696
  console.warn('[dev] env seeding warning:', e.message);
549
697
  }
550
- const mode = opts.local;
698
+ const modeInitial = opts.local;
699
+ const mode = modeInitial;
551
700
  const port = typeof opts.port === 'number' ? opts.port : 0;
552
701
  if (verbose) {
553
702
  console.log(`[dev] options: register=${String(opts.register)} ` +
@@ -633,8 +782,10 @@ const runDev = async (root, opts) => {
633
782
  offline = await launchOffline(root, { stage, port, verbose });
634
783
  }
635
784
  else if (mode === 'inline') {
785
+ // Hard error on failure; no fallback to offline.
636
786
  inlineChild = await launchInline(root, { stage, port, verbose });
637
- } // Watch sources
787
+ }
788
+ // Watch sources
638
789
  const globs = [
639
790
  path.join(root, 'app', 'functions', '**', 'lambda.ts'),
640
791
  path.join(root, 'app', 'functions', '**', 'openapi.ts'),
@@ -667,130 +818,6 @@ const runDev = async (root, opts) => {
667
818
  });
668
819
  });
669
820
  };
670
- const inferDefaultStage = (root, verbose) => {
671
- // Prefer “dev”; if app.config.ts is available and we can inspect it cheaply later, expand behavior.
672
- // For now, return 'dev' (explicit selection via --stage remains available).
673
- if (verbose)
674
- console.log('[dev] inferring stage: dev (explicit --stage overrides)');
675
- return 'dev';
676
- };
677
- const seedEnvForStage = async (root, stage, verbose) => {
678
- // Best effort: import the app config to read declared env keys and concrete values.
679
- // Preserve existing process.env values; only seed when unset.
680
- try {
681
- const appConfigUrl = node_url.pathToFileURL(path.resolve(root, 'app', 'config', 'app.config.ts')).href;
682
- // Dynamically import the TS module under tsx
683
- const mod = (await import(appConfigUrl));
684
- const app = mod.app;
685
- const stages = mod.stages;
686
- const globalKeys = Array.isArray(app?.global?.envKeys)
687
- ? app.global.envKeys
688
- : [];
689
- const stageKeys = Array.isArray(app?.stage?.envKeys)
690
- ? app.stage.envKeys
691
- : [];
692
- const globalParams = (stages?.default).params ?? {};
693
- const stageParams = (stages?.[stage]).params ?? {};
694
- const seedPair = (key, from) => {
695
- if (key in process.env)
696
- return;
697
- const val = from[key];
698
- if (val === undefined)
699
- return;
700
- if (typeof val === 'string') {
701
- process.env[key] = val;
702
- if (verbose)
703
- console.log(`[dev] env: ${key}=${val}`);
704
- return;
705
- }
706
- if (typeof val === 'number' || typeof val === 'boolean') {
707
- const v = String(val);
708
- process.env[key] = v;
709
- if (verbose)
710
- console.log(`[dev] env: ${key}=${v}`);
711
- return;
712
- }
713
- // Non-primitive; skip to avoid [object Object] surprise.
714
- if (verbose)
715
- console.log(`[dev] env: skip ${key} (non-primitive)`);
716
- };
717
- for (const k of globalKeys) {
718
- if (typeof k === 'string') {
719
- seedPair(k, globalParams);
720
- }
721
- }
722
- for (const k of stageKeys) {
723
- if (typeof k === 'string') {
724
- seedPair(k, stageParams);
725
- }
726
- }
727
- // Ensure STAGE itself is present as a last resort
728
- if (!process.env.STAGE) {
729
- process.env.STAGE = stage;
730
- if (verbose)
731
- console.log(`[dev] env: STAGE=${stage}`);
732
- }
733
- }
734
- catch {
735
- // Fallback: seed STAGE only
736
- if (!process.env.STAGE) {
737
- process.env.STAGE = stage;
738
- if (verbose)
739
- console.log(`[dev] env: STAGE=${stage}`);
740
- }
741
- }
742
- };
743
- // Inline local runner: spawn tsx to run a TS server script.
744
- const launchInline = async (root, opts) => {
745
- const { spawn } = await import('node:child_process');
746
- const path = await import('node:path');
747
- const fs = await import('node:fs');
748
- const tsxCli = path.resolve(root, 'node_modules', 'tsx', 'dist', 'cli.js');
749
- const entry = path.resolve(root, 'src', 'cli', 'local', 'inline.server.ts');
750
- if (!fs.existsSync(entry)) {
751
- throw new Error('inline server entry missing: src/cli/local/inline.server.ts');
752
- }
753
- const args = [tsxCli, entry];
754
- if (!fs.existsSync(tsxCli)) {
755
- // Fallback to PATH resolution
756
- args.shift();
757
- args.unshift(process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
758
- }
759
- else {
760
- args.unshift(process.execPath);
761
- }
762
- const spawnChild = () => spawn(args[0], args.slice(1), {
763
- cwd: root,
764
- stdio: 'inherit',
765
- shell: !args[0].endsWith('.js'),
766
- env: {
767
- ...process.env,
768
- SMOZ_STAGE: opts.stage,
769
- SMOZ_PORT: String(opts.port),
770
- SMOZ_VERBOSE: opts.verbose ? '1' : '',
771
- },
772
- });
773
- let child = spawnChild();
774
- const close = async () => new Promise((resolve) => {
775
- // If the process has already exited (exitCode set), resolve immediately.
776
- if (child.exitCode !== null) {
777
- resolve();
778
- return;
779
- }
780
- child.once('exit', () => {
781
- resolve();
782
- });
783
- child.kill('SIGTERM');
784
- setTimeout(() => {
785
- resolve();
786
- }, 1500);
787
- });
788
- const restart = async () => {
789
- await close();
790
- child = spawnChild();
791
- };
792
- return { close, restart };
793
- };
794
821
 
795
822
  const writeIfAbsent = async (outFile, content) => {
796
823
  if (fs.existsSync(outFile))
@@ -1426,7 +1453,7 @@ const main = () => {
1426
1453
  .option('-l, --local [mode]', 'Local server mode: inline|offline', 'inline')
1427
1454
  .option('-s, --stage <name>', 'Stage name (default inferred)')
1428
1455
  .option('-p, --port <n>', 'Port (0=random)', (v) => Number(v), 0)
1429
- .option('-v, --verbose', 'Verbose logging', false)
1456
+ .option('-V, --verbose', 'Verbose logging', false)
1430
1457
  .action(async (opts) => {
1431
1458
  try {
1432
1459
  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
@@ -181,5 +181,5 @@
181
181
  "templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
182
182
  },
183
183
  "type": "module",
184
- "version": "0.2.5"
184
+ "version": "0.2.7"
185
185
  }
@@ -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,
@@ -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
  }