@uipath/maestro-builder-sdk 6.56.0 → 6.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-index.md +6 -2
- package/dist/api-members.md +1 -1
- package/dist/cli/commands/flow.js +2 -0
- package/dist/core/script-function.d.ts +126 -0
- package/dist/core/script-function.js +510 -0
- package/dist/decompile-cli.js +11 -3
- package/dist/decompile.d.ts +9 -0
- package/dist/decompile.js +76 -3
- package/dist/flow-node-type.d.ts +21 -0
- package/dist/load.js +13 -0
- package/dist/nodes/script/flow/factories.d.ts +42 -1
- package/dist/nodes/script/flow/factories.js +8 -9
- package/dist/nodes/script/flow/node-type.js +71 -0
- package/dist/nodes/script/flow/runtime.d.ts +20 -0
- package/dist/nodes/script/flow/runtime.js +29 -0
- package/dist/nodes/script/flow/spec.d.ts +32 -1
- package/dist/script-function-scope.d.ts +57 -0
- package/dist/script-function-scope.js +203 -0
- package/package.json +2 -2
package/dist/decompile.js
CHANGED
|
@@ -112,6 +112,38 @@ let runAbsorbed = new Map();
|
|
|
112
112
|
let runFlowId = 'flow';
|
|
113
113
|
/** `DecompileOptions.readSidecar` for the current run. */
|
|
114
114
|
let runReadSidecar;
|
|
115
|
+
/**
|
|
116
|
+
* A host's own say on decompile's default script form, beside the TypeScript
|
|
117
|
+
* hand-over in core/typescript-module.ts:
|
|
118
|
+
* `globalThis[Symbol.for('uipath.maestro-builder-sdk.scripts')] = 'string'`.
|
|
119
|
+
*/
|
|
120
|
+
const HOST_SCRIPTS = Symbol.for('uipath.maestro-builder-sdk.scripts');
|
|
121
|
+
/**
|
|
122
|
+
* Function scripts by default only under Node. The exact text a function form
|
|
123
|
+
* stores is what the loader's `Function.prototype.toString()` returns, which is
|
|
124
|
+
* the author's own text under Node's type stripping (what `compile` and `uip`
|
|
125
|
+
* run) and a reprint under other loaders. A host that runs the SDK in a page has
|
|
126
|
+
* not been measured, so there a decompiled script stays a string unless asked.
|
|
127
|
+
*/
|
|
128
|
+
function defaultScripts() {
|
|
129
|
+
const host = globalThis[HOST_SCRIPTS];
|
|
130
|
+
if (host === 'function' || host === 'string')
|
|
131
|
+
return host;
|
|
132
|
+
// Real Node, tested as core/typescript-module.ts does: a page bundle may shim
|
|
133
|
+
// `process.versions.node` (cli-browser's banner sets it) but does not define
|
|
134
|
+
// `process.getBuiltinModule`.
|
|
135
|
+
return typeof globalThis.process?.getBuiltinModule === 'function' ? 'function' : 'string';
|
|
136
|
+
}
|
|
137
|
+
/** How this run prints scripts (`DecompileOptions.scripts`). */
|
|
138
|
+
let runScripts = 'function';
|
|
139
|
+
/** The module-level functions print hooks declared this run, in order. */
|
|
140
|
+
let runModuleFunctions = [];
|
|
141
|
+
/**
|
|
142
|
+
* Each step's reserved function name, by node: keyed by the node itself, not its
|
|
143
|
+
* id, because a root step and a subflow step may share an id, and two
|
|
144
|
+
* `function script_<id>` declarations would make the module unloadable.
|
|
145
|
+
*/
|
|
146
|
+
let runFunctionNames = new Map();
|
|
115
147
|
function warnOnce(message) {
|
|
116
148
|
if (runWarned.has(message))
|
|
117
149
|
return;
|
|
@@ -336,6 +368,8 @@ class Imports {
|
|
|
336
368
|
locals = new Set(['flow']);
|
|
337
369
|
/** Names a package may not take bare: the SDK's helpers and every built-in factory. */
|
|
338
370
|
reserved;
|
|
371
|
+
/** The SDK's type-only names (`FlowVars`): `import { type X }`, erased under type stripping. */
|
|
372
|
+
types = new Set();
|
|
339
373
|
constructor() {
|
|
340
374
|
this.reserved = new Set([
|
|
341
375
|
...SDK_HELPER_NAMES,
|
|
@@ -368,14 +402,25 @@ class Imports {
|
|
|
368
402
|
this.locals.add(local);
|
|
369
403
|
return local;
|
|
370
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* A type-only name from the SDK: `import { type X }`, erased under type
|
|
407
|
+
* stripping, so it never needs a runtime export. Taken as a local, so no
|
|
408
|
+
* package factory binds the same name.
|
|
409
|
+
*/
|
|
410
|
+
useType(name) {
|
|
411
|
+
this.types.add(name);
|
|
412
|
+
this.locals.add(name);
|
|
413
|
+
return name;
|
|
414
|
+
}
|
|
371
415
|
/** One import per module: the SDK first, then the packages by name. */
|
|
372
416
|
line(specifier) {
|
|
373
417
|
const lines = [];
|
|
374
418
|
for (const [module, names] of [...this.used].sort(([a], [b]) => (a === undefined ? -1 : b === undefined ? 1 : a.localeCompare(b)))) {
|
|
375
|
-
|
|
419
|
+
const types = module === undefined ? [...this.types].filter((t) => !names.has(t)).sort().map((t) => `type ${t}`) : [];
|
|
420
|
+
if (names.size === 0 && types.length === 0)
|
|
376
421
|
continue;
|
|
377
422
|
const bindings = [...names].sort(([a], [b]) => a.localeCompare(b)).map(([name, local]) => (local === name ? name : `${name} as ${local}`));
|
|
378
|
-
lines.push(`import { ${bindings.join(', ')} } from ${tsLiteral(module ?? specifier)};`);
|
|
423
|
+
lines.push(`import { ${[...bindings, ...types].join(', ')} } from ${tsLiteral(module ?? specifier)};`);
|
|
379
424
|
}
|
|
380
425
|
return lines.join('\n');
|
|
381
426
|
}
|
|
@@ -826,6 +871,11 @@ function emitRegisteredSpec(node, imp, inputNames, graph) {
|
|
|
826
871
|
json: (v) => tsLiteral(v),
|
|
827
872
|
key: (k) => key(k),
|
|
828
873
|
helper: (name) => imp.use(name),
|
|
874
|
+
typeHelper: (name) => imp.useType(name),
|
|
875
|
+
warn: (message) => warnOnce(`step ${tsLiteral(node.id)}: ${message}`),
|
|
876
|
+
functionName: () => moduleFunctionName(node),
|
|
877
|
+
declareFunction: (source) => { runModuleFunctions.push(source); },
|
|
878
|
+
scripts: runScripts,
|
|
829
879
|
};
|
|
830
880
|
const call = impl.print?.(spec, print) ?? {
|
|
831
881
|
factory: impl.factory,
|
|
@@ -2838,6 +2888,9 @@ function foldOutputBacking(flow) {
|
|
|
2838
2888
|
}
|
|
2839
2889
|
export function decompile(flow, options = {}) {
|
|
2840
2890
|
flow = foldOutputBacking(flow);
|
|
2891
|
+
runScripts = options.scripts ?? defaultScripts();
|
|
2892
|
+
runModuleFunctions = [];
|
|
2893
|
+
runFunctionNames = new Map();
|
|
2841
2894
|
const importSpecifier = options.importSpecifier ?? './flow-sdk.js';
|
|
2842
2895
|
runStrict = options.bestEffort === false;
|
|
2843
2896
|
runWarn = options.warn;
|
|
@@ -2991,7 +3044,27 @@ export function decompile(flow, options = {}) {
|
|
|
2991
3044
|
const hoisted = [...carriedConsts, ...runRawManifests]
|
|
2992
3045
|
.map(([name, json]) => `const ${name} = ${json};`)
|
|
2993
3046
|
.join('\n\n');
|
|
2994
|
-
|
|
3047
|
+
const functions = runModuleFunctions.join('\n\n');
|
|
3048
|
+
return [imp.line(importSpecifier), hoisted, chain, functions].filter((p) => p.length > 0).join('\n\n') + '\n';
|
|
3049
|
+
}
|
|
3050
|
+
/**
|
|
3051
|
+
* A step's module-level function name: `script_<id>`, with what an identifier
|
|
3052
|
+
* cannot hold replaced by `_`, and a numeric suffix when two steps (in different
|
|
3053
|
+
* scopes, or with ids that sanitize alike) would get the same one.
|
|
3054
|
+
* The `script_` prefix keeps it clear of every SDK export (all camelCase) and of
|
|
3055
|
+
* the hoisted constants.
|
|
3056
|
+
*/
|
|
3057
|
+
function moduleFunctionName(node) {
|
|
3058
|
+
const have = runFunctionNames.get(node);
|
|
3059
|
+
if (have)
|
|
3060
|
+
return have;
|
|
3061
|
+
const base = `script_${node.id.replace(/[^A-Za-z0-9_$]/g, '_')}`;
|
|
3062
|
+
const taken = new Set(runFunctionNames.values());
|
|
3063
|
+
let name = base;
|
|
3064
|
+
for (let i = 2; taken.has(name); i++)
|
|
3065
|
+
name = `${base}_${i}`;
|
|
3066
|
+
runFunctionNames.set(node, name);
|
|
3067
|
+
return name;
|
|
2995
3068
|
}
|
|
2996
3069
|
/**
|
|
2997
3070
|
* Top-level nodes (no `parentId`) that no root trigger reaches over ANY edge —
|
package/dist/flow-node-type.d.ts
CHANGED
|
@@ -327,6 +327,27 @@ export interface FlowPrintContext {
|
|
|
327
327
|
key(name: string): string;
|
|
328
328
|
/** An authoring helper (`lit`, `input`, …) by name: imported into the printed source, and its identifier. */
|
|
329
329
|
helper(name: string): string;
|
|
330
|
+
/** An exported TYPE (`FlowVars`) by name: imported as `type <name>`, and its identifier. */
|
|
331
|
+
typeHelper(name: string): string;
|
|
332
|
+
/**
|
|
333
|
+
* Report a finding about this step, printed once with the step named.
|
|
334
|
+
* Informational: it never fails `--strict`, so it is for a choice of spelling
|
|
335
|
+
* that loses nothing, not for something recompiling would drop.
|
|
336
|
+
*/
|
|
337
|
+
warn(message: string): void;
|
|
338
|
+
/**
|
|
339
|
+
* The identifier of this step's module-level function: unique in the printed
|
|
340
|
+
* module, the same on every call for the step. Reserving it declares nothing;
|
|
341
|
+
* {@link declareFunction} does.
|
|
342
|
+
*/
|
|
343
|
+
functionName(): string;
|
|
344
|
+
/** Add a declaration to the end of the printed module, after the flow, verbatim. */
|
|
345
|
+
declareFunction(source: string): void;
|
|
346
|
+
/**
|
|
347
|
+
* How scripts print: `function` (the default) as a TypeScript function where
|
|
348
|
+
* the body allows it, `string` always as `script({ code })`.
|
|
349
|
+
*/
|
|
350
|
+
readonly scripts: 'function' | 'string';
|
|
330
351
|
}
|
|
331
352
|
/** What a node-local `check` may ask of the checker. */
|
|
332
353
|
export interface FlowCheckContext {
|
package/dist/load.js
CHANGED
|
@@ -3,6 +3,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
3
3
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { statSync } from 'node:fs';
|
|
5
5
|
import { useThisSdkForAuthoredSource } from './sdk-resolve.js';
|
|
6
|
+
import { scriptFunctionLowerings } from './core/script-function.js';
|
|
7
|
+
import { scriptFunctionScopeProblem } from './script-function-scope.js';
|
|
6
8
|
/** Resolve a user argument (a `.ts` path or a BaseName) to a flow source file. */
|
|
7
9
|
export function resolveFlowFile(arg) {
|
|
8
10
|
const candidates = [arg, `${arg}.flow.ts`, `${arg}.ts`];
|
|
@@ -21,6 +23,7 @@ export function resolveFlowFile(arg) {
|
|
|
21
23
|
}
|
|
22
24
|
export async function loadBuiltFlow(file) {
|
|
23
25
|
useThisSdkForAuthoredSource();
|
|
26
|
+
const firstLowering = scriptFunctionLowerings().length;
|
|
24
27
|
let mod;
|
|
25
28
|
try {
|
|
26
29
|
mod = await import(pathToFileURL(file).href);
|
|
@@ -34,6 +37,16 @@ export async function loadBuiltFlow(file) {
|
|
|
34
37
|
const built = typeof exp.build === 'function' ? exp.build() : exp;
|
|
35
38
|
if (!built || !Array.isArray(built.steps))
|
|
36
39
|
throw new Error(`${file} default export is not a built flow. Did you call .build()?`);
|
|
40
|
+
// A `script(fn)` function was lowered from its own text, which cannot show a
|
|
41
|
+
// name it takes from its module; this source can (script-function-scope.ts).
|
|
42
|
+
// Only a flow that lowered one pays for the program this builds.
|
|
43
|
+
// The functions this import lowered; see scriptFunctionLowerings.
|
|
44
|
+
const lowered = scriptFunctionLowerings().slice(firstLowering);
|
|
45
|
+
if (lowered.length > 0) {
|
|
46
|
+
const problem = scriptFunctionScopeProblem(file, lowered);
|
|
47
|
+
if (problem)
|
|
48
|
+
throw new Error(problem);
|
|
49
|
+
}
|
|
37
50
|
return built;
|
|
38
51
|
}
|
|
39
52
|
/**
|
|
@@ -3,7 +3,48 @@
|
|
|
3
3
|
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
4
|
*/
|
|
5
5
|
import { type ActionSpec } from '../../../core/actions.js';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ScriptReturns } from '../../../core/script-return.js';
|
|
7
|
+
import type { ScriptInputs, ScriptValue } from './spec.js';
|
|
8
|
+
/**
|
|
9
|
+
* Declare an inline script action, as a TypeScript function the compiler checks.
|
|
10
|
+
*
|
|
11
|
+
* The function is never called. Its body, exactly as written, becomes the
|
|
12
|
+
* script; its parameter is an authoring-time name for the Flow runtime's own
|
|
13
|
+
* `$vars` global and is renamed to it. `returns` is the declaration that reaches
|
|
14
|
+
* the `.flow`, and tsc checks the function's return against it.
|
|
15
|
+
*
|
|
16
|
+
* The function may read only what the script runtime has (the ES2020 built-ins
|
|
17
|
+
* and `console`): a module constant, an import or a library throws here, because
|
|
18
|
+
* in the run it would be a `ReferenceError`.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* .step('post', script(parseMessage, { returns: { channel: 'string', threadTs: 'string' } }))
|
|
22
|
+
*
|
|
23
|
+
* function parseMessage(vars: FlowVars) {
|
|
24
|
+
* const m = /archives\/([A-Z0-9]+)\/p(\d{10})(\d{6})/.exec(String(vars.start.output.message));
|
|
25
|
+
* if (!m) throw new Error('not a Slack permalink');
|
|
26
|
+
* return { channel: m[1], threadTs: m[2] + '.' + m[3] };
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @param fn - The script, as a function of the variable scope (or of nothing).
|
|
31
|
+
* @param options - What the function returns (`returns`), as `script({ returns })` takes it.
|
|
32
|
+
* @returns An action spec to pass to `.step(name, …)`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function script<V, const R extends ScriptReturns>(fn: (vars: V) => ScriptValue<R>, options: {
|
|
35
|
+
returns: R;
|
|
36
|
+
}): ActionSpec;
|
|
37
|
+
/**
|
|
38
|
+
* Declare an inline script action, as a TypeScript function, with no declared
|
|
39
|
+
* return: the body's type is inferred as it is for a string body.
|
|
40
|
+
*
|
|
41
|
+
* @param fn - The script, as a function of the variable scope (or of nothing).
|
|
42
|
+
* @param options - No `returns`; pass one to have tsc check the function against it.
|
|
43
|
+
* @returns An action spec to pass to `.step(name, …)`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function script<V>(fn: (vars: V) => unknown, options?: {
|
|
46
|
+
returns?: never;
|
|
47
|
+
}): ActionSpec;
|
|
7
48
|
/**
|
|
8
49
|
* Declare an inline script action.
|
|
9
50
|
*
|
|
@@ -3,13 +3,12 @@
|
|
|
3
3
|
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
4
|
*/
|
|
5
5
|
import { builtinActionSpec } from '../../../core/actions.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return builtinActionSpec({ kind: 'script', inputs });
|
|
6
|
+
import { lowerScriptFunction } from '../../../core/script-function.js';
|
|
7
|
+
import { FLOW_SCRIPT_RUNTIME } from './runtime.js';
|
|
8
|
+
export function script(arg, options) {
|
|
9
|
+
if (typeof arg !== 'function')
|
|
10
|
+
return builtinActionSpec({ kind: 'script', inputs: arg });
|
|
11
|
+
const code = lowerScriptFunction(arg, FLOW_SCRIPT_RUNTIME, `script(${arg.name || 'function'})`);
|
|
12
|
+
const returns = options?.returns;
|
|
13
|
+
return builtinActionSpec({ kind: 'script', inputs: { code, ...(returns !== undefined ? { returns } : {}) } });
|
|
15
14
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { declaredScriptShape, inferScriptShape } from '../../../core/script-return.js';
|
|
8
8
|
import { jsonClone } from '../../../core/json-clone.js';
|
|
9
|
+
import { scriptBodyEarlyError, scriptBodySyntaxError, scriptBodyTypeScriptSyntax, tryLowerScriptFunction } from '../../../core/script-function.js';
|
|
10
|
+
import { FLOW_SCRIPT_RUNTIME } from './runtime.js';
|
|
9
11
|
import { checkScript } from './check.js';
|
|
10
12
|
/**
|
|
11
13
|
* Declare what a `core.action.script` node actually publishes.
|
|
@@ -83,6 +85,72 @@ function liftReturns(node, code, definitionType) {
|
|
|
83
85
|
}
|
|
84
86
|
return declared;
|
|
85
87
|
}
|
|
88
|
+
/** The fallback reason for a body that parses alone but not as the printed function, or comes back changed. */
|
|
89
|
+
const UNCHANGED = 'its text would not come back unchanged as a function body (a line comment on its last line, for one).';
|
|
90
|
+
/**
|
|
91
|
+
* The function a script step prints as, declared after the flow, or undefined
|
|
92
|
+
* when the step keeps the string form (`script({ code })`), with a warning.
|
|
93
|
+
*
|
|
94
|
+
* A designer-edited flow may hold a half-written script, and decompile still has
|
|
95
|
+
* to produce a source that builds, so a body that cannot become a function is a
|
|
96
|
+
* reason to keep the string, never to fail. It becomes one only when all of
|
|
97
|
+
* these hold (docs/SCRIPT_FUNCTION_DESIGN.md, "Decompile"):
|
|
98
|
+
*
|
|
99
|
+
* 1. it parses;
|
|
100
|
+
* 2. it has no TypeScript-only syntax, which the loader would erase or reread
|
|
101
|
+
* (`const n: number`, or `f<T>(x)`, which JavaScript reads as comparisons);
|
|
102
|
+
* 3. `script(fn)` would accept it: it reads only names on the runtime's list
|
|
103
|
+
* (a function that failed this would throw when the source loads);
|
|
104
|
+
* 4. it comes back from compile byte for byte. The function is printed with
|
|
105
|
+
* the body verbatim between its braces, which lowering returns exactly, so
|
|
106
|
+
* this holds for nearly every body that passes the others; it is checked
|
|
107
|
+
* because it is cheap, and a line comment on the last line is the case that
|
|
108
|
+
* fails it (it would swallow the closing brace);
|
|
109
|
+
* 5. the engine compiles it as a strict function of `$vars`: an early error
|
|
110
|
+
* (a duplicate `let`, a `const $vars`, an octal literal) that TypeScript's
|
|
111
|
+
* parser does not report would make the whole module unloadable.
|
|
112
|
+
*
|
|
113
|
+
* The parameter is named `$vars`, so nothing is renamed and the body's text is
|
|
114
|
+
* the node's text.
|
|
115
|
+
*/
|
|
116
|
+
function printAsFunction(code, ctx) {
|
|
117
|
+
if (ctx.scripts !== 'function')
|
|
118
|
+
return undefined;
|
|
119
|
+
const keep = (why) => {
|
|
120
|
+
ctx.warn(`script kept as a string: ${why} Fix the script and decompile again to get a typed function.`);
|
|
121
|
+
return undefined;
|
|
122
|
+
};
|
|
123
|
+
const syntax = scriptBodySyntaxError(code);
|
|
124
|
+
if (syntax)
|
|
125
|
+
return keep(`its body does not parse (${syntax}).`);
|
|
126
|
+
const typed = scriptBodyTypeScriptSyntax(code);
|
|
127
|
+
if (typed)
|
|
128
|
+
return keep(`it contains ${typed}, which a TypeScript loader would erase or reread, changing the script.`);
|
|
129
|
+
const name = ctx.functionName();
|
|
130
|
+
// With authoring's own rules: the printed function is lowered by `script(fn)`
|
|
131
|
+
// when the source is compiled back, so a body that rule refuses (`$vars?.x`,
|
|
132
|
+
// `$vars['x']`, an alias) must stay a string here, or compile would refuse it.
|
|
133
|
+
const lowered = tryLowerScriptFunction(`function ${name}($vars) {${code}}`, FLOW_SCRIPT_RUNTIME, { scopeReadsOnly: true });
|
|
134
|
+
if (!lowered.ok) {
|
|
135
|
+
// Worded as the SDK's list, not as a fault: whether a string body that reads
|
|
136
|
+
// the name really fails in the run is what #1411 has yet to establish.
|
|
137
|
+
if (lowered.free.length > 0) {
|
|
138
|
+
return keep(`it reads ${lowered.free.join(', ')}, which is not on the SDK's list of names the Flow script runtime provides.`);
|
|
139
|
+
}
|
|
140
|
+
// The body parses on its own (checked above) but not between the braces.
|
|
141
|
+
if (lowered.syntax)
|
|
142
|
+
return keep(UNCHANGED);
|
|
143
|
+
return keep(lowered.problems.join(' '));
|
|
144
|
+
}
|
|
145
|
+
if (lowered.code !== code)
|
|
146
|
+
return keep(UNCHANGED);
|
|
147
|
+
// Last, on what will actually be printed: an early error makes the module unloadable.
|
|
148
|
+
const early = scriptBodyEarlyError(code, FLOW_SCRIPT_RUNTIME);
|
|
149
|
+
if (early)
|
|
150
|
+
return keep(`as a function in an ES module (strict code) it would not load: ${early}.`);
|
|
151
|
+
ctx.declareFunction(`function ${name}($vars: ${ctx.typeHelper('FlowVars')}) {${code}}`);
|
|
152
|
+
return name;
|
|
153
|
+
}
|
|
86
154
|
/** Key-sorted copy, so a property map compares by content and not by order. */
|
|
87
155
|
function sortKeys(o) {
|
|
88
156
|
return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
@@ -119,6 +187,9 @@ function scriptAt(version) {
|
|
|
119
187
|
const returns = r === undefined ? undefined
|
|
120
188
|
: typeof r === 'string' ? ctx.string(r)
|
|
121
189
|
: `{ ${Object.entries(r).map(([k, t]) => `${ctx.key(k)}: ${ctx.string(String(t))}`).join(', ')} }`;
|
|
190
|
+
const fn = printAsFunction(inputs.code, ctx);
|
|
191
|
+
if (fn !== undefined)
|
|
192
|
+
return { factory: 'script', positional: [fn], options: [['returns', returns]] };
|
|
122
193
|
return { factory: 'script', options: [['code', ctx.string(inputs.code)], ['returns', returns]] };
|
|
123
194
|
},
|
|
124
195
|
check: ({ inputs }, ctx) => checkScript(inputs, ctx),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Flow script runtime, as `script(fn)` lowers for it. Not part of the public
|
|
3
|
+
* surface: `index.ts` does not re-export this file.
|
|
4
|
+
*/
|
|
5
|
+
import type { ScriptRuntime } from '../../../core/script-function.js';
|
|
6
|
+
/**
|
|
7
|
+
* What a Flow script body can read: the language's ES2020 built-ins, `console`,
|
|
8
|
+
* and the runtime's `$vars`. From the Flow skill's `references/script.md` ("The
|
|
9
|
+
* JavaScript runtime"), which also lists what reads `undefined` in a real run and
|
|
10
|
+
* is therefore absent here: `Intl`, `URL`, `TextEncoder`, `Buffer`, `fetch`,
|
|
11
|
+
* `require`, `structuredClone`. Change this when the runtime changes.
|
|
12
|
+
*
|
|
13
|
+
* Annex B's `escape`/`unescape` are left off on purpose: they URL-encode, and
|
|
14
|
+
* `escape` is the likeliest name for an author's own HTML-escape helper, which
|
|
15
|
+
* the runtime's global would silently replace.
|
|
16
|
+
*
|
|
17
|
+
* Only `script(fn)` checks against it. A string body is not checked, until
|
|
18
|
+
* there is evidence the list is complete (#1411).
|
|
19
|
+
*/
|
|
20
|
+
export declare const FLOW_SCRIPT_RUNTIME: ScriptRuntime;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Flow script body can read: the language's ES2020 built-ins, `console`,
|
|
3
|
+
* and the runtime's `$vars`. From the Flow skill's `references/script.md` ("The
|
|
4
|
+
* JavaScript runtime"), which also lists what reads `undefined` in a real run and
|
|
5
|
+
* is therefore absent here: `Intl`, `URL`, `TextEncoder`, `Buffer`, `fetch`,
|
|
6
|
+
* `require`, `structuredClone`. Change this when the runtime changes.
|
|
7
|
+
*
|
|
8
|
+
* Annex B's `escape`/`unescape` are left off on purpose: they URL-encode, and
|
|
9
|
+
* `escape` is the likeliest name for an author's own HTML-escape helper, which
|
|
10
|
+
* the runtime's global would silently replace.
|
|
11
|
+
*
|
|
12
|
+
* Only `script(fn)` checks against it. A string body is not checked, until
|
|
13
|
+
* there is evidence the list is complete (#1411).
|
|
14
|
+
*/
|
|
15
|
+
export const FLOW_SCRIPT_RUNTIME = {
|
|
16
|
+
scopeGlobal: '$vars',
|
|
17
|
+
name: 'the Flow script runtime',
|
|
18
|
+
globals: new Set([
|
|
19
|
+
'undefined', 'NaN', 'Infinity', 'globalThis', 'console',
|
|
20
|
+
'Object', 'Function', 'Array', 'Number', 'Boolean', 'String', 'Symbol', 'BigInt', 'Date', 'RegExp', 'Math', 'JSON',
|
|
21
|
+
'Promise', 'Proxy', 'Reflect', 'Map', 'Set', 'WeakMap', 'WeakSet',
|
|
22
|
+
'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError',
|
|
23
|
+
'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Atomics',
|
|
24
|
+
'Int8Array', 'Uint8Array', 'Uint8ClampedArray', 'Int16Array', 'Uint16Array', 'Int32Array', 'Uint32Array',
|
|
25
|
+
'Float32Array', 'Float64Array', 'BigInt64Array', 'BigUint64Array',
|
|
26
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
27
|
+
'encodeURI', 'encodeURIComponent', 'decodeURI', 'decodeURIComponent', 'eval',
|
|
28
|
+
]),
|
|
29
|
+
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* in `FlowActionSpecs`. Moved from core/actions.ts in Phase 4 of
|
|
4
4
|
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md.
|
|
5
5
|
*/
|
|
6
|
-
import type { ScriptReturns } from '../../../core/script-return.js';
|
|
6
|
+
import type { ScriptReturns, ScriptReturnType } from '../../../core/script-return.js';
|
|
7
7
|
export interface ScriptInputs {
|
|
8
8
|
/** JavaScript body. Must `return` its result. Referenced via out('<step>'). */
|
|
9
9
|
code: string;
|
|
@@ -42,6 +42,37 @@ export interface ScriptInputs {
|
|
|
42
42
|
*/
|
|
43
43
|
returns?: ScriptReturns;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The runtime's variable scope, as a script function's parameter sees it.
|
|
47
|
+
*
|
|
48
|
+
* `$vars` is the Flow script runtime's own global, not something the SDK
|
|
49
|
+
* provides: the SDK never calls a script function, and `script(fn)` renames the
|
|
50
|
+
* parameter to `$vars` in the body it emits. This type is a loose name for what
|
|
51
|
+
* that global holds, so `vars.start.output.message` type-checks. Any other
|
|
52
|
+
* parameter type works too; the SDK cannot check it against the runtime.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* .step('parse', script(parse, { returns: { amount: 'number' } }))
|
|
56
|
+
*
|
|
57
|
+
* function parse(vars: FlowVars) {
|
|
58
|
+
* return { amount: Number(vars.start.output.amount) };
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export type FlowVars = Record<string, any>;
|
|
63
|
+
/** The TypeScript type one of {@link ScriptReturnType}'s names stands for. */
|
|
64
|
+
export type ScriptValueOf<T extends ScriptReturnType> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'array' ? readonly unknown[] : T extends 'object' ? object : unknown;
|
|
65
|
+
/**
|
|
66
|
+
* What a script function must return for a given `returns`: the type a name
|
|
67
|
+
* stands for, or, for a field map, an object with those fields.
|
|
68
|
+
*
|
|
69
|
+
* This is how `script(fn, { returns })` makes tsc check the function against the
|
|
70
|
+
* declaration that reaches the `.flow`: `returns: { total: 'number' }` over a
|
|
71
|
+
* function that returns a string `total` does not compile.
|
|
72
|
+
*/
|
|
73
|
+
export type ScriptValue<R extends ScriptReturns> = R extends ScriptReturnType ? ScriptValueOf<R> : {
|
|
74
|
+
[F in keyof R]: R[F] extends ScriptReturnType ? ScriptValueOf<R[F]> : never;
|
|
75
|
+
};
|
|
45
76
|
declare module '../../../core/actions.js' {
|
|
46
77
|
interface FlowActionSpecs {
|
|
47
78
|
/** The spec {@link script} builds. */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** One name a script function reads from outside itself. */
|
|
2
|
+
export interface ScriptFunctionCapture {
|
|
3
|
+
/** How the call is named in messages: `script(renderSafe)`. */
|
|
4
|
+
call: string;
|
|
5
|
+
/** The name read. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Where the binding it resolves to is declared, `file:line`. */
|
|
8
|
+
declaredAt: string;
|
|
9
|
+
/** Where the read is, `file:line`. */
|
|
10
|
+
readAt: string;
|
|
11
|
+
}
|
|
12
|
+
/** What the pass found in a source: the captures, and what it could not check. */
|
|
13
|
+
export interface ScriptFunctionScope {
|
|
14
|
+
captures: ScriptFunctionCapture[];
|
|
15
|
+
/** `script(<arg>)` calls whose function-typed argument it could not follow, `file:line` each. */
|
|
16
|
+
unfollowed: string[];
|
|
17
|
+
/**
|
|
18
|
+
* `script(<arg>)` calls whose argument it could not follow and whose type is
|
|
19
|
+
* `any` or `unknown` (`fns.check` on an `any` object, a function from a `.js`
|
|
20
|
+
* file): a function or a string script's options, the pass cannot tell which.
|
|
21
|
+
*/
|
|
22
|
+
untyped: string[];
|
|
23
|
+
/** How many distinct script functions it followed to a declaration. */
|
|
24
|
+
followed: number;
|
|
25
|
+
/**
|
|
26
|
+
* Each followed function's text as Node's type stripping leaves it, whitespace
|
|
27
|
+
* removed: what `Function.prototype.toString()` returns for it under that
|
|
28
|
+
* loader, so a lowered function can be matched to its declaration. Empty when
|
|
29
|
+
* this Node has no `module.stripTypeScriptTypes`.
|
|
30
|
+
*/
|
|
31
|
+
followedTexts: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Every name a `script(fn)` function in `file` (or a file it imports) reads from
|
|
35
|
+
* outside itself, and every call whose function it could not follow.
|
|
36
|
+
*
|
|
37
|
+
* It follows a bare identifier (through `const`, a function declaration and
|
|
38
|
+
* imports between the author's files) and an inline function. Any other
|
|
39
|
+
* function-typed argument (`script(h.check)`, `script(fns.check)`, a wrapper's
|
|
40
|
+
* parameter) is `unfollowed`, and the caller refuses it rather than let a capture
|
|
41
|
+
* through unchecked. An argument that is not a function (`script({ code })`, or
|
|
42
|
+
* options passed by name) is the string form and is not a script function.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scriptFunctionScope(file: string): ScriptFunctionScope;
|
|
45
|
+
/** A whitespace-free key for comparing function texts. */
|
|
46
|
+
export declare function functionTextKey(text: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Why a loaded flow cannot be trusted with its script functions, or undefined.
|
|
49
|
+
*
|
|
50
|
+
* Fails closed: a function the pass could not follow, or fewer functions found in
|
|
51
|
+
* the source than `script(fn)` lowered (a call made through `const sc = script`,
|
|
52
|
+
* a re-exporting module, or a function from a file outside the program), is
|
|
53
|
+
* refused like a capture, because what it reads was not checked.
|
|
54
|
+
*/
|
|
55
|
+
export declare function scriptFunctionScopeProblem(file: string, lowered: readonly string[]): string | undefined;
|
|
56
|
+
/** The message a load refuses with, one line per capture. */
|
|
57
|
+
export declare function scriptFunctionCaptureMessage(captures: readonly ScriptFunctionCapture[]): string;
|