@sanity/runtime-cli 17.0.0 → 17.1.0-next.gcfbd613
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 +24 -23
- package/dist/actions/blueprints/assets.d.ts +2 -1
- package/dist/actions/blueprints/assets.js +2 -2
- package/dist/actions/blueprints/blueprint.d.ts +1 -1
- package/dist/actions/blueprints/wait-for-operation.js +4 -0
- package/dist/actions/functions/test.js +1 -1
- package/dist/actions/sanity/examples.d.ts +4 -4
- package/dist/baseCommands.d.ts +4 -4
- package/dist/baseCommands.js +13 -2
- package/dist/commands/blueprints/add.d.ts +1 -1
- package/dist/commands/blueprints/config.d.ts +1 -1
- package/dist/commands/blueprints/deploy.d.ts +1 -1
- package/dist/commands/blueprints/destroy.d.ts +1 -1
- package/dist/commands/blueprints/doctor.d.ts +0 -1
- package/dist/commands/blueprints/doctor.js +3 -7
- package/dist/commands/blueprints/info.d.ts +1 -1
- package/dist/commands/blueprints/logs.d.ts +1 -1
- package/dist/commands/blueprints/mint-deploy-token.d.ts +1 -1
- package/dist/commands/blueprints/plan.d.ts +1 -1
- package/dist/commands/blueprints/promote.d.ts +1 -1
- package/dist/commands/blueprints/stacks.d.ts +1 -1
- package/dist/commands/functions/add.d.ts +1 -1
- package/dist/commands/functions/build.d.ts +1 -1
- package/dist/commands/functions/dev.d.ts +1 -1
- package/dist/commands/functions/env/add.d.ts +1 -1
- package/dist/commands/functions/env/list.d.ts +1 -1
- package/dist/commands/functions/env/remove.d.ts +1 -1
- package/dist/commands/functions/logs.d.ts +1 -1
- package/dist/commands/functions/test.d.ts +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +7 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/cores/blueprints/deploy.js +5 -4
- package/dist/cores/blueprints/doctor.js +15 -12
- package/dist/cores/functions/build.js +3 -2
- package/dist/cores/functions/test.js +48 -2
- package/dist/server/app.js +39 -5
- package/dist/server/handlers/invoke.d.ts +2 -2
- package/dist/server/handlers/invoke.js +7 -4
- package/dist/server/static/components/console-panel.js +59 -8
- package/dist/server/static/components/rule-panel.js +11 -8
- package/dist/server/static/components/shared-styles.js +24 -21
- package/dist/server/static/favicon-dark.svg +1 -0
- package/dist/server/static/favicon.svg +1 -0
- package/dist/server/static/index.html +2 -2
- package/dist/server/static/vendor/vendor.bundle.js +115 -61
- package/dist/server/static/web-socket.js +41 -0
- package/dist/utils/functions/child-process-wrapper.js +62 -7
- package/dist/utils/functions/find-entry-point.d.ts +1 -1
- package/dist/utils/functions/find-entry-point.js +10 -12
- package/dist/utils/functions/{invoke-local.d.ts → invoke/local.d.ts} +1 -1
- package/dist/utils/functions/invoke/local.js +306 -0
- package/dist/utils/functions/invoke/nested.d.ts +25 -0
- package/dist/utils/functions/invoke/nested.js +41 -0
- package/dist/utils/functions/invoke/prepare.d.ts +20 -0
- package/dist/utils/functions/invoke/prepare.js +101 -0
- package/dist/utils/functions/package-json.d.ts +1 -2
- package/dist/utils/functions/package-json.js +4 -6
- package/dist/utils/functions/prepare-asset.d.ts +2 -2
- package/dist/utils/functions/prepare-asset.js +18 -7
- package/dist/utils/functions/resolve-dependencies.d.ts +4 -1
- package/dist/utils/functions/resolve-dependencies.js +5 -6
- package/dist/utils/functions/resource-to-arc.d.ts +1 -1
- package/dist/utils/functions/resource-to-arc.js +4 -5
- package/dist/utils/functions/should-transpile.d.ts +7 -1
- package/dist/utils/functions/should-transpile.js +10 -10
- package/dist/utils/functions/transpile/function.d.ts +4 -1
- package/dist/utils/functions/transpile/function.js +9 -14
- package/dist/utils/functions/transpile/verify-handler.js +1 -1
- package/dist/utils/isTrueish.d.ts +6 -0
- package/dist/utils/isTrueish.js +9 -0
- package/dist/utils/logger.d.ts +7 -5
- package/dist/utils/logger.js +33 -1
- package/dist/utils/progress.d.ts +5 -0
- package/dist/utils/progress.js +40 -0
- package/dist/utils/types.d.ts +67 -5
- package/oclif.manifest.json +5 -4
- package/package.json +28 -23
- package/dist/server/static/hot-reload.js +0 -16
- package/dist/utils/functions/invoke-local.js +0 -185
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { cwd } from 'node:process';
|
|
4
|
+
import { setTimeout } from 'node:timers';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import * as groq from 'groq-js';
|
|
7
|
+
import config from '../../../config.js';
|
|
8
|
+
import { groupBlueprintResources } from '../../blueprints/group-resources.js';
|
|
9
|
+
import { isGroqContextOptions, } from '../../types.js';
|
|
10
|
+
import { acquireFunctionBuild } from './prepare.js';
|
|
11
|
+
function getChildProcessWrapperPath() {
|
|
12
|
+
return fileURLToPath(new URL('../child-process-wrapper.js', import.meta.url));
|
|
13
|
+
}
|
|
14
|
+
export function sanitizeLogs(logs) {
|
|
15
|
+
return logs.replace(/([a-zA-Z0-9]{10})[a-zA-Z0-9]{65,}/g, '$1**********');
|
|
16
|
+
}
|
|
17
|
+
export async function applyGroqRule(resource, payload, projectId, dataset) {
|
|
18
|
+
const { before, after, payload: data = null } = payload;
|
|
19
|
+
// If there is no rule set return everything
|
|
20
|
+
if (!resource.event)
|
|
21
|
+
return data;
|
|
22
|
+
// default groq rule is: gimme full doc content. otherwise, parse + eval custom rule
|
|
23
|
+
// applying the GROQ filter may result in a slimmer set of documents
|
|
24
|
+
if (resource.event.filter || resource.event.projection) {
|
|
25
|
+
const query = `*[${resource.event.filter || ''}]${resource.event.projection || ''}`;
|
|
26
|
+
try {
|
|
27
|
+
const rule = groq.parse(query, { mode: 'delta' });
|
|
28
|
+
const sanity = projectId && dataset ? { projectId, dataset } : undefined;
|
|
29
|
+
const queryResults = await groq.evaluate(rule, { dataset: [data], before, after, sanity });
|
|
30
|
+
const currentFunctionDocumentSet = await queryResults.get();
|
|
31
|
+
return currentFunctionDocumentSet[0];
|
|
32
|
+
}
|
|
33
|
+
catch (groqErr) {
|
|
34
|
+
const detail = groqErr instanceof Error && `; ${groqErr.message}`;
|
|
35
|
+
throw new Error(`Invalid rule configuration: ${query}${detail}`, { cause: groqErr });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// default groq rule so just return the data
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
export default async function invoke(resource, payload, context, options) {
|
|
42
|
+
if (!resource.src) {
|
|
43
|
+
throw new Error(`Function resource "${resource.name}" is missing the 'src' property.`);
|
|
44
|
+
}
|
|
45
|
+
const { forceColor = true, timeout = 10, onNestedInvoke, depth = 0, callStack = [] } = options;
|
|
46
|
+
// Apply the GROQ rule before preparing a build so a filtered-out event skips
|
|
47
|
+
// the (potentially expensive) transpile entirely.
|
|
48
|
+
let filteredData = payload?.payload?.data || {};
|
|
49
|
+
if (resource.type === 'sanity.function.document' ||
|
|
50
|
+
resource.type === 'sanity.function.media-library.asset') {
|
|
51
|
+
if (!isGroqContextOptions(context)) {
|
|
52
|
+
throw new Error('GROQ-based functions require a context with clientOptions');
|
|
53
|
+
}
|
|
54
|
+
const { projectId, dataset } = context.clientOptions;
|
|
55
|
+
if ('before' in payload) {
|
|
56
|
+
filteredData = await applyGroqRule(resource, payload, projectId, dataset);
|
|
57
|
+
if (typeof filteredData === 'undefined') {
|
|
58
|
+
return {
|
|
59
|
+
logs: `⚠️ Filter "${resource.event?.filter}" returned an empty result. Skipping invoke.`,
|
|
60
|
+
error: undefined,
|
|
61
|
+
json: undefined,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Acquire a shared, reference-counted build. Concurrent invokes of the same
|
|
67
|
+
// function reuse one transpile + temp package.json instead of racing on the
|
|
68
|
+
// per-function build paths; the build is torn down once every invoke releases.
|
|
69
|
+
const build = acquireFunctionBuild(resource, context.blueprintFileDir);
|
|
70
|
+
let functionPath;
|
|
71
|
+
let bundleTimings;
|
|
72
|
+
try {
|
|
73
|
+
const prepared = await build.ready;
|
|
74
|
+
functionPath = prepared.functionPath;
|
|
75
|
+
bundleTimings = prepared.bundleTimings;
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
await build.release();
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
let child;
|
|
83
|
+
let timer;
|
|
84
|
+
let executionStart;
|
|
85
|
+
// The protocol is no longer one-shot: the child stays alive across nested
|
|
86
|
+
// `context.invoke` calls, so several handlers (result/exit/error/timeout)
|
|
87
|
+
// can race to settle this Promise. `settled` ensures exactly one wins and
|
|
88
|
+
// that shutdown/cleanup only runs once.
|
|
89
|
+
let settled = false;
|
|
90
|
+
// Timeout budget is paused while nested invokes run so a function's own
|
|
91
|
+
// time limit isn't consumed by the functions it calls.
|
|
92
|
+
let pendingNested = 0;
|
|
93
|
+
let timerStart = 0;
|
|
94
|
+
let remaining = timeout * 1000;
|
|
95
|
+
const nestedTimings = {};
|
|
96
|
+
// Output the child writes directly to its stdio pipes (anything that escapes
|
|
97
|
+
// the in-process console capture, e.g. a native crash). Hoisted to the
|
|
98
|
+
// Promise scope so both the timeout and exit handlers can surface it.
|
|
99
|
+
let stdout = '';
|
|
100
|
+
let stderr = '';
|
|
101
|
+
function startTimer() {
|
|
102
|
+
timerStart = performance.now();
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
if (settled)
|
|
105
|
+
return;
|
|
106
|
+
settled = true;
|
|
107
|
+
const executionTimeMs = performance.now() - executionStart;
|
|
108
|
+
shutdown();
|
|
109
|
+
// Resolve with error to retain all logs
|
|
110
|
+
resolve({
|
|
111
|
+
json: undefined,
|
|
112
|
+
logs: sanitizeLogs(stdout),
|
|
113
|
+
error: {
|
|
114
|
+
name: 'Error',
|
|
115
|
+
message: `Timeout: Function exceeded the ${timeout}-second time limit.`,
|
|
116
|
+
stack: '',
|
|
117
|
+
},
|
|
118
|
+
timings: {
|
|
119
|
+
...bundleTimings,
|
|
120
|
+
...nestedTimings,
|
|
121
|
+
execute: executionTimeMs,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}, Math.max(0, remaining));
|
|
125
|
+
}
|
|
126
|
+
function pauseTimer() {
|
|
127
|
+
if (pendingNested === 0) {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
remaining -= performance.now() - timerStart;
|
|
130
|
+
}
|
|
131
|
+
pendingNested += 1;
|
|
132
|
+
}
|
|
133
|
+
function resumeTimer() {
|
|
134
|
+
pendingNested -= 1;
|
|
135
|
+
if (pendingNested === 0 && !settled) {
|
|
136
|
+
startTimer();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Resolve a nested `context.invoke(name, payload)` by re-entering the invoke
|
|
140
|
+
// pipeline via the supplied callback, then hand the result back to the child.
|
|
141
|
+
async function handleNestedInvoke(msg) {
|
|
142
|
+
const { id, name, payload: nestedPayload } = msg;
|
|
143
|
+
pauseTimer();
|
|
144
|
+
const nestedStart = performance.now();
|
|
145
|
+
try {
|
|
146
|
+
if (!onNestedInvoke) {
|
|
147
|
+
throw new Error('Nested context.invoke is not supported in this context');
|
|
148
|
+
}
|
|
149
|
+
const nested = await onNestedInvoke(name, nestedPayload ?? {}, context, depth + 1, callStack);
|
|
150
|
+
nestedTimings[`invoke:${name}`] =
|
|
151
|
+
(nestedTimings[`invoke:${name}`] ?? 0) + (performance.now() - nestedStart);
|
|
152
|
+
// Fire-and-forget contract: in production `context.invoke` returns once the
|
|
153
|
+
// message is enqueued (Lambda InvocationType:'Event', SNS/SQS), so a
|
|
154
|
+
// downstream handler error never reaches the caller. Mirror that here —
|
|
155
|
+
// the target ran (it was "dispatched"), so resolve the caller regardless
|
|
156
|
+
// of `nested.error`. The error isn't lost: it travels back in the returned
|
|
157
|
+
// result and is logged/streamed to the UI by the caller (app/test core).
|
|
158
|
+
if (child && !child.killed) {
|
|
159
|
+
const response = {
|
|
160
|
+
type: 'invoke-response',
|
|
161
|
+
id,
|
|
162
|
+
ok: true,
|
|
163
|
+
result: { json: nested.json },
|
|
164
|
+
};
|
|
165
|
+
child.send(JSON.stringify(response));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
// The invoke could not be dispatched at all (unknown function, recursion
|
|
170
|
+
// guard, unsupported context). That is an enqueue-time failure, which in
|
|
171
|
+
// production rejects synchronously, so propagate it to the caller.
|
|
172
|
+
if (child && !child.killed) {
|
|
173
|
+
const response = {
|
|
174
|
+
type: 'invoke-response',
|
|
175
|
+
id,
|
|
176
|
+
ok: false,
|
|
177
|
+
error: {
|
|
178
|
+
message: error instanceof Error ? error.message : String(error),
|
|
179
|
+
stack: error instanceof Error ? (error.stack ?? '') : '',
|
|
180
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
child.send(JSON.stringify(response));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
resumeTimer();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function start() {
|
|
191
|
+
executionStart = performance.now();
|
|
192
|
+
child = spawn('node', ['--enable-source-maps', getChildProcessWrapperPath()], {
|
|
193
|
+
cwd: cwd(),
|
|
194
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'], // changed: use 'pipe' so we can capture output
|
|
195
|
+
env: { ...process.env, ...resource.env, FORCE_COLOR: forceColor ? '1' : '0' },
|
|
196
|
+
});
|
|
197
|
+
child.stdout?.on('data', (data) => {
|
|
198
|
+
stdout += data.toString();
|
|
199
|
+
});
|
|
200
|
+
child.stderr?.on('data', (data) => {
|
|
201
|
+
stderr += data.toString();
|
|
202
|
+
});
|
|
203
|
+
child.on('message', (data) => {
|
|
204
|
+
let msg;
|
|
205
|
+
try {
|
|
206
|
+
msg = JSON.parse(data.toString());
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// Nested invoke request — handle without tearing down the child.
|
|
212
|
+
if (msg?.type === 'invoke-request') {
|
|
213
|
+
handleNestedInvoke({ id: msg.id, name: msg.name, payload: msg.payload });
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Terminal result message.
|
|
217
|
+
if (settled)
|
|
218
|
+
return;
|
|
219
|
+
settled = true;
|
|
220
|
+
const executionTimeMs = performance.now() - executionStart;
|
|
221
|
+
shutdown();
|
|
222
|
+
resolve({
|
|
223
|
+
json: msg.json,
|
|
224
|
+
logs: sanitizeLogs(msg.logs ?? ''),
|
|
225
|
+
error: msg.error,
|
|
226
|
+
timings: {
|
|
227
|
+
...bundleTimings,
|
|
228
|
+
...nestedTimings,
|
|
229
|
+
execute: executionTimeMs,
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
child.on('error', (error) => {
|
|
234
|
+
if (settled)
|
|
235
|
+
return;
|
|
236
|
+
settled = true;
|
|
237
|
+
shutdown();
|
|
238
|
+
reject(new Error(`encountered error ${error.message}`));
|
|
239
|
+
});
|
|
240
|
+
// Resolve on 'close' to drain the pipes
|
|
241
|
+
child.on('close', (code) => {
|
|
242
|
+
if (settled)
|
|
243
|
+
return;
|
|
244
|
+
settled = true;
|
|
245
|
+
const executionTimeMs = performance.now() - executionStart;
|
|
246
|
+
shutdown();
|
|
247
|
+
if (code !== 0) {
|
|
248
|
+
// use captured stderr if available
|
|
249
|
+
const message = stderr.trim().length > 0
|
|
250
|
+
? `Process failed with code ${code}\n${stderr}`
|
|
251
|
+
: `exited with code ${code}`;
|
|
252
|
+
// Resolve with error to retain all logs
|
|
253
|
+
resolve({
|
|
254
|
+
json: undefined,
|
|
255
|
+
logs: sanitizeLogs(stdout),
|
|
256
|
+
error: { name: 'Error', message, stack: '' },
|
|
257
|
+
timings: {
|
|
258
|
+
...bundleTimings,
|
|
259
|
+
...nestedTimings,
|
|
260
|
+
execute: executionTimeMs,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
resolve({
|
|
266
|
+
json: undefined,
|
|
267
|
+
logs: sanitizeLogs(stdout),
|
|
268
|
+
error: undefined,
|
|
269
|
+
timings: {
|
|
270
|
+
...bundleTimings,
|
|
271
|
+
...nestedTimings,
|
|
272
|
+
execute: executionTimeMs,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
startTimer();
|
|
278
|
+
const payload = {
|
|
279
|
+
data: { ...filteredData },
|
|
280
|
+
context: {
|
|
281
|
+
...context,
|
|
282
|
+
local: true,
|
|
283
|
+
resources: groupBlueprintResources(context.resources ?? []),
|
|
284
|
+
...(isGroqContextOptions(context) && {
|
|
285
|
+
clientOptions: {
|
|
286
|
+
...context.clientOptions,
|
|
287
|
+
apiHost: config.apiUrl,
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
const startMessage = { type: 'start', srcPath: functionPath, payload };
|
|
293
|
+
child.send(JSON.stringify(startMessage));
|
|
294
|
+
}
|
|
295
|
+
function shutdown() {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
if (child && !child.killed) {
|
|
298
|
+
child.kill();
|
|
299
|
+
}
|
|
300
|
+
// Release our reference to the shared build; its temp package.json and
|
|
301
|
+
// bundle are cleaned up once the last concurrent invoke releases.
|
|
302
|
+
build.release().catch((err) => console.warn('Build cleanup failed:', err));
|
|
303
|
+
}
|
|
304
|
+
start();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FunctionResource, InvocationResponse, InvokeContextOptions, InvokeExecutionOptions, InvokePayloadOptions } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound on nested `context.invoke` recursion, a backstop against invoke loops.
|
|
4
|
+
* Set to 16 to match Functions service lineage limit.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MAX_INVOKE_DEPTH = 16;
|
|
7
|
+
/**
|
|
8
|
+
* Guard a nested `context.invoke(name)` against runaway recursion and cycles.
|
|
9
|
+
* `depth` is the depth at which the target would run and `callStack` is the chain
|
|
10
|
+
* of function names already running (innermost last). Throws a descriptive error
|
|
11
|
+
* when the call must be rejected; returns normally when it is safe to proceed.
|
|
12
|
+
*
|
|
13
|
+
* This is an enqueue-time check: it runs before the target is dispatched, so the
|
|
14
|
+
* throw propagates to the caller's `context.invoke` (unlike a downstream handler
|
|
15
|
+
* error, which is fire-and-forget). See `handleNestedInvoke` in local.ts.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertNestedInvokeAllowed(name: string, depth: number, callStack: string[]): void;
|
|
18
|
+
/**
|
|
19
|
+
* Execute a single already-resolved function in its own child process as one
|
|
20
|
+
* step of an invoke chain: enforce the depth/cycle guard, run it with the call
|
|
21
|
+
* stack extended by this function, and normalize timings.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runFunction(resource: FunctionResource, payload: InvokePayloadOptions, context: InvokeContextOptions, options: InvokeExecutionOptions): Promise<InvocationResponse & {
|
|
24
|
+
timings: Record<string, number>;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import invoke from './local.js';
|
|
2
|
+
/**
|
|
3
|
+
* Upper bound on nested `context.invoke` recursion, a backstop against invoke loops.
|
|
4
|
+
* Set to 16 to match Functions service lineage limit.
|
|
5
|
+
*/
|
|
6
|
+
export const MAX_INVOKE_DEPTH = 16;
|
|
7
|
+
/**
|
|
8
|
+
* Guard a nested `context.invoke(name)` against runaway recursion and cycles.
|
|
9
|
+
* `depth` is the depth at which the target would run and `callStack` is the chain
|
|
10
|
+
* of function names already running (innermost last). Throws a descriptive error
|
|
11
|
+
* when the call must be rejected; returns normally when it is safe to proceed.
|
|
12
|
+
*
|
|
13
|
+
* This is an enqueue-time check: it runs before the target is dispatched, so the
|
|
14
|
+
* throw propagates to the caller's `context.invoke` (unlike a downstream handler
|
|
15
|
+
* error, which is fire-and-forget). See `handleNestedInvoke` in local.ts.
|
|
16
|
+
*/
|
|
17
|
+
export function assertNestedInvokeAllowed(name, depth, callStack) {
|
|
18
|
+
if (depth > MAX_INVOKE_DEPTH) {
|
|
19
|
+
throw new Error(`Maximum invoke depth (${MAX_INVOKE_DEPTH}) exceeded`);
|
|
20
|
+
}
|
|
21
|
+
if (callStack.includes(name)) {
|
|
22
|
+
throw new Error(`Invoke cycle detected: ${[...callStack, name].join(' → ')}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Execute a single already-resolved function in its own child process as one
|
|
27
|
+
* step of an invoke chain: enforce the depth/cycle guard, run it with the call
|
|
28
|
+
* stack extended by this function, and normalize timings.
|
|
29
|
+
*/
|
|
30
|
+
export async function runFunction(resource, payload, context, options) {
|
|
31
|
+
const { forceColor, timeout, depth = 0, callStack = [], onNestedInvoke } = options;
|
|
32
|
+
assertNestedInvokeAllowed(resource.name, depth, callStack);
|
|
33
|
+
const response = await invoke(resource, payload, context, {
|
|
34
|
+
forceColor,
|
|
35
|
+
timeout: timeout ?? resource.timeout,
|
|
36
|
+
onNestedInvoke,
|
|
37
|
+
depth,
|
|
38
|
+
callStack: [...callStack, resource.name],
|
|
39
|
+
});
|
|
40
|
+
return { ...response, timings: response.timings ?? {} };
|
|
41
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FunctionResource } from '../../types.js';
|
|
2
|
+
/** Everything a child process needs to run a prepared function. */
|
|
3
|
+
export interface PreparedFunction {
|
|
4
|
+
/** Absolute path to the entry file the child should import. */
|
|
5
|
+
functionPath: string;
|
|
6
|
+
/** Timings recorded while bundling, merged into the invocation result. */
|
|
7
|
+
bundleTimings?: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
/** A reference-counted handle to a shared build. Call `release()` once per acquire. */
|
|
10
|
+
export interface FunctionBuild {
|
|
11
|
+
ready: Promise<PreparedFunction>;
|
|
12
|
+
release: () => Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Acquire a shared, reference-counted build for `resource`. The first caller runs
|
|
16
|
+
* the transpile + temp package.json setup; concurrent callers reuse it. Every
|
|
17
|
+
* caller MUST call `release()` exactly once when its child has exited, so the
|
|
18
|
+
* shared artifacts are torn down only after the last in-flight invoke finishes.
|
|
19
|
+
*/
|
|
20
|
+
export declare function acquireFunctionBuild(resource: FunctionResource, blueprintFileDir: string): FunctionBuild;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { findFunctionEntryPoint } from '../find-entry-point.js';
|
|
3
|
+
import { cleanupTempPackageJson, createTempPackageJson, doesPackageJsonExists, } from '../package-json.js';
|
|
4
|
+
import { resolveResourceDependencies } from '../resolve-dependencies.js';
|
|
5
|
+
import { shouldAutoResolveDependencies } from '../should-auto-resolve-deps.js';
|
|
6
|
+
import { shouldTranspileFunction } from '../should-transpile.js';
|
|
7
|
+
import { transpileFunction } from '../transpile/function.js';
|
|
8
|
+
/**
|
|
9
|
+
* In-progress builds keyed by function identity. Concurrent invokes of the same
|
|
10
|
+
* function (e.g. `Promise.all([context.invoke('f'), context.invoke('f')])`) share
|
|
11
|
+
* a single build and a single temp package.json, so they don't race on the
|
|
12
|
+
* deterministic per-function paths (`.build/function-<name>`, the temp
|
|
13
|
+
* package.json). The entry is removed once the last concurrent invoke releases
|
|
14
|
+
* it, so a later, non-overlapping invoke rebuilds and picks up source changes.
|
|
15
|
+
*/
|
|
16
|
+
const builds = new Map();
|
|
17
|
+
function buildKey(resource) {
|
|
18
|
+
return `${resource.name}:${resource.src}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Acquire a shared, reference-counted build for `resource`. The first caller runs
|
|
22
|
+
* the transpile + temp package.json setup; concurrent callers reuse it. Every
|
|
23
|
+
* caller MUST call `release()` exactly once when its child has exited, so the
|
|
24
|
+
* shared artifacts are torn down only after the last in-flight invoke finishes.
|
|
25
|
+
*/
|
|
26
|
+
export function acquireFunctionBuild(resource, blueprintFileDir) {
|
|
27
|
+
const key = buildKey(resource);
|
|
28
|
+
let entry = builds.get(key);
|
|
29
|
+
if (!entry) {
|
|
30
|
+
let cleanupBundle = async () => { };
|
|
31
|
+
const resolvedFunctionPath = path.resolve(blueprintFileDir, resource.src);
|
|
32
|
+
let functionPath = resolvedFunctionPath;
|
|
33
|
+
let createdTempPackageJson = false;
|
|
34
|
+
// If no package.json exists we create a temporary one to avoid the
|
|
35
|
+
// "module type" warning from node.
|
|
36
|
+
const existingPackageJson = doesPackageJsonExists(functionPath);
|
|
37
|
+
const ready = (async () => {
|
|
38
|
+
if (!resource.src) {
|
|
39
|
+
throw new Error(`Function resource "${resource.name}" is missing the 'src' property.`);
|
|
40
|
+
}
|
|
41
|
+
let bundleTimings;
|
|
42
|
+
const shouldTranspile = await shouldTranspileFunction(resource, functionPath);
|
|
43
|
+
if (shouldTranspile.transpile) {
|
|
44
|
+
const transpileResult = await transpileFunction({
|
|
45
|
+
resource,
|
|
46
|
+
functionPath,
|
|
47
|
+
entryPoint: shouldTranspile.entryPoint,
|
|
48
|
+
});
|
|
49
|
+
functionPath = await findFunctionEntryPoint(transpileResult.outputDir);
|
|
50
|
+
bundleTimings = transpileResult.timings;
|
|
51
|
+
cleanupBundle = transpileResult.cleanup;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
functionPath = await findFunctionEntryPoint(functionPath, resource.displayName ?? resource.name);
|
|
55
|
+
}
|
|
56
|
+
const shouldResolveDependencies = await shouldAutoResolveDependencies(resource);
|
|
57
|
+
if (shouldResolveDependencies && existingPackageJson) {
|
|
58
|
+
await resolveResourceDependencies({
|
|
59
|
+
resource,
|
|
60
|
+
functionPath: resolvedFunctionPath,
|
|
61
|
+
blueprintFileDir,
|
|
62
|
+
transpiled: shouldTranspile.transpile,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (!existingPackageJson) {
|
|
66
|
+
createTempPackageJson(functionPath);
|
|
67
|
+
createdTempPackageJson = true;
|
|
68
|
+
}
|
|
69
|
+
return { functionPath, bundleTimings };
|
|
70
|
+
})();
|
|
71
|
+
entry = {
|
|
72
|
+
refs: 0,
|
|
73
|
+
ready,
|
|
74
|
+
teardown: async () => {
|
|
75
|
+
// rmSync is synchronous, so the temp package.json is gone before this
|
|
76
|
+
// function yields — a later invoke of the same function can't observe a
|
|
77
|
+
// half-removed state.
|
|
78
|
+
if (createdTempPackageJson) {
|
|
79
|
+
cleanupTempPackageJson(functionPath);
|
|
80
|
+
}
|
|
81
|
+
await cleanupBundle().catch((err) => console.warn('Bundle cleanup failed:', err));
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
builds.set(key, entry);
|
|
85
|
+
}
|
|
86
|
+
const activeEntry = entry;
|
|
87
|
+
activeEntry.refs += 1;
|
|
88
|
+
let released = false;
|
|
89
|
+
const release = async () => {
|
|
90
|
+
if (released)
|
|
91
|
+
return;
|
|
92
|
+
released = true;
|
|
93
|
+
activeEntry.refs -= 1;
|
|
94
|
+
if (activeEntry.refs === 0) {
|
|
95
|
+
if (builds.get(key) === activeEntry)
|
|
96
|
+
builds.delete(key);
|
|
97
|
+
await activeEntry.teardown();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return { ready: activeEntry.ready, release };
|
|
101
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function doesPackageJsonExists(resource: FunctionResource): boolean;
|
|
1
|
+
export declare function doesPackageJsonExists(functionPath: string): boolean;
|
|
3
2
|
export declare function createTempPackageJson(functionPath: string): boolean;
|
|
4
3
|
export declare function cleanupTempPackageJson(functionPath: string): boolean;
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join
|
|
3
|
-
import { cwd } from 'node:process';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
4
3
|
function getPackageJsonPath(functionPath) {
|
|
5
4
|
const functionDir = dirname(functionPath);
|
|
6
5
|
return join(functionDir, 'package.json');
|
|
7
6
|
}
|
|
8
|
-
export function doesPackageJsonExists(
|
|
9
|
-
if (!
|
|
7
|
+
export function doesPackageJsonExists(functionPath) {
|
|
8
|
+
if (!functionPath) {
|
|
10
9
|
// Cannot determine without a source path
|
|
11
10
|
return false;
|
|
12
11
|
}
|
|
13
|
-
const
|
|
14
|
-
const packageJsonPath = join(absolutePath, 'package.json');
|
|
12
|
+
const packageJsonPath = join(functionPath, 'package.json');
|
|
15
13
|
return existsSync(packageJsonPath);
|
|
16
14
|
}
|
|
17
15
|
export function createTempPackageJson(functionPath) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AssetPrepResult, FunctionResource, InstallerType } from '../types.js';
|
|
2
|
-
export declare function prepareAsset({ resource, }: {
|
|
2
|
+
export declare function prepareAsset({ blueprintFileDir, resource, installer, }: {
|
|
3
|
+
blueprintFileDir: string;
|
|
3
4
|
resource: FunctionResource;
|
|
4
|
-
}, { installer }?: {
|
|
5
5
|
installer?: InstallerType;
|
|
6
6
|
}): Promise<AssetPrepResult>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { rm, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { cwd } from 'node:process';
|
|
5
4
|
import { MAX_ASSET_SIZE } from '../../constants.js';
|
|
6
5
|
import { detectNativeModules, errorMessage } from './detect-native-modules.js';
|
|
7
6
|
import { getFolderSize } from './get-folder-size.js';
|
|
@@ -9,16 +8,22 @@ import { resolveResourceDependencies } from './resolve-dependencies.js';
|
|
|
9
8
|
import { shouldAutoResolveDependencies } from './should-auto-resolve-deps.js';
|
|
10
9
|
import { shouldTranspileFunction } from './should-transpile.js';
|
|
11
10
|
import { transpileFunction } from './transpile/function.js';
|
|
12
|
-
export async function prepareAsset({
|
|
11
|
+
export async function prepareAsset({ blueprintFileDir, resource, installer, }) {
|
|
13
12
|
if (!resource.src)
|
|
14
13
|
throw new Error('Resource src is required');
|
|
15
|
-
|
|
14
|
+
const resolvedFunctionPath = path.resolve(blueprintFileDir, resource.src);
|
|
15
|
+
let functionPath = resolvedFunctionPath;
|
|
16
16
|
let cleanup = async () => { };
|
|
17
17
|
let wasBundled = false;
|
|
18
|
-
const shouldTranspile = await shouldTranspileFunction(resource);
|
|
19
|
-
if (shouldTranspile) {
|
|
18
|
+
const shouldTranspile = await shouldTranspileFunction(resource, functionPath);
|
|
19
|
+
if (shouldTranspile.transpile) {
|
|
20
20
|
try {
|
|
21
|
-
const result = await transpileFunction(
|
|
21
|
+
const result = await transpileFunction({
|
|
22
|
+
resource,
|
|
23
|
+
functionPath,
|
|
24
|
+
entryPoint: shouldTranspile.entryPoint,
|
|
25
|
+
installer,
|
|
26
|
+
});
|
|
22
27
|
functionPath = result.outputDir;
|
|
23
28
|
cleanup = result.cleanup;
|
|
24
29
|
wasBundled = result.bundled;
|
|
@@ -29,7 +34,13 @@ export async function prepareAsset({ resource, }, { installer } = {}) {
|
|
|
29
34
|
}
|
|
30
35
|
const shouldResolveDependencies = await shouldAutoResolveDependencies(resource);
|
|
31
36
|
if (shouldResolveDependencies && !wasBundled) {
|
|
32
|
-
await resolveResourceDependencies(
|
|
37
|
+
await resolveResourceDependencies({
|
|
38
|
+
resource,
|
|
39
|
+
functionPath: resolvedFunctionPath,
|
|
40
|
+
blueprintFileDir,
|
|
41
|
+
transpiled: shouldTranspile.transpile,
|
|
42
|
+
installer,
|
|
43
|
+
});
|
|
33
44
|
await removeArcAutoInstallPackageJson(functionPath);
|
|
34
45
|
}
|
|
35
46
|
if (existsSync(path.join(functionPath, 'node_modules'))) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { FunctionResource, InstallerType } from '../types.js';
|
|
2
|
-
export declare function resolveResourceDependencies(resource
|
|
2
|
+
export declare function resolveResourceDependencies({ resource, transpiled, blueprintFileDir, functionPath, installer, }: {
|
|
3
|
+
resource: FunctionResource;
|
|
4
|
+
blueprintFileDir: string;
|
|
5
|
+
functionPath: string;
|
|
3
6
|
transpiled: boolean;
|
|
4
7
|
installer?: InstallerType;
|
|
5
8
|
}): Promise<void>;
|
|
@@ -3,18 +3,17 @@ import { join } from 'node:path';
|
|
|
3
3
|
import hydrate from '@architect/hydrate';
|
|
4
4
|
import inventory from '@architect/inventory';
|
|
5
5
|
import { convertResourceToArcFormat } from './resource-to-arc.js';
|
|
6
|
-
export async function resolveResourceDependencies(resource,
|
|
7
|
-
const rawArc = await convertResourceToArcFormat(resource, transpiled);
|
|
8
|
-
const inv = await inventory({ rawArc });
|
|
9
|
-
const cwd = inv.inv._project.cwd;
|
|
6
|
+
export async function resolveResourceDependencies({ resource, transpiled, blueprintFileDir, functionPath, installer, }) {
|
|
7
|
+
const rawArc = await convertResourceToArcFormat(resource, functionPath, transpiled);
|
|
8
|
+
const inv = await inventory({ rawArc, cwd: blueprintFileDir });
|
|
10
9
|
let installType = 'npm';
|
|
11
10
|
if (installer) {
|
|
12
11
|
installType = installer;
|
|
13
12
|
}
|
|
14
|
-
else if (existsSync(join(
|
|
13
|
+
else if (existsSync(join(blueprintFileDir, 'pnpm-lock.yaml'))) {
|
|
15
14
|
installType = 'pnpm';
|
|
16
15
|
}
|
|
17
|
-
else if (existsSync(join(
|
|
16
|
+
else if (existsSync(join(blueprintFileDir, 'yarn.lock'))) {
|
|
18
17
|
installType = 'yarn';
|
|
19
18
|
}
|
|
20
19
|
try {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FunctionResource } from '../types.js';
|
|
2
|
-
export declare function convertResourceToArcFormat(resource: FunctionResource, transpiled: boolean): Promise<string>;
|
|
2
|
+
export declare function convertResourceToArcFormat(resource: FunctionResource, functionPath: string, transpiled: boolean): Promise<string>;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export async function convertResourceToArcFormat(resource, transpiled) {
|
|
2
|
+
import { sep } from 'node:path';
|
|
3
|
+
export async function convertResourceToArcFormat(resource, functionPath, transpiled) {
|
|
5
4
|
if (!resource.src) {
|
|
6
5
|
throw Error('Missing `resource.src` property');
|
|
7
6
|
}
|
|
8
7
|
// Get stats from file
|
|
9
|
-
const stats = await stat(
|
|
8
|
+
const stats = await stat(functionPath);
|
|
10
9
|
// Convert resource.src to arc file path format
|
|
11
10
|
const srcPath = resource.src?.split(sep).join('/');
|
|
12
11
|
const entryDir = stats.isFile() ? srcPath.substring(0, srcPath.lastIndexOf('/')) : srcPath;
|
|
13
|
-
|
|
12
|
+
functionPath = transpiled ? `${entryDir}/.build/function-${resource.name}` : entryDir;
|
|
14
13
|
return `@app
|
|
15
14
|
hydrate-function
|
|
16
15
|
|
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
import type { FunctionResource } from '../types.js';
|
|
2
|
-
export
|
|
2
|
+
export type ShouldTranspileResult = {
|
|
3
|
+
transpile: true;
|
|
4
|
+
entryPoint: string;
|
|
5
|
+
} | {
|
|
6
|
+
transpile: false;
|
|
7
|
+
};
|
|
8
|
+
export declare function shouldTranspileFunction(resource: FunctionResource, functionPath: string): Promise<ShouldTranspileResult>;
|