deepline 0.3.422 → 0.3.424
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/bundling-sources/sdk/src/client.ts +15 -8
- package/dist/bundling-sources/sdk/src/index.ts +2 -0
- package/dist/bundling-sources/sdk/src/play.ts +227 -184
- package/dist/bundling-sources/sdk/src/plays/local-file-discovery.ts +9 -2
- package/dist/bundling-sources/sdk/src/plays/local-play-execution.ts +1 -1
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/tool-calls.ts +3 -1
- package/dist/bundling-sources/sdk/src/types.ts +5 -5
- package/dist/bundling-sources/shared_libs/monitors/fleets/verdict.ts +11 -1
- package/dist/bundling-sources/shared_libs/monitors/inventory-reconciliation.ts +38 -4
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-batch-operation.ts +12 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-control-plane-capability.ts +25 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-errors.ts +10 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-identity-claims.ts +201 -0
- package/dist/bundling-sources/shared_libs/monitors/provider-monitor-state.ts +42 -5
- package/dist/bundling-sources/shared_libs/monitors/provider-webhook-monitor-runtime.ts +5 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +37 -36
- package/dist/bundling-sources/shared_libs/plays/bundling/index.ts +179 -38
- package/dist/bundling-sources/shared_libs/plays/dataset.ts +2 -2
- package/dist/bundling-sources/shared_libs/plays/docflow.ts +4 -0
- package/dist/bundling-sources/shared_libs/plays/fetch-http-error.ts +1 -1
- package/dist/bundling-sources/shared_libs/plays/local-file-discovery.ts +12 -4
- package/dist/bundling-sources/shared_libs/plays/play-exports.ts +27 -5
- package/dist/bundling-sources/shared_libs/plays/script-entry.ts +992 -0
- package/dist/bundling-sources/shared_libs/plays/source-authoring-edition.ts +63 -0
- package/dist/bundling-sources/shared_libs/plays/tool-result-types.ts +8 -4
- package/dist/bundling-sources/shared_libs/plays/ts-ast.ts +2 -0
- package/dist/cli/auth-MN7MWTFR.js +40 -0
- package/dist/cli/{auth-DMLBSXLV.mjs → auth-QITAQMHF.mjs} +4 -4
- package/dist/cli/{billing-IFGFPGJV.js → billing-N5AOFDSU.js} +73 -73
- package/dist/cli/{billing-SEXKROBQ.mjs → billing-UX2GH5L2.mjs} +4 -4
- package/dist/cli/{chunk-BL3LY54Y.js → chunk-22XGRO2M.js} +8 -8
- package/dist/cli/{chunk-AARWVK4C.mjs → chunk-2BQUML3X.mjs} +792 -69
- package/dist/cli/{chunk-ERFN2QY4.mjs → chunk-3DNKFH2G.mjs} +1 -1
- package/dist/cli/{chunk-QNBHQM5R.mjs → chunk-3FQUUVN2.mjs} +1 -1
- package/dist/cli/{chunk-4CAVSRQH.mjs → chunk-3O7KATMQ.mjs} +1 -1
- package/dist/cli/{chunk-S4KGXRCV.js → chunk-3VB5DQ7Y.js} +20 -20
- package/dist/cli/{chunk-VT6WJ26Y.mjs → chunk-5MD5I4MM.mjs} +1 -1
- package/dist/cli/{chunk-W74RDGRL.mjs → chunk-5OBCPN2C.mjs} +2 -2
- package/dist/cli/{chunk-LXRDHISW.js → chunk-5YLEB4CL.js} +8 -8
- package/dist/cli/{chunk-2JKQORUX.mjs → chunk-7KNBC4RJ.mjs} +2 -2
- package/dist/cli/{chunk-MLP4CAY7.mjs → chunk-7VC7STG5.mjs} +4 -4
- package/dist/cli/{chunk-KBJP3UAU.js → chunk-7W4N6VRE.js} +35 -35
- package/dist/cli/{chunk-3CE3WGKP.mjs → chunk-7Y5M5YE3.mjs} +2 -2
- package/dist/cli/{chunk-V4RAH5D6.js → chunk-CJD5QKYK.js} +26 -26
- package/dist/cli/{chunk-AYM2KZN3.js → chunk-DD6MM6WX.js} +6 -6
- package/dist/cli/{chunk-63ILLN5H.mjs → chunk-DS7Y7X26.mjs} +2 -2
- package/dist/cli/{chunk-CTNA5QY2.mjs → chunk-EER67G2B.mjs} +43 -28
- package/dist/cli/{chunk-OZHPZCAU.js → chunk-F3MMA7JY.js} +52 -52
- package/dist/cli/{chunk-CO3VMH33.mjs → chunk-FJ5U2IZY.mjs} +28 -27
- package/dist/cli/{chunk-27GJKC5Q.js → chunk-GXOQRZXG.js} +2 -2
- package/dist/cli/{chunk-AXDEH2AM.mjs → chunk-HBW6PEVG.mjs} +2 -2
- package/dist/cli/{chunk-PKBPSMOT.js → chunk-INFLI36H.js} +43 -28
- package/dist/cli/{chunk-4PG4ARL2.mjs → chunk-KPKU53YO.mjs} +1 -1
- package/dist/cli/{chunk-2LVM22ZU.js → chunk-LSZZVHQP.js} +2 -2
- package/dist/cli/{chunk-IMZ56EOH.mjs → chunk-LXTSVUZN.mjs} +2 -2
- package/dist/cli/{chunk-WGO2DY4L.js → chunk-M2VXDQI2.js} +11 -11
- package/dist/cli/{chunk-7OJEYJ6G.mjs → chunk-M67UCQKW.mjs} +3 -3
- package/dist/cli/{chunk-KXPIFC5S.mjs → chunk-MFXQVT7P.mjs} +3 -3
- package/dist/cli/{chunk-IHG7UAHD.mjs → chunk-MLJBCS3E.mjs} +32 -13
- package/dist/cli/{chunk-4EV2FKBI.js → chunk-OSNDC3VF.js} +16 -16
- package/dist/cli/{chunk-PXO253DP.mjs → chunk-OWWFCA7A.mjs} +1 -1
- package/dist/cli/{chunk-VRI3ABTK.js → chunk-PNGJIAJ3.js} +50 -50
- package/dist/cli/{chunk-HCYNAIHZ.js → chunk-QP5ATTMC.js} +4 -4
- package/dist/cli/{chunk-GEJDBXXM.js → chunk-QWRHGGUH.js} +1 -1
- package/dist/cli/{chunk-AD47BXN2.js → chunk-R2QWSSAG.js} +8 -8
- package/dist/cli/{chunk-QADCFSGH.js → chunk-RDIN3NKA.js} +17 -17
- package/dist/cli/{chunk-JRSULUPQ.js → chunk-RISEING7.js} +2 -2
- package/dist/cli/{chunk-M6SXXFMT.mjs → chunk-UOZCQPD4.mjs} +2 -2
- package/dist/cli/{chunk-TP26TAAF.mjs → chunk-UPH6UNUI.mjs} +1 -1
- package/dist/cli/{chunk-UQQRMCOL.js → chunk-UTF57UBW.js} +6 -6
- package/dist/cli/{chunk-FEKJENBB.mjs → chunk-VC5NKF4B.mjs} +3 -3
- package/dist/cli/{chunk-BQR6UN3C.js → chunk-VUYUI35Q.js} +32 -13
- package/dist/cli/{chunk-WP7T4BPE.js → chunk-YA3LMTLU.js} +1 -1
- package/dist/cli/{chunk-OQQOXSYA.js → chunk-YHRS6XTB.js} +845 -122
- package/dist/cli/{chunk-K4X472AT.js → chunk-YMASNIC5.js} +260 -259
- package/dist/cli/{chunk-SDCIROIS.mjs → chunk-ZMU2DDID.mjs} +5 -5
- package/dist/cli/command-docs.js +2 -2
- package/dist/cli/command-docs.mjs +1 -1
- package/dist/cli/{csv-J7UVP4KB.mjs → csv-NM5IHLVM.mjs} +3 -3
- package/dist/cli/{csv-Y4YWZSLB.js → csv-RL53NPRV.js} +12 -12
- package/dist/cli/{db-CQ6NHMLG.mjs → db-FZTIS4QG.mjs} +5 -5
- package/dist/cli/{db-SVFCM6YZ.js → db-RJCFUM2J.js} +26 -26
- package/dist/cli/{describe-FSDW6LTN.js → describe-A2MGAI3Y.js} +24 -24
- package/dist/cli/{describe-Y65BU7ZY.mjs → describe-T77IEILZ.mjs} +16 -16
- package/dist/cli/describe-core.d.mts +2 -2
- package/dist/cli/describe-core.d.ts +2 -2
- package/dist/cli/describe-core.js +4 -4
- package/dist/cli/describe-core.mjs +3 -3
- package/dist/cli/{executions-IW762OIJ.mjs → executions-AMQK53QX.mjs} +4 -4
- package/dist/cli/{executions-SJIKBRHT.js → executions-R6UPRZ5N.js} +6 -6
- package/dist/cli/{feedback-VMJIWAMT.mjs → feedback-BA5ZHVCZ.mjs} +3 -3
- package/dist/cli/{feedback-RCYSUOB3.js → feedback-JFT536KI.js} +7 -7
- package/dist/cli/{git-Q2WY3K6X.mjs → git-5ONARXFO.mjs} +4 -4
- package/dist/cli/git-NHQR3NKF.js +24 -0
- package/dist/cli/index.js +135 -135
- package/dist/cli/index.mjs +37 -37
- package/dist/cli/{legacy-noops-JVEE2VHK.mjs → legacy-noops-7J4K6QUX.mjs} +4 -4
- package/dist/cli/{legacy-noops-U4YPPYXU.js → legacy-noops-WT7M7WYD.js} +7 -7
- package/dist/cli/{monitors-AY4KQRTC.mjs → monitors-N3WDXW7H.mjs} +19 -13
- package/dist/cli/{monitors-BMAOQ37R.js → monitors-YH6PKEIW.js} +99 -93
- package/dist/cli/{org-ME4X6HWH.mjs → org-2V5ELSYS.mjs} +4 -4
- package/dist/cli/{org-G5IURX74.js → org-KGYHMT5P.js} +64 -64
- package/dist/cli/{play-X5GSZOKZ.js → play-2JOQQRZI.js} +606 -567
- package/dist/cli/{play-WLYU7WQL.mjs → play-CL2LCBAF.mjs} +113 -74
- package/dist/cli/{providers-2LBMNB7B.mjs → providers-I7EEPWNW.mjs} +4 -4
- package/dist/cli/{providers-AHEUVD3R.js → providers-VIP6MCWC.js} +7 -7
- package/dist/cli/{quickstart-DP2SKLIG.js → quickstart-33WC2KWE.js} +5 -5
- package/dist/cli/{quickstart-DBLYGHJP.mjs → quickstart-VRBKQODE.mjs} +3 -3
- package/dist/cli/{search-YKJHY3ZJ.mjs → search-7TDSKXOO.mjs} +5 -5
- package/dist/cli/search-A6RZZ4LK.js +30 -0
- package/dist/cli/{secrets-ECLARBO6.mjs → secrets-DGLCIN7Y.mjs} +4 -4
- package/dist/cli/{secrets-2P3I3OEZ.js → secrets-TQH5FXAC.js} +13 -13
- package/dist/cli/{sessions-MC4ZCTSN.js → sessions-4D4UDPIS.js} +5 -5
- package/dist/cli/{sessions-3GDFID7R.mjs → sessions-D6QGKQ2G.mjs} +4 -4
- package/dist/cli/{settings-RTPJXMOA.mjs → settings-6HVXBCVR.mjs} +8 -8
- package/dist/cli/{settings-7SVR2OVL.js → settings-V3QRG4AO.js} +75 -75
- package/dist/cli/{setup-4WLIJX36.js → setup-6WP7D5I3.js} +42 -42
- package/dist/cli/{setup-OKAZZGAW.mjs → setup-CSJ3HSL7.mjs} +7 -7
- package/dist/cli/{skills-2CVL6RG5.mjs → skills-2MTWP4PS.mjs} +5 -5
- package/dist/cli/skills-QOYOARZT.js +29 -0
- package/dist/cli/{tools-KAFHP6J4.mjs → tools-GAR3BWB6.mjs} +15 -15
- package/dist/cli/tools-MGEKAIOU.js +44 -0
- package/dist/cli/update-HMZCLFLZ.js +31 -0
- package/dist/cli/{update-3ZV3W7I4.mjs → update-XV3WV3MU.mjs} +7 -7
- package/dist/cli/{workflow-DKSUNZXE.mjs → workflow-DTZAWSB2.mjs} +9 -9
- package/dist/cli/{workflow-GULDFFP3.js → workflow-WNE7ORMT.js} +19 -19
- package/dist/{client-CCWIUnk7.d.mts → client-BcfFWD4Z.d.mts} +19 -15
- package/dist/{client-vHqBm-OL.d.ts → client-DH-fmT69.d.ts} +19 -15
- package/dist/{compiler-manifest-lzWpbV3w.d.mts → compiler-manifest-DaFuA1ki.d.mts} +35 -32
- package/dist/{compiler-manifest-lzWpbV3w.d.ts → compiler-manifest-DaFuA1ki.d.ts} +35 -32
- package/dist/{index-CtNK-0nI.d.mts → index-0yS9ihly.d.mts} +10 -3
- package/dist/{index-C8Cy-YTm.d.ts → index-CRAScKPg.d.ts} +10 -3
- package/dist/index.d.mts +120 -150
- package/dist/index.d.ts +120 -150
- package/dist/index.js +77 -12
- package/dist/index.mjs +74 -9
- package/dist/install-integrity.json +99 -98
- package/dist/plays/bundle-play-file.d.mts +3 -3
- package/dist/plays/bundle-play-file.d.ts +3 -3
- package/dist/plays/bundle-play-file.mjs +1 -1
- package/dist/plays/play-description.d.mts +2 -2
- package/dist/plays/play-description.d.ts +2 -2
- package/dist/plays/run-request.d.mts +2 -2
- package/dist/plays/run-request.d.ts +2 -2
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +2 -2
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
- package/dist/cli/auth-HD5AM2WM.js +0 -40
- package/dist/cli/git-JAW3UEGA.js +0 -24
- package/dist/cli/search-4K3VSDS5.js +0 -30
- package/dist/cli/skills-ZNSQOXO2.js +0 -29
- package/dist/cli/tools-W4U4FT3R.js +0 -44
- package/dist/cli/update-IE2FPLB5.js +0 -31
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script plays: a play file the runtime runs as written.
|
|
3
|
+
*
|
|
4
|
+
* import deepline from 'deepline';
|
|
5
|
+
* export default async function ({ domain }: { domain: string }) {
|
|
6
|
+
* return { company: await deepline.tools.execute({ ... }) };
|
|
7
|
+
* }
|
|
8
|
+
*
|
|
9
|
+
* The whole module body runs inside the run, with `deepline` bound to it (the
|
|
10
|
+
* SDK's `runScriptPlay`). The play's result is, in order: what a top-level
|
|
11
|
+
* call to the default-exported function returned; else what calling that
|
|
12
|
+
* function with the input returns; else a top-level `return`, else
|
|
13
|
+
* `deepline.output(...)`, else `{}`. So a file that is top-level code, a
|
|
14
|
+
* default-exported function of the input, or both runs as one play, and
|
|
15
|
+
* nothing the author wrote is rewritten beyond turning its default export
|
|
16
|
+
* into a local binding on the same line.
|
|
17
|
+
*/
|
|
18
|
+
import { basename } from 'node:path';
|
|
19
|
+
import { isAstNode, TypeScriptParser, type AstNode } from './ts-ast';
|
|
20
|
+
|
|
21
|
+
export const SCRIPT_SDK = 'deepline';
|
|
22
|
+
/** A play file: `lookup.play.ts`, `.play.js`, `.play.mts`, ... */
|
|
23
|
+
export const PLAY_FILE_PATTERN = /\.play\.[cm]?[jt]sx?$/i;
|
|
24
|
+
const DEFINE_CALLEES = new Set(['definePlay', 'defineWorkflow']);
|
|
25
|
+
const FUNCTION_TYPES = new Set([
|
|
26
|
+
'FunctionDeclaration',
|
|
27
|
+
'FunctionExpression',
|
|
28
|
+
'ArrowFunctionExpression',
|
|
29
|
+
]);
|
|
30
|
+
/** Statements that stay at module scope: imports, and the types an input type may name. */
|
|
31
|
+
const MODULE_SCOPE = new Set([
|
|
32
|
+
'ImportDeclaration',
|
|
33
|
+
'TSTypeAliasDeclaration',
|
|
34
|
+
'TSInterfaceDeclaration',
|
|
35
|
+
]);
|
|
36
|
+
const DECLARATION_TYPES = new Set([
|
|
37
|
+
'FunctionDeclaration',
|
|
38
|
+
'TSDeclareFunction',
|
|
39
|
+
'ClassDeclaration',
|
|
40
|
+
'TSTypeAliasDeclaration',
|
|
41
|
+
'TSInterfaceDeclaration',
|
|
42
|
+
'TSEnumDeclaration',
|
|
43
|
+
]);
|
|
44
|
+
const UNWRAPPED_TYPES = new Set([
|
|
45
|
+
'TSAsExpression',
|
|
46
|
+
'TSSatisfiesExpression',
|
|
47
|
+
'TSTypeAssertion',
|
|
48
|
+
'ParenthesizedExpression',
|
|
49
|
+
'AwaitExpression',
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Code a top-level call to the entry may sit under and still not run on
|
|
53
|
+
* every pass: `if (import.meta.main) main()` is left to the dispatch.
|
|
54
|
+
*/
|
|
55
|
+
const CONDITIONAL_TYPES = new Set([
|
|
56
|
+
...FUNCTION_TYPES,
|
|
57
|
+
'ClassDeclaration',
|
|
58
|
+
'ClassExpression',
|
|
59
|
+
'IfStatement',
|
|
60
|
+
'ConditionalExpression',
|
|
61
|
+
'LogicalExpression',
|
|
62
|
+
'SwitchStatement',
|
|
63
|
+
'ForStatement',
|
|
64
|
+
'ForInStatement',
|
|
65
|
+
'ForOfStatement',
|
|
66
|
+
'WhileStatement',
|
|
67
|
+
'DoWhileStatement',
|
|
68
|
+
'CatchClause',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
/** Where a script's optional calls are, for analyzers that read scripts. */
|
|
72
|
+
type ScriptPlayParts = {
|
|
73
|
+
/**
|
|
74
|
+
* The play's config object literal: `export const config = {...}`, or the
|
|
75
|
+
* one passed to `deepline.config(...)`.
|
|
76
|
+
*/
|
|
77
|
+
config: AstNode | null;
|
|
78
|
+
/** The entry's input parameter type, else the `T` of `deepline.input<T>()`. */
|
|
79
|
+
inputType: AstNode | null;
|
|
80
|
+
/**
|
|
81
|
+
* The play's result values, in source order: the entry's own returns, or
|
|
82
|
+
* without an entry each top-level `return value`; and each
|
|
83
|
+
* `deepline.output(value)`.
|
|
84
|
+
*/
|
|
85
|
+
outputs: AstNode[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** The module's default-exported function of the input, which the runtime calls. */
|
|
89
|
+
export type PlayEntry = {
|
|
90
|
+
/** The function itself. */
|
|
91
|
+
node: AstNode;
|
|
92
|
+
/** What module code calls it by; null when it is anonymous. */
|
|
93
|
+
name: string | null;
|
|
94
|
+
/**
|
|
95
|
+
* `declaration`: `export default [async] function [name](input) {}`.
|
|
96
|
+
* `expression`: `export default <function or arrow>`.
|
|
97
|
+
* `binding`: `export default name`, `name` a top-level function.
|
|
98
|
+
* `specifier`: `export { name as default }`.
|
|
99
|
+
*/
|
|
100
|
+
exportedAs: 'declaration' | 'expression' | 'binding' | 'specifier';
|
|
101
|
+
/** The export statement. */
|
|
102
|
+
exportStatement: AstNode;
|
|
103
|
+
/** The top-level statement declaring a `binding`/`specifier` entry. */
|
|
104
|
+
declaration: AstNode | null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A script play; its nodes carry `start`/`end` spans over the source. */
|
|
108
|
+
export type ScriptPlay = ScriptPlayParts & {
|
|
109
|
+
/** The default-exported play function, or null for top-level code alone. */
|
|
110
|
+
entry: PlayEntry | null;
|
|
111
|
+
/**
|
|
112
|
+
* The module's own unconditional top-level call to the entry, such as
|
|
113
|
+
* `await main()`: its value is the result and the entry is not called again.
|
|
114
|
+
*/
|
|
115
|
+
entryCall: AstNode | null;
|
|
116
|
+
/**
|
|
117
|
+
* The play's own code: top-level statements after the header, without
|
|
118
|
+
* types, config, the entry's declaration and the statement calling it,
|
|
119
|
+
* followed by the entry's body.
|
|
120
|
+
*/
|
|
121
|
+
code: AstNode[];
|
|
122
|
+
/** What the script calls the runtime: `deepline`, or its own default-import name. */
|
|
123
|
+
contextName: string;
|
|
124
|
+
/** Where the leading imports and types end: 0 when there are none. */
|
|
125
|
+
headerEnd: number;
|
|
126
|
+
/**
|
|
127
|
+
* Set when the play's code, besides its `config`/`input` calls and
|
|
128
|
+
* declarations, is only `return [await] handler(deepline, input)`: the play
|
|
129
|
+
* delegates to `handler`, which takes the runtime as `ctx` would.
|
|
130
|
+
*/
|
|
131
|
+
delegate: { handlerName: string } | null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The file `deepline plays run -e '<code>'` writes and runs. A one-off inline
|
|
136
|
+
* play is the only play whose tool calls may omit `description`.
|
|
137
|
+
*/
|
|
138
|
+
export const INLINE_PLAY_FILE = '.inline.play.ts';
|
|
139
|
+
|
|
140
|
+
/** Leading import statements, as `plays run -e` code may begin. */
|
|
141
|
+
const LEADING_IMPORT =
|
|
142
|
+
/^\s*import\s+(?:[\w$*{}\s,]+?\s+from\s+)?['"][^'"]+['"]\s*;?/;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The `.inline.play.ts` text for `plays run -e '<code>'`. The code is the body
|
|
146
|
+
* of the play function, with `input` holding `--input`; its leading imports
|
|
147
|
+
* stay at module scope and `deepline` is imported when it is not. Code that
|
|
148
|
+
* already default-exports its play is written as-is. Every line keeps its
|
|
149
|
+
* number, so errors point at what was typed.
|
|
150
|
+
*/
|
|
151
|
+
export function inlinePlaySource(code: string): string {
|
|
152
|
+
let header = '';
|
|
153
|
+
let body = code;
|
|
154
|
+
for (let match = LEADING_IMPORT.exec(body); match; ) {
|
|
155
|
+
header += match[0];
|
|
156
|
+
body = body.slice(match[0].length);
|
|
157
|
+
match = LEADING_IMPORT.exec(body);
|
|
158
|
+
}
|
|
159
|
+
const runtimeImport =
|
|
160
|
+
/\bimport\s+\w+\s*(,\s*\{[^}]*\}\s*)?from\s+['"]deepline['"]/.test(header)
|
|
161
|
+
? ''
|
|
162
|
+
: `import ${SCRIPT_SDK} from '${SCRIPT_SDK}'; `;
|
|
163
|
+
if (/\bexport\s+default\b/.test(code)) return runtimeImport + code;
|
|
164
|
+
return `${runtimeImport}${header}export default async function (input: any) {${body}\n}\n`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isInlinePlayFile(filePath: string): boolean {
|
|
168
|
+
return basename(filePath) === INLINE_PLAY_FILE;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `lookup.play.ts` → `lookup`; `.inline.play.ts` → `inline`. */
|
|
172
|
+
export function playNameFromPath(filePath: string): string {
|
|
173
|
+
return basename(filePath)
|
|
174
|
+
.replace(/^\.+/, '')
|
|
175
|
+
.replace(/(\.play)?\.[cm]?[jt]sx?$/, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The script play in `source`, or null when it is not one: it calls
|
|
180
|
+
* definePlay, exports something besides a default, or default-exports a
|
|
181
|
+
* context-first `(ctx, input)` handler. Unparseable source is left to the
|
|
182
|
+
* normal pipeline unless it is evidently a script; then the syntax error, with
|
|
183
|
+
* its line, is the author's real problem.
|
|
184
|
+
*/
|
|
185
|
+
export function parseScriptPlay(source: string): ScriptPlay | null {
|
|
186
|
+
const program = programOf(source);
|
|
187
|
+
const form = program ? playForm(program) : null;
|
|
188
|
+
if (!program || !form) return null;
|
|
189
|
+
const { entry } = form;
|
|
190
|
+
const header = headerOf(program);
|
|
191
|
+
const contextName = defaultImportName(header.find(isSdkImport)) ?? SCRIPT_SDK;
|
|
192
|
+
const parts = scriptPlayParts(program, contextName, entry?.node ?? null);
|
|
193
|
+
const configExport = configExportOf(program);
|
|
194
|
+
const headerEnd = (header.at(-1)?.end as number | undefined) ?? 0;
|
|
195
|
+
const [entryCall = null] = entry ? entryCallsOf(program, entry) : [];
|
|
196
|
+
const callStatement = entryCall
|
|
197
|
+
? (program.body as AstNode[]).find(
|
|
198
|
+
(node) =>
|
|
199
|
+
(node.start as number) <= (entryCall.start as number) &&
|
|
200
|
+
(node.end as number) >= (entryCall.end as number),
|
|
201
|
+
)
|
|
202
|
+
: undefined;
|
|
203
|
+
const excluded = new Set(
|
|
204
|
+
[
|
|
205
|
+
entry?.exportStatement,
|
|
206
|
+
entry?.declaration,
|
|
207
|
+
callStatement,
|
|
208
|
+
configExport,
|
|
209
|
+
].filter(Boolean),
|
|
210
|
+
);
|
|
211
|
+
const moduleCode = (program.body as AstNode[]).slice(header.length).filter(
|
|
212
|
+
(node) =>
|
|
213
|
+
!excluded.has(node) &&
|
|
214
|
+
!MODULE_SCOPE.has(node.type) &&
|
|
215
|
+
!(
|
|
216
|
+
node.type === 'ExpressionStatement' &&
|
|
217
|
+
scriptCallName(unwrapped(node.expression), contextName) === 'config'
|
|
218
|
+
) &&
|
|
219
|
+
// Beside a play function, module code that neither awaits nor uses
|
|
220
|
+
// the runtime only names values and helpers, as a definePlay file's
|
|
221
|
+
// module scope does.
|
|
222
|
+
(!entry || usesRuntime(node, contextName)),
|
|
223
|
+
);
|
|
224
|
+
const body = entry?.node.body as AstNode | undefined;
|
|
225
|
+
const code = [
|
|
226
|
+
...moduleCode,
|
|
227
|
+
...(body?.type === 'BlockStatement' ? (body.body as AstNode[]) : []),
|
|
228
|
+
];
|
|
229
|
+
const [parameter] = (entry?.node.params as AstNode[] | undefined) ?? [];
|
|
230
|
+
return {
|
|
231
|
+
entry,
|
|
232
|
+
entryCall,
|
|
233
|
+
code,
|
|
234
|
+
contextName,
|
|
235
|
+
headerEnd,
|
|
236
|
+
delegate: delegateOf(code, contextName),
|
|
237
|
+
config: (configExport && configObjectOf(configExport)) ?? parts.config,
|
|
238
|
+
// The entry's parameter is its input; `(input) => ({ ... })` returns its
|
|
239
|
+
// expression.
|
|
240
|
+
inputType: parameterType(parameter) ?? parts.inputType,
|
|
241
|
+
outputs: body && body.type !== 'BlockStatement' ? [body] : parts.outputs,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* What a play file calls its runtime: `ctx`, `deepline`, and the file's
|
|
247
|
+
* default import from `deepline`. A seed: analyzers add the handler and
|
|
248
|
+
* resolver parameters they discover.
|
|
249
|
+
*/
|
|
250
|
+
export function playRuntimeNames(source: string): ReadonlySet<string> {
|
|
251
|
+
const program = programOf(source);
|
|
252
|
+
const imported = program
|
|
253
|
+
? (program.body as AstNode[]).filter(isSdkImport).map(defaultImportName)
|
|
254
|
+
: [];
|
|
255
|
+
return new Set([
|
|
256
|
+
'ctx',
|
|
257
|
+
SCRIPT_SDK,
|
|
258
|
+
...imported.filter((name): name is string => name !== null),
|
|
259
|
+
]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** A runtime name, or by convention any name ending in `Ctx`. */
|
|
263
|
+
export function isRuntimeReceiver(
|
|
264
|
+
name: string,
|
|
265
|
+
runtimeNames: ReadonlySet<string>,
|
|
266
|
+
): boolean {
|
|
267
|
+
return runtimeNames.has(name) || name.endsWith('Ctx');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* `{ handlerName }` when `code`, without its `config`/`input` signature
|
|
272
|
+
* statements and declarations, is only `return [await] handler(ctx, ...)`.
|
|
273
|
+
*/
|
|
274
|
+
function delegateOf(
|
|
275
|
+
code: AstNode[],
|
|
276
|
+
contextName: string,
|
|
277
|
+
): ScriptPlay['delegate'] {
|
|
278
|
+
const [statement, extra] = code.filter(
|
|
279
|
+
(node) =>
|
|
280
|
+
!isSignatureStatement(node, contextName) && !isDeclarationOnly(node),
|
|
281
|
+
);
|
|
282
|
+
if (extra || statement?.type !== 'ReturnStatement') return null;
|
|
283
|
+
const returned = statement.argument;
|
|
284
|
+
const call =
|
|
285
|
+
isAstNode(returned) && returned.type === 'AwaitExpression'
|
|
286
|
+
? returned.argument
|
|
287
|
+
: returned;
|
|
288
|
+
if (!isAstNode(call) || call.type !== 'CallExpression') return null;
|
|
289
|
+
const [runtime] = call.arguments as AstNode[];
|
|
290
|
+
return isAstNode(call.callee) &&
|
|
291
|
+
call.callee.type === 'Identifier' &&
|
|
292
|
+
runtime?.type === 'Identifier' &&
|
|
293
|
+
(runtime.name === 'ctx' || runtime.name === contextName)
|
|
294
|
+
? { handlerName: call.callee.name as string }
|
|
295
|
+
: null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** `deepline.config({...});` or `const x = deepline.input();`. */
|
|
299
|
+
function isSignatureStatement(node: AstNode, contextName: string): boolean {
|
|
300
|
+
if (node.type === 'ExpressionStatement') {
|
|
301
|
+
return scriptCallName(unwrapped(node.expression), contextName) === 'config';
|
|
302
|
+
}
|
|
303
|
+
if (node.type !== 'VariableDeclaration') return false;
|
|
304
|
+
const declarations = node.declarations as AstNode[];
|
|
305
|
+
return (
|
|
306
|
+
declarations.length === 1 &&
|
|
307
|
+
scriptCallName(unwrapped(declarations[0]!.init), contextName) === 'input'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Functions, classes, types, and `const LIMIT = 100`: names, not work. */
|
|
312
|
+
function isDeclarationOnly(node: AstNode): boolean {
|
|
313
|
+
if (DECLARATION_TYPES.has(node.type)) return true;
|
|
314
|
+
return (
|
|
315
|
+
node.type === 'VariableDeclaration' &&
|
|
316
|
+
node.kind === 'const' &&
|
|
317
|
+
(node.declarations as AstNode[]).every(
|
|
318
|
+
(declarator) =>
|
|
319
|
+
isAstNode(declarator.init) && isLiteralValue(declarator.init),
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A string, number, bigint or regex literal, or a template without `${}`. */
|
|
325
|
+
function isLiteralValue(node: AstNode): boolean {
|
|
326
|
+
return node.type === 'TemplateLiteral'
|
|
327
|
+
? (node.expressions as AstNode[]).length === 0
|
|
328
|
+
: node.type === 'Literal' &&
|
|
329
|
+
(node.regex !== undefined ||
|
|
330
|
+
['string', 'number', 'bigint'].includes(typeof node.value));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `x` without `as`/`satisfies`/`<T>` assertions, parentheses and `await`. */
|
|
334
|
+
function unwrapped(node: unknown): AstNode | null {
|
|
335
|
+
let current = isAstNode(node) ? node : null;
|
|
336
|
+
while (current && UNWRAPPED_TYPES.has(current.type)) {
|
|
337
|
+
const inner = current.expression ?? current.argument;
|
|
338
|
+
current = isAstNode(inner) ? inner : null;
|
|
339
|
+
}
|
|
340
|
+
return current;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parameterType(parameter: AstNode | undefined): AstNode | null {
|
|
344
|
+
// `(input: T = {})` annotates the pattern on the left of the default.
|
|
345
|
+
const target =
|
|
346
|
+
parameter?.type === 'AssignmentPattern' ? parameter.left : parameter;
|
|
347
|
+
const annotation = isAstNode(target) ? target.typeAnnotation : null;
|
|
348
|
+
return isAstNode(annotation) && isAstNode(annotation.typeAnnotation)
|
|
349
|
+
? annotation.typeAnnotation
|
|
350
|
+
: null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function scriptPlayParts(
|
|
354
|
+
program: AstNode,
|
|
355
|
+
contextName: string,
|
|
356
|
+
playFunction: AstNode | null,
|
|
357
|
+
): ScriptPlayParts {
|
|
358
|
+
const parts: ScriptPlayParts = { config: null, inputType: null, outputs: [] };
|
|
359
|
+
const visit = (node: AstNode, insideFunction: boolean): void => {
|
|
360
|
+
if (
|
|
361
|
+
node.type === 'ReturnStatement' &&
|
|
362
|
+
!insideFunction &&
|
|
363
|
+
isAstNode(node.argument)
|
|
364
|
+
) {
|
|
365
|
+
parts.outputs.push(node.argument);
|
|
366
|
+
}
|
|
367
|
+
const call = scriptCallName(node, contextName);
|
|
368
|
+
const [argument] = Array.isArray(node.arguments)
|
|
369
|
+
? node.arguments.filter(isAstNode)
|
|
370
|
+
: [];
|
|
371
|
+
if (call === 'config' && argument?.type === 'ObjectExpression') {
|
|
372
|
+
parts.config ??= argument;
|
|
373
|
+
} else if (call === 'input') {
|
|
374
|
+
parts.inputType ??= typeArgument(node);
|
|
375
|
+
} else if (call === 'output' && argument) {
|
|
376
|
+
parts.outputs.push(argument);
|
|
377
|
+
}
|
|
378
|
+
// The play's own returns: top-level ones in a script, the play function's
|
|
379
|
+
// own in function form; any other function's returns are its own.
|
|
380
|
+
const nested =
|
|
381
|
+
node === playFunction
|
|
382
|
+
? false
|
|
383
|
+
: insideFunction || FUNCTION_TYPES.has(node.type);
|
|
384
|
+
childNodes(node).forEach((child) => visit(child, nested));
|
|
385
|
+
};
|
|
386
|
+
visit(program, playFunction !== null);
|
|
387
|
+
return parts;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Whether `node` awaits or reaches the runtime, outside nested functions. */
|
|
391
|
+
function usesRuntime(node: AstNode, contextName: string): boolean {
|
|
392
|
+
if (
|
|
393
|
+
node.type === 'AwaitExpression' ||
|
|
394
|
+
(node.type === 'Identifier' && node.name === contextName)
|
|
395
|
+
) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
if (FUNCTION_TYPES.has(node.type)) return false;
|
|
399
|
+
return childNodes(node).some((child) => usesRuntime(child, contextName));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** `config`, `input` or `output` when `node` is `<contextName>.<that>(...)`. */
|
|
403
|
+
function scriptCallName(
|
|
404
|
+
node: AstNode | null,
|
|
405
|
+
contextName: string,
|
|
406
|
+
): 'config' | 'input' | 'output' | null {
|
|
407
|
+
if (node?.type !== 'CallExpression' || !isAstNode(node.callee)) return null;
|
|
408
|
+
const callee = node.callee;
|
|
409
|
+
if (
|
|
410
|
+
callee.type !== 'MemberExpression' ||
|
|
411
|
+
callee.computed ||
|
|
412
|
+
!isAstNode(callee.object) ||
|
|
413
|
+
callee.object.type !== 'Identifier' ||
|
|
414
|
+
callee.object.name !== contextName ||
|
|
415
|
+
!isAstNode(callee.property)
|
|
416
|
+
) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const name = callee.property.name;
|
|
420
|
+
return name === 'config' || name === 'input' || name === 'output'
|
|
421
|
+
? name
|
|
422
|
+
: null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function typeArgument(call: AstNode): AstNode | null {
|
|
426
|
+
const instantiation = call.typeArguments ?? call.typeParameters;
|
|
427
|
+
return isAstNode(instantiation) && Array.isArray(instantiation.params)
|
|
428
|
+
? (instantiation.params.find(isAstNode) ?? null)
|
|
429
|
+
: null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The play module as the function the runner calls: imports stay at module
|
|
434
|
+
* scope and the rest of the module runs inside `runScriptPlay`, its default
|
|
435
|
+
* export a local binding, then the entry is dispatched. Every edit stays on
|
|
436
|
+
* its line, so every author line keeps its number.
|
|
437
|
+
*/
|
|
438
|
+
export function wrapScriptPlay(
|
|
439
|
+
source: string,
|
|
440
|
+
script: ScriptPlay,
|
|
441
|
+
{ typed }: { typed: boolean },
|
|
442
|
+
): string {
|
|
443
|
+
const { entry, entryCall } = script;
|
|
444
|
+
// TypeScript sources annotate what they declare, so the wrapped form also
|
|
445
|
+
// type-checks under noImplicitAny.
|
|
446
|
+
const unknownType = typed ? ': unknown' : '';
|
|
447
|
+
const anyType = typed ? ': any' : '';
|
|
448
|
+
const runtimeImport = defaultImportName(
|
|
449
|
+
headerOf(programOf(source)!).find(isSdkImport),
|
|
450
|
+
)
|
|
451
|
+
? `import { runScriptPlay as __deeplineRunScriptPlay } from '${SCRIPT_SDK}';`
|
|
452
|
+
: `import ${SCRIPT_SDK}, { runScriptPlay as __deeplineRunScriptPlay } from '${SCRIPT_SDK}';`;
|
|
453
|
+
const entryBindings = entry
|
|
454
|
+
? ` let __deeplineEntry${anyType}; let __deeplineEntryResult${anyType};`
|
|
455
|
+
: '';
|
|
456
|
+
const prefix =
|
|
457
|
+
`${runtimeImport} export default async function (__deeplineContext${unknownType}, __deeplineInput${anyType}) {` +
|
|
458
|
+
` return __deeplineRunScriptPlay(__deeplineContext, __deeplineInput, async () => {${entryBindings}`;
|
|
459
|
+
const edits: Array<{ at: number; end?: number; text: string }> = [
|
|
460
|
+
{
|
|
461
|
+
at: script.headerEnd,
|
|
462
|
+
text: script.headerEnd === 0 ? `${prefix} ` : ` ${prefix}`,
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
// `export const config = {...}` runs inside the run as `const config`.
|
|
466
|
+
const configExport = configExportOf(programOf(source)!);
|
|
467
|
+
if (configExport) {
|
|
468
|
+
const declared = configExport.declaration as AstNode;
|
|
469
|
+
edits.push({
|
|
470
|
+
at: configExport.start as number,
|
|
471
|
+
end: declared.start as number,
|
|
472
|
+
text: blank(
|
|
473
|
+
source.slice(configExport.start as number, declared.start as number),
|
|
474
|
+
),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
let entryName = '__deeplineEntry';
|
|
478
|
+
if (entry) {
|
|
479
|
+
const statement = entry.exportStatement;
|
|
480
|
+
const declared = statement.declaration as AstNode | undefined;
|
|
481
|
+
if (entry.exportedAs === 'specifier') {
|
|
482
|
+
// `export { main as default }` names a binding that may follow it.
|
|
483
|
+
entryName = entry.name!;
|
|
484
|
+
edits.push({
|
|
485
|
+
at: statement.start as number,
|
|
486
|
+
end: statement.end as number,
|
|
487
|
+
text: blank(
|
|
488
|
+
source.slice(statement.start as number, statement.end as number),
|
|
489
|
+
),
|
|
490
|
+
});
|
|
491
|
+
} else if (entry.exportedAs === 'declaration' && entry.name) {
|
|
492
|
+
// A named function declaration stays one, hoisted as it was.
|
|
493
|
+
entryName = entry.name;
|
|
494
|
+
edits.push({
|
|
495
|
+
at: statement.start as number,
|
|
496
|
+
end: declared!.start as number,
|
|
497
|
+
text: blank(
|
|
498
|
+
source.slice(statement.start as number, declared!.start as number),
|
|
499
|
+
),
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
edits.push({
|
|
503
|
+
at: statement.start as number,
|
|
504
|
+
end: declared!.start as number,
|
|
505
|
+
text: '__deeplineEntry = ',
|
|
506
|
+
});
|
|
507
|
+
if (entry.exportedAs === 'declaration') {
|
|
508
|
+
edits.push({ at: statement.end as number, text: ';' });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (entryCall) {
|
|
512
|
+
edits.push(
|
|
513
|
+
{ at: entryCall.start as number, text: '(__deeplineEntryResult = ' },
|
|
514
|
+
{ at: entryCall.end as number, text: ')' },
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const dispatch = !entry
|
|
519
|
+
? ''
|
|
520
|
+
: entryCall
|
|
521
|
+
? ' return await __deeplineEntryResult;'
|
|
522
|
+
: // A play function that takes no input is called without one.
|
|
523
|
+
` return await ${entryName}(${(entry.node.params as AstNode[]).length ? '__deeplineInput' : ''});`;
|
|
524
|
+
return (
|
|
525
|
+
edits
|
|
526
|
+
// Last first, and a replacement before an insertion at its start.
|
|
527
|
+
.sort(
|
|
528
|
+
(left, right) =>
|
|
529
|
+
right.at - left.at || (right.end ?? right.at) - (left.end ?? left.at),
|
|
530
|
+
)
|
|
531
|
+
.reduce(
|
|
532
|
+
(text, edit) =>
|
|
533
|
+
text.slice(0, edit.at) + edit.text + text.slice(edit.end ?? edit.at),
|
|
534
|
+
source,
|
|
535
|
+
) + `\n;${dispatch} }); }\n`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** `text` as spaces, keeping its line breaks. */
|
|
540
|
+
function blank(text: string): string {
|
|
541
|
+
return text.replace(/[^\n]/g, ' ');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* The rules a script must follow so its config and input contract can be
|
|
546
|
+
* read without running it, and so what it returns is unambiguous. Throws the
|
|
547
|
+
* first violation with its line.
|
|
548
|
+
*/
|
|
549
|
+
export function validateScriptPlay(source: string, script: ScriptPlay): void {
|
|
550
|
+
const { contextName, entry } = script;
|
|
551
|
+
const program = programOf(source)!;
|
|
552
|
+
const statements = program.body as AstNode[];
|
|
553
|
+
const defaultExport = statements.find(isDefaultExport);
|
|
554
|
+
if (defaultExport && !entry) {
|
|
555
|
+
fail(
|
|
556
|
+
source,
|
|
557
|
+
defaultExport,
|
|
558
|
+
'The default export must be the play function: export default async function (input) { ... return { ... }; }.',
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (entry) {
|
|
562
|
+
const calls = entryCallsOf(program, entry);
|
|
563
|
+
if (calls.length > 1) {
|
|
564
|
+
fail(
|
|
565
|
+
source,
|
|
566
|
+
calls[1]!,
|
|
567
|
+
`The play function is called ${calls.length} times at the top of the file; the runtime runs it once per run. Call it once, or not at all.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
// A top-level result beside the play function: which one is the result?
|
|
571
|
+
const moduleResult = (node: AstNode, insideFunction: boolean): void => {
|
|
572
|
+
const call = scriptCallName(node, contextName);
|
|
573
|
+
if (
|
|
574
|
+
!insideFunction &&
|
|
575
|
+
(node.type === 'ReturnStatement' || call === 'output')
|
|
576
|
+
) {
|
|
577
|
+
fail(
|
|
578
|
+
source,
|
|
579
|
+
node,
|
|
580
|
+
"The play function's return is the play's result; this top-level result would compete with it. Return the result from the play function.",
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
const nested = insideFunction || FUNCTION_TYPES.has(node.type);
|
|
584
|
+
childNodes(node).forEach((child) => moduleResult(child, nested));
|
|
585
|
+
};
|
|
586
|
+
moduleResult(program, false);
|
|
587
|
+
}
|
|
588
|
+
const lateImport = statements
|
|
589
|
+
.slice(headerOf(program).length)
|
|
590
|
+
.find((node) => node.type === 'ImportDeclaration');
|
|
591
|
+
if (lateImport) {
|
|
592
|
+
fail(
|
|
593
|
+
source,
|
|
594
|
+
lateImport,
|
|
595
|
+
'Move this import above the play code; imports come first in a script play.',
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const topLevelConfig = new Set(
|
|
599
|
+
statements
|
|
600
|
+
.map((node) =>
|
|
601
|
+
node.type === 'ExpressionStatement' ? node.expression : null,
|
|
602
|
+
)
|
|
603
|
+
.filter((node): node is AstNode => isAstNode(node)),
|
|
604
|
+
);
|
|
605
|
+
const configExport = configExportOf(program);
|
|
606
|
+
if (configExport) {
|
|
607
|
+
const config = configObjectOf(configExport);
|
|
608
|
+
if (!config) {
|
|
609
|
+
fail(
|
|
610
|
+
source,
|
|
611
|
+
configExport,
|
|
612
|
+
"export const config must be one object literal, e.g. export const config = { description: '...' };",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
validateConfigName(source, config);
|
|
616
|
+
}
|
|
617
|
+
const configCalls: AstNode[] = [];
|
|
618
|
+
const inputTypes = new Set<string>();
|
|
619
|
+
const visit = (node: AstNode): void => {
|
|
620
|
+
const call = scriptCallName(node, contextName);
|
|
621
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
622
|
+
if (call === 'config') {
|
|
623
|
+
configCalls.push(node);
|
|
624
|
+
if (!topLevelConfig.has(node)) {
|
|
625
|
+
fail(
|
|
626
|
+
source,
|
|
627
|
+
node,
|
|
628
|
+
`${contextName}.config(...) must be its own top-level statement.`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (
|
|
632
|
+
args.length !== 1 ||
|
|
633
|
+
!isAstNode(args[0]) ||
|
|
634
|
+
args[0].type !== 'ObjectExpression'
|
|
635
|
+
) {
|
|
636
|
+
fail(
|
|
637
|
+
source,
|
|
638
|
+
node,
|
|
639
|
+
`${contextName}.config(...) takes one object literal, e.g. ${contextName}.config({ description: '...' }).`,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
validateConfigName(source, args[0] as AstNode);
|
|
643
|
+
if (configCalls.length > 1) {
|
|
644
|
+
fail(source, node, `${contextName}.config(...) may appear only once.`);
|
|
645
|
+
}
|
|
646
|
+
if (configExport) {
|
|
647
|
+
fail(
|
|
648
|
+
source,
|
|
649
|
+
node,
|
|
650
|
+
`The play config is declared twice: export const config (line ${lineOf(source, configExport)}) and ${contextName}.config(...) (line ${lineOf(source, node)}). Keep only export const config.`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
} else if (call === 'input') {
|
|
654
|
+
if (args.length > 0) {
|
|
655
|
+
fail(
|
|
656
|
+
source,
|
|
657
|
+
node,
|
|
658
|
+
`${contextName}.input() takes no arguments; declare the shape as a type: ${contextName}.input<{ domain: string }>().`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const type = typeArgument(node);
|
|
662
|
+
if (type)
|
|
663
|
+
inputTypes.add(source.slice(type.start as number, type.end as number));
|
|
664
|
+
if (inputTypes.size > 1) {
|
|
665
|
+
fail(
|
|
666
|
+
source,
|
|
667
|
+
node,
|
|
668
|
+
`${contextName}.input<T>() is called with different types (${[...inputTypes].join(', ')}). Use one input type.`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
childNodes(node).forEach(visit);
|
|
673
|
+
};
|
|
674
|
+
visit(program);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* What another play's type check sees for an imported script play: a play
|
|
679
|
+
* reference, which is all `deepline.runPlay(key, child, ...)` needs.
|
|
680
|
+
*/
|
|
681
|
+
export const SCRIPT_PLAY_MODULE_DECLARATION = `import type { DefinedPlay } from '${SCRIPT_SDK}';
|
|
682
|
+
declare const play: DefinedPlay<Record<string, unknown>, Record<string, unknown>>;
|
|
683
|
+
export default play;
|
|
684
|
+
`;
|
|
685
|
+
|
|
686
|
+
/** A config `name`, when present, is a non-empty string literal. */
|
|
687
|
+
function validateConfigName(source: string, config: AstNode): void {
|
|
688
|
+
const name = ((config.properties as AstNode[] | undefined) ?? []).find(
|
|
689
|
+
(property) =>
|
|
690
|
+
property.type === 'Property' &&
|
|
691
|
+
!property.computed &&
|
|
692
|
+
isAstNode(property.key) &&
|
|
693
|
+
(property.key.name === 'name' || property.key.value === 'name'),
|
|
694
|
+
);
|
|
695
|
+
if (
|
|
696
|
+
name &&
|
|
697
|
+
!(
|
|
698
|
+
isAstNode(name.value) &&
|
|
699
|
+
name.value.type === 'Literal' &&
|
|
700
|
+
typeof name.value.value === 'string' &&
|
|
701
|
+
name.value.value.trim()
|
|
702
|
+
)
|
|
703
|
+
) {
|
|
704
|
+
fail(source, name, 'config name must be a non-empty string literal.');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function lineOf(source: string, node: AstNode): number {
|
|
709
|
+
return source.slice(0, node.start as number).split('\n').length;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function fail(source: string, node: AstNode, message: string): never {
|
|
713
|
+
throw new Error(`line ${lineOf(source, node)}: ${message}`);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/** The direct children of an AST node. */
|
|
717
|
+
function childNodes(node: AstNode): AstNode[] {
|
|
718
|
+
return Object.entries(node).flatMap(([key, value]) =>
|
|
719
|
+
key === 'loc'
|
|
720
|
+
? []
|
|
721
|
+
: Array.isArray(value)
|
|
722
|
+
? value.filter(isAstNode)
|
|
723
|
+
: isAstNode(value)
|
|
724
|
+
? [value]
|
|
725
|
+
: [],
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Parsed programs by source text, least recently used first. Analyzers read
|
|
731
|
+
* the same files many times in one admission; a parse that throws is not kept.
|
|
732
|
+
*/
|
|
733
|
+
const programs = new Map<string, AstNode | null>();
|
|
734
|
+
const MAX_PROGRAMS = 16;
|
|
735
|
+
|
|
736
|
+
function programOf(source: string): AstNode | null {
|
|
737
|
+
const program = programs.has(source) ? programs.get(source)! : parse(source);
|
|
738
|
+
programs.delete(source);
|
|
739
|
+
programs.set(source, program);
|
|
740
|
+
if (programs.size > MAX_PROGRAMS) {
|
|
741
|
+
programs.delete(programs.keys().next().value!);
|
|
742
|
+
}
|
|
743
|
+
return program;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Leading imports and types; the play's code is everything after them. */
|
|
747
|
+
function headerOf(program: AstNode): AstNode[] {
|
|
748
|
+
const statements = program.body as AstNode[];
|
|
749
|
+
const firstCode = statements.findIndex(
|
|
750
|
+
(node) => !MODULE_SCOPE.has(node.type),
|
|
751
|
+
);
|
|
752
|
+
return firstCode === -1 ? statements : statements.slice(0, firstCode);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function isSdkImport(node: AstNode): boolean {
|
|
756
|
+
return (
|
|
757
|
+
node.type === 'ImportDeclaration' &&
|
|
758
|
+
isAstNode(node.source) &&
|
|
759
|
+
node.source.value === SCRIPT_SDK
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function parse(source: string): AstNode | null {
|
|
764
|
+
try {
|
|
765
|
+
return TypeScriptParser.parse(source, {
|
|
766
|
+
ecmaVersion: 'latest',
|
|
767
|
+
sourceType: 'module',
|
|
768
|
+
allowHashBang: true,
|
|
769
|
+
// Script plays may end early with a top-level `return`.
|
|
770
|
+
allowReturnOutsideFunction: true,
|
|
771
|
+
}) as unknown as AstNode;
|
|
772
|
+
} catch (error) {
|
|
773
|
+
const looksLikeScript =
|
|
774
|
+
/\bdeepline\s*\./.test(source) &&
|
|
775
|
+
!/\bexport\b|\bdefinePlay\b/.test(source);
|
|
776
|
+
if (!looksLikeScript) return null;
|
|
777
|
+
const line = (error as { loc?: { line?: number } }).loc?.line;
|
|
778
|
+
throw new Error(
|
|
779
|
+
`line ${line ?? '?'}: ${(error as Error).message.replace(/ \(\d+:\d+\)$/, '')}`,
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* A script play exports nothing besides its `config` and a default; it never defines a play
|
|
786
|
+
* itself. `entry` is the default-exported function of the input, or null for
|
|
787
|
+
* top-level code alone or a default export that is not such a function
|
|
788
|
+
* (which `validateScriptPlay` rejects).
|
|
789
|
+
*/
|
|
790
|
+
function playForm(program: AstNode): { entry: PlayEntry | null } | null {
|
|
791
|
+
if (containsDefinePlayCall(program)) return null;
|
|
792
|
+
const configExport = configExportOf(program);
|
|
793
|
+
const exports = (program.body as AstNode[]).filter(
|
|
794
|
+
(node) =>
|
|
795
|
+
node !== configExport && /^Export|^TSExportAssignment$/.test(node.type),
|
|
796
|
+
);
|
|
797
|
+
if (exports.length === 0) return { entry: null };
|
|
798
|
+
const [statement] = exports;
|
|
799
|
+
if (exports.length !== 1 || !isDefaultExport(statement!)) return null;
|
|
800
|
+
const entry = entryOf(program, statement!);
|
|
801
|
+
// A legacy handler takes the context itself, `(ctx, input) => ...`, and the
|
|
802
|
+
// runner keeps calling it that way. A play function takes only its input.
|
|
803
|
+
if (entry && (entry.node.params as AstNode[]).length > 1) return null;
|
|
804
|
+
// Without `import deepline from 'deepline'`, a default-exported play
|
|
805
|
+
// function is still a play: `wrapScriptPlay` adds the import. Any other
|
|
806
|
+
// default export, or a file that imports something else as `deepline`, is
|
|
807
|
+
// a legacy module the runner calls as it always has.
|
|
808
|
+
if (!importsRuntime(program) && (!entry || importsRuntimeName(program))) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
return { entry };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** The top-level `export const config = ...` statement, if any. */
|
|
815
|
+
function configExportOf(program: AstNode): AstNode | null {
|
|
816
|
+
return (
|
|
817
|
+
(program.body as AstNode[]).find(
|
|
818
|
+
(node) =>
|
|
819
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
820
|
+
!node.source &&
|
|
821
|
+
isAstNode(node.declaration) &&
|
|
822
|
+
node.declaration.type === 'VariableDeclaration' &&
|
|
823
|
+
(node.declaration.declarations as AstNode[]).some(
|
|
824
|
+
(declarator) => (declarator.id as AstNode).name === 'config',
|
|
825
|
+
),
|
|
826
|
+
) ?? null
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* The object literal `export const config = {...}` declares, through
|
|
832
|
+
* `as const`/`satisfies T`; null when it declares anything else.
|
|
833
|
+
*/
|
|
834
|
+
function configObjectOf(configExport: AstNode): AstNode | null {
|
|
835
|
+
const declaration = configExport.declaration as AstNode;
|
|
836
|
+
const declarators = declaration.declarations as AstNode[];
|
|
837
|
+
if (declaration.kind !== 'const' || declarators.length !== 1) return null;
|
|
838
|
+
const init = unwrapped(declarators[0]!.init);
|
|
839
|
+
return init?.type === 'ObjectExpression' ? init : null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** `export default ...` or `export { name as default }`. */
|
|
843
|
+
function isDefaultExport(node: AstNode): boolean {
|
|
844
|
+
if (node.type === 'ExportDefaultDeclaration') return true;
|
|
845
|
+
const specifiers = (node.specifiers as AstNode[] | undefined) ?? [];
|
|
846
|
+
return (
|
|
847
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
848
|
+
!node.declaration &&
|
|
849
|
+
!node.source &&
|
|
850
|
+
specifiers.length === 1 &&
|
|
851
|
+
exportedName(specifiers[0]!) === 'default'
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function exportedName(specifier: AstNode): unknown {
|
|
856
|
+
const exported = specifier.exported as AstNode | undefined;
|
|
857
|
+
return exported?.name ?? exported?.value;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/** The function a default export statement names, when it is one. */
|
|
861
|
+
function entryOf(program: AstNode, statement: AstNode): PlayEntry | null {
|
|
862
|
+
if (statement.type === 'ExportDefaultDeclaration') {
|
|
863
|
+
const declared = unwrapped(statement.declaration);
|
|
864
|
+
if (declared && FUNCTION_TYPES.has(declared.type)) {
|
|
865
|
+
const id = declared.id as AstNode | null | undefined;
|
|
866
|
+
return {
|
|
867
|
+
node: declared,
|
|
868
|
+
name:
|
|
869
|
+
declared.type === 'FunctionDeclaration' && id
|
|
870
|
+
? (id.name as string)
|
|
871
|
+
: null,
|
|
872
|
+
exportedAs:
|
|
873
|
+
declared.type === 'FunctionDeclaration'
|
|
874
|
+
? 'declaration'
|
|
875
|
+
: 'expression',
|
|
876
|
+
exportStatement: statement,
|
|
877
|
+
declaration: null,
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return declared?.type === 'Identifier'
|
|
881
|
+
? boundEntry(program, statement, declared.name as string, 'binding')
|
|
882
|
+
: null;
|
|
883
|
+
}
|
|
884
|
+
const [specifier] = statement.specifiers as AstNode[];
|
|
885
|
+
const local = specifier!.local as AstNode;
|
|
886
|
+
return boundEntry(program, statement, local.name as string, 'specifier');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** `name`'s top-level function declaration or `const name = <function>`. */
|
|
890
|
+
function boundEntry(
|
|
891
|
+
program: AstNode,
|
|
892
|
+
exportStatement: AstNode,
|
|
893
|
+
name: string,
|
|
894
|
+
exportedAs: 'binding' | 'specifier',
|
|
895
|
+
): PlayEntry | null {
|
|
896
|
+
for (const node of program.body as AstNode[]) {
|
|
897
|
+
if (
|
|
898
|
+
node.type === 'FunctionDeclaration' &&
|
|
899
|
+
(node.id as AstNode | null)?.name === name
|
|
900
|
+
) {
|
|
901
|
+
return { node, name, exportedAs, exportStatement, declaration: node };
|
|
902
|
+
}
|
|
903
|
+
if (node.type !== 'VariableDeclaration') continue;
|
|
904
|
+
for (const declarator of node.declarations as AstNode[]) {
|
|
905
|
+
const init = unwrapped(declarator.init);
|
|
906
|
+
if (
|
|
907
|
+
(declarator.id as AstNode).name === name &&
|
|
908
|
+
init &&
|
|
909
|
+
FUNCTION_TYPES.has(init.type)
|
|
910
|
+
) {
|
|
911
|
+
return {
|
|
912
|
+
node: init,
|
|
913
|
+
name,
|
|
914
|
+
exportedAs,
|
|
915
|
+
exportStatement,
|
|
916
|
+
declaration: node,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* The module's own calls to the entry that run whenever the module does:
|
|
926
|
+
* outside functions, branches and loops. `if (import.meta.main) main()` is
|
|
927
|
+
* not one; the runtime calls the entry itself.
|
|
928
|
+
*/
|
|
929
|
+
function entryCallsOf(program: AstNode, entry: PlayEntry): AstNode[] {
|
|
930
|
+
if (!entry.name) return [];
|
|
931
|
+
const calls: AstNode[] = [];
|
|
932
|
+
const visit = (node: AstNode): void => {
|
|
933
|
+
if (CONDITIONAL_TYPES.has(node.type)) return;
|
|
934
|
+
if (
|
|
935
|
+
node.type === 'CallExpression' &&
|
|
936
|
+
isAstNode(node.callee) &&
|
|
937
|
+
node.callee.type === 'Identifier' &&
|
|
938
|
+
node.callee.name === entry.name
|
|
939
|
+
) {
|
|
940
|
+
calls.push(node);
|
|
941
|
+
}
|
|
942
|
+
childNodes(node).forEach(visit);
|
|
943
|
+
};
|
|
944
|
+
(program.body as AstNode[])
|
|
945
|
+
.filter(
|
|
946
|
+
(node) => node !== entry.declaration && node !== entry.exportStatement,
|
|
947
|
+
)
|
|
948
|
+
.forEach(visit);
|
|
949
|
+
return calls;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function importsRuntime(program: AstNode): boolean {
|
|
953
|
+
return (program.body as AstNode[]).some(
|
|
954
|
+
(node) => isSdkImport(node) && defaultImportName(node) !== null,
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Whether an import binds the name `deepline`, so adding the runtime's would clash. */
|
|
959
|
+
function importsRuntimeName(program: AstNode): boolean {
|
|
960
|
+
return (program.body as AstNode[]).some(
|
|
961
|
+
(node) =>
|
|
962
|
+
node.type === 'ImportDeclaration' &&
|
|
963
|
+
(node.specifiers as AstNode[]).some(
|
|
964
|
+
(specifier) => (specifier.local as AstNode).name === SCRIPT_SDK,
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function containsDefinePlayCall(node: AstNode): boolean {
|
|
970
|
+
if (node.type === 'CallExpression' && isAstNode(node.callee)) {
|
|
971
|
+
const callee = node.callee;
|
|
972
|
+
const name =
|
|
973
|
+
callee.type === 'Identifier'
|
|
974
|
+
? callee.name
|
|
975
|
+
: callee.type === 'MemberExpression' && isAstNode(callee.property)
|
|
976
|
+
? callee.property.name
|
|
977
|
+
: null;
|
|
978
|
+
if (DEFINE_CALLEES.has(name as string)) return true;
|
|
979
|
+
}
|
|
980
|
+
return childNodes(node).some(containsDefinePlayCall);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function defaultImportName(
|
|
984
|
+
importDeclaration: AstNode | undefined,
|
|
985
|
+
): string | null {
|
|
986
|
+
const specifier = (
|
|
987
|
+
importDeclaration?.specifiers as AstNode[] | undefined
|
|
988
|
+
)?.find((node) => node.type === 'ImportDefaultSpecifier');
|
|
989
|
+
return specifier && isAstNode(specifier.local)
|
|
990
|
+
? (specifier.local.name as string)
|
|
991
|
+
: null;
|
|
992
|
+
}
|