@nanobpm/urban 0.31.0 → 0.31.1
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/runtime/adapters/deno.js +4 -1
- package/dist/runtime/adapters/node.js +3 -1
- package/dist/runtime/core/module-path.d.ts +16 -0
- package/dist/runtime/core/module-path.js +43 -0
- package/package.json +1 -1
- package/src/runtime/adapters/deno.ts +4 -1
- package/src/runtime/adapters/node.ts +3 -1
- package/src/runtime/core/module-path.test.ts +57 -0
- package/src/runtime/core/module-path.ts +44 -0
|
@@ -12,8 +12,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
12
12
|
};
|
|
13
13
|
import { isAbsolute, resolve } from "node:path";
|
|
14
14
|
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
15
16
|
import { DatabaseSync } from "node:sqlite";
|
|
16
17
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
|
+
import { resolveModulePath } from "../core/module-path.js";
|
|
17
19
|
function sqliteParams(params) {
|
|
18
20
|
return params.map((param) => {
|
|
19
21
|
if (param === null ||
|
|
@@ -67,7 +69,8 @@ export function createDenoHost(opts = {}) {
|
|
|
67
69
|
return wrapSqlite(db);
|
|
68
70
|
},
|
|
69
71
|
importModule: (p) => {
|
|
70
|
-
const
|
|
72
|
+
const resolved = resolveModulePath(abs(p), existsSync);
|
|
73
|
+
const href = pathToFileURL(resolved).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
|
|
71
74
|
const mod = import(__rewriteRelativeImportExtension(href));
|
|
72
75
|
return mod;
|
|
73
76
|
},
|
|
@@ -17,6 +17,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
18
|
import { DatabaseSync } from "node:sqlite";
|
|
19
19
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
|
+
import { resolveModulePath } from "../core/module-path.js";
|
|
20
21
|
function sqliteParams(params) {
|
|
21
22
|
return params.map((param) => {
|
|
22
23
|
if (param === null ||
|
|
@@ -115,7 +116,8 @@ export function createNodeHost(opts = {}) {
|
|
|
115
116
|
return wrapNodeSqlite(db);
|
|
116
117
|
},
|
|
117
118
|
importModule: (p) => {
|
|
118
|
-
const
|
|
119
|
+
const resolved = resolveModulePath(abs(p), existsSync);
|
|
120
|
+
const href = pathToFileURL(resolved).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
|
|
119
121
|
const mod = import(__rewriteRelativeImportExtension(href));
|
|
120
122
|
return mod;
|
|
121
123
|
},
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidate extensions tried, in order, for an extensionless module path. Compiled outputs come
|
|
3
|
+
* first (`.js`/`.mjs`/`.cjs`, matching the connector-SDK precedent) so a mixed source+build tree
|
|
4
|
+
* resolves to the importable JS rather than a TS file that fails on a runtime without
|
|
5
|
+
* type-stripping; a from-source tree falls through to the TS variants (`.ts`/`.mts`/`.cts`). The
|
|
6
|
+
* source group mirrors the extensions {@link hasModuleExtension} treats as already-extensioned.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODULE_EXTENSION_CANDIDATES: readonly [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
9
|
+
/**
|
|
10
|
+
* If `path` already has a JS/TS extension, return it unchanged. Otherwise return the first
|
|
11
|
+
* `${path}${ext}` (ext ∈ {@link MODULE_EXTENSION_CANDIDATES}) that `exists`, so an extensionless
|
|
12
|
+
* delegate path resolves to its real source/compiled file. When none exists, return `path`
|
|
13
|
+
* unchanged so the underlying loader surfaces its own (accurate) "module not found" error rather
|
|
14
|
+
* than a guessed extension masking it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveModulePath(path: string, exists: (candidate: string) => boolean): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Resolve a module path that may have been authored WITHOUT a file extension.
|
|
2
|
+
//
|
|
3
|
+
// The OpenAPI delegate surface (ADR 0058) builds a delegate import path from `<dir>/<operationId>`
|
|
4
|
+
// — and an `operationId` cannot carry a file extension (it must be a single safe path segment, and
|
|
5
|
+
// it doubles as a TS type stem). Node's ESM resolver (including `--experimental-strip-types`) and
|
|
6
|
+
// Deno both require an explicit extension for a file: URL, so a bare `import(".../operations/getX")`
|
|
7
|
+
// fails with a "cannot find module" error even though `.../operations/getX.ts` exists. `actions[]`
|
|
8
|
+
// don't hit this because the manifest carries the extension (`"module": "actions/x.ts"`).
|
|
9
|
+
//
|
|
10
|
+
// This mirrors the connector-SDK shim resolution (`connectorShimUrl` in adapters/node.ts): probe
|
|
11
|
+
// the on-disk candidates, PREFERRING a compiled `.js` (the published / `urban run` path, importable
|
|
12
|
+
// on plain Node without TypeScript type-stripping) over the from-source `.ts` (node strip-types /
|
|
13
|
+
// Deno), so both resolve the same author-written `<dir>/<operationId>` path. Preferring compiled
|
|
14
|
+
// first matters in a mixed source+build tree: if both exist and the process has no type-stripping,
|
|
15
|
+
// selecting `.ts` would fail even though `.js` was importable.
|
|
16
|
+
/**
|
|
17
|
+
* Candidate extensions tried, in order, for an extensionless module path. Compiled outputs come
|
|
18
|
+
* first (`.js`/`.mjs`/`.cjs`, matching the connector-SDK precedent) so a mixed source+build tree
|
|
19
|
+
* resolves to the importable JS rather than a TS file that fails on a runtime without
|
|
20
|
+
* type-stripping; a from-source tree falls through to the TS variants (`.ts`/`.mts`/`.cts`). The
|
|
21
|
+
* source group mirrors the extensions {@link hasModuleExtension} treats as already-extensioned.
|
|
22
|
+
*/
|
|
23
|
+
export const MODULE_EXTENSION_CANDIDATES = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
24
|
+
/** True when the path already ends in a JS/TS module extension (.ts/.js/.mts/.cts/.mjs/.cjs). */
|
|
25
|
+
function hasModuleExtension(path) {
|
|
26
|
+
return /\.[cm]?[jt]s$/i.test(path);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* If `path` already has a JS/TS extension, return it unchanged. Otherwise return the first
|
|
30
|
+
* `${path}${ext}` (ext ∈ {@link MODULE_EXTENSION_CANDIDATES}) that `exists`, so an extensionless
|
|
31
|
+
* delegate path resolves to its real source/compiled file. When none exists, return `path`
|
|
32
|
+
* unchanged so the underlying loader surfaces its own (accurate) "module not found" error rather
|
|
33
|
+
* than a guessed extension masking it.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveModulePath(path, exists) {
|
|
36
|
+
if (hasModuleExtension(path))
|
|
37
|
+
return path;
|
|
38
|
+
for (const ext of MODULE_EXTENSION_CANDIDATES) {
|
|
39
|
+
if (exists(path + ext))
|
|
40
|
+
return path + ext;
|
|
41
|
+
}
|
|
42
|
+
return path;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/urban",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.1",
|
|
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",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { isAbsolute, resolve } from "node:path";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
8
9
|
import { DatabaseSync } from "node:sqlite";
|
|
9
10
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
11
|
import type {
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
HttpServer,
|
|
16
17
|
SqliteDb,
|
|
17
18
|
} from "../core/host.ts";
|
|
19
|
+
import { resolveModulePath } from "../core/module-path.ts";
|
|
18
20
|
|
|
19
21
|
type SqliteParam = string | number | bigint | Uint8Array | null;
|
|
20
22
|
|
|
@@ -102,8 +104,9 @@ export function createDenoHost(opts: DenoHostOptions = {}): HostContext {
|
|
|
102
104
|
return wrapSqlite(db);
|
|
103
105
|
},
|
|
104
106
|
importModule: (p) => {
|
|
107
|
+
const resolved = resolveModulePath(abs(p), existsSync);
|
|
105
108
|
const href =
|
|
106
|
-
pathToFileURL(
|
|
109
|
+
pathToFileURL(resolved).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
|
|
107
110
|
const mod: Promise<Record<string, unknown>> = import(href);
|
|
108
111
|
return mod;
|
|
109
112
|
},
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
SqliteDb,
|
|
20
20
|
WatchHandle,
|
|
21
21
|
} from "../core/host.ts";
|
|
22
|
+
import { resolveModulePath } from "../core/module-path.ts";
|
|
22
23
|
|
|
23
24
|
type SqliteParam = string | number | bigint | Uint8Array | null;
|
|
24
25
|
|
|
@@ -138,8 +139,9 @@ export function createNodeHost(opts: NodeHostOptions = {}): HostContext {
|
|
|
138
139
|
return wrapNodeSqlite(db);
|
|
139
140
|
},
|
|
140
141
|
importModule: (p) => {
|
|
142
|
+
const resolved = resolveModulePath(abs(p), existsSync);
|
|
141
143
|
const href =
|
|
142
|
-
pathToFileURL(
|
|
144
|
+
pathToFileURL(resolved).href + (opts.importNonce ? `?v=${opts.importNonce}` : "");
|
|
143
145
|
const mod: Promise<Record<string, unknown>> = import(href);
|
|
144
146
|
return mod;
|
|
145
147
|
},
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { MODULE_EXTENSION_CANDIDATES, resolveModulePath } from "./module-path.ts";
|
|
8
|
+
import { createNodeHost } from "../adapters/node.ts";
|
|
9
|
+
|
|
10
|
+
test("resolveModulePath leaves an already-extensioned path untouched", () => {
|
|
11
|
+
const never = () => {
|
|
12
|
+
throw new Error("exists() must not be consulted for an extensioned path");
|
|
13
|
+
};
|
|
14
|
+
assert.equal(resolveModulePath("/a/b/x.ts", never), "/a/b/x.ts");
|
|
15
|
+
assert.equal(resolveModulePath("/a/b/x.js", never), "/a/b/x.js");
|
|
16
|
+
assert.equal(resolveModulePath("/a/b/x.mjs", never), "/a/b/x.mjs");
|
|
17
|
+
assert.equal(resolveModulePath("/a/b/x.cjs", never), "/a/b/x.cjs");
|
|
18
|
+
assert.equal(resolveModulePath("/a/b/x.mts", never), "/a/b/x.mts");
|
|
19
|
+
assert.equal(resolveModulePath("/a/b/x.cts", never), "/a/b/x.cts");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("resolveModulePath appends the first candidate extension that exists", () => {
|
|
23
|
+
const present = new Set(["/ops/getX.js"]);
|
|
24
|
+
const resolved = resolveModulePath("/ops/getX", (c) => present.has(c));
|
|
25
|
+
assert.equal(resolved, "/ops/getX.js");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("resolveModulePath prefers a compiled .js over .ts when both exist", () => {
|
|
29
|
+
// Matches the connector-SDK precedent (connectorShimUrl): a mixed source+build tree must
|
|
30
|
+
// resolve to the importable .js so it works on plain Node without TypeScript type-stripping.
|
|
31
|
+
const present = new Set(["/ops/getX.ts", "/ops/getX.js"]);
|
|
32
|
+
const resolved = resolveModulePath("/ops/getX", (c) => present.has(c));
|
|
33
|
+
assert.equal(resolved, "/ops/getX.js");
|
|
34
|
+
assert.equal(MODULE_EXTENSION_CANDIDATES[0], ".js");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveModulePath returns the path unchanged when nothing exists", () => {
|
|
38
|
+
const resolved = resolveModulePath("/ops/missing", () => false);
|
|
39
|
+
assert.equal(resolved, "/ops/missing");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("createNodeHost.importModule loads an extensionless .ts delegate from disk", async () => {
|
|
43
|
+
const dir = mkdtempSync(join(tmpdir(), "urban-import-"));
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(
|
|
46
|
+
join(dir, "getVersion.ts"),
|
|
47
|
+
"export const marker = 'from-ts';\nexport default { marker };\n",
|
|
48
|
+
);
|
|
49
|
+
const host = createNodeHost({ cwd: dir });
|
|
50
|
+
// Path carries NO extension — mirrors how the OpenAPI delegate surface builds
|
|
51
|
+
// `<dir>/<operationId>`. Without extension probing this throws ERR_MODULE_NOT_FOUND.
|
|
52
|
+
const mod = await host.importModule(join(dir, "getVersion"));
|
|
53
|
+
assert.equal(mod.marker, "from-ts");
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Resolve a module path that may have been authored WITHOUT a file extension.
|
|
2
|
+
//
|
|
3
|
+
// The OpenAPI delegate surface (ADR 0058) builds a delegate import path from `<dir>/<operationId>`
|
|
4
|
+
// — and an `operationId` cannot carry a file extension (it must be a single safe path segment, and
|
|
5
|
+
// it doubles as a TS type stem). Node's ESM resolver (including `--experimental-strip-types`) and
|
|
6
|
+
// Deno both require an explicit extension for a file: URL, so a bare `import(".../operations/getX")`
|
|
7
|
+
// fails with a "cannot find module" error even though `.../operations/getX.ts` exists. `actions[]`
|
|
8
|
+
// don't hit this because the manifest carries the extension (`"module": "actions/x.ts"`).
|
|
9
|
+
//
|
|
10
|
+
// This mirrors the connector-SDK shim resolution (`connectorShimUrl` in adapters/node.ts): probe
|
|
11
|
+
// the on-disk candidates, PREFERRING a compiled `.js` (the published / `urban run` path, importable
|
|
12
|
+
// on plain Node without TypeScript type-stripping) over the from-source `.ts` (node strip-types /
|
|
13
|
+
// Deno), so both resolve the same author-written `<dir>/<operationId>` path. Preferring compiled
|
|
14
|
+
// first matters in a mixed source+build tree: if both exist and the process has no type-stripping,
|
|
15
|
+
// selecting `.ts` would fail even though `.js` was importable.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Candidate extensions tried, in order, for an extensionless module path. Compiled outputs come
|
|
19
|
+
* first (`.js`/`.mjs`/`.cjs`, matching the connector-SDK precedent) so a mixed source+build tree
|
|
20
|
+
* resolves to the importable JS rather than a TS file that fails on a runtime without
|
|
21
|
+
* type-stripping; a from-source tree falls through to the TS variants (`.ts`/`.mts`/`.cts`). The
|
|
22
|
+
* source group mirrors the extensions {@link hasModuleExtension} treats as already-extensioned.
|
|
23
|
+
*/
|
|
24
|
+
export const MODULE_EXTENSION_CANDIDATES = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
|
|
25
|
+
|
|
26
|
+
/** True when the path already ends in a JS/TS module extension (.ts/.js/.mts/.cts/.mjs/.cjs). */
|
|
27
|
+
function hasModuleExtension(path: string): boolean {
|
|
28
|
+
return /\.[cm]?[jt]s$/i.test(path);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* If `path` already has a JS/TS extension, return it unchanged. Otherwise return the first
|
|
33
|
+
* `${path}${ext}` (ext ∈ {@link MODULE_EXTENSION_CANDIDATES}) that `exists`, so an extensionless
|
|
34
|
+
* delegate path resolves to its real source/compiled file. When none exists, return `path`
|
|
35
|
+
* unchanged so the underlying loader surfaces its own (accurate) "module not found" error rather
|
|
36
|
+
* than a guessed extension masking it.
|
|
37
|
+
*/
|
|
38
|
+
export function resolveModulePath(path: string, exists: (candidate: string) => boolean): string {
|
|
39
|
+
if (hasModuleExtension(path)) return path;
|
|
40
|
+
for (const ext of MODULE_EXTENSION_CANDIDATES) {
|
|
41
|
+
if (exists(path + ext)) return path + ext;
|
|
42
|
+
}
|
|
43
|
+
return path;
|
|
44
|
+
}
|