Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
186e38a
parameterized data loaders
mbostock Jul 17, 2024
bb9d1f9
Merge branch 'main' into mbostock/parameterized
mbostock Jul 25, 2024
9489af8
parameterized routing
mbostock Jul 25, 2024
91ea3ca
tweak
mbostock Jul 25, 2024
fd2fe6e
parameterized pages
mbostock Jul 25, 2024
d91d52f
remove unused import
mbostock Jul 25, 2024
bf09710
more tests
mbostock Jul 25, 2024
fec3c23
Merge branch 'main' into mbostock/parameterized
mbostock Jul 25, 2024
7072fad
more parameter resolution
mbostock Jul 25, 2024
7b1a6d3
maybe fix build
mbostock Jul 25, 2024
3b36363
fix tests?
mbostock Jul 26, 2024
e13dd92
parameterized imports
mbostock Jul 26, 2024
8ee8219
observable.params
mbostock Jul 26, 2024
822de56
a bit safer
mbostock Jul 26, 2024
026414a
more tests
mbostock Jul 26, 2024
5325dda
add todo
mbostock Jul 26, 2024
efded32
fix path confusion
mbostock Jul 26, 2024
d058d42
add todo
mbostock Jul 26, 2024
756a1f0
Merge branch 'main' into mbostock/parameterized
mbostock Aug 3, 2024
c1fa840
transpile parameters
mbostock Aug 3, 2024
5b40739
parameterized code blocks
mbostock Aug 3, 2024
40fff49
type Params
mbostock Aug 4, 2024
66d40d2
tweaks
mbostock Aug 4, 2024
1ea4a5d
more robust replacement
mbostock Aug 4, 2024
7dde70b
binary expressions, too
mbostock Aug 4, 2024
08ef5e9
Merge branch 'main' into mbostock/parameterized
mbostock Aug 10, 2024
c4d6a6e
observable.params["param"]
mbostock Aug 10, 2024
8be55cd
Merge branch 'main' into mbostock/parameterized
mbostock Aug 12, 2024
772913c
handle observable shadowing
mbostock Aug 12, 2024
bbca4ee
DRY routing; more tests
mbostock Aug 12, 2024
a5731c3
consolidate extractor code
mbostock Aug 12, 2024
b8ec048
consolidate data loader routing
mbostock Aug 12, 2024
fd50dec
consolidate page routing
mbostock Aug 12, 2024
b55382b
remove obsolete comment
mbostock Aug 12, 2024
e43ab49
remove obsolete comment
mbostock Aug 12, 2024
877702e
comment
mbostock Aug 12, 2024
055eac3
Merge branch 'main' into mbostock/parameterized
mbostock Aug 18, 2024
12bac60
parameterized archives
mbostock Aug 14, 2024
ec4dc52
don’t allow empty parameter names
mbostock Aug 18, 2024
7cf3d54
don’t allow empty parameter values
mbostock Aug 18, 2024
15b957c
build parameterized pages!
mbostock Aug 19, 2024
0302c39
initial tests
mbostock Aug 19, 2024
be75109
Merge branch 'main' into mbostock/parameterized
mbostock Aug 24, 2024
c323282
docs
mbostock Aug 24, 2024
cdef02c
more docs
mbostock Aug 24, 2024
3f949b5
page loaders
mbostock Aug 25, 2024
44751b6
remove generate-themes script
mbostock Aug 25, 2024
39a58d9
remove obsolete comment
mbostock Aug 25, 2024
6139567
fix watchFiles path
mbostock Aug 25, 2024
d6f3d56
illegal file attachment example
mbostock Aug 26, 2024
cd5df5e
Merge branch 'main' into mbostock/parameterized
mbostock Aug 26, 2024
86df938
Merge branch 'mbostock/parameterized' into mbostock/page-loader
mbostock Aug 26, 2024
bdc332c
error if empty extension
mbostock Aug 26, 2024
a348321
Update src/javascript/params.ts
mbostock Aug 26, 2024
4bf479d
skip parameterized paths
mbostock Aug 26, 2024
1aadd2b
mv /loaders /data-loaders; add /page-loaders
mbostock Aug 26, 2024
23e5ab1
parseArgs style
mbostock Aug 26, 2024
c9d8d7a
minimize churn
mbostock Aug 26, 2024
fc20616
mv dataloader.ts loader.ts
mbostock Aug 26, 2024
8050bcb
partial and multiple parameters
mbostock Aug 26, 2024
392ed29
force millisecond precision
mbostock Aug 26, 2024
481f16c
test if utimes is working
mbostock Aug 26, 2024
51acadc
clear file info cache after utimes
mbostock Aug 26, 2024
74edd24
remove unused import
mbostock Aug 26, 2024
a5e8bf3
dynamicPaths; fix search
mbostock Aug 27, 2024
582d8a8
test page loaders
mbostock Aug 27, 2024
1e799af
more docs
mbostock Aug 27, 2024
8e80da3
remove unused sourcePath
mbostock Aug 27, 2024
ef08bd2
enable test coverage
mbostock Aug 27, 2024
e6471e7
importable page loader tip
mbostock Aug 27, 2024
137f2c3
Merge branch 'main' into mbostock/parameterized
mbostock Aug 27, 2024
3ea7de5
allow iterables, too
mbostock Aug 27, 2024
9382c2a
dynamicPaths as iterable docs
mbostock Aug 27, 2024
7b9b562
page-loaders.md.js; delete test pages
mbostock Aug 27, 2024
1dd5b57
more docs
mbostock Aug 27, 2024
ab4bc6a
Merge branch 'main' into mbostock/parameterized
mbostock Aug 27, 2024
6053441
Merge branch 'main' into mbostock/parameterized
mbostock Aug 27, 2024
dc324d1
dynamicPaths includes page loaders
mbostock Aug 27, 2024
5cd1a60
dynamicPaths docs
mbostock Aug 27, 2024
e6e3600
fix non-ASCII redirect
mbostock Aug 27, 2024
9277efe
more docs
mbostock Aug 27, 2024
456134a
consolidate loading logic
mbostock Aug 28, 2024
e819ebf
ExtractorOptions
mbostock Aug 28, 2024
6c11ff4
more docs
mbostock Aug 28, 2024
769d620
enoent(path)
mbostock Aug 28, 2024
947a5b4
glob is a dependency
mbostock Aug 29, 2024
68af27a
terminate socket on page loader error
mbostock Aug 29, 2024
6164640
don’t pass value as separate arg
mbostock Aug 29, 2024
c3c01ff
Update docs/page-loaders.md.js
mbostock Aug 29, 2024
83202ce
clarify which JavaScript
mbostock Aug 29, 2024
d5d9069
Update docs/page-loaders.md.js
mbostock Aug 29, 2024
eb4bb90
move docs around
mbostock Aug 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/[dir]/[file].json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {parseArgs} from "node:util";

