Skip to content

Commit e467a6b

Browse files
authored
getModuleHash needs to consider data loaders (#1662)
* getModuleHash needs to consider data loaders * getSourceFileHash * implied type * fix comment * more fixes * consolidate * consolidate * consolidate * consolidate
1 parent 18cee9b commit e467a6b

File tree

17 files changed

+106
-25
lines changed

17 files changed

+106
-25
lines changed

src/build.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import {basename, dirname, extname, join} from "node:path/posix";
44
import type {Config} from "./config.js";
55
import {CliError} from "./error.js";
66
import {getClientPath, prepareOutput} from "./files.js";
7-
import {findModule, getLocalModuleHash, getModuleHash, readJavaScript} from "./javascript/module.js";
7+
import {findModule, getModuleHash, readJavaScript} from "./javascript/module.js";
88
import {transpileModule} from "./javascript/transpile.js";
99
import type {Logger, Writer} from "./logger.js";
1010
import type {MarkdownPage} from "./markdown.js";
1111
import {populateNpmCache, resolveNpmImport, rewriteNpmImports} from "./npm.js";
1212
import {isAssetPath, isPathImport, relativePath, resolvePath, within} from "./path.js";
1313
import {renderModule, renderPage} from "./render.js";
1414
import type {Resolvers} from "./resolvers.js";
15-
import {getModuleResolver, getModuleResolvers, getResolvers} from "./resolvers.js";
16-
import {resolveImportPath, resolveStylesheetPath} from "./resolvers.js";
15+
import {getModuleResolvers, getResolvers} from "./resolvers.js";
16+
import {resolveStylesheetPath} from "./resolvers.js";
1717
import {bundleStyles, rollupClient} from "./rollup.js";
1818
import {searchIndex} from "./search.js";
1919
import {Telemetry} from "./telemetry.js";
@@ -230,7 +230,7 @@ export async function build(
230230
// string. Note that this hash is not of the content of the module itself, but
231231
// of the transitive closure of the module and its imports and files.
232232
const resolveLocalImport = async (path: string): Promise<string> => {
233-
const hash = (await getLocalModuleHash(root, path)).slice(0, 8);
233+
const hash = (await loaders.getLocalModuleHash(path)).slice(0, 8);
234234
return applyHash(join("/_import", path), hash);
235235
};
236236
for (const path of localImports) {
@@ -239,7 +239,7 @@ export async function build(
239239
const sourcePath = join(root, module.path);
240240
const importPath = join("_import", module.path);
241241
effects.output.write(`${faint("copy")} ${sourcePath} ${faint("→")} `);
242-
const resolveImport = getModuleResolver(root, path);
242+
const resolveImport = loaders.getModuleResolver(path);
243243
const input = await readJavaScript(sourcePath);
244244
const contents = await transpileModule(input, {
245245
root,
@@ -267,7 +267,7 @@ export async function build(
267267
}
268268
});
269269
const alias = await resolveLocalImport(path);
270-
aliases.set(resolveImportPath(root, path), alias);
270+
aliases.set(loaders.resolveImportPath(path), alias);
271271
await effects.writeFile(alias, contents);
272272
}
273273

src/javascript/module.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ const moduleInfoCache = new Map<string, ModuleInfo>();
5454
* has invalid syntax, returns the hash of empty content; likewise ignores any
5555
* transitive imports or files that are invalid or do not exist.
5656
*/
57-
export function getModuleHash(root: string, path: string): string {
58-
return getModuleHashInternal(root, path).digest("hex");
57+
export function getModuleHash(root: string, path: string, getHash?: (p: string) => string): string {
58+
return getModuleHashInternal(root, path, getHash).digest("hex");
5959
}
6060

61-
function getModuleHashInternal(root: string, path: string): Hash {
61+
function getModuleHashInternal(root: string, path: string, getHash = (p: string) => getFileHash(root, p)): Hash {
6262
const hash = createHash("sha256");
6363
const paths = new Set([path]);
6464
for (const path of paths) {
@@ -73,14 +73,10 @@ function getModuleHashInternal(root: string, path: string): Hash {
7373
paths.add(resolvePath(path, i));
7474
}
7575
for (const i of info.files) {
76-
const f = getFileInfo(root, resolvePath(path, i));
77-
if (!f) continue; // ignore missing file
78-
hash.update(f.hash);
76+
hash.update(getHash(resolvePath(path, i)));
7977
}
8078
} else {
81-
const info = getFileInfo(root, path); // e.g., import.meta.resolve("foo.json")
82-
if (!info) continue; // ignore missing file
83-
hash.update(info.hash);
79+
hash.update(getHash(path)); // e.g., import.meta.resolve("foo.json")
8480
}
8581
}
8682
return hash;
@@ -92,8 +88,8 @@ function getModuleHashInternal(root: string, path: string): Hash {
9288
* during build because we want the hash of the built module to change if the
9389
* version of an imported npm package changes.
9490
*/
95-
export async function getLocalModuleHash(root: string, path: string): Promise<string> {
96-
const hash = getModuleHashInternal(root, path);
91+
export async function getLocalModuleHash(root: string, path: string, getHash?: (p: string) => string): Promise<string> {
92+
const hash = getModuleHashInternal(root, path, getHash);
9793
const info = getModuleInfo(root, path);
9894
if (info) {
9995
const globalPaths = new Set<string>();

src/loader.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import {maybeStat, prepareOutput, visitFiles} from "./files.js";
1212
import {FileWatchers} from "./fileWatchers.js";
1313
import {formatByteSize} from "./format.js";
1414
import type {FileInfo} from "./javascript/module.js";
15-
import {findModule, getFileInfo} from "./javascript/module.js";
15+
import {findModule, getFileInfo, getLocalModuleHash, getModuleHash} from "./javascript/module.js";
1616
import type {Logger, Writer} from "./logger.js";
1717
import type {MarkdownPage, ParseOptions} from "./markdown.js";
1818
import {parseMarkdown} from "./markdown.js";
19+
import {getModuleResolver, resolveImportPath} from "./resolvers.js";
1920
import type {Params} from "./route.js";
2021
import {isParameterized, requote, route} from "./route.js";
2122
import {cyan, faint, green, red, yellow} from "./tty.js";
@@ -306,6 +307,12 @@ export class LoaderResolver {
306307
return path === name ? hash : createHash("sha256").update(hash).update(String(info.mtimeMs)).digest("hex");
307308
}
308309

310+
getOutputFileHash(name: string): string {
311+
const info = this.getOutputInfo(name);
312+
if (!info) throw new Error(`output file not found: ${name}`);
313+
return info.hash;
314+
}
315+
309316
getSourceInfo(name: string): FileInfo | undefined {
310317
return getFileInfo(this.root, this.getSourceFilePath(name));
311318
}
@@ -314,6 +321,22 @@ export class LoaderResolver {
314321
return getFileInfo(this.root, this.getOutputFilePath(name));
315322
}
316323

324+
getLocalModuleHash(path: string): Promise<string> {
325+
return getLocalModuleHash(this.root, path, (p) => this.getOutputFileHash(p));
326+
}
327+
328+
getModuleHash(path: string): string {
329+
return getModuleHash(this.root, path, (p) => this.getSourceFileHash(p));
330+
}
331+
332+
getModuleResolver(path: string, servePath?: string): (specifier: string) => Promise<string> {
333+
return getModuleResolver(this.root, path, servePath, (p) => this.getSourceFileHash(p));
334+
}
335+
336+
resolveImportPath(path: string): string {
337+
return resolveImportPath(this.root, path, (p) => this.getSourceFileHash(p));
338+
}
339+
317340
resolveFilePath(path: string): string {
318341
return `/${join("_file", path)}?sha=${this.getSourceFileHash(path)}`;
319342
}

src/preview.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class PreviewServer {
162162
root,
163163
path,
164164
params: module.params,
165+
resolveImport: loaders.getModuleResolver(path),
165166
resolveFile: (name) => loaders.resolveFilePath(resolvePath(path, name)),
166167
resolveFileInfo: (name) => loaders.getSourceInfo(resolvePath(path, name))
167168
});

src/resolvers.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const builtins = new Map<string, string>([
8383
* them to any files referenced by static HTML.
8484
*/
8585
export async function getResolvers(page: MarkdownPage, config: ResolversConfig): Promise<Resolvers> {
86-
const {root, path, globalStylesheets: defaultStylesheets, loaders} = config;
86+
const {path, globalStylesheets: defaultStylesheets, loaders} = config;
8787
const hash = createHash("sha256").update(page.body).update(JSON.stringify(page.data));
8888
const assets = new Set<string>();
8989
const files = new Set<string>();
@@ -131,7 +131,7 @@ export async function getResolvers(page: MarkdownPage, config: ResolversConfig):
131131
// Compute the content hash.
132132
for (const f of assets) hash.update(loaders.getSourceFileHash(resolvePath(path, f)));
133133
for (const f of files) hash.update(loaders.getSourceFileHash(resolvePath(path, f)));
134-
for (const i of localImports) hash.update(getModuleHash(root, resolvePath(path, i)));
134+
for (const i of localImports) hash.update(loaders.getModuleHash(resolvePath(path, i)));
135135
if (page.style && isPathImport(page.style)) hash.update(loaders.getSourceFileHash(resolvePath(path, page.style)));
136136

137137
// Add implicit imports for standard library built-ins, such as d3 and Plot.
@@ -332,7 +332,7 @@ async function resolveResolvers(
332332

333333
function resolveImport(specifier: string): string {
334334
return isPathImport(specifier)
335-
? relativePath(path, resolveImportPath(root, resolvePath(path, specifier)))
335+
? relativePath(path, loaders.resolveImportPath(resolvePath(path, specifier)))
336336
: builtins.has(specifier)
337337
? relativePath(path, builtins.get(specifier)!)
338338
: specifier.startsWith("observablehq:")
@@ -435,11 +435,12 @@ export async function getModuleStaticImports(root: string, path: string): Promis
435435
export function getModuleResolver(
436436
root: string,
437437
path: string,
438-
servePath = `/${join("_import", path)}`
438+
servePath = `/${join("_import", path)}`,
439+
getHash?: (path: string) => string
439440
): (specifier: string) => Promise<string> {
440441
return async (specifier) => {
441442
return isPathImport(specifier)
442-
? relativePath(servePath, resolveImportPath(root, resolvePath(path, specifier)))
443+
? relativePath(servePath, resolveImportPath(root, resolvePath(path, specifier), getHash))
443444
: builtins.has(specifier)
444445
? relativePath(servePath, builtins.get(specifier)!)
445446
: specifier.startsWith("observablehq:")
@@ -456,8 +457,8 @@ export function resolveStylesheetPath(root: string, path: string): string {
456457
return `/${join("_import", path)}?sha=${getFileHash(root, path)}`;
457458
}
458459

459-
export function resolveImportPath(root: string, path: string): string {
460-
return `/${join("_import", path)}?sha=${getModuleHash(root, path)}`;
460+
export function resolveImportPath(root: string, path: string, getHash?: (name: string) => string): string {
461+
return `/${join("_import", path)}?sha=${getModuleHash(root, path, getHash)}`;
461462
}
462463

463464
// Returns any inputs that are not declared in outputs. These typically refer to
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {test} from "./test.js";
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Data loader test
2+
3+
```js
4+
import {test} from "./import-test.js";
5+
6+
display(await test);
7+
```

test/input/build/data-loaders/test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {FileAttachment} from "npm:@observablehq/stdlib";
2+
3+
export const test = FileAttachment("./test.txt").text();
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
process.stdout.write("test\n");
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test

0 commit comments

Comments
 (0)