@feltdb/core 0.6.8 → 0.6.9

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.
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
+ import net from 'net';
2
3
  import path from 'path';
3
- import { spawn } from 'child_process';
4
4
  const FRAMEWORKS = [
5
5
  { name: 'Next.js', packages: ['next'], pattern: /(?:^|\s)next(?:\s+dev)?(?:\s|$)/ },
6
6
  { name: 'Astro', packages: ['astro'], pattern: /(?:^|\s)astro(?:\s+dev)?(?:\s|$)/ },
@@ -21,107 +21,74 @@ function packageManagerAt(root, declared) {
21
21
  return 'bun';
22
22
  return 'npm';
23
23
  }
24
- function configuredPort(script = '') {
24
+ function scriptPort(script = '') {
25
25
  const match = script.match(/(?:^|\s)(?:PORT=|--port(?:=|\s+)|-p\s+)(\d{1,5})(?:\s|$)/i);
26
26
  if (!match)
27
27
  return undefined;
28
28
  const port = Number(match[1]);
29
29
  return port > 0 && port <= 65535 ? port : undefined;
30
30
  }
31
+ function readJson(file, malformedMessage) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
34
+ }
35
+ catch (error) {
36
+ throw new Error(`${malformedMessage} (${error.message})`);
37
+ }
38
+ }
39
+ /** Read application metadata only. This function never starts an application. */
31
40
  export function detectApplication(root) {
32
41
  const packageFile = path.join(root, 'package.json');
33
- let manifest = {};
34
- if (fs.existsSync(packageFile)) {
35
- try {
36
- manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
37
- }
38
- catch (error) {
39
- throw new Error(`Cannot detect application: malformed package.json (${error.message})`);
40
- }
41
- }
42
- const manager = packageManagerAt(root, typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined);
42
+ const manifest = fs.existsSync(packageFile) ? readJson(packageFile, 'Cannot inspect application: malformed package.json') : {};
43
43
  const script = typeof manifest.scripts?.dev === 'string' ? manifest.scripts.dev.trim() : undefined;
44
44
  const dependencies = { ...manifest.dependencies, ...manifest.devDependencies };
45
45
  const framework = FRAMEWORKS.find(candidate => candidate.packages.some(name => name in dependencies) || (script ? candidate.pattern.test(script) : false));
46
- const invokesFeltDb = !!script && /(?:^|\s)feltdb\s+dev(?:\s|$)/.test(script);
47
- if (script && !invokesFeltDb) {
48
- const executable = process.platform === 'win32' ? `${manager}.cmd` : manager;
49
- return {
50
- packageManager: manager,
51
- framework: framework?.name || 'Custom',
52
- devScript: script,
53
- command: executable,
54
- args: manager === 'npm' || manager === 'bun' ? ['run', 'dev'] : ['dev'],
55
- configuredPort: configuredPort(script),
56
- };
57
- }
58
- // Projects generated by FeltDB historically use `feltdb dev` as their script.
59
- // Keep their Vite application working without recursively spawning this CLI.
60
- if (framework?.name === 'Vite' || fs.existsSync(path.join(root, 'index.html'))) {
61
- const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
62
- return { packageManager: manager, framework: 'Vite', devScript: script, command: executable, args: ['exec', '--', 'vite'] };
63
- }
64
- throw new Error('Cannot determine how to start the application. Add a package.json dev script or pass --app-url.');
46
+ const feltConfigFile = path.join(root, 'feltdb.config.json');
47
+ const feltConfig = fs.existsSync(feltConfigFile) ? readJson(feltConfigFile, 'Cannot inspect application: malformed feltdb.config.json') : {};
48
+ const configuredUrl = typeof feltConfig.appUrl === 'string' ? feltConfig.appUrl
49
+ : typeof feltConfig.applicationUrl === 'string' ? feltConfig.applicationUrl : undefined;
50
+ return {
51
+ packageManager: packageManagerAt(root, typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined),
52
+ framework: framework?.name || (script ? 'Custom' : 'Unknown'),
53
+ devScript: script,
54
+ configuredPort: scriptPort(script),
55
+ configuredUrl,
56
+ };
65
57
  }
