@nanobpm/urban 0.96.1 → 0.96.2
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.js +24 -3
- package/dist/runtime/core/modules/dataops.d.ts +4 -4
- package/dist/runtime/core/modules/dataops.js +11 -10
- package/dist/runtime/core/modules/datasource.d.ts +6 -5
- package/dist/runtime/core/modules/datasource.js +8 -5
- package/dist/toolkit/artifact.d.ts +6 -4
- package/dist/toolkit/artifact.js +6 -6
- package/dist/toolkit/gen.js +2 -1
- package/package.json +1 -1
- package/src/cli-lazy.test.ts +172 -0
- package/src/cli.ts +25 -12
- package/src/runtime/core/modules/dataops.ts +41 -37
- package/src/runtime/core/modules/datasource.ts +8 -5
- package/src/toolkit/artifact.ts +6 -6
- package/src/toolkit/gen.ts +2 -1
package/dist/cli.js
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
|
|
13
13
|
// --port <n>, -h/--help, -v/--version.
|
|
14
|
-
|
|
14
|
+
//
|
|
15
|
+
// Module-load laziness (nano-ide#592): only argument parsing, the stdio/guard helpers and the
|
|
16
|
+
// dispatcher load eagerly. Every command's modules are dynamically imported inside its handler —
|
|
17
|
+
// a top-level import of the toolkit, the scaffold package or the runtime barrel made EVERY
|
|
18
|
+
// command (including the per-data-op `urban data` gateway) pay the full ~2s load on small
|
|
19
|
+
// hosts before dispatching. `urban data` loads only the host detector and the data-op runner.
|
|
20
|
+
// Guarded by cli-lazy.test.ts, which records the URLs each command resolves.
|
|
15
21
|
import { denoGlobal, processGlobal } from "./runtime/adapters/globals.js";
|
|
16
22
|
import { errorMessage, isRecord } from "./runtime/core/guards.js";
|
|
17
|
-
import { scaffold, slugify } from "create-urban-app";
|
|
18
|
-
import { addConnector, createNodeGenIO, previewModels, generate, runGen } from "./toolkit/index.js";
|
|
19
23
|
import { pathToFileURL } from "node:url";
|
|
20
24
|
import { readFileSync } from "node:fs";
|
|
21
25
|
import { spawnSync } from "node:child_process";
|
|
@@ -119,6 +123,9 @@ Global flags:
|
|
|
119
123
|
Engine address: $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2).
|
|
120
124
|
`;
|
|
121
125
|
async function cmdCheck(f) {
|
|
126
|
+
const { selectHost } = await import("./runtime/adapters/detect.js");
|
|
127
|
+
const { loadManifest } = await import("./runtime/core/manifest.js");
|
|
128
|
+
const { collectManifestIssues } = await import("./runtime/core/validate.js");
|
|
122
129
|
// The host is anchored at f.root, so manifest paths are already root-relative —
|
|
123
130
|
// pass the bare manifest filename (prefixing root again would double it).
|
|
124
131
|
const host = selectHost({ cwd: f.root });
|
|
@@ -134,6 +141,7 @@ async function cmdCheck(f) {
|
|
|
134
141
|
return 1;
|
|
135
142
|
}
|
|
136
143
|
async function cmdGen(f) {
|
|
144
|
+
const { createNodeGenIO, generate } = await import("./toolkit/index.js");
|
|
137
145
|
const io = createNodeGenIO();
|
|
138
146
|
const res = await generate({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
|
|
139
147
|
if (res.incomplete) {
|
|
@@ -198,6 +206,7 @@ async function cmdDerive(f) {
|
|
|
198
206
|
console.error("✖ `urban derive` derives models — `--no-models` is contradictory. Use `urban gen --no-models` for type-contracts only.");
|
|
199
207
|
return 2;
|
|
200
208
|
}
|
|
209
|
+
const { createNodeGenIO, previewModels, runGen } = await import("./toolkit/index.js");
|
|
201
210
|
const io = createNodeGenIO();
|
|
202
211
|
// Non-writing preview: emit `{ models: [{ id, kind, xml }], incomplete }` to stdout for the
|
|
203
212
|
// console's read-only model viewer (never touches `processes/`).
|
|
@@ -238,6 +247,7 @@ async function cmdDerive(f) {
|
|
|
238
247
|
return 0;
|
|
239
248
|
}
|
|
240
249
|
async function cmdRun(f, mount) {
|
|
250
|
+
const { runFromEnv } = await import("./runtime/run.js");
|
|
241
251
|
const app = await runFromEnv({
|
|
242
252
|
root: f.root,
|
|
243
253
|
manifestPath: f.manifest,
|
|
@@ -249,11 +259,14 @@ async function cmdRun(f, mount) {
|
|
|
249
259
|
return -1; // keep the process alive; signal handlers stop it
|
|
250
260
|
}
|
|
251
261
|
async function cmdDev(f) {
|
|
262
|
+
const { runDev } = await import("./runtime/devserver.js");
|
|
263
|
+
const { installSignalHandlers } = await import("./runtime/run.js");
|
|
252
264
|
const dev = await runDev({ root: f.root, manifestPath: f.manifest, port: f.port });
|
|
253
265
|
installSignalHandlers(() => dev.stop());
|
|
254
266
|
return -1; // keep the process alive; signal handlers stop the dev server
|
|
255
267
|
}
|
|
256
268
|
async function cmdDeploy(f) {
|
|
269
|
+
const { runFromEnv } = await import("./runtime/run.js");
|
|
257
270
|
const app = await runFromEnv({
|
|
258
271
|
root: f.root,
|
|
259
272
|
manifestPath: f.manifest,
|
|
@@ -306,6 +319,12 @@ export async function cmdData(f, readInput = readStdin) {
|
|
|
306
319
|
}));
|
|
307
320
|
return 0;
|
|
308
321
|
}
|
|
322
|
+
// Lazily the only modules `urban data` loads (nano-ide#592): the host detector (both
|
|
323
|
+
// adapters, no toolkit) and the data-op runner (gateway + SQLite adapter; the toolkit
|
|
324
|
+
// derivers behind the `domaintypes` op load only when that op is dispatched). Imported
|
|
325
|
+
// directly rather than via the runtime barrel, which would pull in the whole runtime.
|
|
326
|
+
const { selectHost } = await import("./runtime/adapters/detect.js");
|
|
327
|
+
const { runDataOp } = await import("./runtime/core/modules/dataops.js");
|
|
309
328
|
const host = selectHost({ cwd: f.root });
|
|
310
329
|
try {
|
|
311
330
|
// Mirror runFromEnv: the host is anchored at f.root, so pass root "." to avoid
|
|
@@ -324,6 +343,7 @@ async function cmdNew(f) {
|
|
|
324
343
|
console.error("usage: urban new <name> [--root <path>] [--deno] [--style model|code] (or --code-first)");
|
|
325
344
|
return 1;
|
|
326
345
|
}
|
|
346
|
+
const { scaffold, slugify } = await import("create-urban-app");
|
|
327
347
|
const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
|
|
328
348
|
const res = await scaffold({ name, dir, deno: f.deno, style: f.style });
|
|
329
349
|
console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
|
|
@@ -362,6 +382,7 @@ async function cmdAdd(f) {
|
|
|
362
382
|
}
|
|
363
383
|
}
|
|
364
384
|
try {
|
|
385
|
+
const { addConnector, createNodeGenIO } = await import("./toolkit/index.js");
|
|
365
386
|
const result = await addConnector({
|
|
366
387
|
root: f.root,
|
|
367
388
|
pkg,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { HostContext } from "../host.ts";
|
|
2
2
|
import type { AppManifest } from "../manifest.ts";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import type { ShapeDecl } from "../../../toolkit/derivers/shapes.ts";
|
|
4
|
+
import type { WorkerBindingDecl } from "../../../toolkit/derivers/worker-io.ts";
|
|
5
|
+
import type { MessageBindingDecl } from "../../../toolkit/derivers/messages.ts";
|
|
6
|
+
import type { MetaDecl } from "../../../toolkit/derivers/meta.ts";
|
|
7
7
|
/** The DB-manager op protocol's known op set — every op (including `domaintypes`) is dispatched by
|
|
8
8
|
* `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers type-safety
|
|
9
9
|
* and catches typos at compile time. */
|
|
@@ -23,16 +23,6 @@ import { loadManifest } from "../manifest.js";
|
|
|
23
23
|
import { validateManifest } from "../validate.js";
|
|
24
24
|
import { makeGateway } from "./gateway.js";
|
|
25
25
|
import { applyMigrations, MIGRATIONS_TABLE, openSqliteSource, resolveAppPath } from "./datasource.js";
|
|
26
|
-
// The `domaintypes` op composes the domain reifier from the pure toolkit derivers (single source of
|
|
27
|
-
// truth for the IDE's codegen, host dry-out nano-bpm#576). Imported directly from the deriver files
|
|
28
|
-
// (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free of `node:*`/Deno; the
|
|
29
|
-
// derivers' only core dependency is the gateway's `TableMeta`/`ColumnMeta` *types*, which erase.
|
|
30
|
-
import { GENERATED_DIR } from "../../../toolkit/artifact.js";
|
|
31
|
-
import { DOMAIN_BINDINGS, emitDomainBindings, emitDomainModel, registryFromManifest, } from "../../../toolkit/derivers/domain.js";
|
|
32
|
-
import { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes, } from "../../../toolkit/derivers/shapes.js";
|
|
33
|
-
import { DOMAIN_DTS, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, } from "../../../toolkit/derivers/worker-io.js";
|
|
34
|
-
import { emitMessageBindings, emitMessageBindingsRuntime, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } from "../../../toolkit/derivers/messages.js";
|
|
35
|
-
import { emitMeta, META_TS } from "../../../toolkit/derivers/meta.js";
|
|
36
26
|
const DEFAULT_MIGRATIONS_DIR = "db/migrations";
|
|
37
27
|
/** JSON-safe a value: BigInt → number (when lossless) or string, recursing objects/arrays. SQLite
|
|
38
28
|
* reports `lastInsertRowid` (and INTEGER columns) as BigInt, which `JSON.stringify` cannot encode. */
|
|
@@ -242,6 +232,17 @@ export async function runDataOp(host, root, manifestPath, req) {
|
|
|
242
232
|
// debounced keystroke has no DB side-effect and pays only for `text` + `shapeDiagnostics`.
|
|
243
233
|
const { default: def, sources } = listSources(manifest);
|
|
244
234
|
const persist = req.write !== false;
|
|
235
|
+
// The derivers are the only toolkit modules `urban data` ever needs, and only this op runs
|
|
236
|
+
// them — load them here, not at module scope, so every other op (the hot path of the data
|
|
237
|
+
// gateway, nano-ide#592) never pays for the `src/toolkit/` tree. Imported directly from the
|
|
238
|
+
// deriver files (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free
|
|
239
|
+
// of host-specific IO.
|
|
240
|
+
const { GENERATED_DIR } = await import("../../../toolkit/artifact.js");
|
|
241
|
+
const { DOMAIN_BINDINGS, emitDomainBindings, emitDomainModel, registryFromManifest, } = await import("../../../toolkit/derivers/domain.js");
|
|
242
|
+
const { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes } = await import("../../../toolkit/derivers/shapes.js");
|
|
243
|
+
const { DOMAIN_DTS, emitWorkerBindings, emitWorkerBindingsRuntime, overlayDerivedWorkerIo, WORKER_BINDINGS_DTS, WORKER_BINDINGS_TS, } = await import("../../../toolkit/derivers/worker-io.js");
|
|
244
|
+
const { emitMessageBindings, emitMessageBindingsRuntime, MESSAGE_BINDINGS_DTS, MESSAGE_BINDINGS_TS, } = await import("../../../toolkit/derivers/messages.js");
|
|
245
|
+
const { emitMeta, META_TS } = await import("../../../toolkit/derivers/meta.js");
|
|
245
246
|
// Introspect each sqlite source's live schema. When persisting, migrate first (the domain
|
|
246
247
|
// model is derived from `gw.schema()`, so a regen that raced ahead of `migrate` on a fresh DB
|
|
247
248
|
// would emit an empty `Domain`); the migration runs on the shared `_urban_migrations` ledger.
|
|
@@ -2,7 +2,6 @@ import type { RuntimeContext } from "../context.ts";
|
|
|
2
2
|
import type { HostContext, SqliteDb } from "../host.ts";
|
|
3
3
|
import type { DomainType } from "../manifest.ts";
|
|
4
4
|
import { Table, type DataSource as GatewayDataSource, type InsertObserver } from "./gateway.ts";
|
|
5
|
-
import { isAbsolutePath } from "../../../toolkit/artifact.ts";
|
|
6
5
|
export declare function sqlitePathFromUrl(url: string): string;
|
|
7
6
|
/** True when `p` is already an absolute path on any host `urban data` targets. `urban data` runs
|
|
8
7
|
* on a Node host that may be Windows, so absolute-path detection can't assume POSIX: this matches
|
|
@@ -10,10 +9,12 @@ export declare function sqlitePathFromUrl(url: string): string;
|
|
|
10
9
|
* that starts with a single backslash ("\data\app.db") and a Windows UNC path
|
|
11
10
|
* ("\\\\server\\share"). A single leading backslash covers both the drive-root and UNC cases,
|
|
12
11
|
* matching Node's `path.win32.isAbsolute`. Used by `resolveAppPath` so a caller-supplied absolute
|
|
13
|
-
* path is never incorrectly prefixed with the app root. Defined
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
12
|
+
* path is never incorrectly prefixed with the app root. Defined here in the runtime (not imported
|
|
13
|
+
* from `toolkit/artifact.ts`) so the `urban data` gateway's static module graph never touches the
|
|
14
|
+
* `src/toolkit/` tree (nano-ide#592); `toolkit/artifact.ts` re-exports this definition so the
|
|
15
|
+
* toolkit's gen path-join and this runtime resolver apply the identical rule (no gen/runtime
|
|
16
|
+
* drift). */
|
|
17
|
+
export declare function isAbsolutePath(p: string): boolean;
|
|
17
18
|
/** Resolve `p` against the app `root`: an absolute `p` (see `isAbsolutePath`) is returned as-is;
|
|
18
19
|
* a relative `p` is joined onto `root`. Trims a trailing separator of either kind off `root` so
|
|
19
20
|
* we never emit a doubled separator. The join separator matches the root's own style so we never
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// runtime side of ADR 0024 (datasource) + ADR 0040 (domain model), scoped to what an app needs.
|
|
5
5
|
import { makeGateway, } from "./gateway.js";
|
|
6
6
|
import { currentJobContext } from "../execContext.js";
|
|
7
|
-
import { isAbsolutePath } from "../../../toolkit/artifact.js";
|
|
8
7
|
import { SQL_IDENT, isReservedObjectName } from "../read-model.js";
|
|
9
8
|
export function sqlitePathFromUrl(url) {
|
|
10
9
|
// Accept "file:./x.db", "file:x.db", "sqlite:./x.db" or a bare path.
|
|
@@ -16,10 +15,14 @@ export function sqlitePathFromUrl(url) {
|
|
|
16
15
|
* that starts with a single backslash ("\data\app.db") and a Windows UNC path
|
|
17
16
|
* ("\\\\server\\share"). A single leading backslash covers both the drive-root and UNC cases,
|
|
18
17
|
* matching Node's `path.win32.isAbsolute`. Used by `resolveAppPath` so a caller-supplied absolute
|
|
19
|
-
* path is never incorrectly prefixed with the app root. Defined
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
|
|
18
|
+
* path is never incorrectly prefixed with the app root. Defined here in the runtime (not imported
|
|
19
|
+
* from `toolkit/artifact.ts`) so the `urban data` gateway's static module graph never touches the
|
|
20
|
+
* `src/toolkit/` tree (nano-ide#592); `toolkit/artifact.ts` re-exports this definition so the
|
|
21
|
+
* toolkit's gen path-join and this runtime resolver apply the identical rule (no gen/runtime
|
|
22
|
+
* drift). */
|
|
23
|
+
export function isAbsolutePath(p) {
|
|
24
|
+
return /^(\/|\\|[A-Za-z]:[/\\])/.test(p);
|
|
25
|
+
}
|
|
23
26
|
/** Resolve `p` against the app `root`: an absolute `p` (see `isAbsolutePath`) is returned as-is;
|
|
24
27
|
* a relative `p` is joined onto `root`. Trims a trailing separator of either kind off `root` so
|
|
25
28
|
* we never emit a doubled separator. The join separator matches the root's own style so we never
|
|
@@ -29,7 +29,9 @@ export declare const RUNTIME_MATERIALIZED_ARTIFACTS: readonly string[];
|
|
|
29
29
|
/** Stable ordering so artifact lists compare deterministically. */
|
|
30
30
|
export declare function sortArtifacts(a: DerivedArtifact[]): DerivedArtifact[];
|
|
31
31
|
/** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
|
|
32
|
-
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
32
|
+
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule is
|
|
33
|
+
* the runtime's `resolveAppPath` side (`runtime/core/modules/datasource.ts`); it is re-exported
|
|
34
|
+
* here so the toolkit's path join (gen.ts) and the runtime resolver agree on whether a manifest
|
|
35
|
+
* path is app-root-relative or absolute (no gen/runtime drift) without the runtime's data gateway
|
|
36
|
+
* statically importing the toolkit tree (nano-ide#592). */
|
|
37
|
+
export { isAbsolutePath } from "../runtime/core/modules/datasource.ts";
|
package/dist/toolkit/artifact.js
CHANGED
|
@@ -28,9 +28,9 @@ export function sortArtifacts(a) {
|
|
|
28
28
|
return [...a].sort((x, y) => (x.path < y.path ? -1 : x.path > y.path ? 1 : 0));
|
|
29
29
|
}
|
|
30
30
|
/** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
|
|
31
|
-
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
31
|
+
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule is
|
|
32
|
+
* the runtime's `resolveAppPath` side (`runtime/core/modules/datasource.ts`); it is re-exported
|
|
33
|
+
* here so the toolkit's path join (gen.ts) and the runtime resolver agree on whether a manifest
|
|
34
|
+
* path is app-root-relative or absolute (no gen/runtime drift) without the runtime's data gateway
|
|
35
|
+
* statically importing the toolkit tree (nano-ide#592). */
|
|
36
|
+
export { isAbsolutePath } from "../runtime/core/modules/datasource.js";
|
package/dist/toolkit/gen.js
CHANGED
|
@@ -19,7 +19,8 @@ function join(root, rel) {
|
|
|
19
19
|
// pass either style without gen/runtime drift over where a file resolves.
|
|
20
20
|
const norm = (s) => s.replace(/\\/g, "/");
|
|
21
21
|
// An absolute `rel` resolves to itself — never prefixed with `root` — mirroring the runtime's
|
|
22
|
-
// resolveAppPath (isAbsolutePath is the shared SoT in
|
|
22
|
+
// resolveAppPath (isAbsolutePath is the shared SoT, defined in runtime's datasource.ts and
|
|
23
|
+
// re-exported from artifact.ts). Without this an absolute
|
|
23
24
|
// manifest path (e.g. "/abs/openapi.json") would be stripped to root-relative and gen would
|
|
24
25
|
// read/derive a different file than the runtime resolves. Trailing edge separators are still
|
|
25
26
|
// trimmed for stable, comparable artifact keys.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.96.
|
|
3
|
+
"version": "0.96.2",
|
|
4
4
|
"description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Module-load laziness guard for nanobpm/nano-ide#592: the CLI used to import the whole
|
|
2
|
+
// toolkit (`create-urban-app`, `./toolkit/index.ts`) and the runtime barrel at the top of
|
|
3
|
+
// cli.ts, so EVERY command — including the per-data-op `urban data` gateway — paid the full
|
|
4
|
+
// load before dispatching. On small hosts that load is ~90% of a command's cost (measured:
|
|
5
|
+
// ~2s CPU on a 4-core Atom vs ~0.1s bare Node start-up). The fix keeps only argument parsing
|
|
6
|
+
// and stdio/guard helpers at the top level and dynamically imports each command's modules
|
|
7
|
+
// inside its handler.
|
|
8
|
+
//
|
|
9
|
+
// These tests pin the laziness per command: they spawn the real CLI entrypoint with a
|
|
10
|
+
// `--import` preload that registers a module-resolution hook recording every resolved URL,
|
|
11
|
+
// then assert which module trees the command did NOT load.
|
|
12
|
+
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
|
+
import { isRecord } from "./runtime/core/guards.ts";
|
|
21
|
+
|
|
22
|
+
const cli = fileURLToPath(new URL("./cli.ts", import.meta.url));
|
|
23
|
+
|
|
24
|
+
// Spawning with --experimental-strip-types runs the TypeScript source directly, so the test
|
|
25
|
+
// exercises the authored cli.ts (no build step needed) — same as the >64 KB stdout test above.
|
|
26
|
+
// bin.mjs is Node-only, so skip under Deno (`test:deno`), whose process.execPath is not Node.
|
|
27
|
+
const runtimeIsNode = !("Deno" in globalThis);
|
|
28
|
+
|
|
29
|
+
interface RunResult {
|
|
30
|
+
stdout: string;
|
|
31
|
+
stderr: string;
|
|
32
|
+
code: number | null;
|
|
33
|
+
resolved: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Spawn `cli.ts <args>` with a preload that records every module URL the process resolves.
|
|
38
|
+
* The preload registers a resolve hook (via `module.register`) on a loader thread that
|
|
39
|
+
* appends each resolved URL to a file; the parent then reads that file.
|
|
40
|
+
*/
|
|
41
|
+
async function runCliRecordingModules(
|
|
42
|
+
dir: string,
|
|
43
|
+
args: string[],
|
|
44
|
+
stdin: string,
|
|
45
|
+
): Promise<RunResult> {
|
|
46
|
+
const resolvedFile = join(dir, "resolved.txt");
|
|
47
|
+
await writeFile(resolvedFile, "");
|
|
48
|
+
const hooks = join(dir, "hooks.mjs");
|
|
49
|
+
await writeFile(
|
|
50
|
+
hooks,
|
|
51
|
+
[
|
|
52
|
+
'import { appendFileSync } from "node:fs";',
|
|
53
|
+
`const file = ${JSON.stringify(resolvedFile)};`,
|
|
54
|
+
"export async function resolve(specifier, context, nextResolve) {",
|
|
55
|
+
" const result = await nextResolve(specifier, context);",
|
|
56
|
+
" try { appendFileSync(file, result.url + \"\\n\"); } catch {}",
|
|
57
|
+
" return result;",
|
|
58
|
+
"}",
|
|
59
|
+
"",
|
|
60
|
+
].join("\n"),
|
|
61
|
+
);
|
|
62
|
+
const preload = join(dir, "preload.mjs");
|
|
63
|
+
await writeFile(
|
|
64
|
+
preload,
|
|
65
|
+
[
|
|
66
|
+
'import { register } from "node:module";',
|
|
67
|
+
`register(${JSON.stringify(pathToFileURL(hooks).href)});`,
|
|
68
|
+
"",
|
|
69
|
+
].join("\n"),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const { stdout, stderr, code } = await new Promise<{
|
|
73
|
+
stdout: Buffer;
|
|
74
|
+
stderr: Buffer;
|
|
75
|
+
code: number | null;
|
|
76
|
+
}>((resolve, reject) => {
|
|
77
|
+
const child = spawn(
|
|
78
|
+
process.execPath,
|
|
79
|
+
["--experimental-strip-types", `--import=${pathToFileURL(preload).href}`, cli, ...args],
|
|
80
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
81
|
+
);
|
|
82
|
+
const out: Buffer[] = [];
|
|
83
|
+
const err: Buffer[] = [];
|
|
84
|
+
child.stdout.on("data", (c: Buffer) => out.push(c));
|
|
85
|
+
child.stderr.on("data", (c: Buffer) => err.push(c));
|
|
86
|
+
child.on("error", reject);
|
|
87
|
+
child.on("close", (code) =>
|
|
88
|
+
resolve({ stdout: Buffer.concat(out), stderr: Buffer.concat(err), code }),
|
|
89
|
+
);
|
|
90
|
+
child.stdin.end(stdin);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { readFile } = await import("node:fs/promises");
|
|
94
|
+
const resolved = (await readFile(resolvedFile, "utf8")).split("\n").filter((l) => l.length > 0);
|
|
95
|
+
return { stdout: stdout.toString("utf8"), stderr: stderr.toString("utf8"), code, resolved };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Resolved URLs under the given package path fragments (the heavy module trees of #592). */
|
|
99
|
+
function heavyLoads(resolved: string[]): string[] {
|
|
100
|
+
return resolved.filter(
|
|
101
|
+
(u) =>
|
|
102
|
+
// Node resolves a workspace package's node_modules symlink to its real target, so in a
|
|
103
|
+
// monorepo checkout `create-urban-app` resolves under `packages/create-urban-app/` — match
|
|
104
|
+
// both the installed and the workspace-real shape or a scaffold regression slips through.
|
|
105
|
+
u.includes("/node_modules/create-urban-app/") ||
|
|
106
|
+
u.includes("/packages/create-urban-app/") ||
|
|
107
|
+
u.includes("/dist/toolkit/") ||
|
|
108
|
+
u.includes("/src/toolkit/"),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test("urban data does not load the toolkit, scaffold or gen modules", {
|
|
113
|
+
skip: runtimeIsNode ? false : "Node-only: spawns cli.ts via process.execPath (Deno's execPath is not Node)",
|
|
114
|
+
}, async () => {
|
|
115
|
+
const dir = await mkdtemp(join(tmpdir(), "urban-cli-lazy-data-"));
|
|
116
|
+
try {
|
|
117
|
+
// A parseable request, so cmdData gets past JSON.parse and actually runs the command-specific
|
|
118
|
+
// dynamic imports (host detector + data-op runner) — malformed JSON would return from the
|
|
119
|
+
// parse-error branch first and prove nothing about the data path's module graph. The root has
|
|
120
|
+
// no manifest, so the op still fails; the CLI answers with its parseable envelope and exits 0.
|
|
121
|
+
const { stdout, stderr, code, resolved } = await runCliRecordingModules(
|
|
122
|
+
dir,
|
|
123
|
+
["data", "--root", dir],
|
|
124
|
+
JSON.stringify({ op: "sources" }),
|
|
125
|
+
);
|
|
126
|
+
assert.equal(code, 0, `CLI exited ${code}, expected 0. stderr:\n${stderr}`);
|
|
127
|
+
const reply: unknown = JSON.parse(stdout);
|
|
128
|
+
assert.ok(isRecord(reply));
|
|
129
|
+
assert.equal(reply.ok, false);
|
|
130
|
+
|
|
131
|
+
const heavy = heavyLoads(resolved);
|
|
132
|
+
assert.deepEqual(
|
|
133
|
+
heavy,
|
|
134
|
+
[],
|
|
135
|
+
`urban data eagerly loaded toolkit/scaffold modules:\n${heavy.join("\n")}`,
|
|
136
|
+
);
|
|
137
|
+
} finally {
|
|
138
|
+
await rm(dir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("urban --version loads neither the toolkit nor the runtime modules", {
|
|
143
|
+
skip: runtimeIsNode ? false : "Node-only: spawns cli.ts via process.execPath (Deno's execPath is not Node)",
|
|
144
|
+
}, async () => {
|
|
145
|
+
const dir = await mkdtemp(join(tmpdir(), "urban-cli-lazy-version-"));
|
|
146
|
+
try {
|
|
147
|
+
const { stdout, stderr, code, resolved } = await runCliRecordingModules(dir, ["--version"], "");
|
|
148
|
+
assert.equal(code, 0, `CLI exited ${code}, expected 0. stderr:\n${stderr}`);
|
|
149
|
+
assert.match(stdout, /^\d+\.\d+\.\d+/);
|
|
150
|
+
|
|
151
|
+
// The only runtime-tree modules the CLI itself may touch eagerly are its two tiny
|
|
152
|
+
// stdio/guard helpers — everything else under runtime/ (the barrel, run.ts, the data-op
|
|
153
|
+
// modules, the engine adapters, …) must stay unloaded until a command needs it.
|
|
154
|
+
const allowed = new Set(["/src/runtime/adapters/globals.ts", "/src/runtime/core/guards.ts"]);
|
|
155
|
+
const heavy = resolved.filter(
|
|
156
|
+
(u) =>
|
|
157
|
+
u.includes("/node_modules/create-urban-app/") ||
|
|
158
|
+
u.includes("/packages/create-urban-app/") ||
|
|
159
|
+
u.includes("/dist/toolkit/") ||
|
|
160
|
+
u.includes("/src/toolkit/") ||
|
|
161
|
+
((u.includes("/dist/runtime/") || u.includes("/src/runtime/")) &&
|
|
162
|
+
![...allowed].some((a) => u.endsWith(a))),
|
|
163
|
+
);
|
|
164
|
+
assert.deepEqual(
|
|
165
|
+
heavy,
|
|
166
|
+
[],
|
|
167
|
+
`urban --version eagerly loaded toolkit/runtime modules:\n${heavy.join("\n")}`,
|
|
168
|
+
);
|
|
169
|
+
} finally {
|
|
170
|
+
await rm(dir, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -11,22 +11,18 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Global flags: --root <dir> (default "."), --manifest <file> (default nano.app.json),
|
|
13
13
|
// --port <n>, -h/--help, -v/--version.
|
|
14
|
+
//
|
|
15
|
+
// Module-load laziness (nano-ide#592): only argument parsing, the stdio/guard helpers and the
|
|
16
|
+
// dispatcher load eagerly. Every command's modules are dynamically imported inside its handler —
|
|
17
|
+
// a top-level import of the toolkit, the scaffold package or the runtime barrel made EVERY
|
|
18
|
+
// command (including the per-data-op `urban data` gateway) pay the full ~2s load on small
|
|
19
|
+
// hosts before dispatching. `urban data` loads only the host detector and the data-op runner.
|
|
20
|
+
// Guarded by cli-lazy.test.ts, which records the URLs each command resolves.
|
|
14
21
|
|
|
15
|
-
import {
|
|
16
|
-
collectManifestIssues,
|
|
17
|
-
installSignalHandlers,
|
|
18
|
-
loadManifest,
|
|
19
|
-
runDataOp,
|
|
20
|
-
runDev,
|
|
21
|
-
runFromEnv,
|
|
22
|
-
selectHost,
|
|
23
|
-
} from "./runtime/index.ts";
|
|
24
22
|
import { denoGlobal, processGlobal } from "./runtime/adapters/globals.ts";
|
|
25
23
|
import type { WritableStdio } from "./runtime/adapters/globals.ts";
|
|
26
24
|
import { errorMessage, isRecord } from "./runtime/core/guards.ts";
|
|
27
|
-
import {
|
|
28
|
-
import type { DataRequest } from "./runtime/index.ts";
|
|
29
|
-
import { addConnector, createNodeGenIO, previewModels, generate, runGen } from "./toolkit/index.ts";
|
|
25
|
+
import type { DataRequest } from "./runtime/core/modules/dataops.ts";
|
|
30
26
|
import { pathToFileURL } from "node:url";
|
|
31
27
|
import { readFileSync } from "node:fs";
|
|
32
28
|
import { spawnSync } from "node:child_process";
|
|
@@ -132,6 +128,9 @@ Engine address: $CAMUNDA_REST_ADDRESS (default http://localhost:8080/v2).
|
|
|
132
128
|
`;
|
|
133
129
|
|
|
134
130
|
async function cmdCheck(f: Flags): Promise<number> {
|
|
131
|
+
const { selectHost } = await import("./runtime/adapters/detect.ts");
|
|
132
|
+
const { loadManifest } = await import("./runtime/core/manifest.ts");
|
|
133
|
+
const { collectManifestIssues } = await import("./runtime/core/validate.ts");
|
|
135
134
|
// The host is anchored at f.root, so manifest paths are already root-relative —
|
|
136
135
|
// pass the bare manifest filename (prefixing root again would double it).
|
|
137
136
|
const host = selectHost({ cwd: f.root });
|
|
@@ -147,6 +146,7 @@ async function cmdCheck(f: Flags): Promise<number> {
|
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
async function cmdGen(f: Flags): Promise<number> {
|
|
149
|
+
const { createNodeGenIO, generate } = await import("./toolkit/index.ts");
|
|
150
150
|
const io = createNodeGenIO();
|
|
151
151
|
const res = await generate({ root: f.root, io, manifestFile: f.manifest, check: f.check, emitModels: f.models });
|
|
152
152
|
if (res.incomplete) {
|
|
@@ -209,6 +209,7 @@ async function cmdDerive(f: Flags): Promise<number> {
|
|
|
209
209
|
);
|
|
210
210
|
return 2;
|
|
211
211
|
}
|
|
212
|
+
const { createNodeGenIO, previewModels, runGen } = await import("./toolkit/index.ts");
|
|
212
213
|
const io = createNodeGenIO();
|
|
213
214
|
|
|
214
215
|
// Non-writing preview: emit `{ models: [{ id, kind, xml }], incomplete }` to stdout for the
|
|
@@ -250,6 +251,7 @@ async function cmdDerive(f: Flags): Promise<number> {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
async function cmdRun(f: Flags, mount?: Record<string, boolean>): Promise<number> {
|
|
254
|
+
const { runFromEnv } = await import("./runtime/run.ts");
|
|
253
255
|
const app = await runFromEnv({
|
|
254
256
|
root: f.root,
|
|
255
257
|
manifestPath: f.manifest,
|
|
@@ -262,12 +264,15 @@ async function cmdRun(f: Flags, mount?: Record<string, boolean>): Promise<number
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
async function cmdDev(f: Flags): Promise<number> {
|
|
267
|
+
const { runDev } = await import("./runtime/devserver.ts");
|
|
268
|
+
const { installSignalHandlers } = await import("./runtime/run.ts");
|
|
265
269
|
const dev = await runDev({ root: f.root, manifestPath: f.manifest, port: f.port });
|
|
266
270
|
installSignalHandlers(() => dev.stop());
|
|
267
271
|
return -1; // keep the process alive; signal handlers stop the dev server
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
async function cmdDeploy(f: Flags): Promise<number> {
|
|
275
|
+
const { runFromEnv } = await import("./runtime/run.ts");
|
|
271
276
|
const app = await runFromEnv({
|
|
272
277
|
root: f.root,
|
|
273
278
|
manifestPath: f.manifest,
|
|
@@ -322,6 +327,12 @@ export async function cmdData(f: Flags, readInput: () => Promise<string> = readS
|
|
|
322
327
|
);
|
|
323
328
|
return 0;
|
|
324
329
|
}
|
|
330
|
+
// Lazily the only modules `urban data` loads (nano-ide#592): the host detector (both
|
|
331
|
+
// adapters, no toolkit) and the data-op runner (gateway + SQLite adapter; the toolkit
|
|
332
|
+
// derivers behind the `domaintypes` op load only when that op is dispatched). Imported
|
|
333
|
+
// directly rather than via the runtime barrel, which would pull in the whole runtime.
|
|
334
|
+
const { selectHost } = await import("./runtime/adapters/detect.ts");
|
|
335
|
+
const { runDataOp } = await import("./runtime/core/modules/dataops.ts");
|
|
325
336
|
const host = selectHost({ cwd: f.root });
|
|
326
337
|
try {
|
|
327
338
|
// Mirror runFromEnv: the host is anchored at f.root, so pass root "." to avoid
|
|
@@ -342,6 +353,7 @@ async function cmdNew(f: Flags): Promise<number> {
|
|
|
342
353
|
console.error("usage: urban new <name> [--root <path>] [--deno] [--style model|code] (or --code-first)");
|
|
343
354
|
return 1;
|
|
344
355
|
}
|
|
356
|
+
const { scaffold, slugify } = await import("create-urban-app");
|
|
345
357
|
const dir = f.root !== "." ? f.root : `./${slugify(name)}`;
|
|
346
358
|
const res = await scaffold({ name, dir, deno: f.deno, style: f.style });
|
|
347
359
|
console.log(`✔ scaffolded "${res.id}" in ${res.dir} (${res.files.length} files)`);
|
|
@@ -380,6 +392,7 @@ async function cmdAdd(f: Flags): Promise<number> {
|
|
|
380
392
|
}
|
|
381
393
|
|
|
382
394
|
try {
|
|
395
|
+
const { addConnector, createNodeGenIO } = await import("./toolkit/index.ts");
|
|
383
396
|
const result = await addConnector({
|
|
384
397
|
root: f.root,
|
|
385
398
|
pkg,
|
|
@@ -27,43 +27,17 @@ import { validateManifest } from "../validate.ts";
|
|
|
27
27
|
import { makeGateway, type DataSource as GatewayDataSource } from "./gateway.ts";
|
|
28
28
|
import { applyMigrations, MIGRATIONS_TABLE, openSqliteSource, resolveAppPath } from "./datasource.ts";
|
|
29
29
|
// The `domaintypes` op composes the domain reifier from the pure toolkit derivers (single source of
|
|
30
|
-
// truth for the IDE's codegen, host dry-out nano-bpm#576).
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
type SourceSchema,
|
|
42
|
-
} from "../../../toolkit/derivers/domain.ts";
|
|
43
|
-
import {
|
|
44
|
-
DOMAIN_MODEL_JSON,
|
|
45
|
-
emitDomainModelJson,
|
|
46
|
-
type FusedMetaDecl,
|
|
47
|
-
resolveShapes,
|
|
48
|
-
type ShapeDecl,
|
|
49
|
-
} from "../../../toolkit/derivers/shapes.ts";
|
|
50
|
-
import {
|
|
51
|
-
DOMAIN_DTS,
|
|
52
|
-
emitWorkerBindings,
|
|
53
|
-
emitWorkerBindingsRuntime,
|
|
54
|
-
overlayDerivedWorkerIo,
|
|
55
|
-
WORKER_BINDINGS_DTS,
|
|
56
|
-
WORKER_BINDINGS_TS,
|
|
57
|
-
type WorkerBindingDecl,
|
|
58
|
-
} from "../../../toolkit/derivers/worker-io.ts";
|
|
59
|
-
import {
|
|
60
|
-
emitMessageBindings,
|
|
61
|
-
emitMessageBindingsRuntime,
|
|
62
|
-
MESSAGE_BINDINGS_DTS,
|
|
63
|
-
MESSAGE_BINDINGS_TS,
|
|
64
|
-
type MessageBindingDecl,
|
|
65
|
-
} from "../../../toolkit/derivers/messages.ts";
|
|
66
|
-
import { emitMeta, META_TS, type MetaDecl } from "../../../toolkit/derivers/meta.ts";
|
|
30
|
+
// truth for the IDE's codegen, host dry-out nano-bpm#576). They are imported lazily inside the
|
|
31
|
+
// `domaintypes` case — a static import would make EVERY `urban data` op (the latency-sensitive
|
|
32
|
+
// gateway path, nano-ide#592) load the whole `src/toolkit/` tree before dispatching, when only
|
|
33
|
+
// `domaintypes` ever runs a deriver. The derivers are pure (no host-specific IO); their only core
|
|
34
|
+
// dependency is the gateway's `TableMeta`/`ColumnMeta` *types*, which erase. The deriver *types* the
|
|
35
|
+
// request carries are type-only imports (erased), so they cost nothing at module load.
|
|
36
|
+
import type { DomainTypeDef, DomainTypeRegistry, SourceSchema } from "../../../toolkit/derivers/domain.ts";
|
|
37
|
+
import type { FusedMetaDecl, ShapeDecl } from "../../../toolkit/derivers/shapes.ts";
|
|
38
|
+
import type { WorkerBindingDecl } from "../../../toolkit/derivers/worker-io.ts";
|
|
39
|
+
import type { MessageBindingDecl } from "../../../toolkit/derivers/messages.ts";
|
|
40
|
+
import type { MetaDecl } from "../../../toolkit/derivers/meta.ts";
|
|
67
41
|
|
|
68
42
|
/** The DB-manager op protocol's known op set — every op (including `domaintypes`) is dispatched by
|
|
69
43
|
* `runDataOp`. Typing `op` as this union (instead of `string`) gives SDK/console callers type-safety
|
|
@@ -349,6 +323,36 @@ export async function runDataOp(
|
|
|
349
323
|
// debounced keystroke has no DB side-effect and pays only for `text` + `shapeDiagnostics`.
|
|
350
324
|
const { default: def, sources } = listSources(manifest);
|
|
351
325
|
const persist = req.write !== false;
|
|
326
|
+
// The derivers are the only toolkit modules `urban data` ever needs, and only this op runs
|
|
327
|
+
// them — load them here, not at module scope, so every other op (the hot path of the data
|
|
328
|
+
// gateway, nano-ide#592) never pays for the `src/toolkit/` tree. Imported directly from the
|
|
329
|
+
// deriver files (not the `toolkit/` barrel, which pulls the Node fs IO) so core stays free
|
|
330
|
+
// of host-specific IO.
|
|
331
|
+
const { GENERATED_DIR } = await import("../../../toolkit/artifact.ts");
|
|
332
|
+
const {
|
|
333
|
+
DOMAIN_BINDINGS,
|
|
334
|
+
emitDomainBindings,
|
|
335
|
+
emitDomainModel,
|
|
336
|
+
registryFromManifest,
|
|
337
|
+
} = await import("../../../toolkit/derivers/domain.ts");
|
|
338
|
+
const { DOMAIN_MODEL_JSON, emitDomainModelJson, resolveShapes } = await import(
|
|
339
|
+
"../../../toolkit/derivers/shapes.ts"
|
|
340
|
+
);
|
|
341
|
+
const {
|
|
342
|
+
DOMAIN_DTS,
|
|
343
|
+
emitWorkerBindings,
|
|
344
|
+
emitWorkerBindingsRuntime,
|
|
345
|
+
overlayDerivedWorkerIo,
|
|
346
|
+
WORKER_BINDINGS_DTS,
|
|
347
|
+
WORKER_BINDINGS_TS,
|
|
348
|
+
} = await import("../../../toolkit/derivers/worker-io.ts");
|
|
349
|
+
const {
|
|
350
|
+
emitMessageBindings,
|
|
351
|
+
emitMessageBindingsRuntime,
|
|
352
|
+
MESSAGE_BINDINGS_DTS,
|
|
353
|
+
MESSAGE_BINDINGS_TS,
|
|
354
|
+
} = await import("../../../toolkit/derivers/messages.ts");
|
|
355
|
+
const { emitMeta, META_TS } = await import("../../../toolkit/derivers/meta.ts");
|
|
352
356
|
// Introspect each sqlite source's live schema. When persisting, migrate first (the domain
|
|
353
357
|
// model is derived from `gw.schema()`, so a regen that raced ahead of `migrate` on a fresh DB
|
|
354
358
|
// would emit an empty `Domain`); the migration runs on the shared `_urban_migrations` ledger.
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type InsertObserver,
|
|
14
14
|
} from "./gateway.ts";
|
|
15
15
|
import { currentJobContext } from "../execContext.ts";
|
|
16
|
-
import { isAbsolutePath } from "../../../toolkit/artifact.ts";
|
|
17
16
|
import { SQL_IDENT, isReservedObjectName } from "../read-model.ts";
|
|
18
17
|
|
|
19
18
|
export function sqlitePathFromUrl(url: string): string {
|
|
@@ -27,10 +26,14 @@ export function sqlitePathFromUrl(url: string): string {
|
|
|
27
26
|
* that starts with a single backslash ("\data\app.db") and a Windows UNC path
|
|
28
27
|
* ("\\\\server\\share"). A single leading backslash covers both the drive-root and UNC cases,
|
|
29
28
|
* matching Node's `path.win32.isAbsolute`. Used by `resolveAppPath` so a caller-supplied absolute
|
|
30
|
-
* path is never incorrectly prefixed with the app root. Defined
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
29
|
+
* path is never incorrectly prefixed with the app root. Defined here in the runtime (not imported
|
|
30
|
+
* from `toolkit/artifact.ts`) so the `urban data` gateway's static module graph never touches the
|
|
31
|
+
* `src/toolkit/` tree (nano-ide#592); `toolkit/artifact.ts` re-exports this definition so the
|
|
32
|
+
* toolkit's gen path-join and this runtime resolver apply the identical rule (no gen/runtime
|
|
33
|
+
* drift). */
|
|
34
|
+
export function isAbsolutePath(p: string): boolean {
|
|
35
|
+
return /^(\/|\\|[A-Za-z]:[/\\])/.test(p);
|
|
36
|
+
}
|
|
34
37
|
|
|
35
38
|
/** Resolve `p` against the app `root`: an absolute `p` (see `isAbsolutePath`) is returned as-is;
|
|
36
39
|
* a relative `p` is joined onto `root`. Trims a trailing separator of either kind off `root` so
|
package/src/toolkit/artifact.ts
CHANGED
|
@@ -48,9 +48,9 @@ export function sortArtifacts(a: DerivedArtifact[]): DerivedArtifact[] {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** True when `p` is an absolute path — POSIX root (`/`), a drive-letter root (`C:\` or `C:/`), or a
|
|
51
|
-
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
51
|
+
* Windows UNC/drive-root backslash (`\`). The single source of truth for the absolute-path rule is
|
|
52
|
+
* the runtime's `resolveAppPath` side (`runtime/core/modules/datasource.ts`); it is re-exported
|
|
53
|
+
* here so the toolkit's path join (gen.ts) and the runtime resolver agree on whether a manifest
|
|
54
|
+
* path is app-root-relative or absolute (no gen/runtime drift) without the runtime's data gateway
|
|
55
|
+
* statically importing the toolkit tree (nano-ide#592). */
|
|
56
|
+
export { isAbsolutePath } from "../runtime/core/modules/datasource.ts";
|
package/src/toolkit/gen.ts
CHANGED
|
@@ -76,7 +76,8 @@ function join(root: string, rel: string): string {
|
|
|
76
76
|
// pass either style without gen/runtime drift over where a file resolves.
|
|
77
77
|
const norm = (s: string): string => s.replace(/\\/g, "/");
|
|
78
78
|
// An absolute `rel` resolves to itself — never prefixed with `root` — mirroring the runtime's
|
|
79
|
-
// resolveAppPath (isAbsolutePath is the shared SoT in
|
|
79
|
+
// resolveAppPath (isAbsolutePath is the shared SoT, defined in runtime's datasource.ts and
|
|
80
|
+
// re-exported from artifact.ts). Without this an absolute
|
|
80
81
|
// manifest path (e.g. "/abs/openapi.json") would be stripped to root-relative and gen would
|
|
81
82
|
// read/derive a different file than the runtime resolves. Trailing edge separators are still
|
|
82
83
|
// trimmed for stable, comparable artifact keys.
|