From 5bc5510c487270fa0bc8fa3160de7f3744ceff30 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 14 Feb 2024 15:53:54 -0800 Subject: [PATCH 1/9] observable convert (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * observable convert * convert --output specifies the output directory, defaults to . convert the file names (scatterplot/2 should be "scatterplot,2") nicer log effects * download attachments skip documents and files we already have * touch (preserve dates) * Update bin/observable.ts * clack, --force * effects.touch * prompt to overwrite * files is optional * disable coverage for convert --------- Co-authored-by: Philippe Rivière --- .github/workflows/test.yml | 2 +- bin/observable.ts | 14 +++- src/convert.ts | 146 +++++++++++++++++++++++++++++++++++++ test/convert-test.ts | 94 ++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 src/convert.ts create mode 100644 test/convert-test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f7b303f54..26a096660 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: node-version: ${{ matrix.version }} cache: yarn - run: yarn --frozen-lockfile - - run: yarn c8 --check-coverage -x src/**/*.d.ts -x src/preview.ts -x src/observableApiConfig.ts -x src/client --lines 80 --per-file yarn test:mocha + - run: yarn c8 --check-coverage -x src/**/*.d.ts -x src/preview.ts -x src/observableApiConfig.ts -x src/client -x src/convert.ts --lines 80 --per-file yarn test:mocha - run: yarn test:tsc - run: | echo ::add-matcher::.github/eslint.json diff --git a/bin/observable.ts b/bin/observable.ts index aec619c8a..f286d9cd3 100644 --- a/bin/observable.ts +++ b/bin/observable.ts @@ -62,7 +62,7 @@ else if (values.help) { /** Commands that use Clack formatting. When handling CliErrors, clack.outro() * will be used for these commands. */ -const CLACKIFIED_COMMANDS = ["create", "deploy", "login"]; +const CLACKIFIED_COMMANDS = ["create", "deploy", "login", "convert"]; try { switch (command) { @@ -78,6 +78,7 @@ try { logout sign-out of Observable deploy deploy a project to Observable whoami check authentication status + convert convert an Observable notebook to Markdown help print usage information version print the version` ); @@ -177,6 +178,17 @@ try { await import("../src/observableApiAuth.js").then((auth) => auth.whoami()); break; } + case "convert": { + const { + positionals, + values: {output, force} + } = helpArgs(command, { + options: {output: {type: "string", default: "."}, force: {type: "boolean", short: "f"}}, + allowPositionals: true + }); + await import("../src/convert.js").then((convert) => convert.convert(positionals, {output: output!, force})); + break; + } default: { console.error(`observable: unknown command '${command}'. See 'observable help'.`); process.exit(1); diff --git a/src/convert.ts b/src/convert.ts new file mode 100644 index 000000000..372d3ec3b --- /dev/null +++ b/src/convert.ts @@ -0,0 +1,146 @@ +import {existsSync} from "node:fs"; +import {utimes, writeFile} from "node:fs/promises"; +import {join} from "node:path"; +import * as clack from "@clack/prompts"; +import wrapAnsi from "wrap-ansi"; +import type {ClackEffects} from "./clack.js"; +import {CliError} from "./error.js"; +import {prepareOutput} from "./files.js"; +import {getObservableUiOrigin} from "./observableApiClient.js"; +import {type TtyEffects, bold, cyan, faint, inverse, link, reset, defaultEffects as ttyEffects} from "./tty.js"; + +export interface ConvertEffects extends TtyEffects { + clack: ClackEffects; + prepareOutput(outputPath: string): Promise; + existsSync(outputPath: string): boolean; + writeFile(outputPath: string, contents: Buffer | string): Promise; + touch(outputPath: string, date: Date | string | number): Promise; +} + +const defaultEffects: ConvertEffects = { + ...ttyEffects, + clack, + async prepareOutput(outputPath: string): Promise { + await prepareOutput(outputPath); + }, + existsSync(outputPath: string): boolean { + return existsSync(outputPath); + }, + async writeFile(outputPath: string, contents: Buffer | string): Promise { + await writeFile(outputPath, contents); + }, + async touch(outputPath: string, date: Date | string | number): Promise { + await utimes(outputPath, (date = new Date(date)), date); + } +}; + +export async function convert( + inputs: string[], + {output, force = false, files: includeFiles = true}: {output: string; force?: boolean; files?: boolean}, + effects: ConvertEffects = defaultEffects +): Promise { + const {clack} = effects; + clack.intro(`${inverse(" observable convert ")}`); + let n = 0; + for (const input of inputs) { + let start = Date.now(); + let s = clack.spinner(); + const url = resolveInput(input); + const name = inferFileName(url); + const path = join(output, name); + if (await maybeFetch(path, force, effects)) { + s.start(`Downloading ${bold(path)}`); + const response = await fetch(url); + if (!response.ok) throw new Error(`error fetching ${url}: ${response.status}`); + const {nodes, files, update_time} = await response.json(); + s.stop(`Downloaded ${bold(path)} ${faint(`in ${(Date.now() - start).toLocaleString("en-US")}ms`)}`); + await effects.prepareOutput(path); + await effects.writeFile(path, convertNodes(nodes)); + await effects.touch(path, update_time); + n++; + if (includeFiles) { + for (const file of files) { + const path = join(output, file.name); + if (await maybeFetch(path, force, effects)) { + start = Date.now(); + s = clack.spinner(); + s.start(`Downloading ${bold(file.name)}`); + const response = await fetch(file.download_url); + if (!response.ok) throw new Error(`error fetching ${file.download_url}: ${response.status}`); + const buffer = Buffer.from(await response.arrayBuffer()); + s.stop(`Downloaded ${bold(file.name)} ${faint(`in ${(Date.now() - start).toLocaleString("en-US")}ms`)}`); + await effects.prepareOutput(path); + await effects.writeFile(path, buffer); + await effects.touch(path, file.create_time); + n++; + } + } + } + } + } + clack.note( + wrapAnsi( + "Due to syntax differences between Observable notebooks and " + + "Observable Framework, converted notebooks may require further " + + "changes to function correctly. To learn more about JavaScript " + + "in Framework, please read:\n\n" + + reset(cyan(link("https://observablehq.com/framework/javascript"))), + Math.min(64, effects.outputColumns) + ), + "Note" + ); + clack.outro( + `${inputs.length} notebook${inputs.length === 1 ? "" : "s"} converted; ${n} file${n === 1 ? "" : "s"} written` + ); +} + +async function maybeFetch(path: string, force: boolean, effects: ConvertEffects): Promise { + const {clack} = effects; + if (effects.existsSync(path) && !force) { + const choice = await clack.confirm({message: `${bold(path)} already exists; replace?`, initialValue: false}); + if (!choice) return false; + if (clack.isCancel(choice)) throw new CliError("Stopped convert", {print: false}); + } + return true; +} + +export function convertNodes(nodes): string { + let string = ""; + let first = true; + for (const node of nodes) { + if (first) first = false; + else string += "\n"; + string += convertNode(node); + } + return string; +} + +export function convertNode(node): string { + let string = ""; + if (node.mode !== "md") string += `\`\`\`${node.mode}${node.pinned ? " echo" : ""}\n`; + string += `${node.value}\n`; + if (node.mode !== "md") string += "```\n"; + return string; +} + +export function inferFileName(input: string): string { + return new URL(input).pathname.replace(/^\/document(\/@[^/]+)?\//, "").replace(/\//g, ",") + ".md"; +} + +export function resolveInput(input: string): string { + let url: URL; + if (isIdSpecifier(input)) url = new URL(`/d/${input}`, getObservableUiOrigin()); + else if (isSlugSpecifier(input)) url = new URL(`/${input}`, getObservableUiOrigin()); + else url = new URL(input); + url.host = `api.${url.host}`; + url.pathname = `/document${url.pathname.replace(/^\/d\//, "/")}`; + return String(url); +} + +function isIdSpecifier(string: string) { + return /^([0-9a-f]{16})(?:@(\d+)|~(\d+)|@(\w+))?$/.test(string); +} + +function isSlugSpecifier(string: string) { + return /^(?:@([0-9a-z_-]+))\/([0-9a-z_-]+(?:\/[0-9]+)?)(?:@(\d+)|~(\d+)|@(\w+))?$/.test(string); +} diff --git a/test/convert-test.ts b/test/convert-test.ts new file mode 100644 index 000000000..f734bb6fe --- /dev/null +++ b/test/convert-test.ts @@ -0,0 +1,94 @@ +import assert from "node:assert"; +import {convertNode, convertNodes, inferFileName, resolveInput} from "../src/convert.js"; + +describe("convertNodes", () => { + it("converts multiple nodes", () => { + assert.strictEqual( + convertNodes([ + {mode: "md", value: "Hello, world!"}, + {mode: "js", value: "1 + 2"} + ]), + "Hello, world!\n\n```js\n1 + 2\n```\n" + ); + }); +}); + +describe("convertNode", () => { + it("passes through Markdown, adding a newline", () => { + assert.strictEqual(convertNode({mode: "md", value: "Hello, world!"}), "Hello, world!\n"); + assert.strictEqual(convertNode({mode: "md", value: "# Hello, world!"}), "# Hello, world!\n"); + assert.strictEqual(convertNode({mode: "md", value: "# Hello, ${'world'}!"}), "# Hello, ${'world'}!\n"); + }); + it("wraps JavaScript in a fenced code block", () => { + assert.strictEqual(convertNode({mode: "js", value: "1 + 2"}), "```js\n1 + 2\n```\n"); + }); + it("converts pinned to echo", () => { + assert.strictEqual(convertNode({mode: "js", pinned: true, value: "1 + 2"}), "```js echo\n1 + 2\n```\n"); + }); +}); + +describe("inferFileName", () => { + it("infers a suitable file name based on identifier", () => { + assert.strictEqual(inferFileName("https://api.observablehq.com/document/1111111111111111"), "1111111111111111.md"); + }); + it("infers a suitable file name based on slug", () => { + assert.strictEqual(inferFileName("https://api.observablehq.com/document/@d3/bar-chart"), "bar-chart.md"); + }); + it("handles a slug with a suffix", () => { + assert.strictEqual(inferFileName("https://api.observablehq.com/document/@d3/bar-chart/2"), "bar-chart,2.md"); + }); + it("handles a different origin", () => { + assert.strictEqual(inferFileName("https://api.example.com/document/@d3/bar-chart"), "bar-chart.md"); + }); +}); + +/* prettier-ignore */ +describe("resolveInput", () => { + it("resolves document identifiers", () => { + assert.strictEqual(resolveInput("1111111111111111"), "https://api.observablehq.com/document/1111111111111111"); + assert.strictEqual(resolveInput("1234567890abcdef"), "https://api.observablehq.com/document/1234567890abcdef"); + }); + it("resolves document slugs", () => { + assert.strictEqual(resolveInput("@d3/bar-chart"), "https://api.observablehq.com/document/@d3/bar-chart"); + assert.strictEqual(resolveInput("@d3/bar-chart/2"), "https://api.observablehq.com/document/@d3/bar-chart/2"); + }); + it("resolves document versions", () => { + assert.strictEqual(resolveInput("1234567890abcdef@123"), "https://api.observablehq.com/document/1234567890abcdef@123"); + assert.strictEqual(resolveInput("1234567890abcdef@latest"), "https://api.observablehq.com/document/1234567890abcdef@latest"); + assert.strictEqual(resolveInput("1234567890abcdef~0"), "https://api.observablehq.com/document/1234567890abcdef~0"); + assert.strictEqual(resolveInput("@d3/bar-chart@123"), "https://api.observablehq.com/document/@d3/bar-chart@123"); + assert.strictEqual(resolveInput("@d3/bar-chart@latest"), "https://api.observablehq.com/document/@d3/bar-chart@latest"); + assert.strictEqual(resolveInput("@d3/bar-chart~0"), "https://api.observablehq.com/document/@d3/bar-chart~0"); + assert.strictEqual(resolveInput("@d3/bar-chart/2@123"), "https://api.observablehq.com/document/@d3/bar-chart/2@123"); + assert.strictEqual(resolveInput("@d3/bar-chart/2@latest"), "https://api.observablehq.com/document/@d3/bar-chart/2@latest"); + assert.strictEqual(resolveInput("@d3/bar-chart/2~0"), "https://api.observablehq.com/document/@d3/bar-chart/2~0"); + }); + it("resolves urls", () => { + assert.strictEqual(resolveInput("https://observablehq.com/1234567890abcdef"), "https://api.observablehq.com/document/1234567890abcdef"); + assert.strictEqual(resolveInput("https://observablehq.com/1234567890abcdef@123"), "https://api.observablehq.com/document/1234567890abcdef@123"); + assert.strictEqual(resolveInput("https://observablehq.com/1234567890abcdef@latest"), "https://api.observablehq.com/document/1234567890abcdef@latest"); + assert.strictEqual(resolveInput("https://observablehq.com/1234567890abcdef~0"), "https://api.observablehq.com/document/1234567890abcdef~0"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart"), "https://api.observablehq.com/document/@d3/bar-chart"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart@123"), "https://api.observablehq.com/document/@d3/bar-chart@123"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart@latest"), "https://api.observablehq.com/document/@d3/bar-chart@latest"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart~0"), "https://api.observablehq.com/document/@d3/bar-chart~0"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart/2"), "https://api.observablehq.com/document/@d3/bar-chart/2"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart/2@123"), "https://api.observablehq.com/document/@d3/bar-chart/2@123"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart/2@latest"), "https://api.observablehq.com/document/@d3/bar-chart/2@latest"); + assert.strictEqual(resolveInput("https://observablehq.com/@d3/bar-chart/2~0"), "https://api.observablehq.com/document/@d3/bar-chart/2~0"); + }); + it("preserves the specified host", () => { + assert.strictEqual(resolveInput("https://example.com/1234567890abcdef"), "https://api.example.com/document/1234567890abcdef"); + assert.strictEqual(resolveInput("https://example.com/1234567890abcdef@123"), "https://api.example.com/document/1234567890abcdef@123"); + assert.strictEqual(resolveInput("https://example.com/1234567890abcdef@latest"), "https://api.example.com/document/1234567890abcdef@latest"); + assert.strictEqual(resolveInput("https://example.com/1234567890abcdef~0"), "https://api.example.com/document/1234567890abcdef~0"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart"), "https://api.example.com/document/@d3/bar-chart"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart@123"), "https://api.example.com/document/@d3/bar-chart@123"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart@latest"), "https://api.example.com/document/@d3/bar-chart@latest"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart~0"), "https://api.example.com/document/@d3/bar-chart~0"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart/2"), "https://api.example.com/document/@d3/bar-chart/2"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart/2@123"), "https://api.example.com/document/@d3/bar-chart/2@123"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart/2@latest"), "https://api.example.com/document/@d3/bar-chart/2@latest"); + assert.strictEqual(resolveInput("https://example.com/@d3/bar-chart/2~0"), "https://api.example.com/document/@d3/bar-chart/2~0"); + }); +}); From 97fe81a76e265402ed25569a1da6f12314cb6842 Mon Sep 17 00:00:00 2001 From: Cindy <37343722+cinxmo@users.noreply.github.com> Date: Wed, 14 Feb 2024 19:48:06 -0500 Subject: [PATCH 2/9] File quota error message (#797) --- src/deploy.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/deploy.ts b/src/deploy.ts index e403c3d21..95d457a13 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -353,6 +353,9 @@ class DeployBuildEffects implements BuildEffects { try { await this.apiClient.postDeployFile(this.deployId, sourcePath, outputPath); } catch (error) { + if (isApiError(error) && error.details.errors.some((e) => e.code === "FILE_QUOTA_EXCEEDED")) { + throw new CliError("You have reached the total file size limit.", {cause: error}); + } // 413 is "Payload Too Large", however sometimes Cloudflare returns a // custom Cloudflare error, 520. Sometimes we also see 502. Handle them all if (isHttpError(error) && (error.statusCode === 413 || error.statusCode === 503 || error.statusCode === 520)) { @@ -363,7 +366,14 @@ class DeployBuildEffects implements BuildEffects { } async writeFile(outputPath: string, content: Buffer | string) { this.logger.log(outputPath); - await this.apiClient.postDeployFileContents(this.deployId, content, outputPath); + try { + await this.apiClient.postDeployFileContents(this.deployId, content, outputPath); + } catch (error) { + if (isApiError(error) && error.details.errors.some((e) => e.code === "FILE_QUOTA_EXCEEDED")) { + throw new CliError("You have reached the total file size limit.", {cause: error}); + } + throw error; + } } } From 1f762ef47edb06352622c4c040f2cf31e6a9cb0d Mon Sep 17 00:00:00 2001 From: Michael Cooper Date: Wed, 14 Feb 2024 18:56:00 -0800 Subject: [PATCH 3/9] always exit with non-zero if a command throws an error (#799) --- bin/observable.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/observable.ts b/bin/observable.ts index f286d9cd3..2a1997c97 100644 --- a/bin/observable.ts +++ b/bin/observable.ts @@ -235,6 +235,7 @@ try { } } } + process.exit(1); } // A wrapper for parseArgs that adds --help functionality with automatic usage. From f6980df1ec3e3430167f40a9f2ae68d3fd1bcc74 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 14 Feb 2024 21:02:58 -0800 Subject: [PATCH 4/9] new cloudflare pages project --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 686e1e232..dd513e0e9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -64,7 +64,7 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - projectName: framework-pages + projectName: framework directory: dist gitHubToken: ${{ secrets.GITHUB_TOKEN }} # TODO: This doesn't include the examples. How can we fix that? From 6db4a856fb171a4ccfa13ec7b5a2f8d22be8ed53 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Wed, 14 Feb 2024 22:07:49 -0800 Subject: [PATCH 5/9] 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2d07c808e..d2c0b9fbb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@observablehq/framework", "license": "ISC", - "version": "1.0.0-rc.8", + "version": "1.0.0", "type": "module", "publishConfig": { "access": "public" From 1695333ecb004c922383e903a269de80cf101498 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 15 Feb 2024 00:21:25 -0800 Subject: [PATCH 6/9] remove deploy client (#802) --- src/client/deploy.js | 44 -------------------------------------------- src/deploy.ts | 2 +- 2 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 src/client/deploy.js diff --git a/src/client/deploy.js b/src/client/deploy.js deleted file mode 100644 index cf1d69f76..000000000 --- a/src/client/deploy.js +++ /dev/null @@ -1,44 +0,0 @@ -export * from "./index.js"; - -const origin = process.env.OBSERVABLE_ORIGIN; -const parent = window.parent; // capture to prevent reassignment - -let listener = null; -let queuedMessages = null; - -async function messaged(event) { - if (!event.isTrusted || event.origin !== origin || event.source !== parent) return; - event.stopImmediatePropagation(); - const message = event.data; - - if (message.type === "hello") { - postMessage({type: "hello"}); - } else if (message.type === "load_script") { - try { - if (listener) throw new Error("a script is already loaded"); - queuedMessages = []; - const module = await import(message.url); - if (module.listener) { - listener = module.listener; - queuedMessages.forEach((m) => Promise.resolve(m).then(listener)); - queuedMessages = null; - } - postMessage({type: "load_script_complete", url: message.url, re: message.id}); - } catch (error) { - postMessage({type: "load_script_error", url: message.url, error: error.message, re: message.id}); - } - } else if (listener) { - listener(message); - } else if (queuedMessages) { - queuedMessages.push(message); - } -} - -let fingerprint = `c-${Math.random().toString(36).slice(2, 8).padStart(6, "0")}`; -let nextId = 0; -function postMessage(message) { - parent.postMessage({id: `${fingerprint}-${++nextId}`, ...message}, origin); -} - -addEventListener("message", messaged); -postMessage({type: "hello"}); diff --git a/src/deploy.ts b/src/deploy.ts index 95d457a13..a37ff34a4 100644 --- a/src/deploy.ts +++ b/src/deploy.ts @@ -296,7 +296,7 @@ export async function deploy( } // Build the project - await build({config, clientEntry: "./src/client/deploy.js"}, new DeployBuildEffects(apiClient, deployId, effects)); + await build({config}, new DeployBuildEffects(apiClient, deployId, effects)); // Mark the deploy as uploaded await apiClient.postDeployUploaded(deployId); From 07a33cd4611f54cf57bb67875593d8bf83b64ca4 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 15 Feb 2024 00:24:15 -0800 Subject: [PATCH 7/9] fix search results scrollbar (#803) --- src/style/layout.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/style/layout.css b/src/style/layout.css index 1b54e1cc0..a0f3985c1 100644 --- a/src/style/layout.css +++ b/src/style/layout.css @@ -490,7 +490,7 @@ #observablehq-search-results { --relevance-width: 32px; position: absolute; - overflow-y: scroll; + overflow-y: auto; top: 6.5rem; left: 0; right: 0.5rem; From 824ec4a0144aa3bb3277f3b345d2b7a438ebdfd2 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 15 Feb 2024 00:45:18 -0800 Subject: [PATCH 8/9] cache data (#804) * cache data * cache examples, too --- .github/workflows/deploy.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dd513e0e9..90df29a55 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,6 +23,17 @@ jobs: yarn.lock 'examples/*/yarn.lock' - run: yarn --frozen-lockfile + - id: date + run: echo "date=$(TZ=America/Los_Angeles date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + - id: cache-data + uses: actions/cache@v4 + with: + path: | + docs/.observablehq/cache + 'examples/*/docs/.observablehq/cache' + key: data-${{ hashFiles('docs/data/*', 'examples/*/docs/data/*') }}-${{ steps.date.outputs.date }} + - if: steps.cache-data.outputs.cache-hit == 'true' + run: find docs/.observablehq/cache examples/*/docs/.observablehq/cache -type f -exec touch {} + - run: yarn build - name: Build example "api" run: yarn --frozen-lockfile && yarn build From 82438330af20086e39b441a8bf30613703770bf0 Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 15 Feb 2024 00:57:02 -0800 Subject: [PATCH 9/9] fix cache path? --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 90df29a55..74254d804 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,7 +30,7 @@ jobs: with: path: | docs/.observablehq/cache - 'examples/*/docs/.observablehq/cache' + examples/*/docs/.observablehq/cache key: data-${{ hashFiles('docs/data/*', 'examples/*/docs/data/*') }}-${{ steps.date.outputs.date }} - if: steps.cache-data.outputs.cache-hit == 'true' run: find docs/.observablehq/cache examples/*/docs/.observablehq/cache -type f -exec touch {} +