-
Notifications
You must be signed in to change notification settings - Fork 177
parameterized routing & page loaders #1523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 21 commits
186e38a
bb9d1f9
9489af8
91ea3ca
fd2fe6e
d91d52f
bf09710
fec3c23
7072fad
7b1a6d3
3b36363
e13dd92
8ee8219
822de56
026414a
5325dda
efded32
d058d42
756a1f0
c1fa840
5b40739
40fff49
66d40d2
1ea4a5d
7dde70b
08ef5e9
c4d6a6e
8be55cd
772913c
bbca4ee
a5731c3
b8ec048
fd50dec
b55382b
e43ab49
877702e
055eac3
12bac60
ec4dc52
7cf3d54
15b957c
0302c39
be75109
c323282
cdef02c
3f949b5
44751b6
39a58d9
6139567
d6f3d56
cd5df5e
86df938
bdc332c
a348321
4bf479d
1aadd2b
23e5ab1
c9d8d7a
fc20616
8050bcb
392ed29
481f16c
51acadc
74edd24
a5e8bf3
582d8a8
1e799af
8e80da3
ef08bd2
e6471e7
137f2c3
3ea7de5
9382c2a
7b9b562
1dd5b57
ab4bc6a
6053441
dc324d1
5cd1a60
e6e3600
9277efe
456134a
e819ebf
6c11ff4
769d620
947a5b4
68af27a
6164640
c3c01ff
83202ce
d5d9069
eb4bb90
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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)); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const foo = observable.params.dir; |
| 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)); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| foo |
| 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"> | ||
| 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)); |
| 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"); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const part = observable.params.part; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| data |
| 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); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"; | ||
|
|
@@ -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}); | ||
mbostock marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (result) return result; | ||
| let dir = dirname(targetPath); | ||
| for (let parent: string; true; dir = parent) { | ||
| parent = dirname(dir); | ||
|
|
@@ -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}); | ||
|
||
| 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) === "") { | ||
|
|
@@ -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; | ||
|
|
@@ -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. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.