Skip to content

Commit 7e8e722

Browse files
committed
import FileAttachment
1 parent f8989a6 commit 7e8e722

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+463
-590
lines changed

docs/javascript/files.md

+7-18
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
TK Should this be called “working with data”?
44

5-
You can load files the built-in `FileAttachment` function or the standard [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) API. We recommend `FileAttachment` because it supports many common data formats, including CSV, TSV, JSON, Apache Arrow, and SQLite. For example, here’s how to load a CSV file:
5+
You can load files the built-in `FileAttachment` function. This is available by default in Markdown, but you can import it like so:
6+
7+
```js echo
8+
import {FileAttachment} from "npm:@observablehq/stdlib";
9+
```
10+
11+
`FileAttachment` supports many common data formats, including CSV, TSV, JSON, Apache Arrow, and SQLite. For example, here’s how to load a CSV file:
612

713
```js echo
814
const gistemp = FileAttachment("gistemp.csv").csv({typed: true});
@@ -34,23 +40,6 @@ const gistemp = FileAttachment("gistemp.csv").csv().then((D) => D.map(coerceRow)
3440

3541
TK An explanation of why coercing types as early as possible is important.
3642

37-
## Fetch
38-
39-
Here’s `fetch` for comparison.
40-
41-
```js run=false
42-
import {autoType, csvParse} from "npm:d3-dsv";
43-
44-
const gistemp = fetch("./gistemp.csv").then(async (response) => {
45-
if (!response.ok) throw new Error(`fetch error: ${response.status}`);
46-
return csvParse(await response.text(), autoType);
47-
});
48-
```
49-
50-
Use `fetch` if you prefer to stick to the web standards, you don’t mind writing a little extra code. 🥴 Also, you’ll need to use `fetch` to load files from imported ES modules; `FileAttachment` only works within Markdown.
51-
52-
**Caution:** If you use `fetch` for a local file, the path *must* start with `./`, `../`, or `/`. This allows us to distinguish between local files and absolute URLs. But that’s a little silly, right? Because unlike `import`, you can’t `fetch` a bare module specifier, so we could be more generous and detect URLs using `/^\w+:/` instead.
53-
5443
## Supported formats
5544

5645
The following type-specific methods are supported:

src/client/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export function define(cell) {
5757
v.define(outputs.length ? `cell ${id}` : null, inputs, body);
5858
variables.push(v);
5959
for (const o of outputs) variables.push(main.variable(true).define(o, [`cell ${id}`], (exports) => exports[o]));
60-
for (const f of files) registerFile(f.name, {url: f.path, mimeType: f.mimeType});
60+
for (const f of files) registerFile(f.name, f);
6161
for (const d of databases) registerDatabase(d.name, d);
6262
}
6363

src/client/stdlib/databaseClient.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function DatabaseClient(name) {
1212
return new DatabaseClientImpl(name, token);
1313
}
1414

15-
const DatabaseClientImpl = class DatabaseClient {
15+
class DatabaseClientImpl {
1616
#token;
1717

1818
constructor(name, token) {
@@ -71,8 +71,9 @@ const DatabaseClientImpl = class DatabaseClient {
7171
async sql() {
7272
return this.query(...this.queryTag.apply(this, arguments));
7373
}
74-
};
74+
}
7575

76+
Object.defineProperty(DatabaseClientImpl, "name", {value: "DatabaseClient"}); // prevent mangling
7677
DatabaseClient.prototype = DatabaseClientImpl.prototype; // instanceof
7778

7879
function coerceBuffer(d) {

src/client/stdlib/fileAttachment.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
const files = new Map();
22

33
export function registerFile(name, file) {
4-
if (file == null) files.delete(name);
5-
else files.set(name, file);
4+
const url = String(new URL(name, location.href));
5+
if (file == null) files.delete(url);
6+
else files.set(url, file);
67
}
78

8-
export function FileAttachment(name) {
9+
export function FileAttachment(name, base = location.href) {
910
if (new.target !== undefined) throw new TypeError("FileAttachment is not a constructor");
10-
const file = files.get((name = `${name}`));
11+
const url = String(new URL(name, base));
12+
const file = files.get(url);
1113
if (!file) throw new Error(`File not found: ${name}`);
12-
const {url, mimeType} = file;
13-
return new FileAttachmentImpl(url, name, mimeType);
14+
const {path, mimeType} = file;
15+
return new FileAttachmentImpl(path, name.split("/").pop(), mimeType);
1416
}
1517

1618
async function remote_fetch(file) {
@@ -85,16 +87,17 @@ class AbstractFile {
8587
}
8688
}
8789

88-
const FileAttachmentImpl = class FileAttachment extends AbstractFile {
90+
class FileAttachmentImpl extends AbstractFile {
8991
constructor(url, name, mimeType) {
9092
super(name, mimeType);
9193
Object.defineProperty(this, "_url", {value: url});
9294
}
9395
async url() {
9496
return (await this._url) + "";
9597
}
96-
};
98+
}
9799

100+
Object.defineProperty(FileAttachmentImpl, "name", {value: "FileAttachment"}); // prevent mangling
98101
FileAttachment.prototype = FileAttachmentImpl.prototype; // instanceof
99102

100103
class ZipArchive {

src/files.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export function getLocalPath(sourcePath: string, name: string): string | null {
1717

1818
export function fileReference(name: string, sourcePath: string): FileReference {
1919
return {
20-
name,
20+
name: relativeUrl(sourcePath, name),
2121
mimeType: mime.getType(name),
22-
path: relativeUrl(sourcePath, resolvePath("_file", sourcePath, name))
22+
path: relativeUrl(sourcePath, join("_file", name))
2323
};
2424
}
2525

src/javascript.ts

+10-11
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import {findAwaits} from "./javascript/awaits.js";
66
import {resolveDatabases} from "./javascript/databases.js";
77
import {findDeclarations} from "./javascript/declarations.js";
88
import {findFeatures} from "./javascript/features.js";
9-
import {rewriteFetches} from "./javascript/fetches.js";
10-
import {defaultGlobals} from "./javascript/globals.js";
119
import {findExports, findImportDeclarations, findImports} from "./javascript/imports.js";
1210
import {createImportResolver, rewriteImports} from "./javascript/imports.js";
1311
import {findReferences} from "./javascript/references.js";
@@ -20,9 +18,11 @@ export interface DatabaseReference {
2018
}
2119

2220
export interface FileReference {
21+
/** The relative path from the source root to the file. */
2322
name: string;
23+
/** The MIME type, if known; derived from the file extension. */
2424
mimeType: string | null;
25-
/** The relative path from the document to the file in _file */
25+
/** The relative path from the page to the file in _file. */
2626
path: string;
2727
}
2828

@@ -93,7 +93,6 @@ export function transpileJavaScript(input: string, options: ParseOptions): Pendi
9393
output.insertRight(input.length, "\n))");
9494
}
9595
await rewriteImports(output, node, sourcePath, createImportResolver(root, "_import"));
96-
rewriteFetches(output, node, sourcePath);
9796
const result = `${node.async ? "async " : ""}(${inputs}) => {
9897
${String(output)}${node.declarations?.length ? `\nreturn {${node.declarations.map(({name}) => name)}};` : ""}
9998
}`;
@@ -143,7 +142,7 @@ export interface JavaScriptNode {
143142
}
144143

145144
function parseJavaScript(input: string, options: ParseOptions): JavaScriptNode {
146-
const {globals = defaultGlobals, inline = false, root, sourcePath} = options;
145+
const {inline = false, root, sourcePath} = options;
147146
// First attempt to parse as an expression; if this fails, parse as a program.
148147
let expression = maybeParseExpression(input, parseOptions);
149148
if (expression?.type === "ClassExpression" && expression.id) expression = null; // treat named class as program
@@ -152,16 +151,16 @@ function parseJavaScript(input: string, options: ParseOptions): JavaScriptNode {
152151
const body = expression ?? Parser.parse(input, parseOptions);
153152
const exports = findExports(body);
154153
if (exports.length) throw syntaxError("Unexpected token 'export'", exports[0], input); // disallow exports
155-
const references = findReferences(body, globals);
156-
findAssignments(body, references, globals, input);
157-
const declarations = expression ? null : findDeclarations(body as Program, globals, input);
158-
const {imports, fetches} = findImports(body, root, sourcePath);
159-
const features = findFeatures(body, root, sourcePath, references, input);
154+
const references = findReferences(body);
155+
findAssignments(body, references, input);
156+
const declarations = expression ? null : findDeclarations(body as Program, input);
157+
const {imports, features: importedFeatures} = findImports(body, root, sourcePath);
158+
const features = findFeatures(body, sourcePath, references, input);
160159
return {
161160
body,
162161
declarations,
163162
references,
164-
features: [...features, ...fetches],
163+
features: [...features, ...importedFeatures],
165164
imports,
166165
expression: !!expression,
167166
async: findAwaits(body).length > 0

src/javascript/assignments.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import type {Expression, Node, Pattern, VariableDeclaration} from "acorn";
1+
import type {Expression, Identifier, Node, Pattern, VariableDeclaration} from "acorn";
22
import {simple} from "acorn-walk";
3+
import {defaultGlobals} from "./globals.js";
34
import {syntaxError} from "./syntaxError.js";
45

5-
export function findAssignments(node: Node, references: Node[], globals: Set<string>, input: string): void {
6+
export function findAssignments(node: Node, references: Identifier[], input: string): void {
67
function checkConst(node: Expression | Pattern | VariableDeclaration) {
78
switch (node.type) {
89
case "Identifier":
910
if (references.includes(node)) throw syntaxError(`Assignment to external variable '${node.name}'`, node, input);
10-
if (globals.has(node.name)) throw syntaxError(`Assignment to global '${node.name}'`, node, input);
11+
if (defaultGlobals.has(node.name)) throw syntaxError(`Assignment to global '${node.name}'`, node, input);
1112
break;
1213
case "ObjectPattern":
1314
node.properties.forEach((node) => checkConst(node.type === "Property" ? node.value : node));

src/javascript/declarations.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type {Identifier, Pattern, Program} from "acorn";
2+
import {defaultGlobals} from "./globals.js";
23
import {syntaxError} from "./syntaxError.js";
34

4-
export function findDeclarations(node: Program, globals: Set<string>, input: string): Identifier[] {
5+
export function findDeclarations(node: Program, input: string): Identifier[] {
56
const declarations: Identifier[] = [];
67

78
function declareLocal(node: Identifier) {
8-
if (globals.has(node.name) || node.name === "arguments") {
9+
if (defaultGlobals.has(node.name) || node.name === "arguments") {
910
throw syntaxError(`Global '${node.name}' cannot be redefined`, node, input);
1011
}
1112
declarations.push(node);

src/javascript/features.ts

+29-34
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,50 @@
1-
import type {Identifier, Literal, Node, TemplateLiteral} from "acorn";
1+
import type {CallExpression, Identifier, Literal, Node, TemplateLiteral} from "acorn";
22
import {simple} from "acorn-walk";
33
import {getLocalPath} from "../files.js";
44
import type {Feature} from "../javascript.js";
55
import {syntaxError} from "./syntaxError.js";
66

7-
export function findFeatures(
8-
node: Node,
9-
root: string,
10-
sourcePath: string,
11-
references: Identifier[],
12-
input: string
13-
): Feature[] {
7+
export function findFeatures(node: Node, path: string, references: Identifier[], input: string): Feature[] {
148
const features: Feature[] = [];
159

1610
simple(node, {
1711
CallExpression(node) {
18-
const {
19-
callee,
20-
arguments: args,
21-
arguments: [arg]
22-
} = node;
23-
12+
const {callee} = node;
2413
// Ignore function calls that are not references to the feature. For
2514
// example, if there’s a local variable called Secret, that will mask the
2615
// built-in Secret and won’t be considered a feature.
27-
if (
28-
callee.type !== "Identifier" ||
29-
(callee.name !== "Secret" && callee.name !== "FileAttachment" && callee.name !== "DatabaseClient") ||
30-
!references.includes(callee)
31-
) {
32-
return;
33-
}
34-
35-
// Forbid dynamic calls.
36-
if (args.length !== 1 || !isStringLiteral(arg)) {
37-
throw syntaxError(`${callee.name} requires a single literal string argument`, node, input);
38-
}
39-
40-
// Forbid file attachments that are not local paths.
41-
const value = getStringLiteralValue(arg);
42-
if (callee.name === "FileAttachment" && !getLocalPath(sourcePath, value)) {
43-
throw syntaxError(`non-local file path: "${value}"`, node, input);
44-
}
45-
46-
features.push({type: callee.name, name: value});
16+
if (callee.type !== "Identifier" || !references.includes(callee)) return;
17+
const {name: type} = callee;
18+
if (type !== "Secret" && type !== "FileAttachment" && type !== "DatabaseClient") return;
19+
features.push(getFeature(type, node, path, input));
4720
}
4821
});
4922

5023
return features;
5124
}
5225

26+
export function getFeature(type: Feature["type"], node: CallExpression, path: string, input: string): Feature {
27+
const {
28+
arguments: args,
29+
arguments: [arg]
30+
} = node;
31+
32+
// Forbid dynamic calls.
33+
if (args.length !== 1 || !isStringLiteral(arg)) {
34+
throw syntaxError(`${type} requires a single literal string argument`, node, input);
35+
}
36+
37+
// Forbid file attachments that are not local paths; normalize the path.
38+
let name: string | null = getStringLiteralValue(arg);
39+
if (type === "FileAttachment") {
40+
const localPath = getLocalPath(path, name);
41+
if (!localPath) throw syntaxError(`non-local file path: ${name}`, node, input);
42+
name = localPath;
43+
}
44+
45+
return {type, name};
46+
}
47+
5348
export function isStringLiteral(node: any): node is Literal | TemplateLiteral {
5449
return (
5550
node &&

src/javascript/fetches.ts

-74
This file was deleted.

0 commit comments

Comments
 (0)