Skip to content

Commit

Permalink
npm imports
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Oct 10, 2023
1 parent e251203 commit ec096f4
Show file tree
Hide file tree
Showing 20 changed files with 105 additions and 33 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ TODO
- ✅ dynamic imports of ES modules
- ✅ static imports of ES modules
- ✅ local ES modules detected as file attachments
- parallelize multiple static import statements
- parse imports from npm:module and generate the corresponding import map
- npm packages
- ✅ parse imports from npm:module and generate the corresponding import map
- npm packages
- parallelize awaits when multiple static import statements
- local Observable Markdown files
- cloud Observable notebooks?
- configurable import map?
- self-host recommended libraries?
- equivalent to version locking/integrity sha
- use ES module imports for recommended libraries
- during build, download ES modules from jsDelivr
- equivalent to version locking/integrity sha?
- standard library 2.0
- remove deprecated features for standard library
- Generators.asyncInput
Expand Down
2 changes: 1 addition & 1 deletion public/client.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Runtime, Library, Inspector} from "/_observablehq/runtime.js";
import {Runtime, Library, Inspector} from "npm:@observablehq/runtime";

const library = Object.assign(new Library(), {width});
const runtime = new Runtime(library);
Expand Down
22 changes: 18 additions & 4 deletions src/javascript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,25 @@ import {findDeclarations} from "./javascript/declarations.js";
import {defaultGlobals} from "./javascript/globals.js";
import {findReferences} from "./javascript/references.js";
import {findFeatures} from "./javascript/features.js";
import {rewriteImports} from "./javascript/imports.js";
import {findImports, rewriteImports} from "./javascript/imports.js";
import {accessSync, constants, statSync} from "node:fs";
import {join} from "node:path";
import {isNodeError} from "./error.js";
import {Sourcemap} from "./sourcemap.js";

export interface FileReference {
name: string;
mimeType: string;
}

export interface ImportReference {
name: string;
}

export interface Transpile {
js: string;
files: {name: string; mimeType: string}[];
files: FileReference[];
imports: ImportReference[];
}

