@dudousxd/nestjs-catalog 0.21.0 → 0.23.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/catalog.pipeline.d.ts +393 -8
- package/dist/catalog.pipeline.js +438 -1
- package/dist/catalog.stage-encoding.d.ts +72 -0
- package/dist/catalog.stage-encoding.js +100 -0
- package/dist/client.d.ts +3 -2
- package/dist/client.js +28 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +21 -3
- package/dist/transform-runner.d.ts +7 -1
- package/dist/transform-runner.js +184 -26
- package/dist/transform-shape.d.ts +106 -0
- package/dist/transform-shape.js +419 -0
- package/package.json +1 -1
package/dist/transform-runner.js
CHANGED
|
@@ -12,11 +12,15 @@ var SubprocessTransformRunner_1;
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.SubprocessTransformRunner = void 0;
|
|
14
14
|
const node_child_process_1 = require("node:child_process");
|
|
15
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
16
|
const node_fs_1 = require("node:fs");
|
|
17
|
+
const promises_1 = require("node:fs/promises");
|
|
16
18
|
const node_os_1 = require("node:os");
|
|
17
19
|
const node_path_1 = require("node:path");
|
|
20
|
+
const node_url_1 = require("node:url");
|
|
18
21
|
const common_1 = require("@nestjs/common");
|
|
19
22
|
const catalog_pipeline_1 = require("./catalog.pipeline");
|
|
23
|
+
const transform_shape_1 = require("./transform-shape");
|
|
20
24
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
25
|
const MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
22
26
|
/**
|
|
@@ -127,7 +131,12 @@ const REPORTED_PACKAGES = ['pandas', 'numpy', 'pyarrow', 'requests'];
|
|
|
127
131
|
* - it runs in a working directory of this runner's choosing but on the host's
|
|
128
132
|
* filesystem, so a service account token under
|
|
129
133
|
* `/var/run/secrets/kubernetes.io/serviceaccount/` is an absolute path away;
|
|
130
|
-
* - it can open sockets, as whatever user the service runs as
|
|
134
|
+
* - it can open sockets, as whatever user the service runs as;
|
|
135
|
+
* - a module-shaped transform is written to a file in that temporary directory
|
|
136
|
+
* for the length of the run, so its own source is briefly on disk. That is
|
|
137
|
+
* not a new exposure — the code is the thing running, and it can read itself
|
|
138
|
+
* from anywhere — but it is a fact worth having written down next to the
|
|
139
|
+
* others rather than discovered in a directory listing.
|
|
131
140
|
*
|
|
132
141
|
* So the allowlist is a guard rail against the accident, and the reachability of
|
|
133
142
|
* everything it names is a property of the process boundary, not a leak to be
|
|
@@ -223,24 +232,40 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
223
232
|
if (!interpreter) {
|
|
224
233
|
throw new Error('No python3 on PATH, so python transforms cannot run here. Use javascript or typescript, or install python in the image.');
|
|
225
234
|
}
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
// Python is not asked: its harness writes the `def`, so a Python transform
|
|
236
|
+
// never states a signature and has nothing to detect. See
|
|
237
|
+
// {@link pythonHarness}.
|
|
238
|
+
const shape = python ? 'body' : (0, transform_shape_1.transformShape)(transform.code);
|
|
239
|
+
// Written to disk only for the module shape, and only for the length of the
|
|
240
|
+
// run. A module has to be *imported* to be a module — its `export default`
|
|
241
|
+
// creates no binding this harness could name, and rewriting the keyword
|
|
242
|
+
// into an assignment would be surgery on somebody's source. A file also
|
|
243
|
+
// gives Node the extension it needs to strip TypeScript, and gives the
|
|
244
|
+
// author stack frames with real line numbers instead of `[eval]`.
|
|
245
|
+
const modulePath = shape === 'module'
|
|
246
|
+
? (0, node_path_1.join)((0, node_os_1.tmpdir)(), `catalog-transform-${(0, node_crypto_1.randomUUID)()}.${transform.language === 'typescript' ? 'mts' : 'mjs'}`)
|
|
247
|
+
: undefined;
|
|
248
|
+
try {
|
|
249
|
+
if (modulePath)
|
|
250
|
+
await (0, promises_1.writeFile)(modulePath, transform.code, 'utf8');
|
|
251
|
+
const args = interpreterArgs(transform, modulePath);
|
|
252
|
+
return await this.execute(interpreter, args, records, context, timeoutMs, shape, started);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
// Unlinked whether the run returned, threw, or was killed on the timeout —
|
|
256
|
+
// the parent settles in all three, so nothing is left in `tmpdir` for an
|
|
257
|
+
// operator to find later and wonder about.
|
|
258
|
+
if (modulePath)
|
|
259
|
+
await (0, promises_1.rm)(modulePath, { force: true });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async execute(interpreter, args, records, context, timeoutMs, shape, started) {
|
|
238
263
|
// An envelope rather than the bare array stdin used to carry. The context
|
|
239
264
|
// travels beside the records rather than in the child's `env`, and that is
|
|
240
265
|
// the deliberate half of it: the child's own environment stays
|
|
241
266
|
// `{PATH, NODE_ENV}`, so nothing about what a transform may read changes by
|
|
242
267
|
// accident when somebody edits the spawn options later.
|
|
243
|
-
const { stdout, stderr } = await this.spawn(interpreter, args, JSON.stringify({ records, context }), timeoutMs);
|
|
268
|
+
const { stdout, stderr } = await this.spawn(interpreter, args, JSON.stringify({ records, context }), timeoutMs, shape);
|
|
244
269
|
let parsed;
|
|
245
270
|
try {
|
|
246
271
|
// The harness prints exactly one JSON line last; anything the code wrote
|
|
@@ -262,7 +287,7 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
262
287
|
elapsedMs: Date.now() - started,
|
|
263
288
|
};
|
|
264
289
|
}
|
|
265
|
-
spawn(command, args, input, timeoutMs) {
|
|
290
|
+
spawn(command, args, input, timeoutMs, shape = 'body') {
|
|
266
291
|
return new Promise((resolve, reject) => {
|
|
267
292
|
const child = (0, node_child_process_1.spawn)(command, args, {
|
|
268
293
|
// An empty environment, not the parent's. A transform has no business
|
|
@@ -321,7 +346,11 @@ let SubprocessTransformRunner = SubprocessTransformRunner_1 = class SubprocessTr
|
|
|
321
346
|
settled = true;
|
|
322
347
|
clearTimeout(timer);
|
|
323
348
|
if (code !== 0 && stdout.trim().length === 0) {
|
|
324
|
-
|
|
349
|
+
// The shape hint rides along here specifically: a body whose author
|
|
350
|
+
// meant it as a module fails before the harness's own try/catch is
|
|
351
|
+
// ever entered, so this branch is the only place that error can be
|
|
352
|
+
// annotated. See {@link transformShapeHint}.
|
|
353
|
+
reject(new Error(`The transform exited with code ${code}. ${stderr.slice(0, 500)}${(0, transform_shape_1.transformShapeHint)(shape, stderr)}`));
|
|
325
354
|
return;
|
|
326
355
|
}
|
|
327
356
|
resolve({ stdout, stderr });
|
|
@@ -369,6 +398,27 @@ exports.SubprocessTransformRunner = SubprocessTransformRunner = SubprocessTransf
|
|
|
369
398
|
(0, common_1.Injectable)(),
|
|
370
399
|
__metadata("design:paramtypes", [Object])
|
|
371
400
|
], SubprocessTransformRunner);
|
|
401
|
+
/**
|
|
402
|
+
* What the interpreter is invoked with, and which harness it is handed.
|
|
403
|
+
*
|
|
404
|
+
* `module-typescript` is Node's own stripping — types are erased, never
|
|
405
|
+
* checked. A transform with a wrong type still runs; the editor's try pane is
|
|
406
|
+
* what catches it, not the compiler.
|
|
407
|
+
*
|
|
408
|
+
* The module shape needs none of that flag here: the harness itself is plain
|
|
409
|
+
* JavaScript, and the author's `.mts` file is stripped on import by its
|
|
410
|
+
* extension. That confines the stripper to the code that asked for it, rather
|
|
411
|
+
* than running this file's own generated source through it as well.
|
|
412
|
+
*/
|
|
413
|
+
function interpreterArgs(transform, modulePath) {
|
|
414
|
+
if (transform.language === 'python')
|
|
415
|
+
return ['-c', pythonHarness(transform.code)];
|
|
416
|
+
const script = modulePath
|
|
417
|
+
? javascriptModuleHarness((0, node_url_1.pathToFileURL)(modulePath).href)
|
|
418
|
+
: javascriptHarness(transform.code);
|
|
419
|
+
const inputType = transform.language === 'typescript' && !modulePath ? 'module-typescript' : 'module';
|
|
420
|
+
return ['--input-type', inputType, '-e', script];
|
|
421
|
+
}
|
|
372
422
|
/**
|
|
373
423
|
* The context for a run that has none: a spec, or a host driving the runner by
|
|
374
424
|
* hand.
|
|
@@ -437,7 +487,15 @@ function withFinalLogs(error, logs) {
|
|
|
437
487
|
return `${error}\n${heading}\n${tail.map((line) => ` ${line}`).join('\n')}`;
|
|
438
488
|
}
|
|
439
489
|
/**
|
|
440
|
-
* The JavaScript and TypeScript harness
|
|
490
|
+
* The JavaScript and TypeScript harness for the **bare-body** shape: the
|
|
491
|
+
* author's code is the inside of a function this string writes.
|
|
492
|
+
*
|
|
493
|
+
* Unchanged, and that is the feature. Every transform stored before the module
|
|
494
|
+
* shape existed is a bare body, and it runs through the identical wrapper with
|
|
495
|
+
* the identical positional parameters and the identical interpreter flags — the
|
|
496
|
+
* new shape is a second path beside this one, not a rewrite of it. See
|
|
497
|
+
* `transform-shape.ts` for the rule that decides which path a given piece of
|
|
498
|
+
* code takes, and why a stored transform cannot be sent down the wrong one.
|
|
441
499
|
*
|
|
442
500
|
* `console.log` is captured rather than left on stdout so user code cannot
|
|
443
501
|
* corrupt the single JSON line this prints — a transform that logs a `{` would
|
|
@@ -462,7 +520,84 @@ function withFinalLogs(error, logs) {
|
|
|
462
520
|
* gets as far as being serialised.
|
|
463
521
|
*/
|
|
464
522
|
function javascriptHarness(code) {
|
|
465
|
-
return
|
|
523
|
+
return `${JAVASCRIPT_PRELUDE}
|
|
524
|
+
try {
|
|
525
|
+
${JAVASCRIPT_PAYLOAD}
|
|
526
|
+
const transform = async (records, context) => { ${code} };
|
|
527
|
+
const rows = await transform(records, context);
|
|
528
|
+
process.stdout.write(JSON.stringify({ rows: rows ?? [], logs: captured() }));
|
|
529
|
+
} catch (error) {
|
|
530
|
+
${JAVASCRIPT_FAILURE}
|
|
531
|
+
}
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The harness for the module shape: import the author's module, call what it
|
|
536
|
+
* exports with one object.
|
|
537
|
+
*
|
|
538
|
+
* Everything above the call is the same prelude the bare-body harness uses —
|
|
539
|
+
* the same six console channels, the same two caps, the same envelope, the same
|
|
540
|
+
* frozen `context`. A transform's log behaviour changing because of the shape it
|
|
541
|
+
* happens to be written in would be exactly as surprising as it changing because
|
|
542
|
+
* of the language, and the constants say why that is not allowed to happen.
|
|
543
|
+
*
|
|
544
|
+
* The code arrives as a **file URL**, not as text spliced into this string, and
|
|
545
|
+
* the difference matters three times over. `export default` binds nothing that
|
|
546
|
+
* an enclosing scope could name, so the module genuinely has to be imported;
|
|
547
|
+
* `.mts` is what tells Node to strip the types, so the extension does the job
|
|
548
|
+
* `--input-type module-typescript` does for a body; and a stack frame reads
|
|
549
|
+
* `catalog-transform-….mts:3:11` rather than `[eval]`, which is the difference
|
|
550
|
+
* between a line number and a shrug.
|
|
551
|
+
*
|
|
552
|
+
* ## What it accepts, and what it refuses
|
|
553
|
+
*
|
|
554
|
+
* `export default`, or a named export called `transform`. Two spellings rather
|
|
555
|
+
* than one because both are things people write without being told to, and
|
|
556
|
+
* because both are *real exports* — neither is a guess about a name in scope.
|
|
557
|
+
*
|
|
558
|
+
* A module that exports neither is **refused, by name**. The alternative is a
|
|
559
|
+
* transform that returns no rows and says nothing about why, which downstream
|
|
560
|
+
* reads as a source that produced nothing — a connector would commit an empty
|
|
561
|
+
* snapshot over live data on the strength of a missing `default` keyword.
|
|
562
|
+
*
|
|
563
|
+
* The import is deliberately not wrapped in a fallback to the body shape. Code
|
|
564
|
+
* that fails to parse as a module has one honest answer — the parse error, with
|
|
565
|
+
* the line — and re-running it in the other shape would replace that with a
|
|
566
|
+
* second, different error about text the author never wrote.
|
|
567
|
+
*/
|
|
568
|
+
function javascriptModuleHarness(moduleUrl) {
|
|
569
|
+
return `${JAVASCRIPT_PRELUDE}
|
|
570
|
+
try {
|
|
571
|
+
${JAVASCRIPT_PAYLOAD}
|
|
572
|
+
const mod = await import(${JSON.stringify(moduleUrl)});
|
|
573
|
+
const exported = typeof mod.default === "function"
|
|
574
|
+
? mod.default
|
|
575
|
+
: typeof mod.transform === "function" ? mod.transform : null;
|
|
576
|
+
if (!exported) {
|
|
577
|
+
const names = Object.keys(mod).filter((key) => key !== "default");
|
|
578
|
+
throw new Error(
|
|
579
|
+
"This transform is a module — it has a top-level \`export\` — so the catalog imported it and " +
|
|
580
|
+
"looked for a function to call. \`export default\` is " + (("default" in mod) ? typeof mod.default : "missing") +
|
|
581
|
+
" and there is no exported \`transform\` function." +
|
|
582
|
+
(names.length > 0 ? " It does export: " + names.join(", ") + "." : "") +
|
|
583
|
+
" Export the function as \`export default\`, or name it \`transform\`."
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const rows = await exported({ records, context });
|
|
587
|
+
process.stdout.write(JSON.stringify({ rows: rows ?? [], logs: captured() }));
|
|
588
|
+
} catch (error) {
|
|
589
|
+
${JAVASCRIPT_FAILURE}
|
|
590
|
+
}
|
|
591
|
+
`;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Capture the console, before any of the author's code can reach it.
|
|
595
|
+
*
|
|
596
|
+
* Shared verbatim by both JavaScript harnesses rather than copied into each: two
|
|
597
|
+
* copies of a log cap are two numbers that drift, and the one that drifts is
|
|
598
|
+
* discovered by a run record nobody can explain.
|
|
599
|
+
*/
|
|
600
|
+
const JAVASCRIPT_PRELUDE = `
|
|
466
601
|
const logs = [];
|
|
467
602
|
let dropped = 0;
|
|
468
603
|
const keep = (line) => {
|
|
@@ -483,8 +618,9 @@ const captured = () => dropped === 0
|
|
|
483
618
|
let input = "";
|
|
484
619
|
process.stdin.setEncoding("utf8");
|
|
485
620
|
for await (const chunk of process.stdin) input += chunk;
|
|
486
|
-
|
|
487
|
-
|
|
621
|
+
`;
|
|
622
|
+
/** Unpack the envelope. `context` is frozen one level down — see below. */
|
|
623
|
+
const JAVASCRIPT_PAYLOAD = `
|
|
488
624
|
const payload = JSON.parse(input || "{}");
|
|
489
625
|
const records = Array.isArray(payload.records) ? payload.records : [];
|
|
490
626
|
// Frozen, and one level down as well, so that a transform assigning to
|
|
@@ -492,20 +628,42 @@ try {
|
|
|
492
628
|
// then confusing whoever reads the next node's code. Nothing propagates out
|
|
493
629
|
// of this process either way; the freeze buys the honest error, not safety.
|
|
494
630
|
const context = Object.freeze({ ...payload.context, env: Object.freeze({ ...payload.context?.env }) });
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
} catch (error) {
|
|
631
|
+
`;
|
|
632
|
+
/** The one JSON line a failed run prints, logs and all. */
|
|
633
|
+
const JAVASCRIPT_FAILURE = `
|
|
499
634
|
process.stdout.write(JSON.stringify({
|
|
500
635
|
error: error instanceof Error ? \`\${error.name}: \${error.message}\` : String(error),
|
|
501
636
|
logs: captured(),
|
|
502
637
|
}));
|
|
503
|
-
}
|
|
504
638
|
`;
|
|
505
|
-
}
|
|
506
639
|
/**
|
|
507
640
|
* The Python harness. `records` in, a list of dicts out.
|
|
508
641
|
*
|
|
642
|
+
* ## Why this did not move to the one-object shape
|
|
643
|
+
*
|
|
644
|
+
* JavaScript moved because a JavaScript transform *states its own signature* —
|
|
645
|
+
* the harness wrote `(records, context)` and the author's code depended on both
|
|
646
|
+
* names being where they were, so a third parameter would have been a change to
|
|
647
|
+
* text nobody was going to re-read. A Python transform states nothing. This
|
|
648
|
+
* harness writes `def transform(records, context):` and indents the author's
|
|
649
|
+
* code into it, so a fourth thing to pass is **one line changed here** and not a
|
|
650
|
+
* single stored transform touched. Python already has the property the object
|
|
651
|
+
* shape was introduced to buy.
|
|
652
|
+
*
|
|
653
|
+
* Moving it anyway would cost the thing the move was for. `records` and
|
|
654
|
+
* `context` are names in scope today; a `payload` dict makes them
|
|
655
|
+
* `payload["records"]` and `payload["context"]`, which is a break in every
|
|
656
|
+
* Python transform in existence — the exact outcome the JavaScript change was
|
|
657
|
+
* designed to avoid. Consistency between the two languages is worth something,
|
|
658
|
+
* but not a migration bought with somebody else's pandas code, and not when the
|
|
659
|
+
* inconsistency is *because* the two languages start from different places.
|
|
660
|
+
*
|
|
661
|
+
* The asymmetry left standing, said out loud: a Python author who writes their
|
|
662
|
+
* own `def transform(...)` at column 0 gets it indented into a nested
|
|
663
|
+
* definition, and the outer `transform` returns `None` — no error, no rows. That
|
|
664
|
+
* is a real footgun and it is older than this change; it is named here so the
|
|
665
|
+
* next person to open this file knows it is known rather than missed.
|
|
666
|
+
*
|
|
509
667
|
* A DataFrame is accepted as a return value and converted, because a transform
|
|
510
668
|
* that reaches for pandas will naturally end with one — making it write
|
|
511
669
|
* `.to_dict("records")` would be a papercut on the only path pandas is worth
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of the two shapes a JavaScript or TypeScript transform is written in,
|
|
3
|
+
* and the one rule that tells them apart.
|
|
4
|
+
*
|
|
5
|
+
* ## The two shapes
|
|
6
|
+
*
|
|
7
|
+
* A transform used to be — and, for everything already stored, still is — a
|
|
8
|
+
* **bare body**: the text between the braces of a function the harness supplies.
|
|
9
|
+
*
|
|
10
|
+
* ```js
|
|
11
|
+
* return records.map((r) => ({ mgmtCd: r["Mgmt Cd"] }));
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* That shape works, and its problem is not syntax. The harness supplied
|
|
15
|
+
* `(records, context)` **positionally**, so the set of things a transform can be
|
|
16
|
+
* given was fixed by the day the second parameter was added: a third one changes
|
|
17
|
+
* the meaning of every signature ever written, and there is no version of
|
|
18
|
+
* "records, context, andNowAlsoThis" that does not make the previous shape a
|
|
19
|
+
* subset by luck rather than by design. So the supported shape is now a real
|
|
20
|
+
* function over **one object**:
|
|
21
|
+
*
|
|
22
|
+
* ```js
|
|
23
|
+
* export default function transform({ records, context }) {
|
|
24
|
+
* return records.map((r) => ({ mgmtCd: r["Mgmt Cd"] }));
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* A field can be added to that object without touching a single stored
|
|
29
|
+
* transform, which is the entire argument for it.
|
|
30
|
+
*
|
|
31
|
+
* ## The rule
|
|
32
|
+
*
|
|
33
|
+
* **A top-level `export` keyword, and nothing else.** Code that has one is a
|
|
34
|
+
* module; code that has none is a body.
|
|
35
|
+
*
|
|
36
|
+
* That is a discriminator rather than a heuristic, and the reason is worth being
|
|
37
|
+
* exact about: `export` is a *syntax error* inside a function body. Every
|
|
38
|
+
* transform stored today runs as a function body today, so no stored transform
|
|
39
|
+
* can contain a top-level `export` — not "probably does not", cannot. Backward
|
|
40
|
+
* compatibility here is a property of the language, not of how good the guess is.
|
|
41
|
+
*
|
|
42
|
+
* ## What the rule deliberately does not look at
|
|
43
|
+
*
|
|
44
|
+
* Not the word `function`. Not a function *declaration* named `transform`
|
|
45
|
+
* either, which is the tempting second rule and is the one that would break
|
|
46
|
+
* real code:
|
|
47
|
+
*
|
|
48
|
+
* ```js
|
|
49
|
+
* function transform(r) { return { mgmtCd: r["Mgmt Cd"] }; }
|
|
50
|
+
* return records.map(transform);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* That is a bare body which declares a local helper it happens to have named
|
|
54
|
+
* `transform`. A detector that called it the new shape would call the helper
|
|
55
|
+
* with `{records, context}` — one object where a record was expected — and store
|
|
56
|
+
* 100,000 rows of `undefined` without erroring once. Silent wrong data is the
|
|
57
|
+
* worst failure available here, so the detector does not offer an opinion about
|
|
58
|
+
* names at all.
|
|
59
|
+
*
|
|
60
|
+
* ## The scan, and its one known limit
|
|
61
|
+
*
|
|
62
|
+
* Strings, template literals (including `${}` nesting), regular-expression
|
|
63
|
+
* literals and both kinds of comment are skipped, so `// export default` and
|
|
64
|
+
* `"export"` are not exports. The keyword must then appear at **statement
|
|
65
|
+
* position** — start of input, or after `;`, `}`, or a newline — at brace,
|
|
66
|
+
* paren and bracket depth zero.
|
|
67
|
+
*
|
|
68
|
+
* Regular-expression literals are found by the usual rule (a `/` is a regex
|
|
69
|
+
* unless what precedes it could end a value), which is the one place a scanner
|
|
70
|
+
* without a full parser can be wrong. Its consequence is bounded on purpose: a
|
|
71
|
+
* misread makes this return `false` for a module, the code is run as a body, and
|
|
72
|
+
* the author gets `SyntaxError: Unexpected token 'export'` — which
|
|
73
|
+
* {@link transformShapeHint} turns into a sentence naming this exact rule. A
|
|
74
|
+
* wrong answer here produces a reported error, never a silently different run.
|
|
75
|
+
*/
|
|
76
|
+
/** What shape a transform's code is in. */
|
|
77
|
+
export type TransformShape = 'module' | 'body';
|
|
78
|
+
/**
|
|
79
|
+
* Does this code declare an ES module — and so ask to be called as a function
|
|
80
|
+
* over one object?
|
|
81
|
+
*
|
|
82
|
+
* See the module docblock for the rule and why it is the rule. Python is not
|
|
83
|
+
* asked this question: its harness writes the `def` itself, so a Python
|
|
84
|
+
* transform never states a signature and never had the problem this detector
|
|
85
|
+
* exists to solve.
|
|
86
|
+
*/
|
|
87
|
+
export declare function transformDeclaresModule(code: string): boolean;
|
|
88
|
+
/** {@link transformDeclaresModule}, as the shape it names. */
|
|
89
|
+
export declare function transformShape(code: string): TransformShape;
|
|
90
|
+
/**
|
|
91
|
+
* The sentence to add when a body-shaped run died on the one syntax error that
|
|
92
|
+
* means the detector and the author disagreed.
|
|
93
|
+
*
|
|
94
|
+
* The rule above is a scan, not a parser, and its documented limit is that a
|
|
95
|
+
* regular-expression literal read as a division can hide a real `export`. The
|
|
96
|
+
* author then sees `Unexpected token 'export'` from code they believe is a
|
|
97
|
+
* perfectly good module, and has no way to know that a *rule they have never
|
|
98
|
+
* read* is what decided otherwise. Naming the rule in the error is the whole
|
|
99
|
+
* difference between a two-minute fix and an afternoon.
|
|
100
|
+
*
|
|
101
|
+
* Deliberately not a fallback re-run in the other shape. Running code twice
|
|
102
|
+
* because the first attempt failed is guessing with extra steps: the second
|
|
103
|
+
* attempt would report a different error for the same text, and neither error
|
|
104
|
+
* would be trustworthy.
|
|
105
|
+
*/
|
|
106
|
+
export declare function transformShapeHint(shape: TransformShape, stderr: string): string;
|