-
Notifications
You must be signed in to change notification settings - Fork 75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Preserve formatting #2444
feat: Preserve formatting #2444
Conversation
inputSourceMap: sourceMap, | ||
retainLines: true, | ||
preserveFormat: true, | ||
compact: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nicolo-ribaudo Reviewing https://github.com/babel/babel/pull/16708/files#diff-ca2bda59eec9c35846e26c1c6247759c92c26357b80d4dc881fcdaf12df1d7e6R35-R39, it seems like compact: true
here should be causing your code to throw, so I suspect this isn’t exercising the new path in my local testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found it. Needed to swap out our fork @agoric/babel-generator
back to @babel/generator
.
f751a41
to
1fef37d
Compare
I’ll have a push soon, but this looks correct, actually: Pre-transform: function TokenType() {}
const beforeExpr = 0;
export function createBinop(name, binop) {
return new TokenType(name, {
beforeExpr,
binop,
});
} Post-transform: function TokenType() {}
const beforeExpr = 0;
function createBinop(name, binop) {
return new TokenType(name, {
beforeExpr,
binop,
});
} I’ll note that there’s a weird but explicable offset on the function declaration because we have erased This was the previous (undesirable) effect: function TokenType() { }
const beforeExpr= 0;
function createBinop(name, binop) {
return new TokenType(name, {
beforeExpr,
binop});
}
})() And the test I’ve proposed is not yet passing because the generator doesn’t add a newline at the end of the file. That presumably means that it needs to catch up with the final line and column of the file to preserve trailing whitespace, or just add a single newline to the end to make sure the output is a valid UNIX text file. |
2c8df48
to
ff89345
Compare
ff89345
to
e8ebd67
Compare
I’m mistaken above about the newlines issue. I’m updating the test fixture and it looks like the test will pass as-is, no changes needed from Babel. |
@@ -0,0 +1,10 @@ | |||
({ imports: $h͏_imports, liveVar: $h͏_live, onceVar: $h͏_once, importMeta: $h͏____meta, }) => (function () { 'use strict'; $h͏_imports([]);Object.defineProperty(createBinop, 'name', {value: "createBinop"});$h͏_once.createBinop(createBinop); // deliberately offset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually surprised by the spaces here. My expectation was that this would have been printed as compact as possible to increase the chances that the next node could be printed in the right place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see. This code with spaces is generated by you with string concatenation after that Babel runs, and not by Babel itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. I also find the spacing arbitrary, emerging from some unnecessary space in the formatting of the code. I’ve pushed a commit to make that compact as expected, regardless of if not being generated by Babel.
@@ -0,0 +1,10 @@ | |||
({ imports: $h͏_imports, liveVar: $h͏_live, onceVar: $h͏_once, importMeta: $h͏____meta, }) => (function () { 'use strict'; $h͏_imports([]);Object.defineProperty(createBinop, 'name', {value: "createBinop"});$h͏_once.createBinop(createBinop); // deliberately offset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is the closing paren for the function expression here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm it should be at then end of the file right? It looks like I forgot to flush some tokens somewhere 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, cloning the PR locally I get this additional line at the end of the file:
})()
Probably @kriskowal accidentally deleted it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed the fix for this. It was present locally when I got the test to pass.
31db01e
to
e04e72d
Compare
06d68e6
to
be5db4d
Compare
b2a3b1a
to
fdc64cb
Compare
From the failure it seems like there is still a bug in Babel. I probably need to enforce semicolons in case where the printer doesn't put them, so that even if the next token is on the same line it works. |
fdc64cb
to
7fe88bb
Compare
I’ve got good results for |
I found the root cause of the error, it's indeed a Babel bug. // input
const results = {
answer1,
answer2,
answer3,
answer4,
answer5,
} // run babel
const out = babel.transformSync(code, {
configFile: false,
parserOpts: { tokens: true },
generatorOpts: { retainLines: true, experimental_preserveFormat: true },
plugins: [
({ template }) => ({
visitor: {
Program(path) {
path.pushContainer("body", template.statement.ast`hello;`);
},
},
}),
],
}); The VariableDeclaration is printed without a trailing semicolon to preserve its original format, but then the new injected |
Closes: #2415 ## Description This change introduces support for TypeScript through type-erasure, using ts-blank-space, which converts type annotations to equivalent blank space. As is consistent with `node --experimental-strip-types`, this only applies to modules with the `.ts`, `.mts`, or `.cts` extensions in packages that are not under `node_modules`, to discourage publishing TypeScript as a source language to npm. ### Security Considerations The choice of `ts-blank-space` is intended to minimize runtime behavior difference between TypeScript and JavaScript, such that a reviewer or a debugger of the generated JavaScript aligns with the expected behavior and original text, to the extent that is possible. This should compose well with #2444. ### Scaling Considerations None. ### Documentation Considerations Contains README and NEWS. ### Testing Considerations Contains spot check tests for TypeScript in the endoScript and endoZipBase64 formats. We stand on much more rigorous testing of the underlying workspace-language-for-extension feature in Compartment Mapper #2625. ### Compatibility Considerations This does not break any prior usage. ### Upgrade Considerations None.
8baf9f2
to
011fb38
Compare
We seem to still have some defects in a similar vein. We have a failing test that exists to ensure that error stacks report true line numbers when using one of our older bundle formats. The source is: export const message = `You're great!`;
export const makeError = msg => Error(msg);
// Without an evasive transform, the following comment will trip the SES censor
// for dynamic imports. */
/** @type {import('./types.js').EncourageFn} */
export const encourage = nick => `Hey ${nick}! ${message}`; The current transform output preserves the line number. (function getExport(require, exports) { 'use strict'; const module = { exports }; 'use strict';Object.defineProperty(exports,'__esModule',{value:true});const message=`You're great!`;
const makeError=(msg)=>Error(msg);
/* Without an evasive transform, the following comment will trip the SES censor*/
/* for dynamic imports. *X/*/
/** @type {IMPORT('./types.js').EncourageFn} */
const encourage=(nick)=>`Hey ${nick}! ${message}`;exports.encourage=encourage;exports.makeError=makeError;exports.message=message;
return module.exports;
})
//# sourceURL=/bundled-source/.../encourage.js The new transformed output injects a lot of unexpected space: (function getExport(require, exports) { 'use strict'; const module = { exports }; 'use strict';
Object.defineProperty(exports,'__esModule' , { value: true });
const message=`You're great!`;
const makeError=(msg)=>Error(msg);/* Without an evasive transform, the following comment will trip the SES censor*//* for dynamic imports. *X/*//** @type {IMPORT('./types.js').EncourageFn} */
const encourage=(nick)=>`Hey ${nick}! ${message}`;
exports. encourage=encourage;
exports. makeError=makeError;
exports. message=message;
return module.exports;
})
//# sourceURL=/bundled-source/.../encourage.js This is the only failing test locally, but is also the only test that is sensitive to stack trace line numbers, so I should make more tests and also attempt to isolate this more. You will note that is a Babel transform that occurs after a prior Rollup transform, which is not present in our modern bundle formats. |
I'll take a look. I'm also working on getting a large part of our transform tests to go through this printer to have a bigger corpus of test cases. That's a funny output, I wonder how it's deciding to inject all those spaces there. |
I started taking a look at that. Something I noticed is that if I disable the "source map unmap" functionality you have in It's not surprising that it has an effect, given that Babel relies on location info to preserve the format, but I don't know yet if it's a bug in Babel on in how you remap locations. |
Ok so what's happening is that this is the input code parsed with Babel: 'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
const message = `You're great!`;
const makeError = msg => Error(msg);
// Without an evasive transform, the following comment will trip the SES censor
// for dynamic imports. */
/** @type {import('./types.js').EncourageFn} */
const encourage = nick => `Hey ${nick}! ${message}`;
exports.encourage = encourage;
exports.makeError = makeError;
exports.message = message; And Babel is trying to preserve the formatting of this input code (and not of what the input code for Rollup was, since Babel doesn't know about it). That's why there are all those "extra" newlines. You need to also re-map the locations The "fake locations" you generate are weird btw. In the test above, in
The extra horizontal space is due to a failing check in Babel. I was assuming it would have never happen, but it's happening due to the mismatch between the locations on the AST nodes and the locations on the tokens. If after fixing the tokens locations in your logic the problem persists, I can adjust the check in Babel. |
Thank you @nicolo-ribaudo, this implies a couple good mitigation options I can look into and I believe we’re unblocked. @michaelfig My intention is to look into making Babel’s new preserveFormat mode work with the older getExport and nestedEvaluate in composition with Rollup by ensuring we return to text between stages. If that exceeds my (short) timebox, my intention is to just disable preserveFormat and live with the current experience. My intention is to (soon) migrate our remaining usage of getExport and nestedEvaluate to the new endoScript format, and maybe even introduce an endoNativeScript format to take advantage of XS Compartment and further improve debug experience for kernel bootstrap scripts. |
I’ve spoken to @michaelfig and we resolved that we have reimplemented enough of Rollup at this stage to pretty easily replace the internals of the legacy getExport and nestedEvaluator generators with ones we trust. So, we will not attempt to get Rollup and Babel to play well together. The end result is that this test will pass without modification, since it won’t be downgraded from ESM to CJS by Rollup, but will go through our ModuleSource shim instead, which will benefit from preserveFormat. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥳
packages/bundle-source/README.md
Outdated
The `getExport` format can import host modules with a CommonJS `require` | ||
function, if there is one in scope. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion:
The `getExport` format can import host modules with a CommonJS `require` | |
function, if there is one in scope. | |
A bundle in `getExport` format can import host modules through a | |
lexically-scoped CommonJS `require` function. |
packages/bundle-source/README.md
Outdated
One can be endowed using a Hardened JavaScript `Compartment`. | ||
|
||
```js | ||
const compartment = new Comaprtment({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const compartment = new Comaprtment({ | |
const compartment = new Compartment({ |
packages/bundle-source/README.md
Outdated
globals: { require }, | ||
__options__: true, // until SES and XS implementations converge | ||
}); | ||
const exports = compartment.evaluate(source); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct me if I'm wrong, but the preceding explanation implies that evaluating source
always returns a function.
const exports = compartment.evaluate(source); | |
const exports = compartment.evaluate(source)(); |
packages/bundle-source/README.md
Outdated
|
||
```js | ||
cosnt { source } = await bundleSource('program.js', { format: 'nestedEvaluate' }); | ||
const compartment = new Comaprtment({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const compartment = new Comaprtment({ | |
const compartment = new Compartment({ |
const compartment = new Comaprtment({ | ||
globals: { | ||
require, | ||
nestedEvaluate: source => compartment.evaluate(source), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Text should explicitly document that a lexically-scoped nestedEvaluate
function is required (for bundles that look for it, e.g. those with submodules) or explain what happens when it is not present.
> [!WARNING] | ||
> The `nestedEvaluate` format was previously implemented using | ||
> [Rollup](https://rollupjs.org/) and is implemented with | ||
> `@endo/compartment-mapper/functor.js` starting with version 4 of | ||
> `@endo/bundle-source`. | ||
> Their behaviors are not identical. | ||
> | ||
> 1. Version 3 used different heuristics than Node.js 18 for inferring whether | ||
> a module was in CommonJS format or ESM format. Version 4 does not guess, | ||
> but relies on the `"type": "module"` directive in `package.json` to indicate | ||
> that a `.js` extension implies ESM format, or respects the explicit `.cjs` | ||
> and `.mjs` extensions. | ||
> 2. Version 3 supports [live | ||
> bindings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#imported_values_can_only_be_modified_by_the_exporter) | ||
> and Version 4 does not. | ||
> 3. Version 3 can import any package that is discoverable by walking parent directories | ||
> until the dependency or devDependeny is found in a `node_modules` directory. | ||
> Version 4 requires that the dependent package explicitly note the dependency | ||
> in `package.json`. | ||
> 4. Version 3 and 4 generate different text. | ||
> Any treatment of that text that is sensitive to the exact shape of the | ||
> text is fragile and may break even between minor and patch versions. | ||
> 5. Version 4 makes flags already supported by `endoZipBase64` format | ||
> universal to all formats, including `dev`, `elideComments`, | ||
> `noTransforms`, and `conditions`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
packages/bundle-source/README.md
Outdated
compartment.evaluate(source); | ||
``` | ||
|
||
Unlike, `getExport` and `nestedEvaluate`, the `dev` option to `bundleSource` is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unlike, `getExport` and `nestedEvaluate`, the `dev` option to `bundleSource` is | |
Unlike `getExport` and `nestedEvaluate`, the `dev` option to `bundleSource` is |
dev: | ||
dev || moduleFormat === 'nestedEvaluate' || moduleFormat === 'getExport', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment indicating that this automatic implication of dev
is limited to "nestedEvaluate" and "getExport", and will not be extended to any future format.
format: | ||
moduleFormat === 'nestedEvaluate' || moduleFormat === 'getExport' | ||
? 'cjs' | ||
: undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe explain this as well.
packages/bundle-source/src/script.js
Outdated
}); | ||
|
||
if (moduleFormat === 'endoScript') { | ||
source = `${source}()`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For parity and resiliency:
source = `${source}()`; | |
source = `(${source})()`; |
Given that this was a significant change in Babel, would it be possible for you to validate it with some of your internal packages that use Endo before actually releasing this PR? |
This is our Agoric SDK integration test pinned to this branch. Agoric/agoric-sdk#10781 There are limitations to the reach of these integration tests, but if the new Babel would build a broken bundle for Agoric’s worker kernel, these integration tests would (and did) discover those problems. We hope to go farther and pin to the Endo branch in tests that run in Docker for much more integrated scenarios. (@mhofman reminded me that these tests have their own lockfiles.) In short, I’m not expecting any Babel concerns to block a release at this stage, but I am expecting that we will find minor irregularities as more engineers begin debugging with contracts using the new bundler. |
dev, | ||
let source = await makeFunctor(powers, entry, { | ||
dev: | ||
dev || moduleFormat === 'nestedEvaluate' || moduleFormat === 'getExport', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave a comment:
If we add
endoScript
, it will not be made an exception to thedev
-must-be-explicit rule. The other thing it will do thatnestedEvaluate
does not, is accept the full range of runtime options, beyondsourceUrlPrefix
.
source, | ||
// TODO sourceMap | ||
// TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove TODO, add comment why empty string.
a0dcdb5
to
5200fdc
Compare
a59b895
to
ccbeab4
Compare
…tions with endoScript implementation *BREAKING CHANGE:* This change replaces the implementation of getExport and nestedEvaluate bundle formats in some ways that are not strictly backward-compatible. First, the Rollup implementation used different heuristics to distinguish a CommonJS module from an ESM, and the new algorithm is more in line with precedent as set in Node.js 18. Second, the Endo implementation does not support live export bindings. Third, the Endo implementation does not tolerate missing dependencies or devDependencies. Any treatment on the generated source that is not just evaluation is fragile and likely to break, and although we make no guarantees about stability of the generated string, this change definitely frustrates some usage in practice, which we have already addressed in Agoric SDK. This change preserves the assumption that getExport and nestedEvaluate may reach for devDependencies of the entry package by default.
ccbeab4
to
afb5c93
Compare
Closes: #926
Description
This change leverages new Babel support for format preservation.
Security Considerations
None
Scaling Considerations
None
Documentation Considerations
None
Testing Considerations
This change includes a test to perform a narrow spot-check of the verbatim output of a module that should be largely preserved.
Compatibility Considerations
None
Upgrade Considerations
None