@karmaniverous/smoz 0.2.6 → 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.
Files changed (2) hide show
  1. package/dist/cli/index.cjs +153 -160
  2. package/package.json +1 -1
@@ -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');
@@ -533,6 +542,146 @@ const runRegister = async (root) => {
533
542
  return { wrote };
534
543
  };
535
544
 
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
+
536
685
  const runDev = async (root, opts) => {
537
686
  const verbose = !!opts.verbose;
538
687
  const stage = typeof opts.stage === 'string'
@@ -547,7 +696,7 @@ const runDev = async (root, opts) => {
547
696
  console.warn('[dev] env seeding warning:', e.message);
548
697
  }
549
698
  const modeInitial = opts.local;
550
- let mode = modeInitial;
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,16 +782,8 @@ const runDev = async (root, opts) => {
633
782
  offline = await launchOffline(root, { stage, port, verbose });
634
783
  }
635
784
  else if (mode === 'inline') {
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
- }
785
+ // Hard error on failure; no fallback to offline.
786
+ inlineChild = await launchInline(root, { stage, port, verbose });
646
787
  }
647
788
  // Watch sources
648
789
  const globs = [
@@ -677,154 +818,6 @@ const runDev = async (root, opts) => {
677
818
  });
678
819
  });
679
820
  };
680
- const inferDefaultStage = (root, verbose) => {
681
- // Prefer “dev”; if app.config.ts is available and we can inspect it cheaply later, expand behavior.
682
- // For now, return 'dev' (explicit selection via --stage remains available).
683
- if (verbose)
684
- console.log('[dev] inferring stage: dev (explicit --stage overrides)');
685
- return 'dev';
686
- };
687
- const seedEnvForStage = async (root, stage, verbose) => {
688
- // Best effort: import the app config to read declared env keys and concrete values.
689
- // Preserve existing process.env values; only seed when unset.
690
- try {
691
- const appConfigUrl = node_url.pathToFileURL(path.resolve(root, 'app', 'config', 'app.config.ts')).href;
692
- // Dynamically import the TS module under tsx
693
- const mod = (await import(appConfigUrl));
694
- const app = mod.app;
695
- const stages = mod.stages;
696
- const globalKeys = Array.isArray(app?.global?.envKeys)
697
- ? app.global.envKeys
698
- : [];
699
- const stageKeys = Array.isArray(app?.stage?.envKeys)
700
- ? app.stage.envKeys
701
- : [];
702
- const globalParams = (stages?.default).params ?? {};
703
- const stageParams = (stages?.[stage]).params ?? {};
704
- const seedPair = (key, from) => {
705
- if (key in process.env)
706
- return;
707
- const val = from[key];
708
- if (val === undefined)
709
- return;
710
- if (typeof val === 'string') {
711
- process.env[key] = val;
712
- if (verbose)
713
- console.log(`[dev] env: ${key}=${val}`);
714
- return;
715
- }
716
- if (typeof val === 'number' || typeof val === 'boolean') {
717
- const v = String(val);
718
- process.env[key] = v;
719
- if (verbose)
720
- console.log(`[dev] env: ${key}=${v}`);
721
- return;
722
- }
723
- // Non-primitive; skip to avoid [object Object] surprise.
724
- if (verbose)
725
- console.log(`[dev] env: skip ${key} (non-primitive)`);
726
- };
727
- for (const k of globalKeys) {
728
- if (typeof k === 'string') {
729
- seedPair(k, globalParams);
730
- }
731
- }
732
- for (const k of stageKeys) {
733
- if (typeof k === 'string') {
734
- seedPair(k, stageParams);
735
- }
736
- }
737
- // Ensure STAGE itself is present as a last resort
738
- if (!process.env.STAGE) {
739
- process.env.STAGE = stage;
740
- if (verbose)
741
- console.log(`[dev] env: STAGE=${stage}`);
742
- }
743
- }
744
- catch {
745
- // Fallback: seed STAGE only
746
- if (!process.env.STAGE) {
747
- process.env.STAGE = stage;
748
- if (verbose)
749
- console.log(`[dev] env: STAGE=${stage}`);
750
- }
751
- }
752
- };
753
- // Inline local runner: spawn tsx to run a TS server script.
754
- const launchInline = async (root, opts) => {
755
- const { spawn, spawnSync } = await import('node:child_process');
756
- const path = await import('node:path');
757
- const fs = await import('node:fs');
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');
761
- if (!fs.existsSync(entry)) {
762
- throw new Error('inline server entry missing in package (dist/mjs/cli/inline-server.js)');
763
- }
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];
777
- }
778
- else {
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.');
791
- }
792
- const spawnChild = () => spawn(cmd, args, {
793
- cwd: root,
794
- stdio: 'inherit',
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.
799
- env: {
800
- ...process.env,
801
- TSX_TSCONFIG_PATHS: '1',
802
- SMOZ_STAGE: opts.stage,
803
- SMOZ_PORT: String(opts.port),
804
- SMOZ_VERBOSE: opts.verbose ? '1' : '',
805
- },
806
- });
807
- let child = spawnChild();
808
- const close = async () => new Promise((resolve) => {
809
- // If the process has already exited (exitCode set), resolve immediately.
810
- if (child.exitCode !== null) {
811
- resolve();
812
- return;
813
- }
814
- child.once('exit', () => {
815
- resolve();
816
- });
817
- child.kill('SIGTERM');
818
- setTimeout(() => {
819
- resolve();
820
- }, 1500);
821
- });
822
- const restart = async () => {
823
- await close();
824
- child = spawnChild();
825
- };
826
- return { close, restart };
827
- };
828
821
 
829
822
  const writeIfAbsent = async (outFile, content) => {
830
823
  if (fs.existsSync(outFile))
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.6"
184
+ "version": "0.2.7"
185
185
  }