Skip to content

Commit f6712c1

Browse files
committed
feat(evasive-transform)!: allow returns outside functions in CommonJS sources
BREAKING CHANGE: This restricts the possible values of the `sourceType` option to `script` and `module` only. Types have been changed to reflect this. *BREAKING*: Do not provide a `sourceType` of `unambiguous`; use `module`, `script`, or omit.
1 parent 1c9cd7f commit f6712c1

File tree

11 files changed

+169
-65
lines changed

11 files changed

+169
-65
lines changed

packages/evasive-transform/NEWS.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
User-visible changes in `@endo/evasive-transform`:
22

3+
# Next release
4+
5+
- The `sourceType` option is now restricted to `script` and `module` only. Function signature types have changed to be more precise.
6+
37
# v1.4.0 (2025-03-11)
48

59
- Adds a `sourceMap` option so that the generated sourcemap can project back to

packages/evasive-transform/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ import fs from 'node:fs/promises';
1717
const source = await fs.readFile('./dist/index.js', 'utf8');
1818
const sourceMap = await fs.readFile('./dist/index.js.map', 'utf8');
1919
const sourceUrl = 'index.js'; // assuming the source map references index.js
20+
// sourceType can be "script" (CJS) or "module" (ESM)
2021
const sourceType = 'script';
2122

2223
const { code, map } = await evadeCensor(source, {
2324
sourceMap,
2425
sourceUrl,
26+
// always provide a sourceType, if known!
2527
sourceType,
2628
});
2729

packages/evasive-transform/src/generate.js

