Skip to content

Commit 87e7cf5

Browse files
committed
chaining data loaders in preview
1 parent ef19d93 commit 87e7cf5

File tree

2 files changed

+44
-26
lines changed

2 files changed

+44
-26
lines changed

src/dataloader.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,10 @@ class CommandLoader extends Loader {
317317
}
318318

319319
async exec(output: WriteStream): Promise<void> {
320-
const subprocess = spawn(this.command, this.args, {windowsHide: true, stdio: ["ignore", output, "inherit"]});
320+
const address = process.env.OBSERVABLEHQ_ADDRESS;
321+
if (address == null) throw new Error("chained data loaders are not implemented in this context");
322+
const env = {...process.env, SERVER: `${address}_chain${this.targetPath}::`};
323+
const subprocess = spawn(this.command, this.args, {windowsHide: true, stdio: ["ignore", output, "inherit"], env});
321324
const code = await new Promise((resolve, reject) => {
322325
subprocess.on("error", reject);
323326
subprocess.on("close", resolve);

src/preview.ts

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,14 @@ export class PreviewServer {
9595
} else {
9696
await new Promise<void>((resolve) => server.listen(port, hostname, resolve));
9797
}
98-
const url = `http://${hostname}:${port}/`;
98+
const address = `http://${hostname}:${port}/`;
9999
if (verbose) {
100100
console.log(`${green(bold("Observable Framework"))} ${faint(`v${process.env.npm_package_version}`)}`);
101-
console.log(`${faint("↳")} ${link(url)}`);
101+
console.log(`${faint("↳")} ${link(address)}`);
102102
console.log("");
103103
}
104-
if (open) openBrowser(url);
104+
if (open) openBrowser(address);
105+
process.env.OBSERVABLEHQ_ADDRESS = address; // global!
105106
return new PreviewServer({server, verbose, ...options});
106107
}
107108

@@ -113,6 +114,7 @@ export class PreviewServer {
113114
const config = await this._readConfig();
114115
const {root, loaders} = config;
115116
if (this._verbose) console.log(faint(req.method!), req.url);
117+
let machine = false; // machine queries don't get a full 404 error page
116118
try {
117119
const url = new URL(req.url!, "http://localhost");
118120
let pathname = decodeURI(url.pathname);
@@ -152,27 +154,18 @@ export class PreviewServer {
152154
if (!isEnoent(error)) throw error;
153155
}
154156
throw new HttpError(`Not found: ${pathname}`, 404);
157+
} else if (pathname.startsWith("/_chain/")) {
158+
machine = true;
159+
const [caller, path] = pathname.slice("/_chain".length).split("::");
160+
// now any file that watches caller should also watch path
161+
console.warn("chained data loader", {caller, path, loaders});
162+
const file = await getFile(path, root, loaders);
163+
if (file !== undefined) return void send(req, file, {root}).pipe(res);
164+
throw new HttpError(`Not found: ${pathname}`, 404);
155165
} else if (pathname.startsWith("/_file/")) {
156166
const path = pathname.slice("/_file".length);
157-
const filepath = join(root, path);
158-
try {
159-
await access(filepath, constants.R_OK);
160-
send(req, pathname.slice("/_file".length), {root}).pipe(res);
161-
return;
162-
} catch (error) {
163-
if (!isEnoent(error)) throw error;
164-
}
165-
166-
// Look for a data loader for this file.
167-
const loader = loaders.find(path);
168-
if (loader) {
169-
try {
170-
send(req, await loader.load(), {root}).pipe(res);
171-
return;
172-
} catch (error) {
173-
if (!isEnoent(error)) throw error;
174-
}
175-
}
167+
const file = await getFile(path, root, loaders);
168+
if (file !== undefined) return void send(req, file, {root}).pipe(res);
176169
throw new HttpError(`Not found: ${pathname}`, 404);
177170
} else {
178171
if ((pathname = normalize(pathname)).startsWith("..")) throw new Error("Invalid path: " + pathname);
@@ -212,7 +205,7 @@ export class PreviewServer {
212205
res.statusCode = 500;
213206
console.error(error);
214207
}
215-
if (req.method === "GET" && res.statusCode === 404) {
208+
if (req.method === "GET" && res.statusCode === 404 && !machine) {
216209
try {
217210
const options = {...config, path: "/404", preview: true};
218211
const source = await readFile(join(root, "404.md"), "utf8");
@@ -224,8 +217,10 @@ export class PreviewServer {
224217
// ignore secondary error (e.g., no 404.md); show the original 404
225218
}
226219
}
227-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
228-
res.end(error instanceof Error ? error.message : "Oops, an error occurred");
220+
if (!machine) {
221+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
222+
res.end(error instanceof Error ? error.message : "Oops, an error occurred");
223+
} else res.end();
229224
}
230225
};
231226

@@ -242,6 +237,26 @@ export class PreviewServer {
242237
}
243238
}
244239

240+
async function getFile(path: string, root: string, loaders: LoaderResolver): Promise<string | undefined> {
241+
const filepath = join(root, path);
242+
try {
243+
await access(filepath, constants.R_OK);
244+
return path;
245+
} catch (error) {
246+
if (!isEnoent(error)) throw error;
247+
}
248+
249+
// Look for a data loader for this file.
250+
const loader = loaders.find(path);
251+
if (loader) {
252+
try {
253+
return await loader.load();
254+
} catch (error) {
255+
if (!isEnoent(error)) throw error;
256+
}
257+
}
258+
}
259+
245260
// Like send, but for in-memory dynamic content.
246261
function end(req: IncomingMessage, res: ServerResponse, content: string, type: string): void {
247262
const etag = `"${createHash("sha256").update(content).digest("base64")}"`;

0 commit comments

Comments
 (0)