Skip to content

Commit 4d1231a

Browse files
committed
pass the address in the effects; implement a temporary server for building
1 parent 87e7cf5 commit 4d1231a

File tree

4 files changed

+51
-35
lines changed

4 files changed

+51
-35
lines changed

src/build.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {parseMarkdown} from "./markdown.js";
1313
import {extractNodeSpecifier} from "./node.js";
1414
import {extractNpmSpecifier, populateNpmCache, resolveNpmImport} from "./npm.js";
1515
import {isAssetPath, isPathImport, relativePath, resolvePath} from "./path.js";
16+
import {preview} from "./preview.js";
1617
import {renderPage} from "./render.js";
1718
import type {Resolvers} from "./resolvers.js";
1819
import {getModuleResolver, getResolvers} from "./resolvers.js";
@@ -150,6 +151,12 @@ export async function build(
150151
}
151152
}
152153

154+
// Launch a server for chained data loaders. TODO configure host & port?
155+
const {server} = await preview({root, verbose: false, hostname: "127.0.0.1"});
156+
const a = server.address();
157+
if (!a || typeof a !== "object") throw new Error("Couldn't launch server for chained data loaders!");
158+
const address = `http://${a.address}:${a.port}/`;
159+
153160
// Copy over the referenced files, accumulating hashed aliases.
154161
for (const file of files) {
155162
let sourcePath = join(root, file);
@@ -161,7 +168,7 @@ export async function build(
161168
continue;
162169
}
163170
try {
164-
sourcePath = join(root, await loader.load(effects));
171+
sourcePath = join(root, await loader.load({...effects, address}));
165172
} catch (error) {
166173
if (!isEnoent(error)) throw error;
167174
effects.logger.error(red("error: missing referenced file"));
@@ -176,6 +183,9 @@ export async function build(
176183
await effects.writeFile(alias, contents);
177184
}
178185

186+
// TODO: server.close() might be enough?
187+
await new Promise((closed) => server.close(closed));
188+
179189
// Download npm imports. TODO It might be nice to use content hashes for
180190
// these, too, but it would involve rewriting the files since populateNpmCache
181191
// doesn’t let you pass in a resolver.

src/dataloader.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,9 @@ export const defaultInterpreters: Record<string, string[]> = {
3434
export interface LoadEffects {
3535
logger: Logger;
3636
output: Writer;
37+
address?: string;
3738
}
3839

39-
const defaultEffects: LoadEffects = {
40-
logger: console,
41-
output: process.stdout
42-
};
43-
4440
export interface LoaderOptions {
4541
root: string;
4642
path: string;
@@ -230,7 +226,7 @@ export abstract class Loader {
230226
* to the source root; this is within the .observablehq/cache folder within
231227
* the source root.
232228
*/
233-
async load(effects = defaultEffects): Promise<string> {
229+
async load(effects): Promise<string> {
234230
const key = join(this.root, this.targetPath);
235231
let command = runningCommands.get(key);
236232
if (!command) {
@@ -287,7 +283,7 @@ export abstract class Loader {
287283
return command;
288284
}
289285

290-
abstract exec(output: WriteStream, effects?: LoadEffects): Promise<void>;
286+
abstract exec(output: WriteStream, effects: LoadEffects): Promise<void>;
291287
}
292288

293289
interface CommandLoaderOptions extends LoaderOptions {
@@ -316,10 +312,9 @@ class CommandLoader extends Loader {
316312
this.args = args;
317313
}
318314

319-
async exec(output: WriteStream): Promise<void> {
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}::`};
315+
async exec(output: WriteStream, effects: LoadEffects): Promise<void> {
316+
if (effects.address == null) throw new Error("chained data loaders are not implemented in this context");
317+
const env = {...process.env, SERVER: `${effects.address}_chain${this.targetPath}::`};
323318
const subprocess = spawn(this.command, this.args, {windowsHide: true, stdio: ["ignore", output, "inherit"], env});
324319
const code = await new Promise((resolve, reject) => {
325320
subprocess.on("error", reject);
@@ -346,7 +341,7 @@ class ZipExtractor extends Loader {
346341
this.inflatePath = inflatePath;
347342
}
348343

349-
async exec(output: WriteStream, effects?: LoadEffects): Promise<void> {
344+
async exec(output: WriteStream, effects: LoadEffects): Promise<void> {
350345
const archivePath = join(this.root, await this.preload(effects));
351346
const file = (await JSZip.loadAsync(await readFile(archivePath))).file(this.inflatePath);
352347
if (!file) throw Object.assign(new Error("file not found"), {code: "ENOENT"});
@@ -373,7 +368,7 @@ class TarExtractor extends Loader {
373368
this.gunzip = gunzip;
374369
}
375370

376-
async exec(output: WriteStream, effects?: LoadEffects): Promise<void> {
371+
async exec(output: WriteStream, effects: LoadEffects): Promise<void> {
377372
const archivePath = join(this.root, await this.preload(effects));
378373
const tar = extract();
379374
const input = createReadStream(archivePath);

src/preview.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {WebSocketServer} from "ws";
1717
import type {Config} from "./config.js";
1818
import {readConfig} from "./config.js";
1919
import type {LoaderResolver} from "./dataloader.js";
20+
import type {LoadEffects} from "./dataloader.js";
2021
import {HttpError, isEnoent, isHttpError, isSystemError} from "./error.js";
2122
import {getClientPath} from "./files.js";
2223
import type {FileWatchers} from "./fileWatchers.js";
@@ -54,17 +55,20 @@ export class PreviewServer {
5455
private readonly _server: ReturnType<typeof createServer>;
5556
private readonly _socketServer: WebSocketServer;
5657
private readonly _verbose: boolean;
58+
private readonly _effects: LoadEffects;
5759

5860
private constructor({
5961
config,
6062
root,
6163
server,
62-
verbose
64+
verbose,
65+
effects
6366
}: {
6467
config?: string;
6568
root?: string;
6669
server: Server;
6770
verbose: boolean;
71+
effects: LoadEffects;
6872
}) {
6973
this._config = config;
7074
this._root = root;
@@ -73,6 +77,7 @@ export class PreviewServer {
7377
this._server.on("request", this._handleRequest);
7478
this._socketServer = new WebSocketServer({server: this._server});
7579
this._socketServer.on("connection", this._handleConnection);
80+
this._effects = effects;
7681
}
7782

7883
static async start({verbose = true, hostname, port, open, ...options}: PreviewOptions) {
@@ -102,8 +107,12 @@ export class PreviewServer {
102107
console.log("");
103108
}
104109
if (open) openBrowser(address);
105-
process.env.OBSERVABLEHQ_ADDRESS = address; // global!
106-
return new PreviewServer({server, verbose, ...options});
110+
const effects: LoadEffects = {
111+
logger: console,
112+
output: process.stdout,
113+
address
114+
};
115+
return new PreviewServer({server, verbose, effects, ...options});
107116
}
108117

109118
async _readConfig() {
@@ -159,12 +168,12 @@ export class PreviewServer {
159168
const [caller, path] = pathname.slice("/_chain".length).split("::");
160169
// now any file that watches caller should also watch path
161170
console.warn("chained data loader", {caller, path, loaders});
162-
const file = await getFile(path, root, loaders);
171+
const file = await this.getFile(path, root, loaders);
163172
if (file !== undefined) return void send(req, file, {root}).pipe(res);
164173
throw new HttpError(`Not found: ${pathname}`, 404);
165174
} else if (pathname.startsWith("/_file/")) {
166175
const path = pathname.slice("/_file".length);
167-
const file = await getFile(path, root, loaders);
176+
const file = await this.getFile(path, root, loaders);
168177
if (file !== undefined) return void send(req, file, {root}).pipe(res);
169178
throw new HttpError(`Not found: ${pathname}`, 404);
170179
} else {
@@ -235,25 +244,25 @@ export class PreviewServer {
235244
get server(): PreviewServer["_server"] {
236245
return this._server;
237246
}
238-
}
239-
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-
}
248247

249-
// Look for a data loader for this file.
250-
const loader = loaders.find(path);
251-
if (loader) {
248+
async getFile(path: string, root: string, loaders: LoaderResolver): Promise<string | undefined> {
249+
const filepath = join(root, path);
252250
try {
253-
return await loader.load();
251+
await access(filepath, constants.R_OK);
252+
return path;
254253
} catch (error) {
255254
if (!isEnoent(error)) throw error;
256255
}
256+
257+
// Look for a data loader for this file.
258+
const loader = loaders.find(path);
259+
if (loader) {
260+
try {
261+
return await loader.load(this._effects);
262+
} catch (error) {
263+
if (!isEnoent(error)) throw error;
264+
}
265+
}
257266
}
258267
}
259268

test/dataloaders-test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {LoaderResolver} from "../src/dataloader.js";
66

77
const noopEffects: LoadEffects = {
88
logger: {log() {}, warn() {}, error() {}},
9-
output: {write() {}}
9+
output: {write() {}},
10+
address: "not implemented"
1011
};
1112

1213
describe("LoaderResolver.find(path)", () => {
@@ -55,7 +56,8 @@ describe("LoaderResolver.find(path, {useStale: true})", () => {
5556
write(a) {
5657
out.push(a);
5758
}
58-
}
59+
},
60+
address: "not implemented"
5961
};
6062
const loader = loaders.find("dataloaders/data1.txt")!;
6163
// save the loader times.

0 commit comments

Comments
 (0)