Skip to content

Commit f5770d5

Browse files
Filvnegi10stefanak-michalmbostock
authored
interpreters (#935)
* Add julia ext and command * Added php support * interpreters * {root, interpreters} * java support (from #846) * fix test * document * pass interpreters to resolvers for file paths * Update docs/config.md Co-authored-by: Mike Bostock <[email protected]> * Update docs/config.md * missing line * add line Co-authored-by: Mike Bostock <[email protected]> * fix tests * revert order * docs; order * restore “in order” * sync describe callback * describe must be sync * LoaderResolver * extract mocks to prevent duplicate tests * shorter * minimize diff * LoaderResolver.getWatchPath * remove todo --------- Co-authored-by: Vikas Negi <[email protected]> Co-authored-by: Michal Štefaňák <[email protected]> Co-authored-by: Mike Bostock <[email protected]>
1 parent a8e7b4d commit f5770d5

23 files changed

+378
-250
lines changed

docs/config.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,48 @@ toc: false
200200

201201
Whether to enable [search](./search) on the project; defaults to false.
202202

203+
## interpreters
204+
205+
The **interpreters** option specifies additional interpreted languages for data loaders, indicating the file extension and associated interpreter. (See [loader routing](./loaders#routing) for more.) The default list of interpreters is:
206+
207+
```js run=false
208+
{
209+
".js": ["node", "--no-warnings=ExperimentalWarning"],
210+
".ts": ["tsx"],
211+
".py": ["python3"],
212+
".r": ["Rscript"],
213+
".R": ["Rscript"],
214+
".rs": ["rust-script"]
215+
".go": ["go", "run"],
216+
".java": ["java"],
217+
".jl": ["julia"],
218+
".php": ["php"],
219+
".sh": ["sh"],
220+
".exe": []
221+
}
222+
```
223+
224+
Keys specify the file extension and values the associated command and arguments. For example, to add Perl (extension `.pl`) and AppleScript (`.scpt`) to the list above:
225+
226+
```js run=false
227+
export default {
228+
interpreters: {
229+
".pl": ["perl"],
230+
".scpt": ["osascript"]
231+
}
232+
};
233+
```
234+
235+
To disable an interpreter, set its value to null. For example, to disable Rust:
236+
237+
```js run=false
238+
export default {
239+
interpreters: {
240+
".rs": null
241+
}
242+
};
243+
```
244+
203245
## markdownIt <a href="https://github.com/observablehq/framework/releases/tag/v1.1.0" target="_blank" class="observablehq-version-badge" data-version="^1.1.0" title="Added in v1.1.0"></a>
204246

205247
A hook for registering additional [markdown-it](https://github.com/markdown-it/markdown-it) plugins. For example, to use [markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote):

docs/loaders.md

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,29 +126,49 @@ Like with any other file, these files from generated archives are live in previe
126126

127127
## Routing
128128

129-
Data loaders live in the source root (typically `docs`) alongside your other source files. When a file is referenced from JavaScript via `FileAttachment`, if the file does not exist, Observable Framework will look for a file of the same name with a double extension to see if there is a corresponding data loader. The following second extensions are checked, in order, with the corresponding language and interpreter:
129+
Data loaders live in the source root (typically `docs`) alongside your other source files. When a file is referenced from JavaScript via `FileAttachment`, if the file does not exist, Observable Framework will look for a file of the same name with a double extension to see if there is a corresponding data loader. By default, the following second extensions are checked, in order, with the corresponding language and interpreter:
130130

131131
- `.js` - JavaScript (`node`)
132132
- `.ts` - TypeScript (`tsx`)
133133
- `.py` - Python (`python3`)
134134
- `.R` - R (`Rscript`)
135135
- `.rs` - Rust (`rust-script`)
136136
- `.go` - Go (`go run`)
137+
- `.java` — Java (`java`; requires Java 11+ and [single-file programs](https://openjdk.org/jeps/330))
138+
- `.jl` - Julia (`julia`)
139+
- `.php` - PHP (`php`)
137140
- `.sh` - shell script (`sh`)
138141
- `.exe` - arbitrary executable
139142

140-
For example, for the file `quakes.csv`, the following data loaders are considered:
143+
For example, for the file `quakes.csv`, the following data loaders are considered: `quakes.csv.js`, `quakes.csv.ts`, `quakes.csv.py`, _etc._ The first match is used.
141144

142-
- `quakes.csv.js`
143-
- `quakes.csv.ts`
144-
- `quakes.csv.py`
145-
- `quakes.csv.R`
146-
- `quakes.csv.rs`
147-
- `quakes.csv.go`
148-
- `quakes.csv.sh`
149-
- `quakes.csv.exe`
145+
<div class="tip">The <b>interpreters</b> <a href="./config#interpreters">configuration option</a> can be used to extend the list of supported extensions.</div>
150146

151-
If you use `.py`, `.R`, `.rs`, or `.go`, the corresponding interpreter (`python3`, `Rscript`, `rust-script`, or `go run`, respectively) must be installed and available on your `$PATH`. Any additional modules, packages, libraries, _etc._, must also be installed before you can use them.
147+
To use an interpreted data loader (anything other than `.exe`), the corresponding interpreter must be installed and available on your `$PATH`. Any additional modules, packages, libraries, _etc._, must also be installed. Some interpreters are not available on all platforms; for example `sh` is only available on Unix-like systems.
148+
149+
<div class="tip">
150+
151+
You can use a virtual environment in Python, such as [uv](https://github.com/astral-sh/uv), to install libraries locally to the project. This is useful when working in multiple projects, and when collaborating; you can also track dependencies in a `requirements.txt` file. To create a virtual environment with uv, run:
152+
153+
```sh
154+
uv venv # Create a virtual environment at .venv.
155+
```
156+
157+
To activate the virtual environment on macOS or Linux:
158+
159+
```sh
160+
source .venv/bin/activate
161+
```
162+
163+
Or on Windows:
164+
165+
```sh
166+
.venv\Scripts\activate
167+
```
168+
169+
You can then run the `observable preview` or `observable build` commands as usual; data loaders will run within the virtual environment. Run the `deactivate` command to exit the virtual environment.
170+
171+
</div>
152172

153173
Data loaders are run in the same working directory in which you run the `observable build` or `observable preview` command, which is typically the project root. In Node, you can access the current working directory by calling `process.cwd()`, and the data loader’s source location with [`import.meta.url`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta). To compute the path of a file relative to the data loader source (rather than relative to the current working directory), use [`import.meta.resolve`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import.meta/resolve). For example, a data loader in `docs/summary.txt.js` could read the file `docs/table.txt` as:
154174

@@ -159,18 +179,18 @@ import {fileURLToPath} from "node:url";
159179
const table = await readFile(fileURLToPath(import.meta.resolve("./table.txt")), "utf-8");
160180
```
161181
162-
Whereas `.js`, `.ts`, `.py`, `.R`, `.rs`, `.go`, and `.sh` data loaders are run via interpreters, `.exe` data loaders are run directly and must have the executable bit set. This is typically done via [`chmod`](https://en.wikipedia.org/wiki/Chmod). For example:
182+
Executable (`.exe`) data loaders are run directly and must have the executable bit set. This is typically done via [`chmod`](https://en.wikipedia.org/wiki/Chmod). For example:
163183
164184
```sh
165185
chmod +x docs/quakes.csv.exe
166186
```
167187
168-
While a `.exe` data loader may be any binary executable (_e.g.,_ compiled from C), it is often convenient to specify another interpreter using a [shebang](<https://en.wikipedia.org/wiki/Shebang_(Unix)>). For example, to write a data loader in Julia:
188+
While a `.exe` data loader may be any binary executable (_e.g.,_ compiled from C), it is often convenient to specify another interpreter using a [shebang](<https://en.wikipedia.org/wiki/Shebang_(Unix)>). For example, to write a data loader in Perl:
169189
170-
```julia
171-
#!/usr/bin/env julia
190+
```perl
191+
#!/usr/bin/env perl
172192

173-
println("hello world")
193+
print("Hello World\n");
174194
```
175195
176196
If multiple requests are made concurrently for the same data loader, the data loader will only run once; each concurrent request will receive the same response.

examples/eia/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ The base map is created in the `us-states.json.js` data loader, which uses [Topo
2929

3030
### Static files
3131

32-
Several static files used in the dashboard were downloaded from the EIA Hourly Electric Grid Monitor [About page](https://www.eia.gov/electricity/gridmonitor/about) (EIA-930 data reference tables), and not created or processed in data loaders. These files contain reference information that expect to remain unchanged, including:
32+
Several static files used in the dashboard were downloaded from the EIA Hourly Electric Grid Monitor [About page](https://www.eia.gov/electricity/gridmonitor/about) (EIA-930 data reference tables), and not created or processed in data loaders. These files contain reference information that expect to remain unchanged, including:
3333

34-
- `eia-bia-reference.csv`: Reference information about balancing authority name, time zone, region, country, etc.
34+
- `eia-bia-reference.csv`: Reference information about balancing authority name, time zone, region, country, _etc._
3535
- `eia-connections-reference.csv`: Reference information about connections between balancing authorities
3636

3737
## Charts
3838

39-
The charts and map are drawn with [Observable Plot](https://observablehq.com/plot/), and saved as components in `components/charts.js` and `components/map.js` to simplify our layout code in `index.md`.
39+
The charts and map are drawn with [Observable Plot](https://observablehq.com/plot/), and saved as components in `components/charts.js` and `components/map.js` to simplify our layout code in `index.md`.
4040

4141
## Thanks
4242

examples/plot/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ The [`npm-downloads.csv.ts`](./docs/data/npm-downloads.csv.ts) loader retrieves
2828

2929
## Big numbers
3030

31-
Key performance indicators are displayed as “big numbers” with, in some cases, a trend indicating growth over one week. Their layout is using the convenience CSS classes _big_, _red_ etc.
31+
Key performance indicators are displayed as “big numbers” with, in some cases, a trend indicating growth over one week. Their layout is using the convenience CSS classes _big_, _red_ _etc._
3232

3333
## Charts
3434

src/build.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {existsSync} from "node:fs";
33
import {access, constants, copyFile, readFile, writeFile} from "node:fs/promises";
44
import {basename, dirname, extname, join} from "node:path/posix";
55
import type {Config} from "./config.js";
6-
import {Loader} from "./dataloader.js";
76
import {CliError, isEnoent} from "./error.js";
87
import {getClientPath, prepareOutput, visitMarkdownFiles} from "./files.js";
98
import {getModuleHash} from "./javascript/module.js";
@@ -16,7 +15,7 @@ import {isPathImport, relativePath, resolvePath} from "./path.js";
1615
import {renderPage} from "./render.js";
1716
import type {Resolvers} from "./resolvers.js";
1817
import {getModuleResolver, getResolvers} from "./resolvers.js";
19-
import {resolveFilePath, resolveImportPath, resolveStylesheetPath} from "./resolvers.js";
18+
import {resolveImportPath, resolveStylesheetPath} from "./resolvers.js";
2019
import {bundleStyles, rollupClient} from "./rollup.js";
2120
import {searchIndex} from "./search.js";
2221
import {Telemetry} from "./telemetry.js";
@@ -48,7 +47,7 @@ export async function build(
4847
{config, addPublic = true}: BuildOptions,
4948
effects: BuildEffects = new FileBuildEffects(config.output)
5049
): Promise<void> {
51-
const {root} = config;
50+
const {root, loaders} = config;
5251
Telemetry.record({event: "build", step: "start"});
5352

5453
// Make sure all files are readable before starting to write output files.
@@ -78,7 +77,7 @@ export async function build(
7877
effects.logger.log(faint("(skipped)"));
7978
continue;
8079
}
81-
const resolvers = await getResolvers(page, {root, path: sourceFile});
80+
const resolvers = await getResolvers(page, {root, path: sourceFile, loaders});
8281
const elapsed = Math.floor(performance.now() - start);
8382
for (const f of resolvers.assets) files.add(resolvePath(sourceFile, f));
8483
for (const f of resolvers.files) files.add(resolvePath(sourceFile, f));
@@ -151,7 +150,7 @@ export async function build(
151150
for (const file of files) {
152151
let sourcePath = join(root, file);
153152
if (!existsSync(sourcePath)) {
154-
const loader = Loader.find(root, join("/", file), {useStale: true});
153+
const loader = loaders.find(join("/", file), {useStale: true});
155154
if (!loader) {
156155
effects.logger.error("missing referenced file", sourcePath);
157156
continue;
@@ -168,7 +167,7 @@ export async function build(
168167
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
169168
const ext = extname(file);
170169
const alias = `/${join("_file", dirname(file), `${basename(file, ext)}.${hash}${ext}`)}`;
171-
aliases.set(resolveFilePath(root, file), alias);
170+
aliases.set(loaders.resolveFilePath(file), alias);
172171
await effects.writeFile(alias, contents);
173172
}
174173

src/config.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {basename, dirname, join} from "node:path/posix";
55
import {cwd} from "node:process";
66
import {pathToFileURL} from "node:url";
77
import type MarkdownIt from "markdown-it";
8+
import {LoaderResolver} from "./dataloader.js";
89
import {visitMarkdownFiles} from "./files.js";
910
import {formatIsoDate, formatLocaleDate} from "./format.js";
1011
import {createMarkdownIt, parseMarkdown} from "./markdown.js";
@@ -54,6 +55,7 @@ export interface Config {
5455
deploy: null | {workspace: string; project: string};
5556
search: boolean; // default to false
5657
md: MarkdownIt;
58+
loaders: LoaderResolver;
5759
}
5860

5961
/**
@@ -115,7 +117,8 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
115117
header = "",
116118
footer = `Built with <a href="https://observablehq.com/" target="_blank">Observable</a> on <a title="${formatIsoDate(
117119
currentDate
118-
)}">${formatLocaleDate(currentDate)}</a>.`
120+
)}">${formatLocaleDate(currentDate)}</a>.`,
121+
interpreters
119122
} = spec;
120123
root = String(root);
121124
output = String(output);
@@ -136,6 +139,7 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
136139
toc = normalizeToc(toc);
137140
deploy = deploy ? {workspace: String(deploy.workspace).replace(/^@+/, ""), project: String(deploy.project)} : null;
138141
search = Boolean(search);
142+
interpreters = normalizeInterpreters(interpreters);
139143
return {
140144
root,
141145
output,
@@ -152,7 +156,8 @@ export async function normalizeConfig(spec: any = {}, defaultRoot = "docs"): Pro
152156
style,
153157
deploy,
154158
search,
155-
md
159+
md,
160+
loaders: new LoaderResolver({root, interpreters})
156161
};
157162
}
158163

@@ -196,6 +201,14 @@ function normalizePage(spec: any): Page {
196201
return {name, path};
197202
}
198203

204+
function normalizeInterpreters(spec: any): Record<string, string[] | null> {
205+
return Object.fromEntries(
206+
Object.entries<any>(spec ?? {}).map(([key, value]): [string, string[] | null] => {
207+
return [String(key), value == null ? null : Array.from(value, String)];
208+
})
209+
);
210+
}
211+
199212
function normalizeToc(spec: any): TableOfContents {
200213
if (typeof spec === "boolean") spec = {show: spec};
201214
let {label = "Contents", show = true} = spec;

0 commit comments

Comments
 (0)