const {values} = parseArgs({
options: {
dir: {
type: "string"
},
file: {
type: "string"
}
}
});

console.log(JSON.stringify(values));
1 change: 1 addition & 0 deletions docs/[dir]/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = observable.params.dir;
11 changes: 11 additions & 0 deletions docs/[dir]/foo.json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {parseArgs} from "node:util";

const {values} = parseArgs({
options: {
dir: {
type: "string"
}
}
});

console.log(JSON.stringify(values));
1 change: 1 addition & 0 deletions docs/[dir]/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
31 changes: 31 additions & 0 deletions docs/[dir]/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Hello dynamic route

is it working

```js
display(await FileAttachment("./foo.txt").text());
```

```js
display(await FileAttachment("./foo.json").json());
```

```js
display(await FileAttachment("./file.json").json());
```

```js
import {foo} from "./foo.js";

display(foo);
```

```js
display(`${observable.params.dir}.json`);
```

```js
display(await FileAttachment("./" + observable.params.dir + ".json").json());
```

<img src="./w3c.png">
Binary file added docs/[dir]/w3c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions docs/data/[test].json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {parseArgs} from "node:util";

const {values} = parseArgs({
options: {
test: {
type: "string"
}
}
});

console.log(JSON.stringify(values));
3 changes: 3 additions & 0 deletions docs/foo/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {FileAttachment} from "npm:@observablehq/stdlib";

