space-data-module-sdk 0.8.11 → 0.8.13
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/README.md +92 -0
- package/bin/space-data-module.js +85 -1
- package/docs/module-publication-standard.md +7 -3
- package/docs/propagator-abi.md +477 -0
- package/include/orbpro/orbpro_propagator_abi.h +312 -0
- package/package.json +7 -1
- package/schemas/PluginManifest.fbs +46 -1
- package/schemas/orbpro/Propagator.fbs +161 -3
- package/src/browser.js +11 -0
- package/src/bundle/index.js +1 -0
- package/src/bundle/sigdomain.js +22 -0
- package/src/capabilities.js +91 -0
- package/src/compliance/index.js +8 -0
- package/src/compliance/pluginCompliance.js +76 -32
- package/src/flow/flowCompiler.js +231 -3
- package/src/flow/flowRuntimeHost.js +26 -0
- package/src/flow/isomorphicFlowHost.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.d.ts +9 -1
- package/src/generated/orbpro/manifest/plugin-family.js +8 -0
- package/src/generated/orbpro/manifest/plugin-family.ts +8 -0
- package/src/generated/orbpro/propagator-abi.js +118 -0
- package/src/generated/orbpro/propagator-abi.ts +199 -0
- package/src/host/browserModuleHarness.js +26 -0
- package/src/host/isomorphicLoader.js +57 -11
- package/src/host/runtimeTargetGate.js +256 -0
- package/src/host/workerModuleHarness.js +7 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +11 -0
- package/src/manifest/normalize.js +113 -4
- package/src/scaffold/copyTemplate.js +71 -0
- package/src/scaffold/index.js +150 -0
- package/src/scaffold/tokens.js +90 -0
- package/src/testing/parityBrowserRunner.js +11 -0
- package/src/testing/parityGate.js +287 -27
- package/templates/propagator-module/README.md +99 -0
- package/templates/propagator-module/build.js +103 -0
- package/templates/propagator-module/package.json +19 -0
- package/templates/propagator-module/plugin-manifest.json +66 -0
- package/templates/propagator-module/src/__MODULE_NAME_SNAKE__.cpp +450 -0
- package/templates/propagator-module/tests/module.build.test.mjs +103 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `space-data-module init` — scaffold a new SDN WASM module skeleton from a
|
|
3
|
+
* family template under `templates/<family>-module/`.
|
|
4
|
+
*
|
|
5
|
+
* Family resolution deliberately mirrors `normalizePluginFamily`
|
|
6
|
+
* (`src/manifest/normalize.js`): an unrecognized value is a THROW, never a
|
|
7
|
+
* silent fallback to a generic template. Two checks apply, in order:
|
|
8
|
+
*
|
|
9
|
+
* 1. Is `--family` even a name in the SDK's plugin-family vocabulary
|
|
10
|
+
* (the same vocabulary `pluginFamily` in a manifest is validated
|
|
11
|
+
* against)? If not, `normalizePluginFamily` itself throws
|
|
12
|
+
* `UnknownPluginFamilyError`, naming the offender and the whole
|
|
13
|
+
* vocabulary.
|
|
14
|
+
* 2. Does that family have an init TEMPLATE (`templates/<family>-module/
|
|
15
|
+
* plugin-manifest.json`)? Most families do not — only `propagator`
|
|
16
|
+
* ships one today. If not, `ScaffoldFamilyTemplateError` names the
|
|
17
|
+
* offender and lists the families that DO have a template.
|
|
18
|
+
*
|
|
19
|
+
* Both refusals name the value and the valid vocabulary; neither ever
|
|
20
|
+
* degrades into "pick the closest template" or "use a blank template".
|
|
21
|
+
*/
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
import { normalizePluginFamily } from "../manifest/normalize.js";
|
|
27
|
+
import { copyTemplateTree, ensureWritableOutputDir } from "./copyTemplate.js";
|
|
28
|
+
import { buildTokenMap } from "./tokens.js";
|
|
29
|
+
|
|
30
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
export const packageRoot = path.resolve(moduleDir, "..", "..");
|
|
32
|
+
export const templatesRoot = path.join(packageRoot, "templates");
|
|
33
|
+
|
|
34
|
+
const TEMPLATE_DIR_SUFFIX = "-module";
|
|
35
|
+
/** The file every real init-scaffold template must have at its root. This is
|
|
36
|
+
* what distinguishes a scaffold template (propagator-module) from the
|
|
37
|
+
* pre-existing ABI-header-only reference dirs under templates/
|
|
38
|
+
* (gpu-module, provider-access-module) that ship no plugin-manifest.json and
|
|
39
|
+
* are not meant to be scaffolded from. */
|
|
40
|
+
const TEMPLATE_MANIFEST_FILE = "plugin-manifest.json";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when `--family` names a value that does not have an init template,
|
|
44
|
+
* even if it IS a recognized `pluginFamily` string. Distinct from
|
|
45
|
+
* `UnknownPluginFamilyError` (which fires first, for values that are not a
|
|
46
|
+
* recognized family AT ALL) — this one fires for real-but-template-less
|
|
47
|
+
* families such as "sensor" today.
|
|
48
|
+
*/
|
|
49
|
+
export class ScaffoldFamilyTemplateError extends Error {
|
|
50
|
+
constructor(value, availableFamilies) {
|
|
51
|
+
const list =
|
|
52
|
+
availableFamilies.length > 0
|
|
53
|
+
? availableFamilies.join(", ")
|
|
54
|
+
: "(none — no templates/*-module directory has a plugin-manifest.json yet)";
|
|
55
|
+
super(
|
|
56
|
+
`No init template for family ${JSON.stringify(value)}. ` +
|
|
57
|
+
`Families with an init template: ${list}.`,
|
|
58
|
+
);
|
|
59
|
+
this.name = "ScaffoldFamilyTemplateError";
|
|
60
|
+
this.code = "unknown-scaffold-family-template";
|
|
61
|
+
this.value = value;
|
|
62
|
+
this.availableFamilies = availableFamilies;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** List family names (e.g. `["propagator"]`) that have a real init template. */
|
|
67
|
+
export function listScaffoldFamilies() {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(templatesRoot, { withFileTypes: true });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error && error.code === "ENOENT") {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
const families = [];
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory() || !entry.name.endsWith(TEMPLATE_DIR_SUFFIX)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const manifestPath = path.join(
|
|
83
|
+
templatesRoot,
|
|
84
|
+
entry.name,
|
|
85
|
+
TEMPLATE_MANIFEST_FILE,
|
|
86
|
+
);
|
|
87
|
+
if (fs.existsSync(manifestPath)) {
|
|
88
|
+
families.push(entry.name.slice(0, -TEMPLATE_DIR_SUFFIX.length));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return families.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `--family` to a template directory, or throw loudly. See the
|
|
96
|
+
* module docstring for the two-stage refusal shape.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveScaffoldTemplateDir(family) {
|
|
99
|
+
// Stage 1: is this even a real plugin family? Throws UnknownPluginFamilyError
|
|
100
|
+
// (naming the value + the FULL manifest vocabulary) if not.
|
|
101
|
+
normalizePluginFamily(family);
|
|
102
|
+
|
|
103
|
+
const normalized = String(family).trim().toLowerCase();
|
|
104
|
+
const available = listScaffoldFamilies();
|
|
105
|
+
if (!available.includes(normalized)) {
|
|
106
|
+
// Stage 2: real family, but no init template ships for it (yet).
|
|
107
|
+
throw new ScaffoldFamilyTemplateError(family, available);
|
|
108
|
+
}
|
|
109
|
+
return path.join(templatesRoot, `${normalized}${TEMPLATE_DIR_SUFFIX}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scaffold a new module skeleton.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} options
|
|
116
|
+
* @param {string} options.family - e.g. "propagator". Must have an init
|
|
117
|
+
* template (see {@link resolveScaffoldTemplateDir}).
|
|
118
|
+
* @param {string} options.name - kebab-case module name, e.g. "my-propagator".
|
|
119
|
+
* @param {string} [options.outDir] - defaults to `./<name>` under cwd.
|
|
120
|
+
* @param {string} [options.pluginId] - defaults to
|
|
121
|
+
* `com.orbpro.<name-with-dots>`.
|
|
122
|
+
* @param {boolean} [options.force] - allow scaffolding into a non-empty
|
|
123
|
+
* `outDir`.
|
|
124
|
+
* @returns {Promise<{ok: true, family: string, name: string, pluginId: string, outDir: string, files: string[]}>}
|
|
125
|
+
*/
|
|
126
|
+
export async function scaffoldModule({
|
|
127
|
+
family,
|
|
128
|
+
name,
|
|
129
|
+
outDir,
|
|
130
|
+
pluginId,
|
|
131
|
+
force = false,
|
|
132
|
+
} = {}) {
|
|
133
|
+
const templateDir = resolveScaffoldTemplateDir(family);
|
|
134
|
+
const tokens = buildTokenMap({ name, pluginId });
|
|
135
|
+
const resolvedOutDir = path.resolve(outDir ?? path.join(process.cwd(), name));
|
|
136
|
+
|
|
137
|
+
await ensureWritableOutputDir(resolvedOutDir, force === true);
|
|
138
|
+
const files = await copyTemplateTree(templateDir, resolvedOutDir, tokens);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
family: String(family).trim().toLowerCase(),
|
|
143
|
+
name,
|
|
144
|
+
pluginId: tokens.PLUGIN_ID,
|
|
145
|
+
outDir: resolvedOutDir,
|
|
146
|
+
files,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export { defaultPluginId } from "./tokens.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token vocabulary for `space-data-module init`.
|
|
3
|
+
*
|
|
4
|
+
* These are the ONLY tokens the scaffold engine substitutes. They are matched
|
|
5
|
+
* as whole `__NAME__` runs so a token can never partially match inside a
|
|
6
|
+
* longer identifier that happens to share a prefix.
|
|
7
|
+
*/
|
|
8
|
+
export const SCAFFOLD_TOKEN_NAMES = Object.freeze([
|
|
9
|
+
"MODULE_NAME",
|
|
10
|
+
"PLUGIN_ID",
|
|
11
|
+
"MODULE_NAME_SNAKE",
|
|
12
|
+
"MODULE_NAME_CAMEL",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const TOKEN_PATTERN = new RegExp(
|
|
16
|
+
`__(${SCAFFOLD_TOKEN_NAMES.join("|")})__`,
|
|
17
|
+
"g",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const MODULE_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Validate a `--name` value against the kebab-case shape every derived token
|
|
24
|
+
* assumes. Refuses loudly rather than silently mangling an unexpected name
|
|
25
|
+
* into something that merely looks plausible.
|
|
26
|
+
*/
|
|
27
|
+
export function assertValidModuleName(name) {
|
|
28
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
29
|
+
throw new Error("--name is required and must be a non-empty string.");
|
|
30
|
+
}
|
|
31
|
+
if (!MODULE_NAME_PATTERN.test(name)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`--name ${JSON.stringify(name)} is not valid. Module names must be ` +
|
|
34
|
+
`lowercase kebab-case: start with a letter, then letters, digits, or ` +
|
|
35
|
+
`single hyphens (e.g. "keplerian-reference").`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `foo-bar` -> `foo_bar` */
|
|
41
|
+
export function toSnakeCase(name) {
|
|
42
|
+
return name.replace(/-/g, "_");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `foo-bar` -> `fooBar` */
|
|
46
|
+
export function toCamelCase(name) {
|
|
47
|
+
return name
|
|
48
|
+
.split(/[-_]+/)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((part, index) =>
|
|
51
|
+
index === 0
|
|
52
|
+
? part.toLowerCase()
|
|
53
|
+
: part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(),
|
|
54
|
+
)
|
|
55
|
+
.join("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `com.orbpro.<module-name-with-dots>` — the documented default `--plugin-id`
|
|
60
|
+
* when the author does not supply one. Hyphens become dots so the plugin id
|
|
61
|
+
* reads as reverse-DNS-style segments, matching how the SDK's own examples
|
|
62
|
+
* namespace multi-word module ids.
|
|
63
|
+
*/
|
|
64
|
+
export function defaultPluginId(name) {
|
|
65
|
+
return `com.orbpro.${name.replace(/-/g, ".")}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build the full token -> replacement-string map for one scaffold run.
|
|
70
|
+
*/
|
|
71
|
+
export function buildTokenMap({ name, pluginId }) {
|
|
72
|
+
assertValidModuleName(name);
|
|
73
|
+
const resolvedPluginId =
|
|
74
|
+
typeof pluginId === "string" && pluginId.trim().length > 0
|
|
75
|
+
? pluginId.trim()
|
|
76
|
+
: defaultPluginId(name);
|
|
77
|
+
return {
|
|
78
|
+
MODULE_NAME: name,
|
|
79
|
+
PLUGIN_ID: resolvedPluginId,
|
|
80
|
+
MODULE_NAME_SNAKE: toSnakeCase(name),
|
|
81
|
+
MODULE_NAME_CAMEL: toCamelCase(name),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Substitute every `__TOKEN__` occurrence in `text` using `tokens`. */
|
|
86
|
+
export function substituteTokens(text, tokens) {
|
|
87
|
+
return text.replace(TOKEN_PATTERN, (match, key) =>
|
|
88
|
+
key in tokens ? tokens[key] : match,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -17,6 +17,7 @@ import { createBrowserModuleHarness } from "../host/browserModuleHarness.js";
|
|
|
17
17
|
const OK = "ok";
|
|
18
18
|
const GUEST_ERROR = "guest-error";
|
|
19
19
|
const TRAP = "trap";
|
|
20
|
+
const OUT_OF_SCOPE = "out-of-declared-scope";
|
|
20
21
|
|
|
21
22
|
function base64ToBytes(value) {
|
|
22
23
|
const binary = atob(String(value ?? ""));
|
|
@@ -42,6 +43,16 @@ function classifyBrowserError(error) {
|
|
|
42
43
|
if (error?.name === "WasiExitError") {
|
|
43
44
|
return { exitClass: GUEST_ERROR, exitDetail: `exit=${error.code}` };
|
|
44
45
|
}
|
|
46
|
+
// The harness refused the artifact because the ARTIFACT declares it does not
|
|
47
|
+
// run here. That is the contract working. Classing it TRAP would score a
|
|
48
|
+
// correct refusal as a P1 cross-runtime divergence — the gate failing the
|
|
49
|
+
// very artifacts the compiler now legitimately emits.
|
|
50
|
+
if (error?.name === "RuntimeTargetError") {
|
|
51
|
+
return {
|
|
52
|
+
exitClass: OUT_OF_SCOPE,
|
|
53
|
+
exitDetail: `declared runtimeTargets [${(error.declaredTargets ?? []).join(", ")}]`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
45
56
|
return {
|
|
46
57
|
exitClass: TRAP,
|
|
47
58
|
exitDetail: `${error?.name ?? "Error"}: ${error?.message ?? String(error)}`,
|