feat(compartment-mapper): Thread url into power makers for Windows support
MODIFY
packages/compartment-mapper/NEWS.md
packages/compartment-mapper/NEWS.md
User-visible changes to the compartment mapper:
+# Next Release
+
+- The `node-powers.js` module now exports `makeReadPowers` and
+ `makeWritePowers` which replace the deprecated functions `makeNodeReadPowers`
+ and `makeNodeWritePowers`.
+ The rename is necessary to facilitate a change to the signature of these
+ methods so that `url` may be accepted as another dependency, to facilitate
+ Windows support.
+ Both accept a bag of Node.js modules that must include `fs` and `url`.
+ The read powers may optionally take the `crypto` module.
+
# 0.5.2 (2021-11-16)
- Adds source URL suffixes to archives, such that the archive hash remains

MODIFY
packages/compartment-mapper/node-powers.js
packages/compartment-mapper/node-powers.js
+export { makeReadPowers, makeWritePowers } from './src/node-powers.js';
+// Deprecated:
export { makeNodeReadPowers, makeNodeWritePowers } from './src/node-powers.js';

MODIFY
packages/compartment-mapper/src/node-modules.js
packages/compartment-mapper/src/node-modules.js
@@ -49,7 +49,9 @@ const q = JSON.stringify;
* @param {string} abs - a fully qualified URL
* @returns {string}
*/
-const resolveLocation = (rel, abs) => new URL(rel, abs).toString();
+const resolveLocation = (rel, abs) => {
+ return new URL(rel, abs).toString();
+};
/**
* @param {string} location

MODIFY
packages/compartment-mapper/src/node-powers.js
packages/compartment-mapper/src/node-powers.js
/** @typedef {import('./types.js').WritePowers} WritePowers */
/**
- * @param {typeof import('fs')} fs
- * @param {typeof import('crypto')} [crypto]
- * @returns {ReadPowers}
+ * @param {string} location
*/
-export const makeNodeReadPowers = (fs, crypto = undefined) => {
+const fakeFileURLToPath = location => {
+ const url = new URL(location);
+ if (url.protocol !== 'file:') {
+ throw new Error(`Cannot convert URL to file path: ${location}`);
+ }
+ return url.pathname;
+};
+
+/**
+ * @param {string} path
+ */
+const fakePathToFileURL = path => {
+ return new URL(path, 'file://').toString();
+};
+
+/**
+ * The implementation of `makeReadPowers` and the deprecated
+ * `makeNodeReadPowers` handles the case when the `url` power is not provided,
+ * but `makeReadPowers` presents a type that requires `url`.
+ *
+ * @param {Object} args
+ * @param {typeof import('fs')} args.fs
+ * @param {typeof import('url')} [args.url]
+ * @param {typeof import('crypto')} [args.crypto]
+ */
+const makeReadPowersSloppy = ({ fs, url = undefined, crypto = undefined }) => {
+ const fileURLToPath =
+ url === undefined ? fakeFileURLToPath : url.fileURLToPath;
+ const pathToFileURL =
+ url === undefined ? fakePathToFileURL : url.pathToFileURL;
+
/**
* @param {string} location
*/
const read = async location => {
try {
- return await fs.promises.readFile(new URL(location).pathname);
+ const path = fileURLToPath(location);
+ return await fs.promises.readFile(path);
} catch (error) {
throw new Error(error.message);
}
@@ -40,12 +69,12 @@ export const makeNodeReadPowers = (fs, crypto = undefined) => {
try {
if (location.endsWith('/')) {
const realPath = await fs.promises.realpath(
- new URL(location).pathname.replace(/\/$/, ''),
+ fileURLToPath(location).replace(/\/$/, ''),
);
- return new URL(`${realPath}/`, location).toString();
+ return `${pathToFileURL(realPath)}/`;
} else {
- const realPath = await fs.promises.realpath(new URL(location).pathname);
- return new URL(realPath, location).toString();
+ const realPath = await fs.promises.realpath(fileURLToPath(location));
+ return pathToFileURL(realPath).toString();
}
} catch {
return location;
@@ -65,17 +94,25 @@ export const makeNodeReadPowers = (fs, crypto = undefined) => {
};
/**
- * @param {typeof import('fs')} fs
- * @returns {WritePowers}
+ * The implementation of `makeWritePowers` and the deprecated
+ * `makeNodeWritePowers` handles the case when the `url` power is not provided,
+ * but `makeWritePowers` presents a type that requires `url`.
+ *
+ * @param {Object} args
+ * @param {typeof import('fs')} args.fs
+ * @param {typeof import('url')} [args.url]
*/
-export const makeNodeWritePowers = fs => {
+const makeWritePowersSloppy = ({ fs, url = undefined }) => {
+ const fileURLToPath =
+ url === undefined ? fakeFileURLToPath : url.fileURLToPath;
+
/**
* @param {string} location
* @param {Uint8Array} data
*/
const write = async (location, data) => {
try {
- return await fs.promises.writeFile(new URL(location).pathname, data);
+ return await fs.promises.writeFile(fileURLToPath(location), data);
} catch (error) {
throw new Error(error.message);
}
@@ -83,3 +120,43 @@ export const makeNodeWritePowers = fs => {
return { write };
};
+
+/**
+ * @param {Object} args
+ * @param {typeof import('fs')} args.fs
+ * @param {typeof import('url')} args.url
+ * @param {typeof import('crypto')} [args.crypto]
+ */
+export const makeReadPowers = makeReadPowersSloppy;
+
+/**
+ * @param {Object} args
+ * @param {typeof import('fs')} args.fs
+ * @param {typeof import('url')} args.url
+ */
+export const makeWritePowers = makeWritePowersSloppy;
+
+/**
+ * @deprecated in favor of makeReadPowers.
+ * It transpires that positional arguments needed to become an arguments bag to
+ * reasonably expand to multiple optional dependencies.
+ *
+ * @param {typeof import('fs')} fs
+ * @param {typeof import('crypto')} [crypto]
+ * @returns {ReadPowers}
+ */
+export const makeNodeReadPowers = (fs, crypto = undefined) => {
+ return makeReadPowersSloppy({ fs, crypto });
+};
+
+/**
+ * @deprecated in favor of makeWritePowers.
+ * It transpires that positional arguments needed to become an arguments bag to
+ * reasonably expand to multiple optional dependencies.
+ *
+ * @param {typeof import('fs')} fs
+ * @returns {WritePowers}
+ */
+export const makeNodeWritePowers = fs => {
+ return makeWritePowersSloppy({ fs });
+};

MODIFY
packages/compartment-mapper/test/app.agar
packages/compartment-mapper/test/app.agar
Binary files a/packages/compartment-mapper/test/app.agar and b/packages/compartment-mapper/test/app.agar differ

MODIFY
packages/compartment-mapper/test/app.agar-make.js
packages/compartment-mapper/test/app.agar-make.js
import 'ses';
import fs from 'fs';
import crypto from 'crypto';
+import url from 'url';
import { writeArchive } from '../archive.js';
-import { makeNodeReadPowers, makeNodeWritePowers } from '../src/node-powers.js';
+import { makeReadPowers, makeWritePowers } from '../src/node-powers.js';
-const readPowers = makeNodeReadPowers(fs, crypto);
-const { write } = makeNodeWritePowers(fs);
+const readPowers = makeReadPowers({ fs, crypto, url });
+const { write } = makeWritePowers({ fs, url });
const fixture = new URL(
'fixtures-0/node_modules/app/main.js',

MODIFY
packages/compartment-mapper/test/scaffold.js
packages/compartment-mapper/test/scaffold.js
import 'ses';
import fs from 'fs';
import crypto from 'crypto';
+import url from 'url';
import { ZipReader, ZipWriter } from '@endo/zip';
import {
loadLocation,
@@ -12,9 +13,9 @@ import {
importArchive,
hashLocation,
} from '../index.js';
-import { makeNodeReadPowers } from '../src/node-powers.js';
+import { makeReadPowers } from '../src/node-powers.js';
-export const readPowers = makeNodeReadPowers(fs, crypto);
+export const readPowers = makeReadPowers({ fs, crypto, url });
const globals = {
globalProperty: 42,

MODIFY
packages/compartment-mapper/test/test-bundle.js
packages/compartment-mapper/test/test-bundle.js
import 'ses';
import fs from 'fs';
+import url from 'url';
import test from 'ava';
import { makeBundle, makeArchive, parseArchive } from '../index.js';
+import { makeReadPowers } from '../node-powers.js';
const fixture = new URL(
'fixtures-0/node_modules/bundle/main.js',
import.meta.url,
).toString();
-const read = async location => fs.promises.readFile(new URL(location).pathname);
+const { read } = makeReadPowers({ fs, url });
const expectedLog = [
'foo',

MODIFY
packages/compartment-mapper/test/test-cycle-cjs.js
packages/compartment-mapper/test/test-cycle-cjs.js
// import "./ses-lockdown.js";
import 'ses';
import fs from 'fs';
+import url from 'url';
import test from 'ava';
import { loadLocation } from '../src/import.js';
-import { makeNodeReadPowers } from '../src/node-powers.js';
+import { makeReadPowers } from '../src/node-powers.js';
-const readPowers = makeNodeReadPowers(fs);
+const readPowers = makeReadPowers({ fs, url });
const { read } = readPowers;
test('reflexive CommonJS cyclic import', async t => {

MODIFY
packages/compartment-mapper/test/test-cycle-mjs.js
packages/compartment-mapper/test/test-cycle-mjs.js
// import "./ses-lockdown.js";
import 'ses';
import fs from 'fs';
+import url from 'url';
import test from 'ava';
import { loadLocation } from '../src/import.js';
-import { makeNodeReadPowers } from '../src/node-powers.js';
+import { makeReadPowers } from '../src/node-powers.js';
-const readPowers = makeNodeReadPowers(fs);
+const readPowers = makeReadPowers({ fs, url });
const { read } = readPowers;
test('reflexive cycle of ESM', async t => {

MODIFY
packages/compartment-mapper/test/test-source-url-rewrite.js
packages/compartment-mapper/test/test-source-url-rewrite.js
@@ -3,15 +3,16 @@ import 'ses';
import test from 'ava';
import fs from 'fs';
import crypto from 'crypto';
+import url from 'url';
import { parseArchive, makeArchive } from '../index.js';
-import { makeNodeReadPowers } from '../node-powers.js';
+import { makeReadPowers } from '../node-powers.js';
const fixtureLocation = new URL(
'fixtures-stack/index.js',
import.meta.url,
).toString();
-const readPowers = makeNodeReadPowers(fs, crypto);
+const readPowers = makeReadPowers({ fs, crypto, url });
test('rewrite source url', async t => {
const locations = new Map();

MODIFY
packages/compartment-mapper/test/test-stack.js
packages/compartment-mapper/test/test-stack.js
@@ -2,15 +2,16 @@ import 'ses';
import test from 'ava';
import fs from 'fs';
import crypto from 'crypto';
+import url from 'url';
import { importLocation, parseArchive, makeArchive } from '../index.js';
-import { makeNodeReadPowers } from '../node-powers.js';
+import { makeReadPowers } from '../node-powers.js';
const fixtureLocation = new URL(
'fixtures-stack/index.js',
import.meta.url,
).toString();
-const readPowers = makeNodeReadPowers(fs, crypto);
+const readPowers = makeReadPowers({ fs, crypto, url });
// This test confirms that the stack trace generated by an error in an archive
// will be influenced by the sourceURL assigned by the importer from the stable

MODIFY
packages/compartment-mapper/test/test-transform.js
packages/compartment-mapper/test/test-transform.js
import 'ses';
import fs from 'fs';
import test from 'ava';
+import url from 'url';
import { loadLocation } from '../src/import.js';
import { makeArchive } from '../src/archive.js';
import { parseArchive } from '../src/import-archive.js';
-import { makeNodeReadPowers } from '../src/node-powers.js';
+import { makeReadPowers } from '../src/node-powers.js';
-const readPowers = makeNodeReadPowers(fs);
+const readPowers = makeReadPowers({ fs, url });
const { read } = readPowers;
test('transforms applied to evaluation', async t => {

feat(compartment-mapper): Thread url into power makers for Windows support