export const data = FileAttachment("./data.txt");
1 change: 1 addition & 0 deletions docs/foo/[part].js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const part = observable.params.part;
1 change: 1 addition & 0 deletions docs/foo/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data
10 changes: 10 additions & 0 deletions docs/foo/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
```js
import {data} from "./Component.js";

display(data);
```
```js
import {part} from "./test2.js";

display(part);
```
21 changes: 11 additions & 10 deletions src/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {createHash} from "node:crypto";
import {existsSync} from "node:fs";
import {access, constants, copyFile, readFile, stat, writeFile} from "node:fs/promises";
import {basename, dirname, extname, join} from "node:path/posix";
import type {Config} from "./config.js";
Expand Down Expand Up @@ -152,21 +151,23 @@ export async function build(

// Copy over the referenced files, accumulating hashed aliases.
for (const file of files) {
let sourcePath = join(root, file);
effects.output.write(`${faint("copy")} ${sourcePath} ${faint("→")} `);
if (!existsSync(sourcePath)) {
const loader = loaders.find(join("/", file), {useStale: true});
if (!loader) {
effects.logger.error(red("error: missing referenced file"));
continue;
}
let sourcePath: string;
effects.output.write(`${faint("copy")} ${join(root, file)} ${faint("→")} `);
const loader = loaders.find(join("/", file), {useStale: true});
if (!loader) {
effects.logger.error(red("error: missing referenced file"));
continue;
}
if ("load" in loader) {
try {
sourcePath = join(root, await loader.load(effects));
} catch (error) {
if (!isEnoent(error)) throw error;
effects.logger.error(red("error: missing referenced file"));
continue;
}
} else {
sourcePath = loader.path;
}
const contents = await readFile(sourcePath);
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
Expand Down Expand Up @@ -205,7 +206,7 @@ export async function build(
const resolveImport = getModuleResolver(root, path);
let input: string;
try {
input = await readJavaScript(sourcePath);
input = await readJavaScript(root, path);
} catch (error) {
if (!isEnoent(error)) throw error;
effects.logger.error(red("error: missing referenced import"));
Expand Down
112 changes: 88 additions & 24 deletions src/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import {createHash} from "node:crypto";
import type {WriteStream} from "node:fs";
import {createReadStream, existsSync, statSync} from "node:fs";
import {open, readFile, rename, unlink} from "node:fs/promises";
import {dirname, extname, join, relative} from "node:path/posix";
import {basename, dirname, extname, join, relative} from "node:path/posix";
import {createGunzip} from "node:zlib";
import {spawn} from "cross-spawn";
import {globSync} from "glob";
import JSZip from "jszip";
import {extract} from "tar-stream";
import {maybeStat, prepareOutput} from "./files.js";
Expand Down Expand Up @@ -68,9 +69,9 @@ export class LoaderResolver {
* abort if we find a matching folder or reach the source root; for example,
* if src/data exists, we won’t look for a src/data.zip.
*/
find(targetPath: string, {useStale = false} = {}): Loader | undefined {
const exact = this.findExact(targetPath, {useStale});
if (exact) return exact;
find(targetPath: string, {useStale = false} = {}): Asset | Loader | undefined {
const result = this.findExact(targetPath, {useStale}) ?? this.findDynamic(targetPath, {useStale});
if (result) return result;
let dir = dirname(targetPath);
for (let parent: string; true; dir = parent) {
parent = dirname(dir);
Expand All @@ -80,31 +81,35 @@ export class LoaderResolver {
}
for (const [ext, Extractor] of extractors) {
const archive = dir + ext;
if (existsSync(join(this.root, archive))) {
return new Extractor({
preload: async () => archive,
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: join(this.root, archive),
root: this.root,
targetPath,
useStale
});
}
const archiveLoader = this.findExact(archive, {useStale});
const archiveLoader = this.findExact(archive, {useStale}) ?? this.findDynamic(archive, {useStale});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more correct to compute the Cartesian cross-product of archive extensions (.zip, .tar, etc.) and interpreter extensions (.js, .py, etc.) here so that a more-specific parameterized match takes precedence over a less-specific parameterized match with a different archive extension. For example, you’d want /foo/bar.tar to take precedence over /[param]/bar.zip.

Although I think this is actually already broken because crawling the parent folders currently requires an exact match in order to look for an archive/extractor; so parameterized archives probably don’t work as intended. We need use glob to match [param] directory names. This will require further investigation. There are a lot of combinations of dynamic routing to consider! 😓

if (archiveLoader) {
return new Extractor({
preload: async (options) => archiveLoader.load(options),
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: archiveLoader.path,
root: this.root,
targetPath,
useStale
});
if ("load" in archiveLoader) {
// archive.zip.js
return new Extractor({
preload: async (options) => archiveLoader.load(options),
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: archiveLoader.path,
root: this.root,
targetPath,
useStale
});
} else {
// archive.zip
return new Extractor({
preload: async () => relative(this.root, archiveLoader.path),
inflatePath: targetPath.slice(archive.length - ext.length + 1),
path: archiveLoader.path,
root: this.root,
targetPath,
useStale
});
}
}
}
}

private findExact(targetPath: string, {useStale}): Loader | undefined {
private findExact(targetPath: string, {useStale}): Asset | Loader | undefined {
if (existsSync(join(this.root, targetPath))) return {path: join(this.root, targetPath)};
for (const [ext, [command, ...args]] of this.interpreters) {
if (!existsSync(join(this.root, targetPath + ext))) continue;
if (extname(targetPath) === "") {
Expand All @@ -123,6 +128,59 @@ export class LoaderResolver {
}
}

private findDynamic(targetPath: string, {useStale}): Asset | Loader | undefined {
const found = this.findDynamicParams(".", join(".", targetPath).split("/"));
if (!found) return;
const {path, params, ext} = found;
if (!ext) return {path: join(this.root, path)};
const [command, ...args] = this.interpreters.get(ext)!;
if (command != null) args.push(join(this.root, path));
return new CommandLoader({
command: command ?? path,
args: args.concat(params),
path: join(this.root, path),
root: this.root,
targetPath,
useStale
});
}

/**
* Finds a parameterized data loader (dynamic route) recursively, such that
* the most specific match is returned.
*/
private findDynamicParams(cwd: string, parts: string[]): {path: string; params: string[]; ext?: string} | undefined {
switch (parts.length) {
case 0:
return;
case 1: {
const [first] = parts;
if (existsSync(join(this.root, cwd, first))) return {path: join(cwd, first), params: []};
const ext1 = extname(first);
for (const ext of this.interpreters.keys()) {
const ext2 = `${ext1}${ext}`;
if (existsSync(join(this.root, cwd, first + ext))) return {path: join(cwd, first + ext), params: [], ext};
for (const file of globSync(`\\[*\\]${ext2}`, {cwd: join(this.root, cwd)})) {
const params = [`--${basename(file, ext2).slice(1, -1)}`, basename(first, ext1)];
return {path: join(cwd, file), params, ext};
}
}
return;
}
default: {
const [first, ...rest] = parts;
if (existsSync(join(this.root, cwd, first))) {
const found = this.findDynamicParams(join(cwd, first), rest);
if (found) return found;
}
for (const dir of globSync("\\[*\\]", {cwd: join(this.root, cwd)})) {
const found = this.findDynamicParams(join(cwd, dir), rest);
if (found) return {...found, params: found.params.concat(`--${dir.slice(1, -1)}`, first)};
}
}
}
}

getWatchPath(path: string): string | undefined {
const exactPath = join(this.root, path);
if (existsSync(exactPath)) return exactPath;
Expand Down Expand Up @@ -193,6 +251,12 @@ export class LoaderResolver {
}
}

/** Used by LoaderResolver.find to represent a static file resolution. */
export interface Asset {
/** The path to the file relative to the current working directory. */
path: string;
}

export abstract class Loader {
/**
* The source root relative to the current working directory, such as src.
Expand Down
1 change: 1 addition & 0 deletions src/javascript/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const defaultGlobals = new Set([
"Number",
"navigator",
"Object",
"observable",
"parseFloat",
"parseInt",
"performance",
Expand Down
Loading