@pulse-compute/cli 0.0.0 → 1.0.0-beta.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/API.md +539 -0
- package/CHANGELOG.md +47 -0
- package/README.md +70 -1
- package/bin/pulse.js +15 -0
- package/bin/pulsewasm-extract.js +22 -0
- package/cli-spec.json +575 -0
- package/completions/_pulse +106 -0
- package/completions/pulse.bash +31 -0
- package/completions/pulse.fish +76 -0
- package/docs/README.md +77 -0
- package/docs/architecture/current-contracts.md +381 -0
- package/docs/architecture/overview.md +93 -0
- package/docs/architecture/vision.md +515 -0
- package/docs/concepts/bodies.md +186 -0
- package/docs/concepts/compilation-and-lowering.md +496 -0
- package/docs/concepts/contracts-and-providers.md +158 -0
- package/docs/concepts/effects-and-continuations.md +243 -0
- package/docs/concepts/entities-and-adapters.md +71 -0
- package/docs/concepts/package-owned-lowering.md +175 -0
- package/docs/concepts/targets-and-hosts.md +62 -0
- package/docs/contributing/README.md +38 -0
- package/docs/contributing/adding-core-provider.md +315 -0
- package/docs/contributing/adding-first-party-lowerer.md +327 -0
- package/docs/contributing/entities-lowering.md +91 -0
- package/docs/contributing/package-lowerer-contract.md +383 -0
- package/docs/contributing/pulse-aware-packages.md +149 -0
- package/docs/examples.md +112 -0
- package/docs/fixtures/inspect-fetch-composition.selected.json +181 -0
- package/docs/getting-started.md +128 -0
- package/docs/guides/compatibility-imports.md +60 -0
- package/docs/guides/deploying-fastly.md +188 -0
- package/docs/guides/deploying-node.md +143 -0
- package/docs/guides/events.md +233 -0
- package/docs/guides/fastly-capabilities.md +153 -0
- package/docs/guides/fetching-and-composition.md +101 -0
- package/docs/guides/grip.md +94 -0
- package/docs/guides/json-schemas.md +233 -0
- package/docs/guides/migrating-from-express.md +192 -0
- package/docs/guides/project-lifecycle.md +143 -0
- package/docs/guides/routing.md +184 -0
- package/docs/guides/troubleshooting.md +47 -0
- package/docs/maintainers/README.md +60 -0
- package/docs/maintainers/codex-maintainer.md +71 -0
- package/docs/maintainers/documentation-deployment.md +147 -0
- package/docs/maintainers/documentation-system.md +58 -0
- package/docs/maintainers/documentation-versioning.md +65 -0
- package/docs/maintainers/maintainer-charter.md +84 -0
- package/docs/maintainers/maintenance-policy.json +778 -0
- package/docs/maintainers/maintenance-policy.md +124 -0
- package/docs/maintainers/npm-publishing.md +154 -0
- package/docs/maintainers/plugin-readiness.json +19 -0
- package/docs/maintainers/plugin-readiness.md +31 -0
- package/docs/maintainers/public-site.md +158 -0
- package/docs/maintainers/release-acceptance.md +250 -0
- package/docs/maintainers/release-manifest.md +88 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +93 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +288 -0
- package/docs/packages/README.md +59 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +71 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +113 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +217 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +256 -0
- package/docs/preview-scope.md +142 -0
- package/docs/reference/README.md +33 -0
- package/docs/reference/cli-spec.json +575 -0
- package/docs/reference/cli.md +457 -0
- package/docs/reference/compatibility-matrix.md +169 -0
- package/docs/reference/diagnostics.md +1872 -0
- package/docs/reference/documentation-versions.json +14 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +554 -0
- package/docs/reference/project-config.schema.json +918 -0
- package/docs/reference/release-manifest.json +427 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +661 -0
- package/documentation-versions.json +14 -0
- package/examples/01-hello-json/.pulse/.gitignore +3 -0
- package/examples/01-hello-json/.pulse/config.ts +16 -0
- package/examples/01-hello-json/README.md +119 -0
- package/examples/01-hello-json/package.json +20 -0
- package/examples/01-hello-json/src/index.ts +9 -0
- package/examples/01-hello-json/tests/pulse.harness.ts +22 -0
- package/examples/01-hello-json/tsconfig.json +14 -0
- package/examples/02-request-schema/.pulse/.gitignore +3 -0
- package/examples/02-request-schema/.pulse/config.ts +17 -0
- package/examples/02-request-schema/README.md +145 -0
- package/examples/02-request-schema/package.json +20 -0
- package/examples/02-request-schema/src/index.ts +18 -0
- package/examples/02-request-schema/src/schemas.ts +20 -0
- package/examples/02-request-schema/tests/pulse.harness.ts +37 -0
- package/examples/02-request-schema/tsconfig.json +14 -0
- package/examples/03-fetch-composition/.pulse/.gitignore +3 -0
- package/examples/03-fetch-composition/.pulse/config.ts +31 -0
- package/examples/03-fetch-composition/README.md +210 -0
- package/examples/03-fetch-composition/package.json +20 -0
- package/examples/03-fetch-composition/src/index.ts +60 -0
- package/examples/03-fetch-composition/tests/pulse.harness.ts +47 -0
- package/examples/03-fetch-composition/tsconfig.json +14 -0
- package/examples/05-fastly-capabilities/.pulse/.gitignore +3 -0
- package/examples/05-fastly-capabilities/.pulse/config.ts +57 -0
- package/examples/05-fastly-capabilities/README.md +257 -0
- package/examples/05-fastly-capabilities/package.json +22 -0
- package/examples/05-fastly-capabilities/src/index.ts +55 -0
- package/examples/05-fastly-capabilities/tests/pulse.harness.ts +66 -0
- package/examples/05-fastly-capabilities/tsconfig.json +14 -0
- package/examples/07-opaque-proxy/.pulse/.gitignore +3 -0
- package/examples/07-opaque-proxy/.pulse/config.ts +38 -0
- package/examples/07-opaque-proxy/README.md +150 -0
- package/examples/07-opaque-proxy/package.json +21 -0
- package/examples/07-opaque-proxy/src/index.ts +9 -0
- package/examples/07-opaque-proxy/tests/pulse.harness.ts +25 -0
- package/examples/07-opaque-proxy/tsconfig.json +14 -0
- package/examples/09-router-lowering/.pulse/.gitignore +3 -0
- package/examples/09-router-lowering/.pulse/config.ts +22 -0
- package/examples/09-router-lowering/README.md +287 -0
- package/examples/09-router-lowering/package.json +22 -0
- package/examples/09-router-lowering/src/index.ts +65 -0
- package/examples/09-router-lowering/tests/pulse.harness.ts +91 -0
- package/examples/09-router-lowering/tsconfig.json +14 -0
- package/examples/10-entities-tools/.pulse/.gitignore +4 -0
- package/examples/10-entities-tools/.pulse/config.ts +23 -0
- package/examples/10-entities-tools/README.md +165 -0
- package/examples/10-entities-tools/package.json +21 -0
- package/examples/10-entities-tools/src/handlers.ts +15 -0
- package/examples/10-entities-tools/src/index.ts +31 -0
- package/examples/10-entities-tools/src/schemas.ts +18 -0
- package/examples/10-entities-tools/tests/pulse.harness.ts +40 -0
- package/examples/10-entities-tools/tools-facade.cjs +158 -0
- package/examples/10-entities-tools/tsconfig.json +14 -0
- package/examples/11-events/.pulse/.gitignore +4 -0
- package/examples/11-events/.pulse/config.ts +24 -0
- package/examples/11-events/README.md +194 -0
- package/examples/11-events/package.json +20 -0
- package/examples/11-events/src/index.ts +27 -0
- package/examples/11-events/src/schemas.ts +19 -0
- package/examples/11-events/tests/pulse.harness.ts +34 -0
- package/examples/11-events/tsconfig.json +15 -0
- package/examples/12-mcp-proxy/.pulse/.gitignore +3 -0
- package/examples/12-mcp-proxy/.pulse/config.ts +25 -0
- package/examples/12-mcp-proxy/README.md +149 -0
- package/examples/12-mcp-proxy/package.json +20 -0
- package/examples/12-mcp-proxy/src/index.ts +17 -0
- package/examples/12-mcp-proxy/tests/pulse.harness.ts +29 -0
- package/examples/12-mcp-proxy/tsconfig.json +14 -0
- package/examples/13-jwt-es256/.pulse/config.ts +19 -0
- package/examples/13-jwt-es256/README.md +195 -0
- package/examples/13-jwt-es256/package.json +21 -0
- package/examples/13-jwt-es256/src/index.ts +21 -0
- package/examples/13-jwt-es256/tests/pulse.harness.ts +54 -0
- package/examples/13-jwt-es256/tsconfig.json +14 -0
- package/examples/README.md +36 -0
- package/package.json +66 -6
- package/project-config.schema.json +918 -0
- package/release-manifest.json +427 -0
- package/src/command-spec.js +279 -0
- package/src/completion.js +113 -0
- package/src/diagnostics.js +350 -0
- package/src/documentation.js +45 -0
- package/src/index.d.ts +133 -0
- package/src/index.js +15 -0
- package/src/internal/command-executor.d.ts +29 -0
- package/src/internal/command-executor.js +143 -0
- package/src/internal/command-plan.d.ts +9 -0
- package/src/internal/command-plan.js +73 -0
- package/src/internal/command-reporter.d.ts +15 -0
- package/src/internal/command-reporter.js +133 -0
- package/src/internal/command-request.d.ts +35 -0
- package/src/internal/command-request.js +154 -0
- package/src/internal/node-http.js +54 -0
- package/src/internal/project-context.d.ts +66 -0
- package/src/internal/project-context.js +175 -0
- package/src/project-config-reference.js +4 -0
- package/src/project-config-schema.d.ts +23 -0
- package/src/project-config-schema.js +271 -0
- package/src/project-config.js +724 -0
- package/src/project-execution.js +2930 -0
- package/src/provider-drivers.js +3 -0
- package/src/target-support.js +3 -0
- package/src/typescript-module-loader.js +269 -0
- package/src/workflow.js +78 -0
- package/src/workspace.js +82 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
const { createRequire } = require('node:module');
|
|
8
|
+
const ts = require('typescript');
|
|
9
|
+
const {
|
|
10
|
+
JAVASCRIPT_APPLICATION_LOADER_VERSION,
|
|
11
|
+
isJavascriptApplicationPlan
|
|
12
|
+
} = require('@pulse-compute/wasm-contracts/project/javascript-application');
|
|
13
|
+
|
|
14
|
+
const TYPESCRIPT_MODULE_LOADER_VERSION = 'pulse.typescript-module-loader.v1';
|
|
15
|
+
const PROJECT_HARNESS_VERSION = 'pulse.project-harness.v1';
|
|
16
|
+
class PulseModuleLoadError extends Error {
|
|
17
|
+
constructor(code, message, detail = {}) { super(message); this.name = 'PulseModuleLoadError'; this.code = code; this.detail = Object.freeze({ ...detail }); }
|
|
18
|
+
}
|
|
19
|
+
function isInside(root, candidate) { const relative = path.relative(path.resolve(root), path.resolve(candidate)); return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); }
|
|
20
|
+
function resolveRelative(fromFile, request) {
|
|
21
|
+
const base = path.resolve(path.dirname(fromFile), request);
|
|
22
|
+
const candidates = [base, ...['.ts','.tsx','.js','.cjs','.mjs','.json'].map((extension) => `${base}${extension}`), ...['.ts','.tsx','.js','.cjs','.mjs','.json'].map((extension) => path.join(base, `index${extension}`))];
|
|
23
|
+
return candidates.find((candidate) => { try { return fs.statSync(candidate).isFile(); } catch (_) { return false; } });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canonicalExistingPath(value) {
|
|
27
|
+
const resolved = path.resolve(value);
|
|
28
|
+
return fs.existsSync(resolved)
|
|
29
|
+
? fs.realpathSync(resolved)
|
|
30
|
+
: resolved;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadTypescriptModule(entryFile, options = {}) {
|
|
34
|
+
const entry = canonicalExistingPath(entryFile);
|
|
35
|
+
const workspaceRoot = canonicalExistingPath(
|
|
36
|
+
options.workspaceRoot || options.root || path.dirname(entry)
|
|
37
|
+
);
|
|
38
|
+
const allowedPackages = new Map(Object.entries(options.allowedPackages || {}));
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
function load(file) {
|
|
41
|
+
const requested = path.resolve(file);
|
|
42
|
+
const resolved = fs.realpathSync(requested);
|
|
43
|
+
if (!isInside(workspaceRoot, resolved)) throw new PulseModuleLoadError('PULSE_TOOLING_MODULE_OUTSIDE_WORKSPACE', `Tooling module escapes the Pulse workspace: ${resolved}`, { workspaceRoot, file: resolved });
|
|
44
|
+
if (cache.has(resolved)) return cache.get(resolved).exports;
|
|
45
|
+
if (path.extname(resolved).toLowerCase() === '.json') { const value = JSON.parse(fs.readFileSync(resolved, 'utf8')); cache.set(resolved, { exports: value }); return value; }
|
|
46
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
47
|
+
const transpiled = ts.transpileModule(source, { fileName: resolved, compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, moduleResolution: ts.ModuleResolutionKind.Node10, esModuleInterop: true, allowJs: true, resolveJsonModule: true }, reportDiagnostics: true });
|
|
48
|
+
const failures = (transpiled.diagnostics || []).filter((entry) => entry.category === ts.DiagnosticCategory.Error);
|
|
49
|
+
if (failures.length) throw new PulseModuleLoadError('PULSE_TOOLING_MODULE_COMPILE_FAILED', `Tooling module could not be compiled: ${resolved}`, { file: resolved, diagnostics: failures.map((entry) => ({ code: entry.code, message: ts.flattenDiagnosticMessageText(entry.messageText, '\n') })) });
|
|
50
|
+
const module = { exports: {} }; cache.set(resolved, module);
|
|
51
|
+
const localRequire = (request) => {
|
|
52
|
+
if (allowedPackages.has(request)) return allowedPackages.get(request);
|
|
53
|
+
if (request.startsWith('.') || request.startsWith('/')) {
|
|
54
|
+
const target = resolveRelative(resolved, request);
|
|
55
|
+
if (!target) throw new PulseModuleLoadError('PULSE_TOOLING_MODULE_IMPORT_NOT_FOUND', `Cannot resolve tooling import ${request} from ${resolved}.`, { request, from: resolved });
|
|
56
|
+
return load(target);
|
|
57
|
+
}
|
|
58
|
+
throw new PulseModuleLoadError('PULSE_TOOLING_MODULE_PACKAGE_UNSUPPORTED', `Tooling module package import is not allowed: ${request}`, { request, from: resolved });
|
|
59
|
+
};
|
|
60
|
+
new vm.Script(`(function(module,exports,require,__filename,__dirname){\n${transpiled.outputText}\n})`, { filename: resolved }).runInThisContext()(module, module.exports, localRequire, resolved, path.dirname(resolved));
|
|
61
|
+
return module.exports;
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({ version: TYPESCRIPT_MODULE_LOADER_VERSION, file: entry, exports: load(entryFile) });
|
|
64
|
+
}
|
|
65
|
+
function moduleDefault(exports) { return exports && Object.prototype.hasOwnProperty.call(exports, 'default') ? exports.default : exports; }
|
|
66
|
+
function loadProjectHarness(file, options = {}) {
|
|
67
|
+
const loaded = loadTypescriptModule(file, options);
|
|
68
|
+
const exported = moduleDefault(loaded.exports);
|
|
69
|
+
const cases = Array.isArray(exported) ? exported : exported && Array.isArray(exported.cases) ? exported.cases : null;
|
|
70
|
+
if (!cases) throw new PulseModuleLoadError('PULSE_TEST_HARNESS_EXPORT_INVALID', 'Pulse test harness must export an ordered case array or an object with a cases array.');
|
|
71
|
+
return Object.freeze({ version: PROJECT_HARNESS_VERSION, file: loaded.file, cases: Object.freeze([...cases]) });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function sourceSha256(value) {
|
|
76
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function packageRequest(packageEntry, request) {
|
|
80
|
+
if (packageEntry.status !== 'package-runtime' || !packageEntry.runtimeEntry) return request;
|
|
81
|
+
if (!packageEntry.runtimeEntry.startsWith('.')) return packageEntry.runtimeEntry;
|
|
82
|
+
const suffix = packageEntry.runtimeEntry.replace(/^\.\/?/, '');
|
|
83
|
+
return suffix ? `${packageEntry.packageName}/${suffix}` : packageEntry.packageName;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function loadJavascriptApplication(plan, options = {}) {
|
|
87
|
+
if (!isJavascriptApplicationPlan(plan)) {
|
|
88
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_APPLICATION_PLAN_INVALID', 'JavaScript application loading requires a normalized Pulse application plan.');
|
|
89
|
+
}
|
|
90
|
+
if (plan.loadable !== true) {
|
|
91
|
+
throw new PulseModuleLoadError(
|
|
92
|
+
plan.blockers[0]?.code || 'PULSE_JAVASCRIPT_APPLICATION_UNAVAILABLE',
|
|
93
|
+
plan.blockers[0]?.message || 'The JavaScript application plan has unresolved target blockers.',
|
|
94
|
+
{ blockers: plan.blockers, planHash: plan.planHash, automaticFallback: false }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
if (typeof options.validateApplication !== 'function') {
|
|
98
|
+
throw new TypeError('JavaScript application loading requires a provider application validator.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const workspaceRoot = canonicalExistingPath( options.workspaceRoot || options.root || '.');
|
|
102
|
+
const modules = new Map(plan.graph.modules.map((entry) => [entry.id, entry]));
|
|
103
|
+
const packages = new Map(plan.packages.map((entry) => [entry.moduleId, entry]));
|
|
104
|
+
const runtimeEdges = new Map();
|
|
105
|
+
for (const edge of plan.graph.edges.filter((entry) => entry.runtime)) {
|
|
106
|
+
const key = `${edge.from}\u0000${edge.specifier}`;
|
|
107
|
+
if (runtimeEdges.has(key)) {
|
|
108
|
+
throw new PulseModuleLoadError(
|
|
109
|
+
'PULSE_JAVASCRIPT_GRAPH_EDGE_AMBIGUOUS',
|
|
110
|
+
`JavaScript application graph has more than one runtime edge for ${edge.specifier}.`,
|
|
111
|
+
{ from: edge.from, specifier: edge.specifier }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
runtimeEdges.set(key, edge);
|
|
115
|
+
}
|
|
116
|
+
const allowedPackages = new Map(Object.entries(options.allowedPackages || {}));
|
|
117
|
+
const workspaceRequire = options.require || createRequire(path.join(workspaceRoot, 'package.json'));
|
|
118
|
+
const cache = new Map();
|
|
119
|
+
const loadedPackages = new Set();
|
|
120
|
+
|
|
121
|
+
function loadPackage(target, edge) {
|
|
122
|
+
const packageEntry = packages.get(target.id);
|
|
123
|
+
if (!packageEntry) {
|
|
124
|
+
throw new PulseModuleLoadError(
|
|
125
|
+
'PULSE_JAVASCRIPT_PACKAGE_PLAN_MISSING',
|
|
126
|
+
`JavaScript application package ${target.packageName || edge.specifier} is absent from the target plan.`,
|
|
127
|
+
{ moduleId: target.id, specifier: edge.specifier, source: edge.source }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (packageEntry.status === 'unavailable') {
|
|
131
|
+
throw new PulseModuleLoadError(
|
|
132
|
+
packageEntry.reasonCode || 'PULSE_JAVASCRIPT_PACKAGE_REALIZATION_UNAVAILABLE',
|
|
133
|
+
`Package ${packageEntry.packageName} is unavailable under the JavaScript target.`,
|
|
134
|
+
{ ...packageEntry, source: edge.source, automaticFallback: false }
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const request = packageRequest(packageEntry, edge.specifier);
|
|
138
|
+
if (allowedPackages.has(edge.specifier)) {
|
|
139
|
+
loadedPackages.add(edge.specifier);
|
|
140
|
+
return allowedPackages.get(edge.specifier);
|
|
141
|
+
}
|
|
142
|
+
if (allowedPackages.has(request)) {
|
|
143
|
+
loadedPackages.add(request);
|
|
144
|
+
return allowedPackages.get(request);
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const value = workspaceRequire(request);
|
|
148
|
+
loadedPackages.add(request);
|
|
149
|
+
return value;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const code = error && error.code === 'ERR_REQUIRE_ESM'
|
|
152
|
+
? 'PULSE_JAVASCRIPT_PACKAGE_ESM_UNSUPPORTED'
|
|
153
|
+
: 'PULSE_JAVASCRIPT_PACKAGE_MODULE_UNAVAILABLE';
|
|
154
|
+
throw new PulseModuleLoadError(
|
|
155
|
+
code,
|
|
156
|
+
`Cannot load JavaScript package import ${request} from ${edge.source?.file || plan.workspace.entry}.`,
|
|
157
|
+
{ request, packageName: packageEntry.packageName, source: edge.source, causeCode: error && error.code }
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function loadModule(moduleId) {
|
|
163
|
+
if (cache.has(moduleId)) return cache.get(moduleId).exports;
|
|
164
|
+
const moduleRecord = modules.get(moduleId);
|
|
165
|
+
if (!moduleRecord) throw new PulseModuleLoadError('PULSE_JAVASCRIPT_GRAPH_MODULE_MISSING', `Unknown JavaScript application module ${moduleId}.`, { moduleId });
|
|
166
|
+
if (moduleRecord.kind !== 'project') {
|
|
167
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_APPLICATION_ENTRY_INVALID', 'The JavaScript application entry must be a project module.', { moduleId, kind: moduleRecord.kind });
|
|
168
|
+
}
|
|
169
|
+
const requested = path.resolve(workspaceRoot, moduleRecord.path);
|
|
170
|
+
if (!isInside(workspaceRoot, requested)) {
|
|
171
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_MODULE_OUTSIDE_WORKSPACE', `JavaScript application module escapes the Pulse workspace: ${moduleRecord.path}`, { workspaceRoot, module: moduleRecord.path });
|
|
172
|
+
}
|
|
173
|
+
let resolved;
|
|
174
|
+
try {
|
|
175
|
+
resolved = fs.realpathSync(requested);
|
|
176
|
+
} catch (_) {
|
|
177
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_MODULE_NOT_FOUND', `JavaScript application module is missing: ${moduleRecord.path}`, { workspaceRoot, module: moduleRecord.path });
|
|
178
|
+
}
|
|
179
|
+
if (!isInside(workspaceRoot, resolved)) {
|
|
180
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_MODULE_OUTSIDE_WORKSPACE', `JavaScript application module resolves outside the Pulse workspace: ${moduleRecord.path}`, { workspaceRoot, module: moduleRecord.path, resolved });
|
|
181
|
+
}
|
|
182
|
+
if (path.extname(resolved).toLowerCase() === '.json') {
|
|
183
|
+
const bytes = fs.readFileSync(resolved);
|
|
184
|
+
if (sourceSha256(bytes) !== moduleRecord.contentHash) throw new PulseModuleLoadError('PULSE_JAVASCRIPT_MODULE_SOURCE_CHANGED', `JavaScript application module changed after planning: ${moduleRecord.path}`, { module: moduleRecord.path, expected: moduleRecord.contentHash, actual: sourceSha256(bytes) });
|
|
185
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
186
|
+
cache.set(moduleId, { exports: value });
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
const source = fs.readFileSync(resolved, 'utf8');
|
|
190
|
+
const actualHash = sourceSha256(source);
|
|
191
|
+
if (actualHash !== moduleRecord.contentHash) {
|
|
192
|
+
throw new PulseModuleLoadError(
|
|
193
|
+
'PULSE_JAVASCRIPT_MODULE_SOURCE_CHANGED',
|
|
194
|
+
`JavaScript application module changed after planning: ${moduleRecord.path}`,
|
|
195
|
+
{ module: moduleRecord.path, expected: moduleRecord.contentHash, actual: actualHash }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const transpiled = ts.transpileModule(source, {
|
|
199
|
+
fileName: resolved,
|
|
200
|
+
compilerOptions: {
|
|
201
|
+
target: ts.ScriptTarget.ES2022,
|
|
202
|
+
module: ts.ModuleKind.CommonJS,
|
|
203
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
204
|
+
esModuleInterop: true,
|
|
205
|
+
allowJs: true,
|
|
206
|
+
resolveJsonModule: true
|
|
207
|
+
},
|
|
208
|
+
reportDiagnostics: true
|
|
209
|
+
});
|
|
210
|
+
const failures = (transpiled.diagnostics || []).filter((entry) => entry.category === ts.DiagnosticCategory.Error);
|
|
211
|
+
if (failures.length) {
|
|
212
|
+
throw new PulseModuleLoadError(
|
|
213
|
+
'PULSE_JAVASCRIPT_MODULE_COMPILE_FAILED',
|
|
214
|
+
`JavaScript application module could not be compiled: ${moduleRecord.path}`,
|
|
215
|
+
{ module: moduleRecord.path, diagnostics: failures.map((entry) => ({ code: entry.code, message: ts.flattenDiagnosticMessageText(entry.messageText, '\n') })) }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const module = { exports: {} };
|
|
219
|
+
cache.set(moduleId, module);
|
|
220
|
+
const localRequire = (request) => {
|
|
221
|
+
const edge = runtimeEdges.get(`${moduleId}\u0000${request}`);
|
|
222
|
+
if (!edge) {
|
|
223
|
+
throw new PulseModuleLoadError(
|
|
224
|
+
'PULSE_JAVASCRIPT_IMPORT_NOT_IN_GRAPH',
|
|
225
|
+
`Runtime import ${request} is not present in the sealed reachable graph for ${moduleRecord.path}.`,
|
|
226
|
+
{ request, from: moduleRecord.path, graphHash: plan.graph.graphHash }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const target = modules.get(edge.to);
|
|
230
|
+
if (!target) throw new PulseModuleLoadError('PULSE_JAVASCRIPT_GRAPH_MODULE_MISSING', `JavaScript application edge ${edge.id} has no target module.`, { edge });
|
|
231
|
+
if (target.kind === 'project') return loadModule(target.id);
|
|
232
|
+
if (target.kind === 'package') return loadPackage(target, edge);
|
|
233
|
+
if (target.kind === 'external') {
|
|
234
|
+
try {
|
|
235
|
+
return workspaceRequire(target.externalSpecifier || request);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_EXTERNAL_MODULE_UNAVAILABLE', `Cannot load external JavaScript module ${request}.`, { request, source: edge.source, causeCode: error && error.code });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw new PulseModuleLoadError('PULSE_JAVASCRIPT_MODULE_KIND_UNSUPPORTED', `Unsupported JavaScript application module kind ${target.kind}.`, { target, edge });
|
|
241
|
+
};
|
|
242
|
+
new vm.Script(`(function(module,exports,require,__filename,__dirname){\n${transpiled.outputText}\n})`, { filename: resolved })
|
|
243
|
+
.runInThisContext()(module, module.exports, localRequire, resolved, path.dirname(resolved));
|
|
244
|
+
return module.exports;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const exports = loadModule(plan.graph.entryModuleId);
|
|
248
|
+
const application = moduleDefault(exports);
|
|
249
|
+
let validated;
|
|
250
|
+
try {
|
|
251
|
+
validated = options.validateApplication(application);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
throw new PulseModuleLoadError(
|
|
254
|
+
error && error.code || 'PULSE_JAVASCRIPT_APPLICATION_EXPORT_INVALID',
|
|
255
|
+
error && error.message || 'Pulse JavaScript application default export is invalid.',
|
|
256
|
+
{ entry: plan.workspace.entry, planHash: plan.planHash }
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return Object.freeze({
|
|
260
|
+
version: JAVASCRIPT_APPLICATION_LOADER_VERSION,
|
|
261
|
+
plan,
|
|
262
|
+
application: validated,
|
|
263
|
+
entry: plan.workspace.entry,
|
|
264
|
+
modulesLoaded: cache.size,
|
|
265
|
+
packagesLoaded: Object.freeze([...loadedPackages].sort())
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = Object.freeze({ TYPESCRIPT_MODULE_LOADER_VERSION, PROJECT_HARNESS_VERSION, JAVASCRIPT_APPLICATION_LOADER_VERSION, PulseModuleLoadError, loadTypescriptModule, moduleDefault, loadProjectHarness, loadJavascriptApplication });
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {
|
|
5
|
+
CONFIG_FILE_NAMES,
|
|
6
|
+
PulseProjectError
|
|
7
|
+
} = require('./project-config.js');
|
|
8
|
+
const { exitCodeForDiagnostic } = require('./diagnostics.js');
|
|
9
|
+
const { COMMANDS, renderUsage } = require('./command-spec.js');
|
|
10
|
+
const { parseCommandRequest, normalizeCommandRequest } = require('./internal/command-request.js');
|
|
11
|
+
const { commandRequiresProjectContext, resolveProjectContext } = require('./internal/project-context.js');
|
|
12
|
+
const { createCommandPlan } = require('./internal/command-plan.js');
|
|
13
|
+
const {
|
|
14
|
+
executeCommandPlan,
|
|
15
|
+
summarizeExecutionError
|
|
16
|
+
} = require('./internal/command-executor.js');
|
|
17
|
+
const { createCommandReporter } = require('./internal/command-reporter.js');
|
|
18
|
+
|
|
19
|
+
const CLI_VERSION = require('../package.json').version;
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
return renderUsage({ version: CLI_VERSION, configFiles: CONFIG_FILE_NAMES });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function planningOptions(request, cwd, projectContext) {
|
|
26
|
+
return {
|
|
27
|
+
cwd,
|
|
28
|
+
version: CLI_VERSION,
|
|
29
|
+
projectContext
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runPulseWorkflowCli(argv, io = {}) {
|
|
34
|
+
const stdout = io.stdout || process.stdout;
|
|
35
|
+
const stderr = io.stderr || process.stderr;
|
|
36
|
+
const cwd = path.resolve(io.cwd || process.cwd());
|
|
37
|
+
const environment = io.env || process.env;
|
|
38
|
+
const reporter = createCommandReporter({
|
|
39
|
+
stdout,
|
|
40
|
+
stderr,
|
|
41
|
+
cliVersion: CLI_VERSION,
|
|
42
|
+
usageText: usage()
|
|
43
|
+
});
|
|
44
|
+
let request;
|
|
45
|
+
try {
|
|
46
|
+
request = parseCommandRequest(argv);
|
|
47
|
+
if (request.kind === 'command' && request.help) request = normalizeCommandRequest({ help: true });
|
|
48
|
+
const projectContext = commandRequiresProjectContext(request)
|
|
49
|
+
? resolveProjectContext(request, { cwd, environment })
|
|
50
|
+
: undefined;
|
|
51
|
+
const plan = createCommandPlan(request, planningOptions(request, cwd, projectContext));
|
|
52
|
+
if (request.kind === 'command' && request.dryRun) {
|
|
53
|
+
reporter.reportPlan(request, plan);
|
|
54
|
+
return { status: 0, plan };
|
|
55
|
+
}
|
|
56
|
+
const execution = await executeCommandPlan(plan, request, {
|
|
57
|
+
projectContext,
|
|
58
|
+
environment,
|
|
59
|
+
cliVersion: CLI_VERSION,
|
|
60
|
+
onEvent: (event) => reporter.reportEvent(request, event)
|
|
61
|
+
});
|
|
62
|
+
return reporter.reportExecution(request, plan, execution);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const summary = summarizeExecutionError(error);
|
|
65
|
+
reporter.reportDiagnostic(summary, { request, argv });
|
|
66
|
+
return {
|
|
67
|
+
status: exitCodeForDiagnostic(summary.code, error instanceof PulseProjectError ? 2 : 1),
|
|
68
|
+
error: summary
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
CLI_VERSION,
|
|
75
|
+
COMMANDS,
|
|
76
|
+
runPulseWorkflowCli,
|
|
77
|
+
usage
|
|
78
|
+
};
|
package/src/workspace.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_DISCOVERY_VERSION = 'pulse.workspace-discovery.v1';
|
|
7
|
+
const PROJECT_WORKSPACE_VERSION = 'pulse.project-workspace.v1';
|
|
8
|
+
const CONVENTIONAL_CONFIG = path.join('.pulse', 'config.ts');
|
|
9
|
+
|
|
10
|
+
class PulseWorkspaceError extends Error {
|
|
11
|
+
constructor(code, message, detail = {}) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'PulseWorkspaceError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.detail = Object.freeze({ ...detail });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function fileExists(file) { try { return fs.statSync(file).isFile(); } catch (_) { return false; } }
|
|
19
|
+
function directoryExists(dir) { try { return fs.statSync(dir).isDirectory(); } catch (_) { return false; } }
|
|
20
|
+
function canonicalDirectory(dir) { return fs.realpathSync(path.resolve(dir)); }
|
|
21
|
+
function canonicalExistingFile(file) { return fs.realpathSync(path.resolve(file)); }
|
|
22
|
+
function isInside(root, candidate) {
|
|
23
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
24
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
25
|
+
}
|
|
26
|
+
function findWorktreeBoundary(start) {
|
|
27
|
+
let current = canonicalDirectory(start);
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (fs.existsSync(path.join(current, '.git'))) return current;
|
|
30
|
+
const parent = path.dirname(current);
|
|
31
|
+
if (parent === current) return current;
|
|
32
|
+
current = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function discoverConventionalWorkspace(start) {
|
|
36
|
+
const origin = canonicalDirectory(start);
|
|
37
|
+
const boundary = findWorktreeBoundary(origin);
|
|
38
|
+
let current = origin;
|
|
39
|
+
for (;;) {
|
|
40
|
+
const configFile = path.join(current, CONVENTIONAL_CONFIG);
|
|
41
|
+
if (fileExists(configFile)) {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
version: PROJECT_WORKSPACE_VERSION,
|
|
44
|
+
kind: 'conventional',
|
|
45
|
+
root: current,
|
|
46
|
+
configFile: canonicalExistingFile(configFile),
|
|
47
|
+
discoveryStart: origin,
|
|
48
|
+
boundary,
|
|
49
|
+
source: current === origin ? 'current' : 'ancestor',
|
|
50
|
+
configInsideWorkspace: true
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (current === boundary) break;
|
|
54
|
+
const parent = path.dirname(current);
|
|
55
|
+
if (parent === current || !isInside(boundary, parent)) break;
|
|
56
|
+
current = parent;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
function resolveWorkspace(options = {}) {
|
|
61
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
62
|
+
const startCandidate = options.directory ? path.resolve(cwd, options.directory) : cwd;
|
|
63
|
+
if (!directoryExists(startCandidate)) {
|
|
64
|
+
throw new PulseWorkspaceError('PULSE_PROJECT_ROOT_MISSING', `Pulse project directory does not exist: ${startCandidate}`, { projectRoot: startCandidate });
|
|
65
|
+
}
|
|
66
|
+
const start = canonicalDirectory(startCandidate);
|
|
67
|
+
return discoverConventionalWorkspace(start);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = Object.freeze({
|
|
71
|
+
WORKSPACE_DISCOVERY_VERSION,
|
|
72
|
+
PROJECT_WORKSPACE_VERSION,
|
|
73
|
+
CONVENTIONAL_CONFIG,
|
|
74
|
+
PulseWorkspaceError,
|
|
75
|
+
fileExists,
|
|
76
|
+
directoryExists,
|
|
77
|
+
isInside,
|
|
78
|
+
findWorktreeBoundary,
|
|
79
|
+
discoverConventionalWorkspace,
|
|
80
|
+
resolveWorkspace,
|
|
81
|
+
resolveProjectWorkspace: resolveWorkspace
|
|
82
|
+
});
|