Skip to content

Commit ed091b1

Browse files
committed
build & deploy client bundles
1 parent 7afd8c7 commit ed091b1

File tree

4 files changed

+53
-25
lines changed

4 files changed

+53
-25
lines changed

src/build.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ import {resolvePath} from "./url.js";
1616

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

19+
// TODO Remove library helpers (e.g., duckdb) when they are published to npm.
20+
const CLIENT_BUNDLES: [entry: string, name: string][] = [
21+
["./src/client/index.js", "client.js"],
22+
["./src/client/stdlib.js", "stdlib.js"],
23+
["./src/client/stdlib/dot.js", "stdlib/dot.js"],
24+
["./src/client/stdlib/duckdb.js", "stdlib/duckdb.js"],
25+
["./src/client/stdlib/mermaid.js", "stdlib/mermaid.js"],
26+
["./src/client/stdlib/sqlite.js", "stdlib/sqlite.js"],
27+
["./src/client/stdlib/tex.js", "stdlib/tex.js"],
28+
["./src/client/stdlib/xslx.js", "stdlib/xslx.js"]
29+
];
30+
1931
export interface BuildOptions {
2032
sourceRoot: string;
2133
outputRoot?: string;
@@ -65,12 +77,14 @@ export async function build(
6577
}
6678

6779
if (addPublic) {
68-
// Generate the client bundle.
69-
const clientPath = getClientPath();
70-
const outputPath = join("_observablehq", "client.js");
71-
effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `);
72-
const code = await rollupClient(clientPath, {minify: true});
73-
await effects.writeFile(outputPath, code);
80+
// Generate the client bundles.
81+
for (const [entry, name] of CLIENT_BUNDLES) {
82+
const clientPath = getClientPath(entry);
83+
const outputPath = join("_observablehq", name);
84+
effects.output.write(`${faint("bundle")} ${clientPath} ${faint("→")} `);
85+
const code = await rollupClient(clientPath, {minify: true});
86+
await effects.writeFile(outputPath, code);
87+
}
7488
// Copy over the public directory.
7589
const publicRoot = relative(cwd(), join(dirname(fileURLToPath(import.meta.url)), "..", "public"));
7690
for await (const publicFile of visitFiles(publicRoot)) {

src/rollup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {rollup} from "rollup";
66
import esbuild from "rollup-plugin-esbuild";
77
import {relativeUrl} from "./url.js";
88

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

40-
export function getClientPath(entry = "./src/client/index.js"): string {
40+
export function getClientPath(entry: string): string {
4141
return relative(cwd(), join(dirname(fileURLToPath(import.meta.url)), "..", entry));
4242
}

test/deploy-test.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,24 @@ import type {Logger} from "../src/logger.js";
77
import {commandRequiresAuthenticationMessage} from "../src/observableApiAuth.js";
88
import type {DeployConfig} from "../src/observableApiConfig.js";
99
import {MockLogger} from "./mocks/logger.js";
10-
import {
11-
ObservableApiMock,
12-
invalidApiKey,
13-
userWithTwoWorkspaces,
14-
userWithZeroWorkspaces,
15-
validApiKey
16-
} from "./mocks/observableApi.js";
10+
import {ObservableApiMock} from "./mocks/observableApi.js";
11+
import {invalidApiKey, userWithTwoWorkspaces, userWithZeroWorkspaces, validApiKey} from "./mocks/observableApi.js";
12+
13+
// These files are implicitly generated by the CLI. This may change over time,
14+
// so they’re enumerated here for clarity. TODO We should enforce that these
15+
// files are specifically uploaded, rather than just the number of files.
16+
const EXTRA_FILES: string[] = [
17+
"_observablehq/client.js",
18+
"_observablehq/runtime.js",
19+
"_observablehq/stdlib.js",
20+
"_observablehq/stdlib/dot.js",
21+
"_observablehq/stdlib/duckdb.js",
22+
"_observablehq/stdlib/mermaid.js",
23+
"_observablehq/stdlib/sqlite.js",
24+
"_observablehq/stdlib/tex.js",
25+
"_observablehq/stdlib/xslx.js",
26+
"_observablehq/style.css"
27+
];
1728

1829
class MockDeployEffects implements DeployEffects {
1930
public logger = new MockLogger();
@@ -67,7 +78,9 @@ class MockDeployEffects implements DeployEffects {
6778
}
6879
}
6980

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

7285
describe("deploy", () => {
7386
it("makes expected API calls for a new project", async () => {
@@ -77,7 +90,7 @@ describe("deploy", () => {
7790
.handleGetUser()
7891
.handlePostProject({projectId})
7992
.handlePostDeploy({projectId, deployId})
80-
.handlePostDeployFile({deployId, repeat: 3})
93+
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
8194
.handlePostDeployUploaded({deployId})
8295
.start();
8396

@@ -96,7 +109,7 @@ describe("deploy", () => {
96109
const deployId = "deploy456";
97110
const apiMock = new ObservableApiMock()
98111
.handlePostDeploy({projectId, deployId})
99-
.handlePostDeployFile({deployId, repeat: 3})
112+
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
100113
.handlePostDeployUploaded({deployId})
101114
.start();
102115

@@ -129,7 +142,7 @@ describe("deploy", () => {
129142
.handleGetUser({user: userWithTwoWorkspaces})
130143
.handlePostProject({projectId})
131144
.handlePostDeploy({projectId, deployId})
132-
.handlePostDeployFile({deployId, repeat: 3})
145+
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
133146
.handlePostDeployUploaded({deployId})
134147
.start();
135148
const effects = new MockDeployEffects();
@@ -232,7 +245,7 @@ describe("deploy", () => {
232245
.handleGetUser()
233246
.handlePostProject({projectId})
234247
.handlePostDeploy({projectId, deployId})
235-
.handlePostDeployFile({deployId, repeat: 3})
248+
.handlePostDeployFile({deployId, repeat: EXTRA_FILES.length + 1})
236249
.handlePostDeployUploaded({deployId, status: 500})
237250
.start();
238251
const effects = new MockDeployEffects();

test/mocks/observableApi.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {getObservableApiHost} from "../../src/observableApiClient.js";
33

44
export const validApiKey = "MOCK-VALID-KEY";
55
export const invalidApiKey = "MOCK-INVALID-KEY";
6+
67
const emptyErrorBody = JSON.stringify({errors: []});
8+
79
export class ObservableApiMock {
810
private _agent: MockAgent | null = null;
911
private _handlers: ((pool: Interceptable) => void)[] = [];
@@ -76,11 +78,10 @@ export class ObservableApiMock {
7678
const response = status == 204 ? "" : emptyErrorBody;
7779
const headers = authorizationHeader(status != 401);
7880
this._handlers.push((pool) => {
79-
for (let i = 0; i < repeat; i++) {
80-
pool
81-
.intercept({path: `/cli/deploy/${deployId}/file`, method: "POST", headers: headersMatcher(headers)})
82-
.reply(status, response);
83-
}
81+
pool
82+
.intercept({path: `/cli/deploy/${deployId}/file`, method: "POST", headers: headersMatcher(headers)})
83+
.reply(status, response)
84+
.times(repeat);
8485
});
8586
return this;
8687
}

0 commit comments

Comments
 (0)