export interface TranspileOptions {
Expand Down Expand Up @@ -47,15 +57,17 @@ export function transpileJavaScript(input: string, {id, root, ...options}: Trans
${String(output)}${node.declarations?.length ? `\nreturn {${node.declarations.map(({name}) => name)}};` : ""}
}});
`,
files
files,
imports: node.imports
};
} catch (error) {
if (!(error instanceof SyntaxError)) throw error;
return {
// TODO: Add error details to the response to improve code rendering.
js: `define({id: ${id}, body: () => { throw new SyntaxError(${JSON.stringify(error.message)}); }});
`,
files: []
files: [],
imports: []
};
}
}
Expand Down Expand Up @@ -93,11 +105,13 @@ export function parseJavaScript(
const references = findReferences(body, globals, input);
const declarations = expression ? null : findDeclarations(body, globals, input);
const features = findFeatures(body, references, input);
const imports = findImports(body);
return {
body,
declarations,
references,
features,
imports,
expression: !!expression,
async: findAwaits(body).length > 0
};
Expand Down
20 changes: 20 additions & 0 deletions src/javascript/imports.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import {simple} from "acorn-walk";
import {getStringLiteralValue, isStringLiteral} from "./features.js";

export function findImports(root) {
const imports = [];

simple(root, {
ImportExpression: findImport,
ImportDeclaration: findImport
});

function findImport(node) {
if (isStringLiteral(node.source)) {
const value = getStringLiteralValue(node.source);
if (value.startsWith("npm:")) {
imports.push({name: value});
}
}
}

return imports;
}

// TODO parallelize multiple static imports
export function rewriteImports(output, root) {
simple(root.body, {
Expand Down
14 changes: 9 additions & 5 deletions src/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ import MarkdownIt from "markdown-it";
import type {RuleCore} from "markdown-it/lib/parser_core.js";
import type {RuleInline} from "markdown-it/lib/parser_inline.js";
import type {RenderRule} from "markdown-it/lib/renderer.js";
import {transpileJavaScript} from "./javascript.js";
import {type FileReference, type ImportReference, transpileJavaScript} from "./javascript.js";

interface ParseContext {
id: number;
js: string;
files: {name: string; mimeType: string}[];
files: FileReference[];
imports: ImportReference[];
}

export interface ParseResult {
title: string | null;
html: string;
js: string;
data: {[key: string]: any} | null;
files: {name: string; mimeType: string}[];
files: FileReference[];
imports: ImportReference[];
}

function makeFenceRenderer(root: string, baseRenderer: RenderRule): RenderRule {
Expand All @@ -30,6 +32,7 @@ function makeFenceRenderer(root: string, baseRenderer: RenderRule): RenderRule {
const transpile = transpileJavaScript(token.content, {id, root});
context.js += `\n${transpile.js}`;
context.files.push(...transpile.files);
context.imports.push(...transpile.imports);
result += `<div id="cell-${id}" class="observablehq observablehq--block"></div>\n`;
}
if (language !== "js" || option === "show" || option === "no-run") {
Expand Down Expand Up @@ -198,15 +201,16 @@ export function parseMarkdown(source: string, root: string): ParseResult {
md.core.ruler.before("linkify", "placeholder", transformPlaceholderCore);
md.renderer.rules.placeholder = makePlaceholderRenderer(root);
md.renderer.rules.fence = makeFenceRenderer(root, md.renderer.rules.fence!);
const context: ParseContext = {id: 0, js: "", files: []};
const context: ParseContext = {id: 0, js: "", files: [], imports: []};
const tokens = md.parse(parts.content, context);
const html = md.renderer.render(tokens, md.options, context);
return {
html,
js: context.js,
data: isEmpty(parts.data) ? null : parts.data,
title: parts.data?.title ?? findTitle(tokens) ?? null,
files: context.files
files: context.files,
imports: context.imports
};
}

Expand Down
26 changes: 23 additions & 3 deletions src/render.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {computeHash} from "./hash.js";
import type {FileReference, ImportReference} from "./javascript.js";
import type {ParseResult} from "./markdown.js";
import {parseMarkdown} from "./markdown.js";

export interface Render {
html: string;
files: {name: string; mimeType: string}[];
files: FileReference[];
imports: ImportReference[];
}

export interface RenderOptions {
Expand All @@ -15,12 +17,20 @@ export interface RenderOptions {

export function renderPreview(source: string, options: RenderOptions): Render {
const parseResult = parseMarkdown(source, options.root);
return {html: render(parseResult, {...options, preview: true, hash: computeHash(source)}), files: parseResult.files};
return {
html: render(parseResult, {...options, preview: true, hash: computeHash(source)}),
files: parseResult.files,
imports: parseResult.imports
};
}

export function renderServerless(source: string, options: RenderOptions): Render {
const parseResult = parseMarkdown(source, options.root);
return {html: render(parseResult, options), files: parseResult.files};
return {
html: render(parseResult, options),
files: parseResult.files,
imports: parseResult.imports
};
}

type RenderInternalOptions =
Expand All @@ -36,6 +46,16 @@ ${
parseResult.title ? `<title>${escapeData(parseResult.title)}</title>\n` : ""
}<link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap">
<link rel="stylesheet" type="text/css" href="/_observablehq/style.css">
<script type="importmap">
${JSON.stringify({
imports: Object.fromEntries(
parseResult.imports
.filter(({name}) => name.startsWith("npm:"))
.map(({name}) => [name, `https://cdn.jsdelivr.net/npm/${name.slice(4)}/+esm`])
.concat([["npm:@observablehq/runtime", "/_observablehq/runtime.js"]])
)
})}
</script>
<link rel="modulepreload" href="/_observablehq/runtime.js">
<script type="module">
Expand Down
3 changes: 2 additions & 1 deletion test/output/block-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/dollar-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/double-quote-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/embedded-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": "Embedded expression",
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/escaped-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/fenced-code.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": "Fenced code",
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/heading-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/hello-world.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": "Hello, world!",
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/inline-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/script-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": "Script expression",
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/single-quote-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/template-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": null,
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/tex-expression.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"data": null,
"title": "Hello, ${tex`\\KaTeX`}",
"files": []
"files": [],
"imports": []
}
3 changes: 2 additions & 1 deletion test/output/yaml-frontmatter.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
]
},
"title": "YAML",
"files": []
"files": [],
"imports": []
}

0 comments on commit ec096f4

Please sign in to comment.