+51-43
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
3030
/**
3131
* Options for {@link generateCode} (no source map generated)
3232
*
33-
* @typedef GenerateAstOptionsWithoutSourceMap
33+
* @typedef GenerateAstOptions
3434
* @property {string} [source]
3535
* @property {undefined} [sourceUrl] - This should be undefined or otherwise not provided
3636
* @internal
@@ -40,62 +40,70 @@ const generator = /** @type {typeof import('@babel/generator')['default']} */ (
4040
* The result of {@link generate}; depends on whether a `sourceUrl` was
4141
* provided to the options.
4242
*
43-
* @template {string|undefined} [SourceUrl=undefined]
44-
* @typedef {{code: string, map: SourceUrl extends string ? string : never}} TransformedResult
43+
* @typedef {{code: string, map: undefined}} TransformedResult
4544
* @internal
4645
*/
4746

4847
/**
49-
* Generates new code from a Babel AST; returns code and source map
48+
* The result of {@link generate}; depends on whether a `sourceUrl` was
49+
* provided to the options.
5050
*
51-
* @callback GenerateAstWithSourceMap
51+
* @typedef {TransformedResult & { map: NonNullable<import('@babel/generator').GeneratorResult['map']>}} TransformedResultWithSourceMap
52+
* @internal
53+
*/
54+
55+
/**
56+
* Generates new code from a Babel AST; returns code and source map
57+
*@overload
5258
* @param {import('@babel/types').File} ast - Babel "File" AST
5359
* @param {GenerateAstOptionsWithSourceMap} options - Options for the transform
54-
* @returns {TransformedResult<string>}
60+
* @returns {TransformedResultWithSourceMap}
5561
* @internal
5662
*/
5763

5864
/**
5965
* Generates new code from a Babel AST; returns code only
60-
*
61-
* @callback GenerateAstWithoutSourceMap
66+
*@overload
6267
* @param {import('@babel/types').File} ast - Babel "File" AST
63-
* @param {GenerateAstOptionsWithoutSourceMap} [options] - Options for the transform
64-
* @returns {TransformedResult<undefined>}
68+
* @param {GenerateAstOptions} [options] - Options for the transform
69+
* @returns {TransformedResult}
6570
* @internal
6671
*/
67-
export const generate =
68-
/** @type {GenerateAstWithSourceMap & GenerateAstWithoutSourceMap} */ (
69-
(ast, options) => {
70-
// TODO Use options?.sourceUrl when resolved:
71-
// https://github.com/Agoric/agoric-sdk/issues/8671
72-
const sourceUrl = options ? options.sourceUrl : undefined;
73-
const inputSourceMap =
74-
options && 'sourceMap' in options ? options.sourceMap : undefined;
75-
const source = options ? options.source : undefined;
76-
const result = generator(
77-
ast,
78-
{
79-
sourceFileName: sourceUrl,
80-
sourceMaps: Boolean(sourceUrl),
81-
// @ts-expect-error Property missing on versioned types
82-
inputSourceMap,
83-
retainLines: true,
84-
...(source === undefined
85-
? {}
86-
: { experimental_preserveFormat: true }),
87-
},
88-
source,
89-
);
9072

91-
if (sourceUrl) {
92-
return {
93-
code: result.code,
94-
map: result.map,
95-
};
96-
}
97-
return {
98-
code: result.code,
99-
};
100-
}
73+
/**
74+
* Generates new code from a Babel AST; returns code only
75+
*
76+
* @param {import('@babel/types').File} ast - Babel "File" AST
77+
* @param {GenerateAstOptions} [options] - Options for the transform
78+
* @internal
79+
*/
80+
export const generate = (ast, options) => {
81+
// TODO Use options?.sourceUrl when resolved:
82+
// https://github.com/Agoric/agoric-sdk/issues/8671
83+
const sourceUrl = options ? options.sourceUrl : undefined;
84+
const inputSourceMap =
85+
options && 'sourceMap' in options ? options.sourceMap : undefined;
86+
const source = options ? options.source : undefined;
87+
const result = generator(
88+
ast,
89+
{
90+
sourceFileName: sourceUrl,
91+
sourceMaps: Boolean(sourceUrl),
92+
// @ts-expect-error Property missing on versioned types
93+
inputSourceMap,
94+
retainLines: true,
95+
...(source === undefined ? {} : { experimental_preserveFormat: true }),
96+
},
97+
source,
10198
);
99+
100+
if (sourceUrl) {
101+
return {
102+
code: result.code,
103+
map: result.map,
104+
};
105+
}
106+
return {
107+
code: result.code,
108+
};
109+
};

packages/evasive-transform/src/index.js

+54-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
/**
8-
* @import {TransformedResult} from './generate.js'
8+
* @import {TransformedResult, TransformedResultWithSourceMap} from './generate.js'
99
*/
1010

1111
import { transformAst } from './transform-ast.js';
@@ -30,10 +30,33 @@ import { generate } from './generate.js';
3030
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
3131
* value will be a source map object; otherwise it will be `undefined`.
3232
*
33-
* @template {EvadeCensorOptions} T
33+
* @overload
3434
* @param {string} source - Source code to transform
35-
* @param {T} [options] - Options for the transform
36-
* @returns {TransformedResult<T['sourceUrl']>} Object containing new code and optionally source map object (ready for stringification)
35+
* @param {EvadeCensorOptions & {sourceUrl: string}} options - Options for the transform
36+
* @returns {TransformedResultWithSourceMap} Object containing new code and optionally source map object (ready for stringification)
37+
* @public
38+
*/
39+
40+
/**
41+
* Apply SES censorship evasion transforms on the given code `source`
42+
*
43+
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
44+
* value will be a source map object; otherwise it will be `undefined`.
45+
*
46+
* @overload
47+
* @param {string} source - Source code to transform
48+
* @param {EvadeCensorOptions} [options] - Options for the transform
49+
* @returns {TransformedResult} Object containing new code and optionally source map object (ready for stringification)
50+
* @public
51+
*/
52+
/**
53+
* Apply SES censorship evasion transforms on the given code `source`
54+
*
55+
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
56+
* value will be a source map object; otherwise it will be `undefined`.
57+
*
58+
* @param {string} source - Source code to transform
59+
* @param {EvadeCensorOptions} [options] - Options for the transform
3760
* @public
3861
*/
3962
export function evadeCensorSync(source, options) {
@@ -64,10 +87,34 @@ export function evadeCensorSync(source, options) {
6487
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
6588
* value will be a source map object; otherwise it will be `undefined`.
6689
*
67-
* @template {EvadeCensorOptions} T
90+
* @overload
91+
* @param {string} source - Source code to transform
92+
* @param {EvadeCensorOptions & {sourceUrl: string}} options - Options for the transform
93+
* @returns {Promise<TransformedResultWithSourceMap>} Object containing new code and source map object (ready for stringification)
94+
* @public
95+
*/
96+
97+
/**
98+
* Apply SES censorship evasion transforms on the given code `source`
99+
*
100+
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
101+
* value will be a source map object; otherwise it will be `undefined`.
102+
*
103+
* @overload
104+
* @param {string} source - Source code to transform
105+
* @param {EvadeCensorOptions} [options] - Options for the transform
106+
* @returns {Promise<TransformedResult>} Object containing new code
107+
* @public
108+
*/
109+
110+
/**
111+
* Apply SES censorship evasion transforms on the given code `source`
112+
*
113+
* If the `sourceUrl` option is provided, the `map` property of the fulfillment
114+
* value will be a source map object; otherwise it will be `undefined`.
115+
*
68116
* @param {string} source - Source code to transform
69-
* @param {T} [options] - Options for the transform
70-
* @returns {Promise<TransformedResult<T['sourceUrl']>>} Object containing new code and optionally source map object (ready for stringification)
117+
* @param {EvadeCensorOptions} [options] - Options for the transform
71118
* @public
72119
*/
73120
export async function evadeCensor(source, options) {

packages/evasive-transform/src/parse-ast.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import * as babelParser from '@babel/parser';
99
const { parse: parseBabel } = babelParser;
1010

1111
/**
12-
* This is the same type as `@babel/parser`'s `ParserOptions['sourceType']`, but
12+
* This is a subset of `@babel/parser`'s `ParserOptions['sourceType']`, but
1313
* re-implemented here for decoupling purposes.
1414
*
15-
* Still, this is likely Babel-specific.
16-
*
17-
* @typedef {'module'|'script'|'unambiguous'} SourceType
15+
* @typedef {'module' | 'script'} SourceType
1816
* @public
1917
*/
2018

@@ -38,6 +36,7 @@ export function parseAst(source, opts = {}) {
3836
return parseBabel(source, {
3937
tokens: true,
4038
createParenthesizedExpressions: true,
39+
allowReturnOutsideFunction: opts.sourceType === 'script',
4140
...opts,
4241
});
4342
}

packages/evasive-transform/test/_prepare-test-env-ava-fixture.js

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,19 @@ import path from 'node:path';
22
import fs from 'node:fs/promises';
33
import url from 'url';
44

5-
import test from '@endo/ses-ava/prepare-endo.js';
5+
import someTest from '@endo/ses-ava/prepare-endo.js';
6+
7+
/**
8+
* @import {TestFn} from 'ava'
9+
*/
10+
11+
/**
12+
* For custom context
13+
*/
14+
const test =
15+
/** @type {TestFn<{source: string, sourceMap: string, sourceUrl: string}>} */ (
16+
someTest
17+
);
618

719
/**
820
* Path to fixture's bundled source code

packages/evasive-transform/test/elide-comment.test.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ test('evadeCensor with elideComments erases the interior of block comments', t =
3535
* Comment
3636
* @param {type} name
3737
*/`,
38-
{ elideComments: true },
38+
{ elideComments: true, sourceType: 'script' },
3939
);
4040
t.is(
4141
object.code,
@@ -47,7 +47,10 @@ test('evadeCensor with elideComments erases the interior of block comments', t =
4747
});
4848

4949
test('evadeCensor with elideComments elides line comments', t => {
50-
const object = evadeCensorSync(`// hello`, { elideComments: true });
50+
const object = evadeCensorSync(`// hello`, {
51+
elideComments: true,
52+
sourceType: 'script',
53+
});
5154
t.is(object.code, `//`);
5255
});
5356

@@ -64,6 +67,7 @@ test('evadeCensor with elideComments preserves jsdoc @preserve comments', t => {
6467
*/`;
6568
const object = evadeCensorSync(comment, {
6669
elideComments: true,
70+
sourceType: 'script',
6771
});
6872
t.is(object.code, comment);
6973
});
@@ -73,6 +77,7 @@ test('evadeCensor with elideComments preserves initial jsdoc @preserve comments'
7377
*/`;
7478
const object = evadeCensorSync(comment, {
7579
elideComments: true,
80+
sourceType: 'script',
7681
});
7782
t.is(object.code, comment);
7883
});
@@ -83,6 +88,7 @@ test('evadeCensor with elideComments preserves artless-but-valid jsdoc @preserve
8388
*/`;
8489
const object = evadeCensorSync(comment, {
8590
elideComments: true,
91+
sourceType: 'script',
8692
});
8793
t.is(object.code, comment);
8894
});
@@ -103,6 +109,7 @@ test('evadeCensor with elideComments preserves jsdoc @license comments', t => {
103109
*/`;
104110
const object = evadeCensorSync(comment, {
105111
elideComments: true,
112+
sourceType: 'script',
106113
});
107114
t.is(object.code, comment);
108115
});
@@ -113,6 +120,7 @@ test('evadeCensor with elideComments preserves jsdoc @cc_on comments', t => {
113120
*/`;
114121
const object = evadeCensorSync(comment, {
115122
elideComments: true,
123+
sourceType: 'script',
116124
});
117125
t.is(object.code, comment);
118126
});
@@ -123,6 +131,7 @@ test('evadeCensor with elideComments does not preserve jsdoc @copyrighteous comm
123131
*/`;
124132
const object = evadeCensorSync(comment, {
125133
elideComments: true,
134+
sourceType: 'script',
126135
});
127136
t.is(
128137
object.code,
@@ -143,6 +152,7 @@ test('evadeCensor with elideComments preserves automatically-inserted-semicolon
143152
`;
144153
const object = evadeCensorSync(comment, {
145154
elideComments: true,
155+
sourceType: 'script',
146156
});
147157
t.is((0, eval)(comment), undefined);
148158
t.is((0, eval)(object.code), undefined);
@@ -159,9 +169,12 @@ test('evadeCensor with stripComments preserves automatically-inserted-semicolon
159169
})();
160170
`;
161171
const object = evadeCensorSync(comment, {
172+
// @ts-expect-error no such thing
162173
stripComments: true,
174+
sourceType: 'script',
163175
});
164176
t.is((0, eval)(comment), undefined);
177+
// @ts-expect-error should not be a thing
165178
t.is((0, eval)(object.code), undefined);
166179
});
167180

0 commit comments

Comments
 (0)