Skip to content

Commit b66e152

Browse files
authored
annotate local paths (#1731)
OBSERVABLE_ANNOTATE_FILES
1 parent ac2f95f commit b66e152

File tree

7 files changed

+98
-18
lines changed

7 files changed

+98
-18
lines changed

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
"test": "concurrently npm:test:mocha npm:test:tsc npm:test:lint npm:test:prettier",
2727
"test:coverage": "c8 --check-coverage --lines 80 --per-file yarn test:mocha",
2828
"test:build": "rimraf test/build && cross-env npm_package_version=1.0.0-test node build.js --sourcemap --outdir=test/build \"{src,test}/**/*.{ts,js,css}\" --ignore \"test/input/**\" --ignore \"test/output/**\" --ignore \"test/preview/dashboard/**\" --ignore \"**/*.d.ts\" && cp -r templates test/build",
29-
"test:mocha": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 -p \"test/build/test/**/*-test.js\"",
30-
"test:mocha:serial": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/*-test.js\"",
29+
"test:mocha": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 -p \"test/build/test/**/*-test.js\" && yarn test:annotate",
30+
"test:mocha:serial": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/*-test.js\" && yarn test:annotate",
31+
"test:annotate": "yarn test:build && cross-env OBSERVABLE_ANNOTATE_FILES=true TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/annotate.js\"",
3132
"test:lint": "eslint src test --max-warnings=0",
3233
"test:prettier": "prettier --check src test",
3334
"test:tsc": "tsc --noEmit",

src/javascript/annotate.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {isPathImport} from "../path.js";
2+
3+
/**
4+
* Annotate a path to a local import or file so it can be reworked server-side.
5+
*/
6+
7+
const annotate = process.env["OBSERVABLE_ANNOTATE_FILES"];
8+
if (typeof annotate === "string" && annotate !== "true")
9+
throw new Error(`unsupported OBSERVABLE_ANNOTATE_FILES value: ${annotate}`);
10+
export default annotate
11+
? function (uri: string): string {
12+
return `${JSON.stringify(uri)}${isPathImport(uri) ? "/* observablehq-file */" : ""}`;
13+
}
14+
: JSON.stringify;

src/javascript/transpile.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {isPathImport, relativePath, resolvePath, resolveRelativePath} from "../p
77
import {getModuleResolver} from "../resolvers.js";
88
import type {Params} from "../route.js";
99
import {Sourcemap} from "../sourcemap.js";
10+
import annotate from "./annotate.js";
1011
import type {FileExpression} from "./files.js";
1112
import {findFiles} from "./files.js";
1213
import type {ExportNode, ImportNode} from "./imports.js";
@@ -101,7 +102,7 @@ export async function transpileModule(
101102

102103
async function rewriteImportSource(source: StringLiteral) {
103104
const specifier = getStringLiteralValue(source);
104-
output.replaceLeft(source.start, source.end, JSON.stringify(await resolveImport(specifier)));
105+
output.replaceLeft(source.start, source.end, annotate(await resolveImport(specifier)));
105106
}
106107

107108
for (const {name, node} of findFiles(body, path, input)) {
@@ -111,17 +112,15 @@ export async function transpileModule(
111112
output.replaceLeft(
112113
source.start,
113114
source.end,
114-
`${JSON.stringify(
115+
`${
115116
info
116-
? {
117-
name: p,
118-
mimeType: mime.getType(name) ?? undefined,
119-
path: relativePath(servePath, resolveFile(name)),
120-
lastModified: info.mtimeMs,
121-
size: info.size
122-
}
123-
: p
124-
)}, import.meta.url`
117+
? `{"name":${JSON.stringify(p)},"mimeType":${JSON.stringify(
118+
mime.getType(name) ?? undefined
119+
)},"path":${annotate(relativePath(servePath, resolveFile(name)))},"lastModified":${JSON.stringify(
120+
info.mtimeMs
121+
)},"size":${JSON.stringify(info.size)}}`
122+
: JSON.stringify(p)
123+
}, import.meta.url`
125124
);
126125
}
127126

@@ -137,7 +136,7 @@ export async function transpileModule(
137136
if (isImportMetaResolve(node) && isStringLiteral(source)) {
138137
const value = getStringLiteralValue(source);
139138
const resolution = isPathImport(value) && !isJavaScript(value) ? resolveFile(value) : await resolveImport(value);
140-
output.replaceLeft(source.start, source.end, JSON.stringify(resolution));
139+
output.replaceLeft(source.start, source.end, annotate(resolution));
141140
}
142141
}
143142

@@ -205,7 +204,7 @@ function rewriteImportDeclarations(
205204
for (const node of declarations) {
206205
output.delete(node.start, node.end + +(output.input[node.end] === "\n"));
207206
specifiers.push(rewriteImportSpecifiers(node));
208-
imports.push(`import(${JSON.stringify(resolve(getStringLiteralValue(node.source as StringLiteral)))})`);
207+
imports.push(`import(${annotate(resolve(getStringLiteralValue(node.source as StringLiteral)))})`);
209208
}
210209
if (declarations.length > 1) {
211210
output.insertLeft(0, `const [${specifiers.join(", ")}] = await Promise.all([${imports.join(", ")}]);\n`);

src/node.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup";
1313
import {rollup} from "rollup";
1414
import esbuild from "rollup-plugin-esbuild";
1515
import {prepareOutput, toOsPath} from "./files.js";
16+
import annotate from "./javascript/annotate.js";
1617
import type {ImportReference} from "./javascript/imports.js";
1718
import {isJavaScript, parseImports} from "./javascript/imports.js";
1819
import {parseNpmSpecifier, rewriteNpmImports} from "./npm.js";
@@ -86,7 +87,7 @@ function isBadCommonJs(specifier: string): boolean {
8687
}
8788

8889
function shimCommonJs(specifier: string, require: NodeRequire): string {
89-
return `export {${Object.keys(require(specifier))}} from ${JSON.stringify(specifier)};\n`;
90+
return `export {${Object.keys(require(specifier))}} from ${annotate(specifier)};\n`;
9091
}
9192

9293
async function bundle(

src/npm.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {CallExpression} from "acorn";
55
import {simple} from "acorn-walk";
66
import {maxSatisfying, rsort, satisfies, validRange} from "semver";
77
import {isEnoent} from "./error.js";
8+
import annotate from "./javascript/annotate.js";
89
import type {ExportNode, ImportNode, ImportReference} from "./javascript/imports.js";
910
import {isImportMetaResolve, parseImports} from "./javascript/imports.js";
1011
import {parseProgram} from "./javascript/parse.js";
@@ -64,7 +65,7 @@ export function rewriteNpmImports(input: string, resolve: (s: string) => string
6465
const value = getStringLiteralValue(source);
6566
const resolved = resolve(value);
6667
if (resolved === undefined || value === resolved) return;
67-
output.replaceLeft(source.start, source.end, JSON.stringify(resolved));
68+
output.replaceLeft(source.start, source.end, annotate(resolved));
6869
}
6970

7071
// TODO Preserve the source map, but download it too.

src/rollup.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {AstNode, OutputChunk, Plugin, ResolveIdResult} from "rollup";
66
import {rollup} from "rollup";
77
import esbuild from "rollup-plugin-esbuild";
88
import {getClientPath, getStylePath} from "./files.js";
9+
import annotate from "./javascript/annotate.js";
910
import type {StringLiteral} from "./javascript/source.js";
1011
import {getStringLiteralValue, isStringLiteral} from "./javascript/source.js";
1112
import {resolveNpmImport} from "./npm.js";
@@ -177,7 +178,7 @@ function importMetaResolve(path: string, resolveImport: ImportResolver): Plugin
177178
for (const source of resolves) {
178179
const specifier = getStringLiteralValue(source);
179180
const resolution = await resolveImport(specifier);
180-
if (resolution) output.replaceLeft(source.start, source.end, JSON.stringify(relativePath(path, resolution)));
181+
if (resolution) output.replaceLeft(source.start, source.end, annotate(relativePath(path, resolution)));
181182
}
182183

183184
return {code: String(output)};

test/javascript/annotate.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* This file is not suffixed with '-test'; it expects to run with an extra
3+
* OBSERVABLE_ANNOTATE_FILES=true environment variable.
4+
*/
5+
import assert from "node:assert";
6+
import type {TranspileModuleOptions} from "../../src/javascript/transpile.js";
7+
import {transpileModule} from "../../src/javascript/transpile.js";
8+
import {fromJsDelivrPath, rewriteNpmImports} from "../../src/npm.js";
9+
import {relativePath} from "../../src/path.js";
10+
11+
// prettier-ignore
12+
describe("annotates", () => {
13+
const options: TranspileModuleOptions = {root: "src", path: "test.js"};
14+
it("npm imports", async () => {
15+
const input = 'import "npm:d3-array";';
16+
const output = (await transpileModule(input, options)).split("\n").pop()!;
17+
assert.strictEqual(output, 'import "../_npm/[email protected]/_esm.js"/* observablehq-file */;');
18+
});
19+
it("node imports", async () => {
20+
const input = 'import "d3-array";';
21+
const output = (await transpileModule(input, options)).split("\n").pop()!;
22+
assert.strictEqual(output, 'import "../_node/[email protected]/index.js"/* observablehq-file */;');
23+
});
24+
it("dynamic imports", async () => {
25+
const input = 'import("d3-array");';
26+
const output = (await transpileModule(input, options)).split("\n").pop()!;
27+
assert.strictEqual(output, 'import("../_node/[email protected]/index.js"/* observablehq-file */);');
28+
});
29+
it("/npm/ exports", () => {
30+
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/dist/d3.js", v)), 'export * from "../../[email protected]/dist/d3-array.js"/* observablehq-file */;\n');
31+
});
32+
it("/npm/ imports", () => {
33+
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/dist/d3.js", v)), 'import "../../[email protected]/dist/d3-array.js"/* observablehq-file */;\n');
34+
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/dist/d3-array.js";\n', (v) => resolve("/_npm/[email protected]/d3.js", v)), 'import "../[email protected]/dist/d3-array.js"/* observablehq-file */;\n');
35+
});
36+
it("named imports", () => {
37+
assert.strictEqual(rewriteNpmImports('import {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import {sort} from "../[email protected]/_esm.js"/* observablehq-file */;\n');
38+
});
39+
it("empty imports", () => {
40+
assert.strictEqual(rewriteNpmImports('import "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import "../[email protected]/_esm.js"/* observablehq-file */;\n');
41+
});
42+
it("default imports", () => {
43+
assert.strictEqual(rewriteNpmImports('import d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import d3 from "../[email protected]/_esm.js"/* observablehq-file */;\n');
44+
});
45+
it("namespace imports", () => {
46+
assert.strictEqual(rewriteNpmImports('import * as d3 from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import * as d3 from "../[email protected]/_esm.js"/* observablehq-file */;\n');
47+
});
48+
it("named exports", () => {
49+
assert.strictEqual(rewriteNpmImports('export {sort} from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'export {sort} from "../[email protected]/_esm.js"/* observablehq-file */;\n');
50+
});
51+
it("namespace exports", () => {
52+
assert.strictEqual(rewriteNpmImports('export * from "/npm/[email protected]/+esm";\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'export * from "../[email protected]/_esm.js"/* observablehq-file */;\n');
53+
});
54+
it("dynamic imports with static module specifiers", () => {
55+
assert.strictEqual(rewriteNpmImports('import("/npm/[email protected]/+esm");\n', (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js"/* observablehq-file */);\n');
56+
assert.strictEqual(rewriteNpmImports("import(`/npm/[email protected]/+esm`);\n", (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js"/* observablehq-file */);\n');
57+
assert.strictEqual(rewriteNpmImports("import('/npm/[email protected]/+esm');\n", (v) => resolve("/_npm/[email protected]/_esm.js", v)), 'import("../[email protected]/_esm.js"/* observablehq-file */);\n');
58+
});
59+
});
60+
61+
function resolve(path: string, specifier: string): string {
62+
return specifier.startsWith("/npm/") ? relativePath(path, fromJsDelivrPath(specifier)) : specifier;
63+
}

0 commit comments

Comments
 (0)