Skip to content
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

Return rejected promise when stringify import specifier throws #15290

Merged
merged 6 commits into from Dec 23, 2022

Conversation

SuperSodaSea
Copy link
Contributor

@SuperSodaSea SuperSodaSea commented Dec 17, 2022

Q                       A
Fixed Issues? Fixes #15261
Patch: Bug Fix? 👍
Major: Breaking Change?
Minor: New Feature?
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes?
License MIT

When using dynamic import (import(specifier)), the toString of the specifier should be evaluated immediately, not in promise. And if the toString throws, import() should return a rejected promise instead of throwing it immediately.

See #15261 (comment) for more context.

Tests

➕ Added
  • babel-plugin-proposal-dynamic-import
    • amd/to-string
    • commonjs/exec-throw
    • commonjs/exec-to-primitive
    • commonjs/exec-to-string-order
    • commonjs/exec-to-string-throw
    • commonjs/template-literal
    • systemjs/to-string
〰️ Output Changed
  • babel-plugin-proposal-dynamic-import
    • commonjs/to-string

@babel-bot
Copy link
Collaborator

babel-bot commented Dec 17, 2022

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/53692/

Copy link
Contributor

@JLHwung JLHwung left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM except for the toPrimitive cases.

new Promise(r => r(${source}))
.then(s => ${buildRequire(t.identifier("s"), file)})
(source =>
new Promise(r => r("" + source))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new Promise(r => r("" + source))
new Promise(r => r(${
t.isTemplateLiteral(source) ? source : t.templateLiteral([], [source])
}))

The transform from ${x} to "" + x relies on the ignoreToPrimitiveHint assumption.

Consider an exotic object:

var source = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') {
      return "foo"
    }
    return null
  }
}

"" + source will be null while ${source} will be "foo". The ToString operation should respect the toPrimitive symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, and I go further to move the common logic to function buildDynamicImport:

export function buildDynamicImport(
node: t.CallExpression,
deferToThen: boolean,
wrapWithPromise: boolean,
builder: (specifier: t.Expression) => t.Expression,
): t.Expression {

And in transform-modules-amd/commonjs/system we only need to write code once:

buildDynamicImport(
path.node,
false,
false,
specifier => template.expression.ast`
new Promise((${resolveId}, ${rejectId}) =>
${requireId}(
[${specifier}],
imported => ${t.cloneNode(resolveId)}(${result}),
${t.cloneNode(rejectId)}
)
)
`,
),

buildDynamicImport(path.node, true, false, specifier =>
buildRequire(specifier, file),
),

buildDynamicImport(path.node, false, true, specifier =>
t.callExpression(
t.memberExpression(
t.identifier(state.contextIdent),
t.identifier("import"),
),
[specifier],
),
),

@@ -1,3 +1,3 @@
define(["require"], function (_require) {
var modP = new Promise((_resolve, _reject) => _require(["mod"], imported => _resolve(imported), _reject));
var modP = Promise.resolve().then(() => new Promise((_resolve, _reject) => _require(["mod"], imported => _resolve(imported), _reject)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the AMD require function is already asynchronous, we don't need to defer it to .then — the original code was fine. Same for SystemJS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I just need to set the buildDynamicImport's runInThendeferToThen to false for it.

buildDynamicImport(
path.node,
false,
false,
specifier => template.expression.ast`
new Promise((${resolveId}, ${rejectId}) =>
${requireId}(
[${specifier}],
imported => ${t.cloneNode(resolveId)}(${result}),
${t.cloneNode(rejectId)}
)
)
`,
),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the logic looks like this:

CommonJS (Deferred to then):

if (deferToThen) {
return template.expression.ast`
(specifier =>
new Promise(r => r(${specifierToString}))
.then(s => ${builder(t.identifier("s"))})
)(${specifier})
`;

SystemJS (Needs to return a rejected promise when toString throws so wrap it in a promise):

} else if (wrapWithPromise) {
return template.expression.ast`
(specifier =>
new Promise(r => r(${builder(specifierToString)}))
)(${specifier})
`;

AMD (Builder already returns a new Promise so do nothing):

} else {
return template.expression.ast`
(specifier => ${builder(specifierToString)})(${specifier})
`;

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@nicolo-ribaudo nicolo-ribaudo changed the title Improve dynamic import spec compliance Return rejected promise when stringify import specifier throws Dec 23, 2022
@nicolo-ribaudo nicolo-ribaudo merged commit 64d5a92 into babel:main Dec 23, 2022
@nicolo-ribaudo nicolo-ribaudo added the PR: Spec Compliance 👓 A type of pull request used for our changelog categories label Dec 23, 2022
@SuperSodaSea SuperSodaSea deleted the fix/dynamic-import branch December 23, 2022 17:19
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Mar 25, 2023
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: Spec Compliance 👓 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: Error in toString during dynamic import should result in rejected promise
4 participants