diff --git a/src/build.ts b/src/build.ts index 539776d3f..fd1cad628 100644 --- a/src/build.ts +++ b/src/build.ts @@ -63,8 +63,9 @@ export async function build( effects.logger.log(`${faint("found")} ${pageCount} ${faint(`page${pageCount === 1 ? "" : "s"} in`)} ${root}`); // Render .md files, building a list of file attachments as we go. - const files: string[] = []; - const imports: string[] = []; + const files = new Set(); + const localImports = new Set(); + const globalImports = new Set(); const styles: Style[] = []; for await (const sourceFile of visitMarkdownFiles(root)) { const sourcePath = join(root, sourceFile); @@ -73,8 +74,21 @@ export async function build( const path = join("/", dirname(sourceFile), basename(sourceFile, ".md")); const render = await renderServerless(sourcePath, {path, ...config}); const resolveFile = ({name}) => resolvePath(sourceFile, name); - files.push(...render.files.map(resolveFile)); - imports.push(...render.imports.filter((i) => i.type === "local").map(resolveFile)); + render.files.map(resolveFile).forEach(files.add, files); + render.imports.filter((i) => i.type === "local").forEach((i) => localImports.add(resolveFile(i))); + render.imports.filter((i) => i.type === "global").forEach((i) => globalImports.add(i.name)); + if (render.inputs.includes("FileAttachment")) { + // TODO If you have a FileAttachment, you might also need to preload + // - npm:d3-dsv if you call FileAttachment.csv or FileAttachment.tsv + // - npm:apache-arrow if you call FileAttachment.arrow or FileAttachment.parquet + // - npm:parquet-wasm if you call FileAttachment.parquet + // - npm:@observablehq/sqlite if you call FileAttachment.sqlite + // - npm:@observablehq/zip if you call FileAttachment.zip + // - npm:@observablehq/xlsx if you call FileAttachment.xlsx + globalImports.add("npm:@observablehq/sqlite"); + globalImports.add("npm:@observablehq/zip"); + globalImports.add("npm:@observablehq/xlsx"); + } await effects.writeFile(outputPath, render.html); const style = mergeStyle(path, render.data?.style, render.data?.theme, config.style); if (style && !styles.some((s) => styleEquals(s, style))) styles.push(style); @@ -83,29 +97,27 @@ export async function build( // Add imported local scripts. for (const script of config.scripts) { if (!/^\w+:/.test(script.src)) { - imports.push(script.src); + localImports.add(script.src); } } // Generate the client bundles. if (addPublic) { - for (const [entry, name] of [ - [clientEntry, "client.js"], - ["./src/client/stdlib.js", "stdlib.js"], - // TODO Prune this list based on which libraries are actually used. - // TODO Remove library helpers (e.g., duckdb) when they are published to npm. - ["./src/client/stdlib/dot.js", "stdlib/dot.js"], - ["./src/client/stdlib/duckdb.js", "stdlib/duckdb.js"], - ["./src/client/stdlib/inputs.css", "stdlib/inputs.css"], - ["./src/client/stdlib/inputs.js", "stdlib/inputs.js"], - ["./src/client/stdlib/mermaid.js", "stdlib/mermaid.js"], - ["./src/client/stdlib/sqlite.js", "stdlib/sqlite.js"], - ["./src/client/stdlib/tex.js", "stdlib/tex.js"], - ["./src/client/stdlib/vega-lite.js", "stdlib/vega-lite.js"], - ["./src/client/stdlib/xlsx.js", "stdlib/xlsx.js"], - ["./src/client/stdlib/zip.js", "stdlib/zip.js"], - ...(config.search ? [["./src/client/search.js", "search.js"]] : []) - ]) { + const bundles: [entry: string, name: string][] = []; + bundles.push([clientEntry, "client.js"]); + bundles.push(["./src/client/stdlib.js", "stdlib.js"]); + if (config.search) bundles.push(["./src/client/search.js", "search.js"]); + for (const lib of ["dot", "duckdb", "inputs", "mermaid", "sqlite", "tex", "vega-lite", "xlsx", "zip"]) { + if (globalImports.has(`npm:@observablehq/${lib}`)) { + bundles.push([`./src/client/stdlib/${lib}.js`, `stdlib/${lib}.js`]); + } + } + for (const lib of ["inputs"]) { + if (globalImports.has(`npm:@observablehq/${lib}`)) { + bundles.push([`./src/client/stdlib/${lib}.css`, `stdlib/${lib}.css`]); + } + } + for (const [entry, name] of bundles) { const clientPath = getClientPath(entry); const outputPath = join("_observablehq", name); effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `); @@ -159,7 +171,7 @@ export async function build( // Copy over the imported modules. const importResolver = createImportResolver(root); - for (const file of imports) { + for (const file of localImports) { const sourcePath = join(root, file); const outputPath = join("_import", file); if (!existsSync(sourcePath)) { diff --git a/src/javascript/features.ts b/src/javascript/features.ts index 5e969567c..2605e9a28 100644 --- a/src/javascript/features.ts +++ b/src/javascript/features.ts @@ -10,6 +10,9 @@ export function findFeatures(node: Node, path: string, references: Identifier[], const featureMap = getFeatureReferenceMap(node); const features: Feature[] = []; + // TODO If the FileAttachment is part of a member expression, we should be + // able to tell which method they’re calling on the file attachment, and thus + // determine which bundles need to be included in the generated build. simple(node, { CallExpression(node) { const {callee} = node; diff --git a/src/libraries.ts b/src/libraries.ts index 54da0f3c7..d905a14ab 100644 --- a/src/libraries.ts +++ b/src/libraries.ts @@ -1,10 +1,11 @@ import {resolveNpmImport} from "./javascript/imports.js"; -export function getImplicitSpecifiers(inputs: Set): Set { +export function getImplicitSpecifiers(inputs: Iterable): Set { return addImplicitSpecifiers(new Set(), inputs); } -export function addImplicitSpecifiers(specifiers: Set, inputs: Set): typeof specifiers { +export function addImplicitSpecifiers(specifiers: Set, iterable: Iterable): typeof specifiers { + const inputs = new Set(iterable); if (inputs.has("d3")) specifiers.add("npm:d3"); if (inputs.has("Plot")) specifiers.add("npm:d3").add("npm:@observablehq/plot"); if (inputs.has("htl") || inputs.has("html") || inputs.has("svg")) specifiers.add("npm:htl"); diff --git a/src/markdown.ts b/src/markdown.ts index 570acb452..734d5cd9b 100644 --- a/src/markdown.ts +++ b/src/markdown.ts @@ -16,6 +16,7 @@ import {computeHash} from "./hash.js"; import {parseInfo} from "./info.js"; import type {FileReference, ImportReference, PendingTranspile, Transpile} from "./javascript.js"; import {transpileJavaScript} from "./javascript.js"; +import {getImplicitSpecifiers} from "./libraries.js"; import {transpileTag} from "./tag.js"; import {resolvePath} from "./url.js"; @@ -38,6 +39,7 @@ export interface ParseResult { data: {[key: string]: any} | null; files: FileReference[]; imports: ImportReference[]; + inputs: string[]; pieces: HtmlPiece[]; cells: CellPiece[]; hash: string; @@ -431,18 +433,47 @@ export async function parseMarkdown(sourcePath: string, {root, path}: ParseOptio const context: ParseContext = {files: [], imports: [], pieces: [], startLine: 0, currentLine: 0}; const tokens = md.parse(parts.content, context); const html = md.renderer.render(tokens, md.options, context); // Note: mutates context.pieces, context.files! + const cells = await toParseCells(context.pieces); + const inputs = findUnboundInputs(cells); + const imports = context.imports; + for (const name of getImplicitSpecifiers(inputs)) imports.push({type: "global", name}); return { html, data: isEmpty(parts.data) ? null : parts.data, title: parts.data?.title ?? findTitle(tokens) ?? null, files: context.files, - imports: context.imports, + imports, + inputs, pieces: toParsePieces(context.pieces), - cells: await toParseCells(context.pieces), + cells, hash: await computeMarkdownHash(source, root, path, context.imports) }; } +// Returns any inputs that are not declared in outputs. These typically refer to +// symbols provided by the standard library, such as d3 and Inputs. +function findUnboundInputs(cells: CellPiece[]): string[] { + const outputs = new Set(); + const inputs = new Set(); + for (const cell of cells) { + if (cell.outputs) { + for (const output of cell.outputs) { + outputs.add(output); + } + } + } + for (const cell of cells) { + if (cell.inputs) { + for (const input of cell.inputs) { + if (!outputs.has(input)) { + inputs.add(input); + } + } + } + } + return Array.from(inputs); +} + async function computeMarkdownHash( contents: string, root: string, diff --git a/src/render.ts b/src/render.ts index 906de4a5f..cc4b18a1b 100644 --- a/src/render.ts +++ b/src/render.ts @@ -6,7 +6,7 @@ import {type Html, html} from "./html.js"; import type {ImportResolver} from "./javascript/imports.js"; import {createImportResolver, resolveModuleIntegrity, resolveModulePreloads} from "./javascript/imports.js"; import type {FileReference, ImportReference, Transpile} from "./javascript.js"; -import {addImplicitSpecifiers, addImplicitStylesheets} from "./libraries.js"; +import {addImplicitStylesheets} from "./libraries.js"; import {type ParseResult, parseMarkdown} from "./markdown.js"; import {type PageLink, findLink, normalizePath} from "./pager.js"; import {getPreviewStylesheet} from "./preview.js"; @@ -17,6 +17,7 @@ export interface Render { html: string; files: FileReference[]; imports: ImportReference[]; + inputs: string[]; data: ParseResult["data"]; } @@ -31,6 +32,7 @@ export async function renderPreview(sourcePath: string, options: RenderOptions): html: await render(parseResult, {...options, preview: true}), files: parseResult.files, imports: parseResult.imports, + inputs: parseResult.inputs, data: parseResult.data }; } @@ -41,6 +43,7 @@ export async function renderServerless(sourcePath: string, options: RenderOption html: await render(parseResult, options), files: parseResult.files, imports: parseResult.imports, + inputs: parseResult.inputs, data: parseResult.data }; } @@ -200,8 +203,6 @@ async function renderHead( if (style) stylesheets.add(style); const specifiers = new Set(["npm:@observablehq/runtime", "npm:@observablehq/stdlib"]); for (const {name} of parseResult.imports) specifiers.add(name); - const inputs = new Set(parseResult.cells.flatMap((cell) => cell.inputs ?? [])); - addImplicitSpecifiers(specifiers, inputs); await addImplicitStylesheets(stylesheets, specifiers); const preloads = new Set([relativeUrl(path, "/_observablehq/client.js")]); for (const specifier of specifiers) preloads.add(await resolver(path, specifier)); diff --git a/test/deploy-test.ts b/test/deploy-test.ts index 1ff3090d3..3881944c0 100644 --- a/test/deploy-test.ts +++ b/test/deploy-test.ts @@ -21,26 +21,6 @@ import { import {MockAuthEffects} from "./observableApiAuth-test.js"; import {MockConfigEffects} from "./observableApiConfig-test.js"; -// These files are implicitly generated. This may change over time, so they’re -// enumerated here for clarity. TODO We should enforce that these files are -// specifically uploaded, rather than just the number of files. -const EXTRA_FILES: string[] = [ - "_observablehq/client.js", - "_observablehq/runtime.js", - "_observablehq/stdlib.js", - "_observablehq/stdlib/dot.js", - "_observablehq/stdlib/duckdb.js", - "_observablehq/stdlib/inputs.css", - "_observablehq/stdlib/inputs.js", - "_observablehq/stdlib/mermaid.js", - "_observablehq/stdlib/sqlite.js", - "_observablehq/stdlib/tex.js", - "_observablehq/stdlib/vega-lite.js", - "_observablehq/stdlib/xlsx.js", - "_observablehq/stdlib/zip.js", - "_observablehq/style.css" -]; - interface MockDeployEffectsOptions { apiKey?: string | null; deployConfig?: DeployConfig | null; @@ -148,7 +128,11 @@ describe("deploy", () => { .handleGetCurrentUser() .handleGetProject(DEPLOY_CONFIG) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId, deployStatus: "uploaded"}) .start(); @@ -165,13 +149,14 @@ describe("deploy", () => { const oldTitle = `${TEST_CONFIG.title!} old`; getCurrentObservableApi() .handleGetCurrentUser() - .handleGetProject({ - ...DEPLOY_CONFIG, - title: oldTitle - }) + .handleGetProject({...DEPLOY_CONFIG, title: oldTitle}) .handleUpdateProject({projectId: DEPLOY_CONFIG.projectId, title: TEST_CONFIG.title!}) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId}) .start(); @@ -191,7 +176,11 @@ describe("deploy", () => { .handleGetCurrentUser() .handleGetProject(deployConfig) .handlePostDeploy({projectId: deployConfig.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId}) .start(); @@ -214,7 +203,11 @@ describe("deploy", () => { }) .handlePostProject({projectId: DEPLOY_CONFIG.projectId}) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId}) .start(); @@ -443,7 +436,11 @@ describe("deploy", () => { .handleGetCurrentUser() .handleGetProject(DEPLOY_CONFIG) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId, status: 500}) .start(); const effects = new MockDeployEffects({deployConfig: DEPLOY_CONFIG}); @@ -548,7 +545,11 @@ describe("deploy", () => { projectId: newProjectId }) .handlePostDeploy({projectId: newProjectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId}) .start(); @@ -632,7 +633,11 @@ describe("deploy", () => { .handleGetCurrentUser() .handleGetProject(DEPLOY_CONFIG) .handlePostDeploy({projectId: DEPLOY_CONFIG.projectId, deployId}) - .handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1}) + .handlePostDeployFile({deployId, clientName: "index.html"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/theme-air,near-midnight.css"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/client.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/runtime.js"}) + .handlePostDeployFile({deployId, clientName: "_observablehq/stdlib.js"}) .handlePostDeployUploaded({deployId}) .handleGetDeploy({deployId}) .start(); diff --git a/test/mocks/observableApi.ts b/test/mocks/observableApi.ts index 2ce0f290d..7fdd33f83 100644 --- a/test/mocks/observableApi.ts +++ b/test/mocks/observableApi.ts @@ -215,14 +215,20 @@ class ObservableApiMock { handlePostDeployFile({ deployId, + clientName, status = 204, repeat = 1 - }: {deployId?: string; status?: number; repeat?: number} = {}): ObservableApiMock { + }: {deployId?: string; clientName?: string; status?: number; repeat?: number} = {}): ObservableApiMock { const response = status == 204 ? "" : emptyErrorBody; const headers = authorizationHeader(status !== 403); this.addHandler((pool) => { pool - .intercept({path: `/cli/deploy/${deployId}/file`, method: "POST", headers: headersMatcher(headers)}) + .intercept({ + path: `/cli/deploy/${deployId}/file`, + method: "POST", + headers: headersMatcher(headers), + body: clientName === undefined ? undefined : formDataMatcher({client_name: clientName}) + }) .reply(status, response) .times(repeat); }); @@ -327,6 +333,18 @@ function headersMatcher(expected: Record): (headers: Re }; } +function formDataMatcher(expected: Record): (body: string) => boolean { + // actually FormData, not string + return (actual: any) => { + for (const key in expected) { + if (!(actual.get(key) === expected[key])) { + return false; + } + } + return true; + }; +} + const userBase = { id: "0000000000000000", login: "mock-user", diff --git a/test/output/block-expression.json b/test/output/block-expression.json index 9d487b632..a8c8e2649 100644 --- a/test/output/block-expression.json +++ b/test/output/block-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/build/search-public/_observablehq/stdlib/dot.js b/test/output/build/search-public/_observablehq/stdlib/dot.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/duckdb.js b/test/output/build/search-public/_observablehq/stdlib/duckdb.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/inputs.css b/test/output/build/search-public/_observablehq/stdlib/inputs.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/inputs.js b/test/output/build/search-public/_observablehq/stdlib/inputs.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/mermaid.js b/test/output/build/search-public/_observablehq/stdlib/mermaid.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/sqlite.js b/test/output/build/search-public/_observablehq/stdlib/sqlite.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/tex.js b/test/output/build/search-public/_observablehq/stdlib/tex.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/vega-lite.js b/test/output/build/search-public/_observablehq/stdlib/vega-lite.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/xlsx.js b/test/output/build/search-public/_observablehq/stdlib/xlsx.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/search-public/_observablehq/stdlib/zip.js b/test/output/build/search-public/_observablehq/stdlib/zip.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/dot.js b/test/output/build/simple-public/_observablehq/stdlib/dot.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/duckdb.js b/test/output/build/simple-public/_observablehq/stdlib/duckdb.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/inputs.css b/test/output/build/simple-public/_observablehq/stdlib/inputs.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/inputs.js b/test/output/build/simple-public/_observablehq/stdlib/inputs.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/mermaid.js b/test/output/build/simple-public/_observablehq/stdlib/mermaid.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/sqlite.js b/test/output/build/simple-public/_observablehq/stdlib/sqlite.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/tex.js b/test/output/build/simple-public/_observablehq/stdlib/tex.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/vega-lite.js b/test/output/build/simple-public/_observablehq/stdlib/vega-lite.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/xlsx.js b/test/output/build/simple-public/_observablehq/stdlib/xlsx.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/build/simple-public/_observablehq/stdlib/zip.js b/test/output/build/simple-public/_observablehq/stdlib/zip.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/output/comment.json b/test/output/comment.json index f8bac4464..24ad9a284 100644 --- a/test/output/comment.json +++ b/test/output/comment.json @@ -3,6 +3,7 @@ "title": null, "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/dollar-expression.json b/test/output/dollar-expression.json index d43bfff06..46e766a5c 100644 --- a/test/output/dollar-expression.json +++ b/test/output/dollar-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/dot-graphviz.json b/test/output/dot-graphviz.json index 1ef42d24b..6f94475b7 100644 --- a/test/output/dot-graphviz.json +++ b/test/output/dot-graphviz.json @@ -2,7 +2,20 @@ "data": null, "title": null, "files": [], - "imports": [], + "imports": [ + { + "type": "global", + "name": "npm:@observablehq/dot" + }, + { + "type": "global", + "name": "npm:@viz-js/viz" + } + ], + "inputs": [ + "dot", + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/double-quote-expression.json b/test/output/double-quote-expression.json index 959545d4a..472985156 100644 --- a/test/output/double-quote-expression.json +++ b/test/output/double-quote-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/embedded-expression.json b/test/output/embedded-expression.json index dfbee94e5..5c95227a1 100644 --- a/test/output/embedded-expression.json +++ b/test/output/embedded-expression.json @@ -3,6 +3,9 @@ "title": "Embedded expression", "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/escaped-expression.json b/test/output/escaped-expression.json index c30ba6808..a7c9bf652 100644 --- a/test/output/escaped-expression.json +++ b/test/output/escaped-expression.json @@ -3,6 +3,7 @@ "title": null, "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/fenced-code-options.json b/test/output/fenced-code-options.json index 494aa030e..546046d4e 100644 --- a/test/output/fenced-code-options.json +++ b/test/output/fenced-code-options.json @@ -3,6 +3,7 @@ "title": "Fenced code options", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/fenced-code.json b/test/output/fenced-code.json index e9e1f4e38..51571b112 100644 --- a/test/output/fenced-code.json +++ b/test/output/fenced-code.json @@ -3,6 +3,7 @@ "title": "Fenced code", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/fetch-parent-dir.json b/test/output/fetch-parent-dir.json index a8a6e4892..471f38256 100644 --- a/test/output/fetch-parent-dir.json +++ b/test/output/fetch-parent-dir.json @@ -14,6 +14,9 @@ } ], "imports": [], + "inputs": [ + "FileAttachment" + ], "pieces": [ { "type": "html", diff --git a/test/output/heading-expression.json b/test/output/heading-expression.json index 4d3e21ac7..4a2cc5f13 100644 --- a/test/output/heading-expression.json +++ b/test/output/heading-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/hello-world.json b/test/output/hello-world.json index 10ef10d22..b9de9fa75 100644 --- a/test/output/hello-world.json +++ b/test/output/hello-world.json @@ -3,6 +3,7 @@ "title": "Hello, world!", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/inline-expression.json b/test/output/inline-expression.json index e5daf86be..200a312fb 100644 --- a/test/output/inline-expression.json +++ b/test/output/inline-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/local-fetch.json b/test/output/local-fetch.json index 1a6b0045c..cb8a86ea8 100644 --- a/test/output/local-fetch.json +++ b/test/output/local-fetch.json @@ -9,6 +9,10 @@ } ], "imports": [], + "inputs": [ + "FileAttachment", + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/malformed-block.json b/test/output/malformed-block.json index 013deeca0..6b9a55a6d 100644 --- a/test/output/malformed-block.json +++ b/test/output/malformed-block.json @@ -3,6 +3,7 @@ "title": "Malformed block", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/markdown-in-html.json b/test/output/markdown-in-html.json index 635190fd0..a4abe4ce5 100644 --- a/test/output/markdown-in-html.json +++ b/test/output/markdown-in-html.json @@ -3,6 +3,7 @@ "title": "Markdown in HTML", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/mermaid.json b/test/output/mermaid.json index 9b4fb412c..6b5fa3507 100644 --- a/test/output/mermaid.json +++ b/test/output/mermaid.json @@ -2,7 +2,24 @@ "data": null, "title": null, "files": [], - "imports": [], + "imports": [ + { + "type": "global", + "name": "npm:@observablehq/mermaid" + }, + { + "type": "global", + "name": "npm:mermaid" + }, + { + "type": "global", + "name": "npm:d3" + } + ], + "inputs": [ + "mermaid", + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/script-expression.json b/test/output/script-expression.json index b7879f82d..32effef37 100644 --- a/test/output/script-expression.json +++ b/test/output/script-expression.json @@ -3,6 +3,7 @@ "title": "Script expression", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/single-quote-expression.json b/test/output/single-quote-expression.json index 5e237f21e..19f14ff8c 100644 --- a/test/output/single-quote-expression.json +++ b/test/output/single-quote-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/template-expression.json b/test/output/template-expression.json index e6543018d..dd1701ab9 100644 --- a/test/output/template-expression.json +++ b/test/output/template-expression.json @@ -3,6 +3,9 @@ "title": null, "files": [], "imports": [], + "inputs": [ + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/tex-block.json b/test/output/tex-block.json index 2421e2b6b..7cb4194c0 100644 --- a/test/output/tex-block.json +++ b/test/output/tex-block.json @@ -2,7 +2,20 @@ "data": null, "title": null, "files": [], - "imports": [], + "imports": [ + { + "type": "global", + "name": "npm:@observablehq/tex" + }, + { + "type": "global", + "name": "npm:katex" + } + ], + "inputs": [ + "tex", + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/tex-expression.json b/test/output/tex-expression.json index 8461bce16..5bfd29896 100644 --- a/test/output/tex-expression.json +++ b/test/output/tex-expression.json @@ -2,7 +2,20 @@ "data": null, "title": "Hello, ", "files": [], - "imports": [], + "imports": [ + { + "type": "global", + "name": "npm:@observablehq/tex" + }, + { + "type": "global", + "name": "npm:katex" + } + ], + "inputs": [ + "tex", + "display" + ], "pieces": [ { "type": "html", diff --git a/test/output/wellformed-block.json b/test/output/wellformed-block.json index 3a4cc944a..7c33a12aa 100644 --- a/test/output/wellformed-block.json +++ b/test/output/wellformed-block.json @@ -3,6 +3,7 @@ "title": "Well-formed block", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html", diff --git a/test/output/yaml-frontmatter.json b/test/output/yaml-frontmatter.json index db463a478..2a4b9f66f 100644 --- a/test/output/yaml-frontmatter.json +++ b/test/output/yaml-frontmatter.json @@ -9,6 +9,7 @@ "title": "YAML", "files": [], "imports": [], + "inputs": [], "pieces": [ { "type": "html",