Skip to content

Commit

Permalink
build & deploy client bundles
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Dec 4, 2023
1 parent 7afd8c7 commit ed091b1
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 25 deletions.
26 changes: 20 additions & 6 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ import {resolvePath} from "./url.js";

const EXTRA_FILES = new Map([["node_modules/@observablehq/runtime/dist/runtime.js", "_observablehq/runtime.js"]]);

// TODO Remove library helpers (e.g., duckdb) when they are published to npm.
const CLIENT_BUNDLES: [entry: string, name: string][] = [
["./src/client/index.js", "client.js"],
["./src/client/stdlib.js", "stdlib.js"],
["./src/client/stdlib/dot.js", "stdlib/dot.js"],
["./src/client/stdlib/duckdb.js", "stdlib/duckdb.js"],
["./src/client/stdlib/mermaid.js", "stdlib/mermaid.js"],
["./src/client/stdlib/sqlite.js", "stdlib/sqlite.js"],
["./src/client/stdlib/tex.js", "stdlib/tex.js"],
["./src/client/stdlib/xslx.js", "stdlib/xslx.js"]
];

export interface BuildOptions {
sourceRoot: string;
outputRoot?: string;
Expand Down Expand Up @@ -65,12 +77,14 @@ export async function build(
}

if (addPublic) {
// Generate the client bundle.
const clientPath = getClientPath();
const outputPath = join("_observablehq", "client.js");
effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `);
const code = await rollupClient(clientPath, {minify: true});
await effects.writeFile(outputPath, code);
// Generate the client bundles.
for (const [entry, name] of CLIENT_BUNDLES) {
const clientPath = getClientPath(entry);
const outputPath = join("_observablehq", name);
effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `);
const code = await rollupClient(clientPath, {minify: true});
await effects.writeFile(outputPath, code);
}
// Copy over the public directory.
const publicRoot = relative(cwd(), join(dirname(fileURLToPath(import.meta.url)), "..", "public"));
for await (const publicFile of visitFiles(publicRoot)) {
Expand Down
4 changes: 2 additions & 2 deletions src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {rollup} from "rollup";
import esbuild from "rollup-plugin-esbuild";
import {relativeUrl} from "./url.js";

export async function rollupClient(clientPath = getClientPath(), {minify = false} = {}): Promise<string> {
export async function rollupClient(clientPath: string, {minify = false} = {}): Promise<string> {
const bundle = await rollup({
input: clientPath,
external: [/^https:/],
Expand Down Expand Up @@ -37,6 +37,6 @@ function resolveImport(source: string, specifier: string | AstNode): ResolveIdRe
: null;
}

export function getClientPath(entry = "./src/client/index.js"): string {
export function getClientPath(entry: string): string {
return relative(cwd(), join(dirname(fileURLToPath(import.meta.url)), "..", entry));
}
37 changes: 25 additions & 12 deletions test/deploy-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,24 @@ import type {Logger} from "../src/logger.js";
import {commandRequiresAuthenticationMessage} from "../src/observableApiAuth.js";
import type {DeployConfig} from "../src/observableApiConfig.js";
import {MockLogger} from "./mocks/logger.js";
import {
ObservableApiMock,
invalidApiKey,
userWithTwoWorkspaces,
userWithZeroWorkspaces,
validApiKey
} from "./mocks/observableApi.js";
import {ObservableApiMock} from "./mocks/observableApi.js";
import {invalidApiKey, userWithTwoWorkspaces, userWithZeroWorkspaces, validApiKey} from "./mocks/observableApi.js";

// These files are implicitly generated by the CLI. This may change over time,
// so they’re enumerated here for clarity. TODO We should enforce that these
// files are specifically uploaded, rather than just the number of files.
const EXTRA_FILES: string[] = [
"_observablehq/client.js",
"_observablehq/runtime.js",
"_observablehq/stdlib.js",
"_observablehq/stdlib/dot.js",
"_observablehq/stdlib/duckdb.js",
"_observablehq/stdlib/mermaid.js",
"_observablehq/stdlib/sqlite.js",
"_observablehq/stdlib/tex.js",
"_observablehq/stdlib/xslx.js",
"_observablehq/style.css"
];

class MockDeployEffects implements DeployEffects {
public logger = new MockLogger();
Expand Down Expand Up @@ -67,7 +78,9 @@ class MockDeployEffects implements DeployEffects {
}
}

const TEST_SOURCE_ROOT = "test/example-dist";
// This test should have exactly one index.md in it, and nothing else; that one
// page is why we +1 to the number of extra files.
const TEST_SOURCE_ROOT = "test/input/build/simple-public";

describe("deploy", () => {
it("makes expected API calls for a new project", async () => {
Expand All @@ -77,7 +90,7 @@ describe("deploy", () => {
.handleGetUser()
.handlePostProject({projectId})
.handlePostDeploy({projectId, deployId})
.handlePostDeployFile({deployId, repeat: 3})
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
.handlePostDeployUploaded({deployId})
.start();

Expand All @@ -96,7 +109,7 @@ describe("deploy", () => {
const deployId = "deploy456";
const apiMock = new ObservableApiMock()
.handlePostDeploy({projectId, deployId})
.handlePostDeployFile({deployId, repeat: 3})
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
.handlePostDeployUploaded({deployId})
.start();

Expand Down Expand Up @@ -129,7 +142,7 @@ describe("deploy", () => {
.handleGetUser({user: userWithTwoWorkspaces})
.handlePostProject({projectId})
.handlePostDeploy({projectId, deployId})
.handlePostDeployFile({deployId, repeat: 3})
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
.handlePostDeployUploaded({deployId})
.start();
const effects = new MockDeployEffects();
Expand Down Expand Up @@ -232,7 +245,7 @@ describe("deploy", () => {
.handleGetUser()
.handlePostProject({projectId})
.handlePostDeploy({projectId, deployId})
.handlePostDeployFile({deployId, repeat: 3})
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
.handlePostDeployUploaded({deployId, status: 500})
.start();
const effects = new MockDeployEffects();
Expand Down
11 changes: 6 additions & 5 deletions test/mocks/observableApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {getObservableApiHost} from "../../src/observableApiClient.js";

export const validApiKey = "MOCK-VALID-KEY";
export const invalidApiKey = "MOCK-INVALID-KEY";

const emptyErrorBody = JSON.stringify({errors: []});

export class ObservableApiMock {
private _agent: MockAgent | null = null;
private _handlers: ((pool: Interceptable) => void)[] = [];
Expand Down Expand Up @@ -76,11 +78,10 @@ export class ObservableApiMock {
const response = status == 204 ? "" : emptyErrorBody;
const headers = authorizationHeader(status != 401);
this._handlers.push((pool) => {
for (let i = 0; i < repeat; i++) {
pool
.intercept({path: `/cli/deploy/${deployId}/file`, method: "POST", headers: headersMatcher(headers)})
.reply(status, response);
}
pool
.intercept({path: `/cli/deploy/${deployId}/file`, method: "POST", headers: headersMatcher(headers)})
.reply(status, response)
.times(repeat);
});
return this;
}
Expand Down

0 comments on commit ed091b1

Please sign in to comment.