66
- const URL_PATTERN = /https?:\/\/(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])(?::\d{1,5})?(?:\/[^\s\x1b]*)?/gi;
67
- export function applicationUrls(output) {
68
- const plain = output.replace(/\x1b\[[0-?]*[ -\/]*[@-~]/g, '');
69
- return [...plain.matchAll(URL_PATTERN)].map(match => match[0].replace(/[),.;]+$/, ''));
58
+ export function applicationUrlCandidate(project, environment = process.env) {
59
+ if (environment.FELTDB_APP_URL)
60
+ return environment.FELTDB_APP_URL;
61
+ if (project.configuredUrl)
62
+ return project.configuredUrl;
63
+ if (project.configuredPort)
64
+ return `http://127.0.0.1:${project.configuredPort}`;
65
+ return undefined;
70
66
  }
71
- export function startApplication(project, root, options = {}) {
72
- const env = { ...process.env };
73
- if (options.appPort)
74
- env.PORT = String(options.appPort);
75
- const args = [...project.args];
76
- // This is only the legacy Vite fallback, never an override of an existing script.
77
- if (project.framework === 'Vite' && !project.devScript?.replace(/\s+/g, ' ').match(/^(?:npm run )?vite/)) {
78
- args.push('--host', '127.0.0.1');
79
- if (options.appPort)
80
- args.push('--port', String(options.appPort));
67
+ export function applicationIsRunning(url, timeoutMs = 750) {
68
+ let parsed;
69
+ try {
70
+ parsed = new URL(url);
81
71
  }
82
- if (options.appPort && project.devScript) {
83
- const flag = project.framework === 'Next.js' ? '-p' : '--port';
84
- if (['Vite', 'Next.js', 'Astro', 'Nuxt', 'SvelteKit', 'Remix'].includes(project.framework)) {
85
- if (project.packageManager === 'npm')
86
- args.push('--');
87
- args.push(flag, String(options.appPort));
88
- }
72
+ catch {
73
+ return Promise.resolve(false);
89
74
  }
90
- const child = spawn(project.command, args, { cwd: root, env, stdio: ['inherit', 'pipe', 'pipe'] });
91
- const url = new Promise((resolve, reject) => {
92
- let settled = false;
93
- const finish = (value) => { if (!settled) {
94
- settled = true;
95
- clearTimeout(timer);
96
- resolve(value);
97
- } };
98
- const inspect = (chunk) => {
99
- const text = chunk.toString();
100
- process.stdout.write(text);
101
- const found = applicationUrls(text);
102
- if (found.length)
103
- finish(found[0]);
104
- };
105
- child.stdout?.on('data', inspect);
106
- child.stderr?.on('data', inspect);
107
- child.once('error', error => { if (!settled) {
108
- settled = true;
109
- clearTimeout(timer);
110
- reject(error);
111
- } });
112
- child.once('exit', code => { if (!settled) {
113
- settled = true;
114
- clearTimeout(timer);
115
- reject(new Error(`Application exited before reporting its URL (status ${code ?? 'unknown'})`));
116
- } });
117
- const timer = setTimeout(() => {
118
- if (!settled) {
119
- settled = true;
120
- reject(new Error('Unable to determine application URL from dev-server output. Pass --app-url or --app-port.'));
121
- }
122
- }, options.startupTimeoutMs ?? 30000);
123
- if (options.appPort)
124
- finish(`http://127.0.0.1:${options.appPort}`);
75
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
76
+ return Promise.resolve(false);
77
+ const port = Number(parsed.port || (parsed.protocol === 'https:' ? 443 : 80));
78
+ return new Promise(resolve => {
79
+ const socket = net.createConnection({ host: parsed.hostname, port });
80
+ const done = (running) => { socket.destroy(); resolve(running); };
81
+ socket.setTimeout(timeoutMs);
82
+ socket.once('connect', () => done(true));
83
+ socket.once('timeout', () => done(false));
84
+ socket.once('error', () => done(false));
125
85
  });
126
- return { child, url };
86
+ }
87
+ /** Discover only a deterministically configured, already-running application. */
88
+ export async function discoverApplicationUrl(root) {
89
+ const project = detectApplication(root);
90
+ const candidate = applicationUrlCandidate(project);
91
+ if (!candidate)
92
+ return { project };
93
+ return { project, url: await applicationIsRunning(candidate) ? candidate : undefined };
127
94
  }
@@ -9,7 +9,7 @@ import { createRequire } from 'module';
9
9
  import { spawn, spawnSync } from 'child_process';
10
10
  import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
11
11
  import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
12
- import { detectApplication, startApplication } from './application.js';
12
+ import { detectApplication, discoverApplicationUrl } from './application.js';
13
13
  function loadProjectEnvironment(file = path.resolve('.env.local')) {
14
14
  if (!fs.existsSync(file))
15
15
  return;
@@ -495,6 +495,45 @@ async function handleDev(args) {
495
495
  const runtimeNamespace = config.runtime === 'managed'
496
496
  ? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
497
497
  : config.namespace;
498
+ const hasAppUrl = args.includes('--app-url');
499
+ const appUrlArgument = hasAppUrl ? args[args.indexOf('--app-url') + 1] : undefined;
500
+ if (hasAppUrl && !appUrlArgument)
501
+ throw new Error('--app-url requires a URL');
502
+ const hasAppPort = args.includes('--app-port') || args.includes('--port');
503
+ const appPortValue = args.includes('--app-port')
504
+ ? args[args.indexOf('--app-port') + 1]
505
+ : args.includes('--port') ? args[args.indexOf('--port') + 1] : undefined;
506
+ if (!hasAppUrl && hasAppPort && !appPortValue)
507
+ throw new Error('--app-port requires a port');
508
+ const appPort = appPortValue ? Number(appPortValue) : undefined;
509
+ if (appUrlArgument) {
510
+ try {
511
+ const parsed = new URL(appUrlArgument);
512
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
513
+ throw new Error('unsupported protocol');
514
+ }
515
+ catch {
516
+ throw new Error(`Invalid --app-url: ${appUrlArgument}`);
517
+ }
518
+ }
519
+ if (!hasAppUrl && appPortValue && (!Number.isInteger(appPort) || appPort < 1 || appPort > 65535))
520
+ throw new Error(`Invalid --app-port: ${appPortValue}`);
521
+ let discoveredApplication;
522
+ if (!hasAppUrl && !appPort) {
523
+ discoveredApplication = await discoverApplicationUrl(projectDir);
524
+ }
525
+ else {
526
+ try {
527
+ discoveredApplication = { project: detectApplication(projectDir) };
528
+ }
529
+ catch {
530
+ discoveredApplication = { project: { packageManager: 'npm', framework: 'External' } };
531
+ }
532
+ }
533
+ const appUrl = appUrlArgument || (appPort ? `http://127.0.0.1:${appPort}` : discoveredApplication.url);
534
+ if (!appUrl) {
535
+ throw new Error('No application URL was detected.\nStart your application and run:\n feltdb dev --app-url http://127.0.0.1:<port>\nOr configure applicationUrl in feltdb.config.json.');
536
+ }
498
537
  const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
499
538
  const studioPort = String(await availablePort(requestedStudioPort));
500
539
  if (studioPort !== String(requestedStudioPort))
@@ -549,49 +588,13 @@ async function handleDev(args) {
549
588
  pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
550
589
  pairingToken = token.token;
551
590
  }
552
- const hasAppUrl = args.includes('--app-url');
553
- const appUrlArgument = hasAppUrl ? args[args.indexOf('--app-url') + 1] : undefined;
554
- if (hasAppUrl && !appUrlArgument)
555
- throw new Error('--app-url requires a URL');
556
- const hasAppPort = args.includes('--app-port') || args.includes('--port');
557
- const appPortValue = args.includes('--app-port')
558
- ? args[args.indexOf('--app-port') + 1]
559
- : args.includes('--port') ? args[args.indexOf('--port') + 1] : undefined;
560
- if (!hasAppUrl && hasAppPort && !appPortValue)
561
- throw new Error('--app-port requires a port');
562
- const appPort = appPortValue ? Number(appPortValue) : undefined;
563
- if (appUrlArgument) {
564
- try {
565
- const parsed = new URL(appUrlArgument);
566
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
567
- throw new Error('unsupported protocol');
568
- }
569
- catch {
570
- throw new Error(`Invalid --app-url: ${appUrlArgument}`);
571
- }
572
- }
573
- if (!hasAppUrl && appPortValue && (!Number.isInteger(appPort) || appPort < 1 || appPort > 65535))
574
- throw new Error(`Invalid --app-port: ${appPortValue}`);
575
- const application = appUrlArgument ? undefined : detectApplication(projectDir);
576
- const runningApplication = application ? startApplication(application, projectDir, { appPort }) : undefined;
577
- let appUrl;
578
- try {
579
- appUrl = appUrlArgument || await runningApplication.url;
580
- }
581
- catch (error) {
582
- runningApplication?.child.kill('SIGTERM');
583
- pairingDiscoveryServer?.close();
584
- await localAuthority?.close();
585
- stopSelfHosted();
586
- throw error;
587
- }
588
591
  console.log('FeltDB Dev Server');
589
592
  console.log(` Authority: ${process.env.VITE_FELTDB_URL}`);
590
593
  console.log(` Studio: http://127.0.0.1:${studioPort}`);
591
594
  console.log(` Pairing: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
592
595
  console.log('Application');
593
- console.log(` Framework: ${application?.framework || 'External'}`);
594
- console.log(` Dev command: ${application ? [application.command.replace(/\.cmd$/, ''), ...application.args].join(' ') : '(already running)'}`);
596
+ console.log(` Framework: ${discoveredApplication.project.framework}`);
597
+ console.log(' Dev command: (managed externally)');
595
598
  console.log(` URL: ${appUrl}`);
596
599
  console.log(` Namespace: ${config.namespace}`);
597
600
  console.log(` Runtime: ${config.runtime}`);
@@ -610,18 +613,8 @@ async function handleDev(args) {
610
613
  const open = !args.includes('--no-open');
611
614
  console.log(`Application: ${appUrl}`);
612
615
  console.log(`Studio: http://127.0.0.1:${studioPort}\n`);
613
- let shuttingDown = false;
614
- const stopApplication = () => { if (runningApplication && !runningApplication.child.killed)
615
- runningApplication.child.kill('SIGTERM'); };
616
- const stopAll = () => { shuttingDown = true; stopApplication(); pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
617
- // In an inherited terminal Ctrl-C can reach Vite before this parent process.
618
- // Never leave Studio (or its port) running after the application exits.
619
- runningApplication?.child.once('exit', code => {
620
- if (shuttingDown)
621
- return;
622
- stopSelfHosted();
623
- process.exit(code ?? 0);
624
- });
616
+ console.log(`Using existing application at ${appUrl}\n`);
617
+ const stopAll = () => { pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
625
618
  process.once('exit', stopAll);
626
619
  process.once('SIGINT', () => { stopAll(); process.exit(130); });
627
620
  process.once('SIGTERM', () => { stopAll(); process.exit(143); });
package/dist/cli/index.js CHANGED
@@ -23,7 +23,7 @@ import * as path from 'path';
23
23
  import * as readline from 'readline';
24
24
  import { getClient } from './api-client.js';
25
25
  import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
26
- const VERSION = '0.6.8';
26
+ const VERSION = '0.6.9';
27
27
  function prompt(question) {
28
28
  const rl = readline.createInterface({
29
29
  input: process.stdin,
@@ -1,4 +1,4 @@
1
1
  // One release train keeps generated applications installable. The repository
2
2
  // validation script checks these values against every workspace manifest.
3
- export const FELTDB_PACKAGE_VERSION = '0.6.8';
3
+ export const FELTDB_PACKAGE_VERSION = '0.6.9';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feltdb/core",
3
- "version": "0.6.8",
3
+ "version": "0.6.9",
4
4
  "description": "FeltDB Core - Application-facing database with Browser, Local, and Server runtimes. Durable state, reactive APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",