@pulse-compute/cli 0.0.0 → 1.0.0-beta.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/API.md +572 -0
- package/CHANGELOG.md +71 -0
- package/README.md +71 -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 +470 -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 +300 -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 +70 -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 +202 -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 +100 -0
- package/docs/maintainers/maintenance-policy.json +809 -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 +293 -0
- package/docs/maintainers/release-manifest.md +89 -0
- package/docs/maintainers/repository-setup.md +145 -0
- package/docs/maintainers/scope-policy.md +105 -0
- package/docs/maintainers/support-and-triage.md +74 -0
- package/docs/maintainers/testing.md +334 -0
- package/docs/packages/README.md +60 -0
- package/docs/packages/assets.md +85 -0
- package/docs/packages/cli.md +190 -0
- package/docs/packages/crypto.md +78 -0
- package/docs/packages/entities.md +129 -0
- package/docs/packages/grip.md +82 -0
- package/docs/packages/implementation-packages.md +122 -0
- package/docs/packages/jwt.md +55 -0
- package/docs/packages/provider-fastly.md +243 -0
- package/docs/packages/pulse.md +159 -0
- package/docs/packages/runtime.md +265 -0
- package/docs/packages/s3.md +101 -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 +22 -0
- package/docs/reference/environment.md +347 -0
- package/docs/reference/handler-authoring.md +264 -0
- package/docs/reference/project-config.md +582 -0
- package/docs/reference/project-config.schema.json +981 -0
- package/docs/reference/release-manifest.json +441 -0
- package/docs/reference/shell-completion.md +23 -0
- package/documentation-site.json +668 -0
- package/documentation-versions.json +22 -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 +981 -0
- package/release-manifest.json +441 -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 +2946 -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,724 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { compileProjectConfigFile } = require('@pulse-compute/wasm-compiler/project-config-compiler');
|
|
6
|
+
const configRuntime = require('@pulse-compute/wasm-contracts/project/config-runtime');
|
|
7
|
+
const {
|
|
8
|
+
normalizeCryptoConfiguration
|
|
9
|
+
} = require('@pulse-compute/wasm-contracts/crypto/contracts');
|
|
10
|
+
const { reportingDescriptor } = require('@pulse-compute/wasm-contracts/logging');
|
|
11
|
+
const eventContracts = require(path.join(
|
|
12
|
+
path.dirname(require.resolve('@pulse-compute/wasm-contracts')),
|
|
13
|
+
'events',
|
|
14
|
+
'contracts.js'
|
|
15
|
+
));
|
|
16
|
+
const { extractSchemaRegistry } = require('@pulse-compute/wasm-schema-json/compiler/schema-registry');
|
|
17
|
+
const { loadProjectHarness: loadHarnessModule } = require('./typescript-module-loader.js');
|
|
18
|
+
const { resolveWorkspace, PulseWorkspaceError, isInside } = require('./workspace.js');
|
|
19
|
+
const { getProviderDriver, providerIds, resolveProviderToolchain } = require('./provider-drivers.js');
|
|
20
|
+
const {
|
|
21
|
+
PROJECT_CONFIG_SCHEMA_VERSION,
|
|
22
|
+
CONFIG_DISCOVERY,
|
|
23
|
+
configRuntimeRule,
|
|
24
|
+
configDefault,
|
|
25
|
+
validateConfigValue,
|
|
26
|
+
validateProjectConfigStructure,
|
|
27
|
+
projectConfigSchemaDocument
|
|
28
|
+
} = require('./project-config-schema.js');
|
|
29
|
+
|
|
30
|
+
const PROJECT_CONFIG_VERSION = 'pulse.project-config.v4';
|
|
31
|
+
const CONFIG_FILE_NAMES = CONFIG_DISCOVERY;
|
|
32
|
+
const PROJECT_PROVIDERS = providerIds();
|
|
33
|
+
|
|
34
|
+
class PulseProjectError extends Error {
|
|
35
|
+
constructor(code, message, detail = {}) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'PulseProjectError';
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.detail = Object.freeze({ ...detail });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function slash(value) {
|
|
44
|
+
return String(value).replace(/\\/g, '/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function relative(file, root) {
|
|
48
|
+
const value = path.relative(root, file);
|
|
49
|
+
return slash(value || '.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fileExists(file) {
|
|
53
|
+
try { return fs.statSync(file).isFile(); }
|
|
54
|
+
catch (_) { return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizeHeaders(headers) {
|
|
58
|
+
if (!headers) return [];
|
|
59
|
+
if (Array.isArray(headers)) {
|
|
60
|
+
return headers.map((entry) => {
|
|
61
|
+
if (!Array.isArray(entry) || entry.length < 2) throw new PulseProjectError('PULSE_CONFIG_HEADERS_INVALID', 'Headers must be [name, value] pairs.', { headers });
|
|
62
|
+
return Object.freeze([String(entry[0]), String(entry[1])]);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (typeof headers === 'object') return Object.freeze(Object.entries(headers).map(([name, value]) => Object.freeze([String(name), String(value)])));
|
|
66
|
+
throw new PulseProjectError('PULSE_CONFIG_HEADERS_INVALID', 'Headers must be an object or [name, value] pairs.', { headersType: typeof headers });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeExpectedHeaders(headers) {
|
|
70
|
+
if (!headers) return [];
|
|
71
|
+
const entries = Array.isArray(headers) ? headers : Object.entries(headers);
|
|
72
|
+
return Object.freeze(entries.map((entry) => {
|
|
73
|
+
if (!Array.isArray(entry) || entry.length < 2) {
|
|
74
|
+
throw new PulseProjectError('PULSE_CONFIG_HEADERS_INVALID', 'Expected headers must be [name, value] pairs.', { headers });
|
|
75
|
+
}
|
|
76
|
+
const value = Array.isArray(entry[1]) ? Object.freeze(entry[1].map(String)) : String(entry[1]);
|
|
77
|
+
return Object.freeze([String(entry[0]), value]);
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeRequest(input = {}) {
|
|
82
|
+
const pathValue = String(input.path || (() => {
|
|
83
|
+
try { return new URL(String(input.url || 'http://127.0.0.1/')).pathname; }
|
|
84
|
+
catch (_) { return '/'; }
|
|
85
|
+
})());
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
method: String(input.method || 'GET').toUpperCase(),
|
|
88
|
+
url: input.url ? String(input.url) : `http://127.0.0.1${pathValue}`,
|
|
89
|
+
path: pathValue,
|
|
90
|
+
headers: normalizeHeaders(input.headers),
|
|
91
|
+
body: Object.prototype.hasOwnProperty.call(input, 'body') ? input.body : undefined
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeHarnessEvent(input, field) {
|
|
96
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
97
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `${field} must be an event frame object.`);
|
|
98
|
+
}
|
|
99
|
+
const unknown = Object.keys(input).filter((key) => !['type', 'schema', 'schemaId', 'payload'].includes(key)).sort();
|
|
100
|
+
if (unknown.length > 0) {
|
|
101
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `${field} contains unsupported fields: ${unknown.join(', ')}.`, { field, unknown });
|
|
102
|
+
}
|
|
103
|
+
const hasSchema = Object.prototype.hasOwnProperty.call(input, 'schema');
|
|
104
|
+
const hasSchemaId = Object.prototype.hasOwnProperty.call(input, 'schemaId');
|
|
105
|
+
if (hasSchema === hasSchemaId) {
|
|
106
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `${field} requires exactly one of schema or schemaId.`, { field });
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return eventContracts.normalizeEventFrame({
|
|
110
|
+
version: eventContracts.EVENT_FRAME_VERSION,
|
|
111
|
+
type: input.type,
|
|
112
|
+
schemaId: hasSchema ? input.schema : input.schemaId,
|
|
113
|
+
...(Object.prototype.hasOwnProperty.call(input, 'payload') ? { payload: input.payload } : {})
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `${field} is not a canonical bounded event frame: ${error.message}`, {
|
|
117
|
+
field,
|
|
118
|
+
eventCode: error.code,
|
|
119
|
+
eventDetail: error.detail
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeExpectation(input = {}, kind = 'http') {
|
|
125
|
+
if (typeof input === 'string') {
|
|
126
|
+
if (kind === 'http') return Object.freeze({ error: Object.freeze({ name: input }) });
|
|
127
|
+
input = { error: input };
|
|
128
|
+
}
|
|
129
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new PulseProjectError('PULSE_TEST_EXPECT_INVALID', 'Test expect must be an object or error name string.');
|
|
130
|
+
const error = input.error
|
|
131
|
+
? Object.freeze(typeof input.error === 'string' ? { name: input.error } : { ...input.error })
|
|
132
|
+
: undefined;
|
|
133
|
+
if (kind === 'event') {
|
|
134
|
+
if (input.emitted !== undefined && !Array.isArray(input.emitted)) {
|
|
135
|
+
throw new PulseProjectError('PULSE_TEST_EXPECT_INVALID', 'Event test expect.emitted must be an ordered array of event frames.');
|
|
136
|
+
}
|
|
137
|
+
const status = input.status === undefined ? (error ? 'failed' : 'completed') : String(input.status);
|
|
138
|
+
if (!Object.values(eventContracts.EVENT_EXECUTION_STATUS).includes(status)) {
|
|
139
|
+
throw new PulseProjectError('PULSE_TEST_EXPECT_INVALID', 'Event test expect.status must be completed or failed.', { status });
|
|
140
|
+
}
|
|
141
|
+
if (status === eventContracts.EVENT_EXECUTION_STATUS.COMPLETED && error) {
|
|
142
|
+
throw new PulseProjectError('PULSE_TEST_EXPECT_INVALID', 'A completed event expectation cannot include expect.error.');
|
|
143
|
+
}
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
error,
|
|
146
|
+
status,
|
|
147
|
+
emitted: Object.freeze((input.emitted || []).map((event, index) => normalizeHarnessEvent(event, `expect.emitted[${index}]`)))
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
error,
|
|
152
|
+
status: input.status === undefined ? (error ? undefined : 200) : Number(input.status),
|
|
153
|
+
bodyClass: input.bodyClass === undefined ? undefined : String(input.bodyClass),
|
|
154
|
+
json: Object.prototype.hasOwnProperty.call(input, 'json') ? input.json : undefined,
|
|
155
|
+
text: Object.prototype.hasOwnProperty.call(input, 'text') ? String(input.text) : undefined,
|
|
156
|
+
headers: input.headers ? normalizeExpectedHeaders(input.headers) : undefined
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizeTestCase(input, index) {
|
|
161
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) throw new PulseProjectError('PULSE_TEST_CASE_INVALID', `Test case ${index + 1} must be an object.`);
|
|
162
|
+
const name = String(input.name || `case-${index + 1}`);
|
|
163
|
+
const kind = input.kind === undefined ? 'http' : String(input.kind);
|
|
164
|
+
if (!['http', 'event'].includes(kind)) {
|
|
165
|
+
throw new PulseProjectError('PULSE_TEST_CASE_INVALID', `Test case ${name} kind must be http or event.`, { name, kind });
|
|
166
|
+
}
|
|
167
|
+
if (kind === 'event' && !Object.prototype.hasOwnProperty.call(input, 'event')) {
|
|
168
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `Event test case ${name} requires event.`, { name });
|
|
169
|
+
}
|
|
170
|
+
if (kind === 'http' && Object.prototype.hasOwnProperty.call(input, 'event')) {
|
|
171
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `HTTP test case ${name} cannot contain event; set kind to event.`, { name });
|
|
172
|
+
}
|
|
173
|
+
if (kind === 'event' && Object.prototype.hasOwnProperty.call(input, 'request')) {
|
|
174
|
+
throw new PulseProjectError('PULSE_TEST_EVENT_INVALID', `Event test case ${name} cannot contain an HTTP request.`, { name });
|
|
175
|
+
}
|
|
176
|
+
const maxBodyBytes = input.maxBodyBytes === undefined ? undefined : Number(input.maxBodyBytes);
|
|
177
|
+
const continuationTtlMs = input.continuationTtlMs === undefined ? undefined : Number(input.continuationTtlMs);
|
|
178
|
+
if (!validateConfigValue('harness.maxBodyBytes', maxBodyBytes)) {
|
|
179
|
+
throw new PulseProjectError('PULSE_TEST_BODY_LIMIT_INVALID', `Test case ${name} maxBodyBytes must be a positive safe integer.`, { name, maxBodyBytes });
|
|
180
|
+
}
|
|
181
|
+
if (!validateConfigValue('harness.continuationTtlMs', continuationTtlMs)) {
|
|
182
|
+
throw new PulseProjectError('PULSE_TEST_CONTINUATION_TTL_INVALID', `Test case ${name} continuationTtlMs must be a non-negative safe integer.`, { name, continuationTtlMs });
|
|
183
|
+
}
|
|
184
|
+
return Object.freeze({
|
|
185
|
+
name,
|
|
186
|
+
kind,
|
|
187
|
+
...(kind === 'event'
|
|
188
|
+
? { event: normalizeHarnessEvent(input.event, `Test case ${name} event`) }
|
|
189
|
+
: { request: normalizeRequest(input.request) }),
|
|
190
|
+
fetches: Object.freeze({ ...(input.fetches || {}) }),
|
|
191
|
+
config: Object.freeze({ ...(input.config || {}) }),
|
|
192
|
+
secrets: Object.freeze({ ...(input.secrets || {}) }),
|
|
193
|
+
kv: Object.freeze({ ...(input.kv || {}) }),
|
|
194
|
+
grip: Object.freeze({ ...(input.grip || {}) }),
|
|
195
|
+
maxBodyBytes,
|
|
196
|
+
continuationTtlMs,
|
|
197
|
+
expect: normalizeExpectation(input.expect || {}, kind)
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resolveConfiguredProvider(value, options = {}) {
|
|
202
|
+
const raw = value && typeof value === 'object' && !Array.isArray(value) ? value.kind : value;
|
|
203
|
+
const provider = String(raw || configDefault('provider')).trim();
|
|
204
|
+
try {
|
|
205
|
+
return resolveProviderToolchain(provider, { projectRoot: options.projectRoot });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw new PulseProjectError(
|
|
208
|
+
error && error.code || 'PULSE_PROVIDER_UNSUPPORTED',
|
|
209
|
+
error && error.message || `Unsupported Pulse provider "${provider}".`,
|
|
210
|
+
error && error.detail || { provider, official: PROJECT_PROVIDERS }
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeProvider(value, options = {}) {
|
|
216
|
+
return resolveConfiguredProvider(value, options).id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeProviderConfig(value, provider, options = {}) {
|
|
220
|
+
const driver = getProviderDriver(provider, { projectRoot: options.projectRoot });
|
|
221
|
+
if (typeof driver.normalizeConfig !== 'function') {
|
|
222
|
+
throw new PulseProjectError('PULSE_PROVIDER_UNSUPPORTED', `Provider "${provider}" has no configuration normalizer.`, { provider });
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return driver.normalizeConfig(value);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw new PulseProjectError(
|
|
228
|
+
error && error.code || 'PULSE_PROVIDER_CONFIG_INVALID',
|
|
229
|
+
error && error.message || `Provider "${provider}" configuration is invalid.`,
|
|
230
|
+
error && error.detail || { provider }
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeSchemaConfig(value, projectRoot) {
|
|
236
|
+
if (value === undefined) {
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
active: false,
|
|
239
|
+
defaultNamespace: 'app',
|
|
240
|
+
contentTypePolicy: configDefault('schemas.contentTypePolicy'),
|
|
241
|
+
maxBytes: configDefault('schemas.maxBytes'),
|
|
242
|
+
entries: Object.freeze([])
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
246
|
+
throw new PulseProjectError('PULSE_SCHEMA_CONFIG_INVALID', 'Pulse schema policy must be configured as an object.', { valueType: Array.isArray(value) ? 'array' : typeof value });
|
|
247
|
+
}
|
|
248
|
+
if (Object.prototype.hasOwnProperty.call(value, 'json') || Object.prototype.hasOwnProperty.call(value, 'defaultNamespace')) {
|
|
249
|
+
throw new PulseProjectError(
|
|
250
|
+
'PULSE_SCHEMA_JSON_DECLARATIONS_RETIRED',
|
|
251
|
+
'schemas.json and schemas.defaultNamespace are retired. Point pulse.schema at a default-exported defineSchemaRegistry(...) module.',
|
|
252
|
+
{
|
|
253
|
+
retiredKeys: Object.freeze(['schemas.json', 'schemas.defaultNamespace']),
|
|
254
|
+
canonicalEntrypoint: 'pulse.schema'
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const unsupported = Object.keys(value).filter((key) => !['contentTypePolicy', 'maxBytes'].includes(key)).sort();
|
|
259
|
+
if (unsupported.length > 0) {
|
|
260
|
+
throw new PulseProjectError('PULSE_SCHEMA_POLICY_KEY_UNSUPPORTED', `Unsupported schema policy key(s): ${unsupported.join(', ')}.`, {
|
|
261
|
+
keys: unsupported,
|
|
262
|
+
allowed: Object.freeze(['contentTypePolicy', 'maxBytes'])
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const defaultNamespace = 'app';
|
|
266
|
+
const contentTypePolicy = String(value.contentTypePolicy || configDefault('schemas.contentTypePolicy'));
|
|
267
|
+
if (!validateConfigValue('schemas.contentTypePolicy', contentTypePolicy)) {
|
|
268
|
+
throw new PulseProjectError('PULSE_SCHEMA_CONTENT_TYPE_POLICY_INVALID', 'Pulse schema contentTypePolicy must be "accept-json-or-missing" or "require-json".', { contentTypePolicy });
|
|
269
|
+
}
|
|
270
|
+
const maxBytes = Number(value.maxBytes === undefined ? configDefault('schemas.maxBytes') : value.maxBytes);
|
|
271
|
+
if (!validateConfigValue('schemas.maxBytes', maxBytes)) {
|
|
272
|
+
throw new PulseProjectError('PULSE_SCHEMA_BODY_LIMIT_INVALID', 'Pulse schema maxBytes must be a positive safe integer.', { maxBytes });
|
|
273
|
+
}
|
|
274
|
+
return Object.freeze({
|
|
275
|
+
active: false,
|
|
276
|
+
defaultNamespace,
|
|
277
|
+
contentTypePolicy,
|
|
278
|
+
maxBytes,
|
|
279
|
+
entries: Object.freeze([])
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeProject(raw, options = {}) {
|
|
284
|
+
if (
|
|
285
|
+
raw
|
|
286
|
+
&& raw.schemas
|
|
287
|
+
&& typeof raw.schemas === 'object'
|
|
288
|
+
&& !Array.isArray(raw.schemas)
|
|
289
|
+
&& (
|
|
290
|
+
Object.prototype.hasOwnProperty.call(raw.schemas, 'json')
|
|
291
|
+
|| Object.prototype.hasOwnProperty.call(raw.schemas, 'defaultNamespace')
|
|
292
|
+
)
|
|
293
|
+
) {
|
|
294
|
+
throw new PulseProjectError(
|
|
295
|
+
'PULSE_SCHEMA_JSON_DECLARATIONS_RETIRED',
|
|
296
|
+
'schemas.json and schemas.defaultNamespace are retired. Point pulse.schema at a default-exported defineSchemaRegistry(...) module.',
|
|
297
|
+
{
|
|
298
|
+
retiredKeys: Object.freeze(['schemas.json', 'schemas.defaultNamespace']),
|
|
299
|
+
canonicalEntrypoint: 'pulse.schema'
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const configIssues = validateProjectConfigStructure(raw);
|
|
304
|
+
if (configIssues.length > 0) {
|
|
305
|
+
throw new PulseProjectError('PULSE_CONFIG_LOAD_FAILED', 'Pulse config does not match the supported project configuration schema.', {
|
|
306
|
+
validation: configIssues
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
const configFile = options.configFile && path.resolve(options.configFile);
|
|
310
|
+
const projectRoot = path.resolve(options.projectRoot || (configFile ? path.dirname(configFile) : options.cwd || process.cwd()));
|
|
311
|
+
const entryValue = raw.entry || configDefault('entry');
|
|
312
|
+
const entryFile = path.resolve(projectRoot, entryValue);
|
|
313
|
+
const configuredProvider = raw.provider || configDefault('provider');
|
|
314
|
+
const providerResolution = resolveConfiguredProvider(configuredProvider, { projectRoot });
|
|
315
|
+
const provider = providerResolution.id;
|
|
316
|
+
const providerSelector = providerResolution.selector;
|
|
317
|
+
const driver = providerResolution.driver;
|
|
318
|
+
const providerConfig = (() => {
|
|
319
|
+
if (typeof driver.normalizeConfig !== 'function') {
|
|
320
|
+
throw new PulseProjectError('PULSE_PROVIDER_UNSUPPORTED', `Provider "${provider}" has no configuration normalizer.`, { provider });
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
return driver.normalizeConfig(configuredProvider);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new PulseProjectError(
|
|
326
|
+
error && error.code || 'PULSE_PROVIDER_CONFIG_INVALID',
|
|
327
|
+
error && error.message || `Provider "${provider}" configuration is invalid.`,
|
|
328
|
+
error && error.detail || { provider }
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
const outDir = path.resolve(projectRoot, options.outDir || raw.outDir || configDefault('outDir'));
|
|
333
|
+
const devInput = raw.dev && typeof raw.dev === 'object' ? raw.dev : {};
|
|
334
|
+
const tests = Array.isArray(options.tests) ? options.tests : [];
|
|
335
|
+
const schemas = normalizeSchemaConfig(raw.schemas, projectRoot);
|
|
336
|
+
const crypto = raw.crypto === undefined
|
|
337
|
+
? null
|
|
338
|
+
: Object.freeze({
|
|
339
|
+
source: 'project',
|
|
340
|
+
declaration: normalizeCryptoConfiguration(raw.crypto, { location: 'crypto' })
|
|
341
|
+
});
|
|
342
|
+
const reporting = reportingDescriptor(raw.reporting === undefined ? configDefault('reporting') : raw.reporting);
|
|
343
|
+
const duplicateTestNames = tests.map((entry) => entry.name).filter((name, index, names) => names.indexOf(name) !== index);
|
|
344
|
+
if (duplicateTestNames.length > 0) {
|
|
345
|
+
throw new PulseProjectError('PULSE_TEST_CASE_DUPLICATE', `Pulse test case names must be unique: ${[...new Set(duplicateTestNames)].join(', ')}`, { names: [...new Set(duplicateTestNames)] });
|
|
346
|
+
}
|
|
347
|
+
const host = String(options.host || devInput.host || configDefault('dev.host')).trim();
|
|
348
|
+
const maxBodyBytes = Number(devInput.maxBodyBytes === undefined ? configDefault('dev.maxBodyBytes') : devInput.maxBodyBytes);
|
|
349
|
+
if (!validateConfigValue('dev.host', host)) throw new PulseProjectError('PULSE_DEV_HOST_INVALID', 'Pulse dev host must not be empty.');
|
|
350
|
+
if (!validateConfigValue('dev.maxBodyBytes', maxBodyBytes)) {
|
|
351
|
+
throw new PulseProjectError('PULSE_DEV_BODY_LIMIT_INVALID', 'Pulse dev maxBodyBytes must be a positive safe integer.', { maxBodyBytes });
|
|
352
|
+
}
|
|
353
|
+
const project = {
|
|
354
|
+
version: PROJECT_CONFIG_VERSION,
|
|
355
|
+
root: projectRoot,
|
|
356
|
+
configFile,
|
|
357
|
+
entryFile,
|
|
358
|
+
provider,
|
|
359
|
+
providerSelector,
|
|
360
|
+
providerToolchain: Object.freeze({
|
|
361
|
+
selector: providerResolution.selector,
|
|
362
|
+
id: providerResolution.id,
|
|
363
|
+
packageName: providerResolution.packageName,
|
|
364
|
+
packageVersion: providerResolution.packageVersion,
|
|
365
|
+
contractVersion: providerResolution.contractVersion,
|
|
366
|
+
entrypoint: providerResolution.entrypoint,
|
|
367
|
+
official: providerResolution.official
|
|
368
|
+
}),
|
|
369
|
+
providerConfig,
|
|
370
|
+
reporting: reporting.name,
|
|
371
|
+
reportingLevel: reporting.level,
|
|
372
|
+
reportingPolicy: reporting,
|
|
373
|
+
crypto,
|
|
374
|
+
schemas,
|
|
375
|
+
outDir,
|
|
376
|
+
dev: Object.freeze({
|
|
377
|
+
host,
|
|
378
|
+
port: Number(options.port !== undefined ? options.port : (devInput.port !== undefined ? devInput.port : configDefault('dev.port'))),
|
|
379
|
+
watch: options.watch === undefined ? (devInput.watch === undefined ? configDefault('dev.watch') : Boolean(devInput.watch)) : Boolean(options.watch),
|
|
380
|
+
maxBodyBytes,
|
|
381
|
+
config: Object.freeze({ ...(devInput.config || {}) }),
|
|
382
|
+
secrets: Object.freeze({ ...(devInput.secrets || {}) }),
|
|
383
|
+
kv: Object.freeze({ ...(devInput.kv || {}) }),
|
|
384
|
+
fetches: Object.freeze({ ...(devInput.fetches || {}) }),
|
|
385
|
+
networkFetch: devInput.networkFetch !== undefined
|
|
386
|
+
? Boolean(devInput.networkFetch)
|
|
387
|
+
: (providerConfig.local && providerConfig.local.networkFetch !== undefined
|
|
388
|
+
? providerConfig.local.networkFetch
|
|
389
|
+
: driver.defaultLocalNetworkFetch === true)
|
|
390
|
+
}),
|
|
391
|
+
tests: Object.freeze(tests),
|
|
392
|
+
raw
|
|
393
|
+
};
|
|
394
|
+
return Object.freeze(project);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
function projectErrorFrom(error, fallbackCode = 'PULSE_CONFIG_LOAD_FAILED') {
|
|
399
|
+
if (error instanceof PulseProjectError) return error;
|
|
400
|
+
if (error instanceof PulseWorkspaceError || (error && error.name === 'PulseModuleLoadError')) {
|
|
401
|
+
return new PulseProjectError(error.code || fallbackCode, error.message, error.detail || error.details || {});
|
|
402
|
+
}
|
|
403
|
+
const code = error && error.code ? String(error.code) : fallbackCode;
|
|
404
|
+
const detail = error && (error.detail || error.details) ? (error.detail || error.details) : {};
|
|
405
|
+
return new PulseProjectError(code, error && error.message ? error.message : String(error), detail);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function plainObject(value) {
|
|
409
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
410
|
+
const proto = Object.getPrototypeOf(value);
|
|
411
|
+
return proto === Object.prototype || proto === null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function resolveWorkspaceFile(root, pointer, field) {
|
|
415
|
+
if (pointer === null || pointer === undefined) return null;
|
|
416
|
+
const file = path.resolve(root, pointer);
|
|
417
|
+
if (!isInside(root, file)) {
|
|
418
|
+
throw new PulseProjectError('PULSE_PROJECT_POINTER_ESCAPE', `${field} must remain inside the Pulse workspace.`, {
|
|
419
|
+
root,
|
|
420
|
+
pointer,
|
|
421
|
+
file
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return file;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function loadProjectHarness(file, projectRoot) {
|
|
428
|
+
if (!file) return Object.freeze([]);
|
|
429
|
+
if (!fileExists(file)) {
|
|
430
|
+
throw new PulseProjectError('PULSE_TEST_HARNESS_NOT_FOUND', `Pulse test harness does not exist: ${file}`, {
|
|
431
|
+
file,
|
|
432
|
+
projectRoot
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
let loaded;
|
|
436
|
+
try {
|
|
437
|
+
loaded = loadHarnessModule(file, { workspaceRoot: projectRoot });
|
|
438
|
+
} catch (error) {
|
|
439
|
+
throw projectErrorFrom(error, 'PULSE_TEST_HARNESS_LOAD_FAILED');
|
|
440
|
+
}
|
|
441
|
+
const normalized = loaded.cases.map(normalizeTestCase);
|
|
442
|
+
const duplicates = normalized
|
|
443
|
+
.map((entry) => entry.name)
|
|
444
|
+
.filter((name, index, names) => names.indexOf(name) !== index);
|
|
445
|
+
if (duplicates.length > 0) {
|
|
446
|
+
throw new PulseProjectError('PULSE_TEST_CASE_DUPLICATE', `Pulse test case names must be unique: ${[...new Set(duplicates)].join(', ')}`, {
|
|
447
|
+
names: [...new Set(duplicates)]
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return Object.freeze(normalized);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function conventionalProjectProjection(plan, options = {}) {
|
|
454
|
+
const fragments = plan.fragments || {};
|
|
455
|
+
const dev = plainObject(fragments.dev) ? fragments.dev : {};
|
|
456
|
+
const providerSelector = plan.profile.host;
|
|
457
|
+
let provider = providerSelector;
|
|
458
|
+
const driver = getProviderDriver(providerSelector, { projectRoot: options.projectRoot });
|
|
459
|
+
const configured = plainObject(fragments[driver.id])
|
|
460
|
+
? fragments[driver.id]
|
|
461
|
+
: (plainObject(fragments.provider) ? fragments.provider : undefined);
|
|
462
|
+
if (configured) provider = { kind: providerSelector, ...configured };
|
|
463
|
+
return {
|
|
464
|
+
entry: plan.pulse.entry,
|
|
465
|
+
provider,
|
|
466
|
+
outDir: typeof fragments.outDir === 'string' ? fragments.outDir : 'dist',
|
|
467
|
+
reporting: plan.profile.reporting,
|
|
468
|
+
schemas: plainObject(fragments.schemas) ? fragments.schemas : undefined,
|
|
469
|
+
dev: {
|
|
470
|
+
host: typeof dev.host === 'string' ? dev.host : '127.0.0.1',
|
|
471
|
+
port: dev.port === undefined ? 8787 : dev.port,
|
|
472
|
+
watch: dev.watch === undefined ? true : Boolean(dev.watch),
|
|
473
|
+
maxBodyBytes: dev.maxBodyBytes,
|
|
474
|
+
networkFetch: dev.networkFetch,
|
|
475
|
+
config: plainObject(dev.config) ? dev.config : {},
|
|
476
|
+
secrets: plainObject(dev.secrets) ? dev.secrets : {},
|
|
477
|
+
kv: plainObject(dev.kv) ? dev.kv : {},
|
|
478
|
+
fetches: plainObject(dev.fetches) ? dev.fetches : {}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function loadConventionalProject(workspace, options = {}) {
|
|
484
|
+
const sourceName = relative(workspace.configFile, workspace.root);
|
|
485
|
+
const environmentProfile = options.environmentProfile !== undefined
|
|
486
|
+
? options.environmentProfile
|
|
487
|
+
: (options.env && Object.prototype.hasOwnProperty.call(options.env, 'PULSE_PROFILE')
|
|
488
|
+
? options.env.PULSE_PROFILE
|
|
489
|
+
: process.env.PULSE_PROFILE);
|
|
490
|
+
const selection = {
|
|
491
|
+
cliProfile: options.profile,
|
|
492
|
+
environmentProfile
|
|
493
|
+
};
|
|
494
|
+
let compiled;
|
|
495
|
+
let evaluated;
|
|
496
|
+
try {
|
|
497
|
+
compiled = compileProjectConfigFile(workspace.configFile, { source: sourceName, selection });
|
|
498
|
+
const loaded = require('./typescript-module-loader.js').loadTypescriptModule(workspace.configFile, {
|
|
499
|
+
workspaceRoot: workspace.configInsideWorkspace === false ? path.dirname(workspace.configFile) : workspace.root,
|
|
500
|
+
allowedPackages: {
|
|
501
|
+
'@pulse-compute/pulse': Object.freeze({ defineConfig: configRuntime.defineConfig })
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
const exported = require('./typescript-module-loader.js').moduleDefault(loaded.exports);
|
|
505
|
+
evaluated = configRuntime.evaluateConfigFactory(exported, {
|
|
506
|
+
source: sourceName,
|
|
507
|
+
cliProfile: selection.cliProfile,
|
|
508
|
+
environmentProfile: selection.environmentProfile
|
|
509
|
+
});
|
|
510
|
+
} catch (error) {
|
|
511
|
+
throw projectErrorFrom(error);
|
|
512
|
+
}
|
|
513
|
+
if (
|
|
514
|
+
compiled.declaration.projectHash !== evaluated.declaration.projectHash
|
|
515
|
+
|| compiled.plan.planHash !== evaluated.plan.planHash
|
|
516
|
+
) {
|
|
517
|
+
throw new PulseProjectError('PULSE_CONFIG_PLAN_PARITY_FAILED', 'Static and runtime Pulse config evaluation produced different canonical plans.', {
|
|
518
|
+
configFile: workspace.configFile,
|
|
519
|
+
staticProjectHash: compiled.declaration.projectHash,
|
|
520
|
+
runtimeProjectHash: evaluated.declaration.projectHash,
|
|
521
|
+
staticPlanHash: compiled.plan.planHash,
|
|
522
|
+
runtimePlanHash: evaluated.plan.planHash
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const plan = compiled.plan;
|
|
527
|
+
const testsFile = resolveWorkspaceFile(workspace.root, plan.pulse.tests, 'pulse.tests');
|
|
528
|
+
const schemaFile = resolveWorkspaceFile(workspace.root, plan.pulse.schema, 'pulse.schema');
|
|
529
|
+
const tests = loadProjectHarness(testsFile, workspace.root);
|
|
530
|
+
const projected = conventionalProjectProjection(plan, { projectRoot: workspace.root });
|
|
531
|
+
const project = normalizeProject(projected, {
|
|
532
|
+
...options,
|
|
533
|
+
cwd: workspace.root,
|
|
534
|
+
projectRoot: workspace.root,
|
|
535
|
+
configFile: workspace.configFile,
|
|
536
|
+
tests
|
|
537
|
+
});
|
|
538
|
+
let schemaExtraction = null;
|
|
539
|
+
if (schemaFile) {
|
|
540
|
+
try {
|
|
541
|
+
schemaExtraction = extractSchemaRegistry(schemaFile, { projectRoot: workspace.root });
|
|
542
|
+
} catch (error) {
|
|
543
|
+
throw projectErrorFrom(error, 'PULSE_SCHEMA_REGISTRY_EXTRACT_FAILED');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const schemaPolicy = project.schemas;
|
|
547
|
+
const schemas = schemaExtraction
|
|
548
|
+
? Object.freeze({
|
|
549
|
+
active: schemaExtraction.registry.schemas.length > 0,
|
|
550
|
+
defaultNamespace: schemaPolicy.defaultNamespace,
|
|
551
|
+
contentTypePolicy: schemaPolicy.contentTypePolicy,
|
|
552
|
+
maxBytes: schemaPolicy.maxBytes,
|
|
553
|
+
entries: Object.freeze([]),
|
|
554
|
+
registry: schemaExtraction.registry,
|
|
555
|
+
codecInputs: schemaExtraction.codecInputs,
|
|
556
|
+
dependencies: schemaExtraction.dependencies
|
|
557
|
+
})
|
|
558
|
+
: Object.freeze({
|
|
559
|
+
...schemaPolicy,
|
|
560
|
+
registry: null,
|
|
561
|
+
codecInputs: null,
|
|
562
|
+
dependencies: Object.freeze([])
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
return Object.freeze({
|
|
566
|
+
...project,
|
|
567
|
+
schemas,
|
|
568
|
+
workspace,
|
|
569
|
+
target: plan.profile.target,
|
|
570
|
+
reporting: plan.profile.reporting,
|
|
571
|
+
reportingLevel: project.reportingLevel,
|
|
572
|
+
reportingPolicy: project.reportingPolicy,
|
|
573
|
+
strict: plan.pulse.strict,
|
|
574
|
+
profile: plan.profile,
|
|
575
|
+
selectedProfile: plan.profile,
|
|
576
|
+
projectDeclaration: compiled.declaration,
|
|
577
|
+
profilePlan: plan,
|
|
578
|
+
projectHash: compiled.declaration.projectHash,
|
|
579
|
+
planHash: plan.planHash,
|
|
580
|
+
configParity: Object.freeze({
|
|
581
|
+
status: 'matched',
|
|
582
|
+
projectHash: compiled.declaration.projectHash,
|
|
583
|
+
planHash: plan.planHash
|
|
584
|
+
}),
|
|
585
|
+
bindings: plan.bindings,
|
|
586
|
+
fragments: plan.fragments,
|
|
587
|
+
crypto: plan.crypto,
|
|
588
|
+
schemaFile,
|
|
589
|
+
testsFile,
|
|
590
|
+
tests
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function validateResolvedProject(project) {
|
|
595
|
+
if (!fileExists(project.entryFile)) {
|
|
596
|
+
throw new PulseProjectError('PULSE_ENTRY_NOT_FOUND', `Pulse entry does not exist: ${project.entryFile}`, {
|
|
597
|
+
entry: project.entryFile,
|
|
598
|
+
projectRoot: project.root
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
if (!validateConfigValue('dev.port', project.dev.port)) {
|
|
602
|
+
throw new PulseProjectError('PULSE_DEV_PORT_INVALID', 'Pulse dev port must be an integer from 0 to 65535.', {
|
|
603
|
+
port: project.dev.port
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return project;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function resolveProject(options = {}) {
|
|
610
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
611
|
+
let workspace;
|
|
612
|
+
try {
|
|
613
|
+
workspace = resolveWorkspace({
|
|
614
|
+
cwd,
|
|
615
|
+
directory: options.directory
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
throw projectErrorFrom(error, 'PULSE_CONFIG_NOT_FOUND');
|
|
619
|
+
}
|
|
620
|
+
if (!workspace) {
|
|
621
|
+
throw new PulseProjectError('PULSE_CONFIG_NOT_FOUND', `No .pulse/config.ts found from ${options.directory ? path.resolve(cwd, options.directory) : cwd}.`, {
|
|
622
|
+
searched: CONFIG_FILE_NAMES,
|
|
623
|
+
hint: 'Create .pulse/config.ts or run pulse init.'
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return validateResolvedProject(loadConventionalProject(workspace, { ...options, cwd }));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function projectJson(project) {
|
|
630
|
+
const schemaRegistry = project.schemas.registry;
|
|
631
|
+
const schemaIds = schemaRegistry
|
|
632
|
+
? schemaRegistry.schemas.map((entry) => entry.id)
|
|
633
|
+
: project.schemas.entries.map((entry) => entry.id);
|
|
634
|
+
const schemaSources = schemaRegistry
|
|
635
|
+
? [...new Set(project.schemas.dependencies.map((entry) => relative(entry, project.root)))]
|
|
636
|
+
: [...new Set(project.schemas.entries.map((entry) => relative(entry.sourceFile, project.root)))];
|
|
637
|
+
return Object.freeze({
|
|
638
|
+
version: project.version,
|
|
639
|
+
root: project.root,
|
|
640
|
+
workspace: Object.freeze({
|
|
641
|
+
version: project.workspace.version,
|
|
642
|
+
kind: project.workspace.kind,
|
|
643
|
+
root: project.workspace.root,
|
|
644
|
+
configFile: project.workspace.configFile,
|
|
645
|
+
source: project.workspace.source,
|
|
646
|
+
boundary: project.workspace.boundary,
|
|
647
|
+
configInsideWorkspace: project.workspace.configInsideWorkspace !== false
|
|
648
|
+
}),
|
|
649
|
+
configFile: project.configFile,
|
|
650
|
+
entryFile: project.entryFile,
|
|
651
|
+
target: project.target || 'native',
|
|
652
|
+
strict: project.strict !== false,
|
|
653
|
+
reporting: project.reporting,
|
|
654
|
+
reportingLevel: project.reportingLevel,
|
|
655
|
+
reportingPolicy: project.reportingPolicy,
|
|
656
|
+
selectedProfile: project.selectedProfile ? Object.freeze({ ...project.selectedProfile }) : null,
|
|
657
|
+
projectHash: project.projectHash || null,
|
|
658
|
+
planHash: project.planHash || null,
|
|
659
|
+
configParity: project.configParity,
|
|
660
|
+
bindings: project.bindings || Object.freeze({ config: Object.freeze([]), secret: Object.freeze([]) }),
|
|
661
|
+
fragmentKeys: Object.freeze(Object.keys(project.fragments || {}).sort()),
|
|
662
|
+
crypto: project.crypto || null,
|
|
663
|
+
provider: project.provider,
|
|
664
|
+
providerToolchain: project.providerToolchain,
|
|
665
|
+
providerConfig: getProviderDriver(project.providerSelector || project.provider, {
|
|
666
|
+
projectRoot: project.root
|
|
667
|
+
}).projectConfigDocument(project.providerConfig),
|
|
668
|
+
schemas: Object.freeze({
|
|
669
|
+
active: project.schemas.active,
|
|
670
|
+
pointer: project.schemaFile ? relative(project.schemaFile, project.root) : null,
|
|
671
|
+
defaultNamespace: project.schemas.defaultNamespace,
|
|
672
|
+
contentTypePolicy: project.schemas.contentTypePolicy,
|
|
673
|
+
maxBytes: project.schemas.maxBytes,
|
|
674
|
+
count: schemaIds.length,
|
|
675
|
+
ids: Object.freeze(schemaIds),
|
|
676
|
+
sources: Object.freeze(schemaSources),
|
|
677
|
+
authority: schemaRegistry ? 'pulse.schema' : null,
|
|
678
|
+
registryVersion: schemaRegistry ? schemaRegistry.version : null,
|
|
679
|
+
registryHash: schemaRegistry ? schemaRegistry.registryHash : null,
|
|
680
|
+
responseCases: schemaRegistry ? Object.freeze(schemaRegistry.responses.map((entry) => entry.id)) : Object.freeze([]),
|
|
681
|
+
codecRealization: schemaRegistry ? 'cross-target-codecs' : 'inactive'
|
|
682
|
+
}),
|
|
683
|
+
outDir: project.outDir,
|
|
684
|
+
dev: Object.freeze({
|
|
685
|
+
host: project.dev.host,
|
|
686
|
+
port: project.dev.port,
|
|
687
|
+
watch: project.dev.watch,
|
|
688
|
+
maxBodyBytes: project.dev.maxBodyBytes,
|
|
689
|
+
networkFetch: project.dev.networkFetch,
|
|
690
|
+
configuredValues: Object.freeze({
|
|
691
|
+
config: Object.keys(project.dev.config).length,
|
|
692
|
+
secrets: Object.keys(project.dev.secrets).length,
|
|
693
|
+
kv: Object.keys(project.dev.kv).length,
|
|
694
|
+
fetches: Object.keys(project.dev.fetches).length
|
|
695
|
+
})
|
|
696
|
+
}),
|
|
697
|
+
testHarness: project.testsFile ? relative(project.testsFile, project.root) : null,
|
|
698
|
+
tests: project.tests.map((entry) => entry.name)
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
module.exports = {
|
|
703
|
+
PROJECT_CONFIG_VERSION,
|
|
704
|
+
PROJECT_CONFIG_SCHEMA_VERSION,
|
|
705
|
+
CONFIG_FILE_NAMES,
|
|
706
|
+
PROJECT_PROVIDERS,
|
|
707
|
+
PulseProjectError,
|
|
708
|
+
loadConventionalProject,
|
|
709
|
+
normalizeProject,
|
|
710
|
+
resolveProject,
|
|
711
|
+
projectJson,
|
|
712
|
+
normalizeHeaders,
|
|
713
|
+
normalizeExpectedHeaders,
|
|
714
|
+
normalizeRequest,
|
|
715
|
+
normalizeExpectation,
|
|
716
|
+
normalizeTestCase,
|
|
717
|
+
normalizeProvider,
|
|
718
|
+
normalizeProviderConfig,
|
|
719
|
+
normalizeSchemaConfig,
|
|
720
|
+
loadProjectHarness,
|
|
721
|
+
relative,
|
|
722
|
+
slash,
|
|
723
|
+
projectConfigSchemaDocument
|
|
724
|
+
};
|