-
Notifications
You must be signed in to change notification settings - Fork 5
fix(ses): cyclic star export with renaming reexport (issue #59) - refresh for #3276 feedback #379
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
Closed
Closed
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
7962515
fix(ses): cyclic star export with renaming reexport (issue #59)
kriskowal b778253
test(ses): unused-live-binding parity for #59 cyclic star-export
c771da0
refactor(ses): extract makeNotifierWithResolver helper (issue #59)
1e15f03
test(ses): CommonJS reexporter parity in cyclic star-export (issue #59)
36c9384
test(compartment-mapper): cyclic CommonJS reexporter parity fixture +…
64095ee
test(compartment-mapper): ESM-in-CJS-cycle divergence parity test (#5…
0ac8b9a
test(ses): reframe cyclic CJS reexporter test prose; reference compar…
65d1310
test(compartment-mapper): unused-live-binding parity fixture + tests …
de535df
test(ses): TDZ-observation matrix for cyclic star export with renamin…
22867cc
docs(ses): document construction-time-notifiers consideration (issue …
d837202
test(ses): named-reexport variant of cyclic-export failure mode (issu…
88667a6
fix(ses): enforce TDZ for cross-module namespace reads during cycle (…
c5d3d56
fix(ses): enforce TDZ for cross-module named-reexport reads during cycle
9b4885a
docs(ses): archive construction-time-notifiers analysis to the garden…
c3f06bd
test(compartment-mapper): parity fixtures for the issue endojs/endo#5…
8f9f302
test(compartment-mapper): consolidate cyclic TDZ matrix into a table-…
5013d3c
docs(ses): expand changeset to cover TDZ enforcement alongside the cy…
2104fd7
test(compartment-mapper): drop issue-number references from the cycle…
e95ebe5
test(compartment-mapper): merge cycle-rename-tdz SES+Node parity test…
586be4c
docs(ses): add @module to notifier-with-resolver; link via {@link} (#…
a71b4d9
refactor(ses): strict equality and else over early return in notifier…
814dbaa
test(ses): TODO comment linking the CjsModuleSource mock to endojs/en…
9fbaaf7
test(compartment-mapper): merge remaining SES+Node parity tests into …
cb597b2
test(compartment-mapper): drop issue citations from unused-assertions…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| 'ses': patch | ||
| --- | ||
|
|
||
| Fix a star-export cycle defect where a module reached more than once via `export *` and a renaming reexport with a different exported name (`export { y as x } from ...`) raised a spurious `SyntaxError: ... does not provide an export named 'X'` (latterly `TypeError: notify is not a function`). | ||
| The reexport wire-up now installs a deferred forwarding notifier that resolves through the upstream's notifier table on first subscription, so cyclic star-export fixed-points converge. | ||
|
|
||
| Additionally, enforce ECMA-262 temporal-dead-zone semantics for cross-module reads through a module namespace import during cycle evaluation, for both the `export *` and `export { y } from` reexport forms. | ||
| Previously, when the importing side of a cycle observed the upstream's binding through a namespace import (`r.y`) while the upstream's body was still on the evaluation stack and the binding's declaration had not yet been evaluated, the read returned the uninitialized slot value instead of raising; SES now matches Node.js's reference behavior and raises `ReferenceError` for `const` and `let` bindings during the TDZ window, while `var` bindings continue to read `undefined` because the hoisting preamble pre-initializes them before any downstream observation. | ||
|
|
||
| Resolves endojs/endo#59. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
packages/compartment-mapper/test/_cycle-cjs-reexporter-assertions.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Shared assertion logic for the cyclic CommonJS reexporter scenario. | ||
| * Both the Node.js parity test and the Compartment Mapper test import from | ||
| * this module so the expected values live in exactly one place. If both | ||
| * tests pass, parity with Node.js is verified by construction. | ||
| * | ||
| * The fixture under fixtures-cycle-cjs-reexporter/node_modules/app/ exercises | ||
| * this arrangement, all three modules being CommonJS: | ||
| * | ||
| * star-reexporter.cjs: Object.assign(exports, require('./export-renamer.cjs')); | ||
| * export-renamer.cjs: require('./star-reexporter.cjs'); | ||
| * Object.defineProperty(exports, 'x', { | ||
| * get() { return module.exports.y; }, enumerable: true }); | ||
| * exports.y = 45; | ||
| * main.js: const reexp = require('./star-reexporter.cjs'); | ||
| * const ren = require('./export-renamer.cjs'); | ||
| * exports.captured = reexp.x; | ||
| * exports.namespace1 = { x: reexp.x, y: reexp.y }; | ||
| * exports.namespace2 = { x: ren.x, y: ren.y }; | ||
| * | ||
| * In a pure-CommonJS cycle, the reexporter's `Object.assign` reads the | ||
| * renamer's `x` getter after the renamer has set `y = 45`, so the copied | ||
| * value is 45. Both namespaces project { x: 45, y: 45 }. Node.js and the | ||
| * compartment mapper agree on this shape, so the same assertions apply to | ||
| * both layers. | ||
| * | ||
| * The companion divergence scenario (ESM module participating in a cycle | ||
| * with a CommonJS module) is exercised by fixtures-cycle-esm-in-cjs and | ||
| * its tests; Node.js rejects that topology with ERR_REQUIRE_CYCLE_MODULE | ||
| * while SES allows it. | ||
| * | ||
| * @module | ||
| */ | ||
|
|
||
| /** @import {ExecutionContext} from 'ava' */ | ||
|
|
||
| export const expectedCaptured = 45; | ||
| export const expectedNamespace1 = { x: 45, y: 45 }; | ||
| export const expectedNamespace2 = { x: 45, y: 45 }; | ||
|
|
||
| /** | ||
| * @param {ExecutionContext} t | ||
| * @param {object} namespace | ||
| */ | ||
| export const assertCycleCjsReexporter = (t, namespace) => { | ||
| t.is(namespace.captured, expectedCaptured); | ||
| t.deepEqual( | ||
| { x: namespace.namespace1.x, y: namespace.namespace1.y }, | ||
| expectedNamespace1, | ||
| ); | ||
| t.deepEqual( | ||
| { x: namespace.namespace2.x, y: namespace.namespace2.y }, | ||
| expectedNamespace2, | ||
| ); | ||
| }; |
45 changes: 45 additions & 0 deletions
45
packages/compartment-mapper/test/_cycle-rename-assertions.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /** | ||
| * Shared assertion logic for the cyclic star-export with renaming reexport | ||
| * regression (endojs/endo#59). Both the Node.js parity test and the | ||
| * Compartment Mapper test import from this module so the expected values | ||
| * live in exactly one place. If both tests pass, parity with Node.js is | ||
| * verified by construction. | ||
| * | ||
| * The fixture under fixtures-cycle-rename/node_modules/app/ exercises this | ||
| * arrangement: | ||
| * | ||
| * star-reexporter.js: export * from './export-renamer.js'; | ||
| * export-renamer.js: export { y as x } from './star-reexporter.js'; | ||
| * export var y = 45; | ||
| * main.js: import { x } from './star-reexporter.js'; | ||
| * import * as ns1 from './star-reexporter.js'; | ||
| * import * as ns2 from './export-renamer.js'; | ||
| * export const captured = x; | ||
| * export const namespace1 = { x: ns1.x, y: ns1.y }; | ||
| * export const namespace2 = { x: ns2.x, y: ns2.y }; | ||
| * | ||
| * Before the fix, the SES linker visited star-reexporter while its | ||
| * star-imported notifier for `y` had not yet been wired. The synchronous | ||
| * wireUp at the cycle's back-edge then passed `undefined` as the upstream | ||
| * notifier, manifesting as `TypeError: notify is not a function`. Node.js | ||
| * does not exhibit the defect, so the parity test pinned both layers to a | ||
| * single expected shape. | ||
| * | ||
| * @module | ||
| */ | ||
|
|
||
| /** @import {ExecutionContext} from 'ava' */ | ||
|
|
||
| export const expectedCaptured = 45; | ||
| export const expectedNamespace1 = { x: 45, y: 45 }; | ||
| export const expectedNamespace2 = { x: 45, y: 45 }; | ||
|
|
||
| /** | ||
| * @param {ExecutionContext} t | ||
| * @param {object} namespace | ||
| */ | ||
| export const assertCycleRename = (t, namespace) => { | ||
| t.is(namespace.captured, expectedCaptured); | ||
| t.deepEqual(namespace.namespace1, expectedNamespace1); | ||
| t.deepEqual(namespace.namespace2, expectedNamespace2); | ||
| }; |
48 changes: 48 additions & 0 deletions
48
packages/compartment-mapper/test/_cycle-rename-unused-assertions.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * Shared assertion logic for the unused-live-binding shape of the cyclic | ||
| * star-export with renaming reexport regression. This is the companion to | ||
| * the populated shape exercised by `_cycle-rename-assertions.js`; the only | ||
| * difference is that the renamer's `export var y` here has no initializer, | ||
| * so the live binding is declared but never updated. Every projection of the | ||
| * cycle therefore reads `undefined`. Both the Node.js parity test and the | ||
| * Compartment Mapper test import from this module so the expected values | ||
| * live in exactly one place; if both tests pass, parity with Node.js is | ||
| * verified by construction. | ||
| * | ||
| * The fixture under fixtures-cycle-rename-unused/node_modules/app/ exercises | ||
| * this arrangement: | ||
| * | ||
| * star-reexporter.js: export * from './export-renamer.js'; | ||
| * export-renamer.js: export { y as x } from './star-reexporter.js'; | ||
| * export var y; | ||
| * main.js: import { x } from './star-reexporter.js'; | ||
| * import * as ns1 from './star-reexporter.js'; | ||
| * import * as ns2 from './export-renamer.js'; | ||
| * export const captured = x; | ||
| * export const namespace1 = { x: ns1.x, y: ns1.y }; | ||
| * export const namespace2 = { x: ns2.x, y: ns2.y }; | ||
| * | ||
| * The deferring closure introduced by the cyclic-star-export fix queues | ||
| * subscribers until the upstream notifier resolves, then forwards them. | ||
| * With no initializer the upstream's value never updates, so every read is | ||
| * `undefined`. Node.js exhibits the same shape, so the parity test pins | ||
| * both layers to a single expected projection. | ||
| * | ||
| * @module | ||
| */ | ||
|
|
||
| /** @import {ExecutionContext} from 'ava' */ | ||
|
|
||
| export const expectedCaptured = undefined; | ||
| export const expectedNamespace1 = { x: undefined, y: undefined }; | ||
| export const expectedNamespace2 = { x: undefined, y: undefined }; | ||
|
|
||
| /** | ||
| * @param {ExecutionContext} t | ||
| * @param {object} namespace | ||
| */ | ||
| export const assertCycleRenameUnused = (t, namespace) => { | ||
| t.is(namespace.captured, expectedCaptured); | ||
| t.deepEqual(namespace.namespace1, expectedNamespace1); | ||
| t.deepEqual(namespace.namespace2, expectedNamespace2); | ||
| }; |
56 changes: 56 additions & 0 deletions
56
packages/compartment-mapper/test/cycle-cjs-reexporter.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Cyclic CommonJS reexporter scenario exercised twice in this module, | ||
| * back-to-back: once through the compartment-mapper test scaffold (the SES | ||
| * treatment) and once through plain Node.js (the parity treatment). Both | ||
| * treatments target the same fixture and assert the same expected values | ||
| * through the shared assertion module. The paired registration makes the | ||
| * shared coverage legible at a glance and pins the compartment mapper's | ||
| * CommonJS cycle behavior to Node.js's reference behavior. | ||
| * | ||
| * This is the pure-CommonJS counterpart to the ESM-in-CJS-cycle divergence | ||
| * exercised by cycle-esm-in-cjs.test.js, where Node.js rejects the topology | ||
| * with ERR_REQUIRE_CYCLE_MODULE but SES allows it. | ||
| */ | ||
|
|
||
| /** @import {ExecutionContext} from 'ava' */ | ||
|
|
||
| import 'ses'; | ||
| import test from 'ava'; | ||
| import { scaffold } from './scaffold.js'; | ||
| import { assertCycleCjsReexporter } from './_cycle-cjs-reexporter-assertions.js'; | ||
|
|
||
| const fixture = new URL( | ||
| 'fixtures-cycle-cjs-reexporter/node_modules/app/main.js', | ||
| import.meta.url, | ||
| ).toString(); | ||
|
|
||
| const fixtureAssertionCount = 3; | ||
|
|
||
| /** | ||
| * @param {ExecutionContext} t | ||
| * @param {{namespace: object}} result | ||
| */ | ||
| const assertFixture = (t, { namespace }) => { | ||
| assertCycleCjsReexporter(t, namespace); | ||
| }; | ||
|
|
||
| // SES treatment: load through the compartment-mapper scaffold, which | ||
| // exercises loadLocation, importLocation, and the archive paths. | ||
| scaffold( | ||
| 'cycle-cjs-reexporter (ses)', | ||
| test, | ||
| fixture, | ||
| assertFixture, | ||
| fixtureAssertionCount, | ||
| ); | ||
|
|
||
| // Node.js parity treatment: dynamically import the same `main.js` directly | ||
| // under plain Node.js (no SES, no compartment mapper) and assert the same | ||
| // expected values. Node exposes a CommonJS module's `module.exports` as the | ||
| // namespace's default export, so the shared assertion module is reused by | ||
| // projecting through `default`. | ||
| test('cycle-cjs-reexporter (node parity)', async t => { | ||
| t.plan(3); | ||
| const moduleNamespace = await import(fixture); | ||
| assertCycleCjsReexporter(t, moduleNamespace.default); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| /** | ||
| * Cyclic ESM-in-CommonJS divergence scenario exercised twice in this module, | ||
| * back-to-back: once through the compartment-mapper test scaffold (the SES | ||
| * treatment, where the topology loads and `main.bridgeValue` resolves to 42) | ||
| * and once through plain Node.js (the parity treatment, where Node rejects | ||
| * the topology with ERR_REQUIRE_CYCLE_MODULE). The paired registration | ||
| * verifies the divergence programmatically rather than narratively: SES | ||
| * allows the topology that Node rejects. | ||
| * | ||
| * Topology (under fixtures-cycle-esm-in-cjs/node_modules/app/): | ||
| * | ||
| * main.mjs: import * as bridge from './bridge.cjs'; | ||
| * export const bridgeValue = bridge.value; | ||
| * bridge.cjs: const m = require('./peer.mjs'); | ||
| * exports.value = m.value; | ||
| * peer.mjs: import { value as bridgeValue } from './bridge.cjs'; | ||
| * export const value = 42; | ||
| * | ||
| * On the SES side, bridge.cjs reads `m.value` from peer.mjs's namespace | ||
| * after the cycle's back-edge has reached peer.mjs (which then re-entered | ||
| * bridge.cjs). Because the ESM side resolves through live bindings, by the | ||
| * time main reads bridge.value the snapshot capture in bridge.cjs sees | ||
| * peer.mjs's `value = 42`. | ||
| */ | ||
|
|
||
| /** @import {ExecutionContext} from 'ava' */ | ||
|
|
||
| import 'ses'; | ||
| import test from 'ava'; | ||
| import process from 'process'; | ||
| import { spawnSync } from 'child_process'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { scaffold } from './scaffold.js'; | ||
|
|
||
| const fixtureUrl = new URL( | ||
| 'fixtures-cycle-esm-in-cjs/node_modules/app/main.mjs', | ||
| import.meta.url, | ||
| ); | ||
| const fixture = fixtureUrl.toString(); | ||
|
|
||
| const fixtureAssertionCount = 1; | ||
|
|
||
| /** | ||
| * @param {ExecutionContext} t | ||
| * @param {{namespace: object}} result | ||
| */ | ||
| const assertFixture = (t, { namespace }) => { | ||
| t.is(namespace.bridgeValue, 42); | ||
| }; | ||
|
|
||
| // SES treatment: load through the compartment-mapper scaffold. SES allows | ||
| // the topology Node rejects and exposes the cycle's snapshot / live-binding | ||
| // shape on the namespace. | ||
| scaffold( | ||
| 'cycle-esm-in-cjs divergence (ses)', | ||
| test, | ||
| fixture, | ||
| assertFixture, | ||
| fixtureAssertionCount, | ||
| ); | ||
|
|
||
| // Node.js parity treatment: spawn a fresh Node process to execute the same | ||
| // fixture. The expected outcome is a non-zero exit with | ||
| // ERR_REQUIRE_CYCLE_MODULE printed on stderr. Spawning isolates the failure | ||
| // from the test runner's own module graph and keeps the rest of the suite | ||
| // running. Together with the SES treatment above, this pins the divergence | ||
| // programmatically: SES allows what Node rejects. | ||
| test('cycle-esm-in-cjs divergence (node parity)', t => { | ||
| t.plan(2); | ||
| const result = spawnSync(process.execPath, [fileURLToPath(fixtureUrl)], { | ||
| encoding: 'utf8', | ||
| }); | ||
| t.not( | ||
| result.status, | ||
| 0, | ||
| `Expected Node to reject ESM-in-CJS-cycle, got exit ${result.status}`, | ||
| ); | ||
| t.regex( | ||
| result.stderr, | ||
| /ERR_REQUIRE_CYCLE_MODULE/, | ||
| `Expected ERR_REQUIRE_CYCLE_MODULE in stderr, got:\n${result.stderr}`, | ||
| ); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Please update to reflect sum of changes.
Uh oh!
There was an error while loading. Please reload this page.
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.
Addressed in 39b880d: the changeset now describes both user-visible changes the cycle-rename branch carries: the original deferred forwarding notifier that resolves the spurious cycle-rename
SyntaxError, and the ECMA-262 temporal-dead-zone enforcement for cross-module namespace reads during cycle evaluation (covering bothexport *andexport { y } fromreexport forms).