feat(compartment-mapper): Archive source URL suffixes
Fixes
MODIFY
packages/compartment-mapper/NEWS.md
packages/compartment-mapper/NEWS.md
User-visible changes to the compartment mapper:
+# Next releaase
+
+- Adds source URL suffixes to archives, such that the archive hash remains
+ orthogonal to the local directory but has sufficient information that editors
+ like VS Code can match the suffix to a file in the IDE workspace.
+
# 0.5.1 (2021-08-12)
- Adds support for reflexive import specifiers, so modules in package named

MODIFY
packages/compartment-mapper/src/import-archive.js
packages/compartment-mapper/src/import-archive.js
@@ -37,6 +37,7 @@ const parserForLanguage = {
/**
* @callback ArchiveImportHookMaker
* @param {string} packageLocation
+ * @param {string} packageName
* @returns {ImportHook}
*/
@@ -55,7 +56,7 @@ const makeArchiveImportHookMaker = (
) => {
// per-assembly:
/** @type {ArchiveImportHookMaker} */
- const makeImportHook = packageLocation => {
+ const makeImportHook = (packageLocation, packageName) => {
// per-compartment:
const { modules } = compartments[packageLocation];
/** @type {ImportHook} */
@@ -99,6 +100,7 @@ const makeArchiveImportHookMaker = (
moduleSpecifier,
`file:///${moduleLocation}`,
packageLocation,
+ packageName,
);
return record;
};

MODIFY
packages/compartment-mapper/src/import-hook.js
packages/compartment-mapper/src/import-hook.js
@@ -56,7 +56,7 @@ export const makeImportHookMaker = (
) => {
// per-assembly:
/** @type {ImportHookMaker} */
- const makeImportHook = (packageLocation, parse) => {
+ const makeImportHook = (packageLocation, packageName, parse) => {
// per-compartment:
packageLocation = resolveLocation(packageLocation, baseLocation);
const packageSources = sources[packageLocation] || {};
@@ -121,6 +121,7 @@ export const makeImportHookMaker = (
candidateSpecifier,
moduleLocation,
packageLocation,
+ packageName,
);
const {
parser,

MODIFY
packages/compartment-mapper/src/link.js
packages/compartment-mapper/src/link.js
@@ -76,7 +76,7 @@ const makeExtensionParser = (
parserForLanguage,
transforms,
) => {
- return async (bytes, specifier, location, packageLocation) => {
+ return async (bytes, specifier, location, packageLocation, packageName) => {
let language;
if (has(languageForModuleSpecifier, specifier)) {
language = languageForModuleSpecifier[specifier];
@@ -105,7 +105,7 @@ const makeExtensionParser = (
);
}
const parse = parserForLanguage[language];
- return parse(bytes, specifier, location, packageLocation);
+ return parse(bytes, specifier, location, packageLocation, packageName);
};
};
@@ -336,6 +336,7 @@ export const link = (
)) {
const {
location,
+ name,
modules = {},
parsers: languageForExtension = {},
types: languageForModuleSpecifier = {},
@@ -352,7 +353,7 @@ export const link = (
parserForLanguage,
moduleTransforms,
);
- const importHook = makeImportHook(location, parse, exitModules);
+ const importHook = makeImportHook(location, name, parse);
const moduleMapHook = makeModuleMapHook(
compartments,
compartmentName,

MODIFY
packages/compartment-mapper/src/node-modules.js
packages/compartment-mapper/src/node-modules.js
@@ -448,6 +448,7 @@ const translateGraph = (
}
compartments[packageLocation] = {
label,
+ name,
location: packageLocation,
modules,
scopes,

MODIFY
packages/compartment-mapper/src/parse-archive-cjs.js
packages/compartment-mapper/src/parse-archive-cjs.js
/** @typedef {import('ses').ThirdPartyStaticModuleInterface} ThirdPartyStaticModuleInterface */
import { analyzeCommonJS } from '@endo/cjs-module-analyzer';
+import { join } from './node-module-specifier.js';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
@@ -15,21 +16,21 @@ freeze(noopExecute);
/** @type {import('./types.js').ParseFn} */
export const parseArchiveCjs = async (
bytes,
- _specifier,
- location,
+ specifier,
+ _location,
_packageLocation,
+ packageName,
) => {
const source = textDecoder.decode(bytes);
-
- if (typeof location !== 'string') {
- throw new TypeError(
- `Cannot create CommonJS static module record, module location must be a string, got ${location}`,
- );
- }
+ const base = packageName
+ .split('/')
+ .slice(-1)
+ .join('/');
+ const sourceLocation = `.../${join(base, specifier)}`;
const { requires: imports, exports, reexports } = analyzeCommonJS(
source,
- location,
+ sourceLocation,
);
const pre = textEncoder.encode(
@@ -37,7 +38,7 @@ export const parseArchiveCjs = async (
imports,
exports,
reexports,
- source: `(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n//# sourceURL=${location}`,
+ source: `(function (require, exports, module, __filename, __dirname) { ${source} //*/\n})\n//# sourceURL=${sourceLocation}`,
}),
);

MODIFY
packages/compartment-mapper/src/parse-archive-mjs.js
packages/compartment-mapper/src/parse-archive-mjs.js
// @ts-check
import { StaticModuleRecord } from '@endo/static-module-record';
+import { join } from './node-module-specifier.js';
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
@@ -8,12 +9,18 @@ const textDecoder = new TextDecoder();
/** @type {import('./types.js').ParseFn} */
export const parseArchiveMjs = async (
bytes,
- _specifier,
+ specifier,
_location,
_packageLocation,
+ packageName,
) => {
const source = textDecoder.decode(bytes);
- const record = new StaticModuleRecord(source);
+ const base = packageName
+ .split('/')
+ .slice(-1)
+ .join('/');
+ const sourceLocation = `.../${join(base, specifier)}`;
+ const record = new StaticModuleRecord(source, sourceLocation);
const pre = textEncoder.encode(JSON.stringify(record));
return {
parser: 'pre-mjs-json',

MODIFY
packages/compartment-mapper/src/types.js
packages/compartment-mapper/src/types.js
@@ -38,6 +38,9 @@ export const moduleJSDocTypes = true;
*
* @typedef {Object} CompartmentDescriptor
* @property {string} label
+ * @property {string} name - the name of the originating package suitable for
+ * constructing a sourceURL prefix that will match it to files in a developer
+ * workspace.
* @property {string} location
* @property {Record<string, ModuleDescriptor>} modules
* @property {Record<string, ScopeDescriptor>} scopes
@@ -168,6 +171,7 @@ export const moduleJSDocTypes = true;
/**
* @callback ImportHookMaker
* @param {string} packageLocation
+ * @param {string} packageName
* @param {ParseFn} parse
* @param {Record<string, Object>} exitModules
* @returns {ImportHook}
@@ -179,6 +183,7 @@ export const moduleJSDocTypes = true;
* @param {string} specifier
* @param {string} location
* @param {string} packageLocation
+ * @param {string} packageName
* @returns {Promise<{
* bytes: Uint8Array,
* parser: Language,

ADD
packages/compartment-mapper/test/fixtures-stack/index.js
+// 1
+// 2
+throw new Error('here'); // 3
+// 4
+// 5

ADD
packages/compartment-mapper/test/test-stack.js
+import 'ses';
+import test from 'ava';
+import fs from 'fs';
+import crypto from 'crypto';
+import { importLocation, parseArchive, makeArchive } from '../index.js';
+import { makeNodeReadPowers } from '../node-powers.js';
+
+const fixtureLocation = new URL(
+ 'fixtures-stack/index.js',
+ import.meta.url,
+).toString();
+
+const readPowers = makeNodeReadPowers(fs, crypto);
+
+// 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
+// information in the compartment map, in a format that VS Code at least
+// recognizes in the Terminal window and will click through to the line and
+// column of the matching file in the workspace.
+test('archive stack trace source', async t => {
+ const archive = await makeArchive(readPowers, fixtureLocation);
+ const app = await parseArchive(archive);
+
+ let error;
+ try {
+ await app.import();
+ } catch (_error) {
+ error = _error;
+ }
+
+ t.assert(error);
+ t.assert(
+ error.stack.includes(
+ '.../compartment-mapper/test/fixtures-stack/index.js:3:',
+ ),
+ );
+});
+
+// Whereas, we expect the same program executed directly from local files to
+// have a fully qualified file URL in the stack trace.
+test('disk stack trace source', async t => {
+ let error;
+ try {
+ await importLocation(readPowers, fixtureLocation);
+ } catch (_error) {
+ error = _error;
+ }
+
+ t.assert(error);
+ t.log(error.stack);
+ t.assert(
+ error.stack.includes(
+ '/packages/compartment-mapper/test/fixtures-stack/index.js:3:',
+ ),
+ );
+});

feat(compartment-mapper): Archive source URL suffixes (#930)

Fixes https://github.com/Agoric/agoric-sdk/issues/3899