Skip to content

Commit

Permalink
Merge branch 'main' into fil/rollup-css-minify
Browse files Browse the repository at this point in the history
  • Loading branch information
Fil authored Feb 15, 2024
2 parents b7d8113 + 8243833 commit e1be685
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 51 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,7 +75,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?
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion bin/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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`
);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -223,6 +235,7 @@ try {
}
}
}
process.exit(1);
}

// A wrapper for parseArgs that adds --help functionality with automatic usage.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@observablehq/framework",
"license": "ISC",
"version": "1.0.0-rc.8",
"version": "1.0.0",
"type": "module",
"publishConfig": {
"access": "public"
Expand Down
44 changes: 0 additions & 44 deletions src/client/deploy.js

This file was deleted.

146 changes: 146 additions & 0 deletions src/convert.ts
Original file line number Diff line number Diff line change
@@ -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<void>;
existsSync(outputPath: string): boolean;
writeFile(outputPath: string, contents: Buffer | string): Promise<void>;
touch(outputPath: string, date: Date | string | number): Promise<void>;
}

const defaultEffects: ConvertEffects = {
...ttyEffects,
clack,
async prepareOutput(outputPath: string): Promise<void> {
await prepareOutput(outputPath);
},
existsSync(outputPath: string): boolean {
return existsSync(outputPath);
},
async writeFile(outputPath: string, contents: Buffer | string): Promise<void> {
await writeFile(outputPath, contents);
},
async touch(outputPath: string, date: Date | string | number): Promise<void> {
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<void> {
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<boolean> {
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);
}
14 changes: 12 additions & 2 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/style/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit e1be685

Please sign in to comment.