Skip to content

Commit

Permalink
Merge branch 'main' into fil/save-search-query
Browse files Browse the repository at this point in the history
  • Loading branch information
Fil committed Jan 24, 2025
2 parents 551a112 + 82412a4 commit 7f9e657
Show file tree
Hide file tree
Showing 51 changed files with 1,048 additions and 679 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
test:
strategy:
matrix:
version: [20, 21]
version: [20, 22]
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
Expand Down
6 changes: 3 additions & 3 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,11 @@ footer: ({path}) => `<a href="https://github.com/example/test/blob/main/src${pat

The base path when serving the site. Currently this only affects the custom 404 page, if any.

## preserveIndex <a href="https://github.com/observablehq/framework/pulls/1784" class="observablehq-version-badge" data-version="prerelease" title="Added in #1784"></a>
## preserveIndex <a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in 1.13.0"></a>

Whether page links should preserve `/index` for directories. Defaults to false. If true, a link to `/` will be formatted as `/index` if the **preserveExtension** option is false or `/index.html` if the **preserveExtension** option is true.

## preserveExtension <a href="https://github.com/observablehq/framework/pulls/1784" class="observablehq-version-badge" data-version="prerelease" title="Added in #1784"></a>
## preserveExtension <a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in 1.13.0"></a>

Whether page links should preserve the `.html` extension. Defaults to false. If true, a link to `/foo` will be formatted as `/foo.html`.

Expand Down Expand Up @@ -301,7 +301,7 @@ export default {
};
```

## duckdb <a href="https://github.com/observablehq/framework/pull/1734" class="observablehq-version-badge" data-version="prerelease" title="Added in #1734"></a>
## duckdb <a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in 1.13.0"></a>

The **duckdb** option configures [self-hosting](./lib/duckdb#self-hosting-of-extensions) and loading of [DuckDB extensions](./lib/duckdb#extensions) for use in [SQL code blocks](./sql) and the `sql` and `DuckDBClient` built-ins. For example, a geospatial data app might enable the [`spatial`](https://duckdb.org/docs/extensions/spatial/overview.html) and [`h3`](https://duckdb.org/community_extensions/extensions/h3.html) extensions like so:

Expand Down
6 changes: 4 additions & 2 deletions docs/embeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ In addition to standalone apps, you can use Framework to embed interactive views
- [exported files](#exported-files) for hotlinking images, data, and other assets, or
- [iframe embeds](#iframe-embeds) for compatibility.

You can deploy to Observable Cloud for [additional features](https://observablehq.com/documentation/data-apps/embeds)<a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in v1.13.0"></a> including secure private embedding on approved domains and analytics to see which exports are used.

## Exported modules

Framework allows [JavaScript modules](./imports#local-imports) to be exported for use in another application. Exported modules are vanilla JavaScript and behave identically in an external web application as on a Framework page. As with local modules, exported modules can load data from a [static file](./files) or a [data loader](./data-loaders), [import](./imports) other local modules, and import libraries from [npm](./imports#npm-imports) or [JSR](./imports#jsr-imports).
Expand Down Expand Up @@ -75,9 +77,9 @@ document.body.append(await Chart());
</script>
```

<div class="warning" label="Coming soon">
<div class="tip">

Observable Cloud support for cross-origin resource sharing (CORS) is not yet generally available and is needed for exported modules. If you are interested in beta-testing this feature, please [email us](mailto:support@observablehq.com). For public apps, you can use a third-party host supporting CORS such as GitHub Pages.
Observable Cloud supports [cross-origin resource sharing](https://observablehq.com/documentation/data-apps/embeds#cors) (CORS), which is needed for exported modules.

</div>

Expand Down
6 changes: 3 additions & 3 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ This command will ask you a series of questions in order to initialize your new
<span class="muted">│</span> <span class="muted">Yes, include sample files</span>
<span class="muted">│</span>
<span class="green">◇</span> Install dependencies?
<span class="muted">│</span> <span class="muted">Yes, via yarn</span>
<span class="muted">│</span> <span class="muted">Yes, via npm</span>
<span class="muted">│</span>
<span class="green">◇</span> Initialize a git repository?
<span class="muted">│</span> <span class="muted">Yes</span>
Expand All @@ -122,7 +122,7 @@ This command will ask you a series of questions in order to initialize your new
<span class="green">◇</span> Next steps… <span class="muted">──────────╮</span>
<span class="muted">│</span> <span class="muted">│</span>
<span class="muted">│</span> <span class="focus">cd hello-framework</span> <span class="muted">│</span>
<span class="muted">│</span> <span class="focus">yarn dev</span> <span class="muted">│</span>
<span class="muted">│</span> <span class="focus">npm run dev</span> <span class="muted">│</span>
<span class="muted">│</span> <span class="muted">│</span>
<span class="muted">├────────────────────────╯</span>
<span class="muted">│</span>
Expand All @@ -148,7 +148,7 @@ Or with Yarn:

You should see something like this:

<pre data-copy="none"><b class="green">Observable Framework</b> v1.12.0
<pre data-copy="none"><b class="green">Observable Framework</b> v1.13.2
↳ <u><a href="http://127.0.0.1:3000/" style="color: inherit;">http://127.0.0.1:3000/</a></u></pre>

<div class="note">
Expand Down
4 changes: 2 additions & 2 deletions docs/lib/duckdb.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const sql = DuckDBClient.sql({quakes: `https://earthquake.usgs.gov/earthquakes/f
SELECT * FROM quakes ORDER BY updated DESC;
```

## Extensions <a href="https://github.com/observablehq/framework/pull/1734" class="observablehq-version-badge" data-version="prerelease" title="Added in #1734"></a>
## Extensions <a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in 1.13.0"></a>

[DuckDB extensions](https://duckdb.org/docs/extensions/overview.html) extend DuckDB’s functionality, adding support for additional file formats, new types, and domain-specific functions. For example, the [`json` extension](https://duckdb.org/docs/data/json/overview.html) provides a `read_json` method for reading JSON files:

Expand Down Expand Up @@ -195,7 +195,7 @@ In the future, we’d like to allow DuckDB to be configured globally (beyond jus

## Versioning

Framework currently uses [DuckDB-Wasm 1.29.0](https://github.com/duckdb/duckdb-wasm/releases/tag/v1.29.0), which aligns with [DuckDB 1.1.1](https://github.com/duckdb/duckdb/releases/tag/v1.1.1). You can load a different version of DuckDB-Wasm by importing `npm:@duckdb/duckdb-wasm` directly, for example:
Framework currently uses [DuckDB-Wasm 1.29.0](https://github.com/duckdb/duckdb-wasm/releases/tag/v1.29.0) <a href="https://github.com/observablehq/framework/releases/tag/v1.13.0" class="observablehq-version-badge" data-version="^1.13.0" title="Added in 1.13.0"></a>, which aligns with [DuckDB 1.1.1](https://github.com/duckdb/duckdb/releases/tag/v1.1.1). You can load a different version of DuckDB-Wasm by importing `npm:@duckdb/duckdb-wasm` directly, for example:

```js run=false
import * as duckdb from "npm:@duckdb/[email protected]";
Expand Down
6 changes: 5 additions & 1 deletion docs/telemetry.md.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import {readFile} from "node:fs/promises";

process.stdout.write(`# Telemetry
process.stdout.write(`---
index: true
---
# Telemetry
Observable Framework collects anonymous usage data to help us improve the product. This data is sent to Observable and is not shared with third parties. Telemetry data is covered by [Observable’s privacy policy](https://observablehq.com/privacy-policy).
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.12.0",
"version": "1.13.2",
"type": "module",
"publishConfig": {
"access": "public"
Expand Down
16 changes: 13 additions & 3 deletions src/client/stdlib/generators/dark.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import {observe} from "./observe.js";

// Watches dark mode based on theme and user preference.
// TODO: in preview, also watch for changes in the theme meta.
export function dark() {
return observe((notify: (dark: boolean) => void) => {
let dark: boolean | undefined;
const media = matchMedia("(prefers-color-scheme: dark)");
const probe = document.createElement("div");
probe.style.transitionProperty = "color, background-color";
probe.style.transitionDuration = "1ms";
const changed = () => {
const d = getComputedStyle(document.body).getPropertyValue("color-scheme") === "dark";
const s = getComputedStyle(document.body).getPropertyValue("color-scheme").split(/\s+/);
let d: boolean;
if (s.includes("light") && s.includes("dark")) d = media.matches;
else d = s.includes("dark");
if (dark === d) return; // only notify if changed
notify((dark = d));
};
document.body.appendChild(probe);
changed();
probe.addEventListener("transitionstart", changed);
media.addEventListener("change", changed);
return () => media.removeEventListener("change", changed);
return () => {
probe.removeEventListener("transitionstart", changed);
media.removeEventListener("change", changed);
};
});
}
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ function readPages(root: string, md: MarkdownIt): Page[] {
return pages;
}

let currentDate: Date | null = null;
export let currentDate: Date | null = null;

/** For testing only! */
export function setCurrentDate(date: Date | null): void {
Expand Down
3 changes: 2 additions & 1 deletion src/javascript/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {extname, join} from "node:path/posix";
import type {Program} from "acorn";
import type {TransformOptions} from "esbuild";
import {transform, transformSync} from "esbuild";
import {currentDate} from "../config.js";
import {resolveJsrImport} from "../jsr.js";
import {resolveNodeImport} from "../node.js";
import {resolveNpmImport} from "../npm.js";
Expand Down Expand Up @@ -199,7 +200,7 @@ export function getFileInfo(root: string, path: string): FileInfo | undefined {
const stat = statSync(key);
if (!stat.isFile()) return; // ignore non-files
accessSync(key, constants.R_OK); // verify that file is readable
mtimeMs = Math.floor(stat.mtimeMs);
mtimeMs = Math.floor((currentDate ?? stat.mtimeMs) as number);
size = stat.size;
} catch {
fileInfoCache.delete(key); // delete stale entry
Expand Down
5 changes: 3 additions & 2 deletions src/libraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ export function getImplicitDownloads(imports: Iterable<string>, duckdb?: DuckDBC
}
}
}
if (set.has("npm:@observablehq/sqlite")) {
implicits.add("npm:sql.js/dist/sql-wasm.js");
if (set.has("npm:sql.js")) {
implicits.add("npm:sql.js/dist/sql-wasm.wasm");
}
if (set.has("npm:leaflet")) {
Expand Down Expand Up @@ -176,7 +175,9 @@ export function getImplicitDependencies(imports: Iterable<string>): Set<string>
if (set.has("npm:@observablehq/duckdb")) implicits.add("npm:@duckdb/duckdb-wasm");
if (set.has("npm:@observablehq/inputs")) implicits.add("npm:htl").add("npm:isoformat");
if (set.has("npm:@observablehq/mermaid")) implicits.add("npm:mermaid");
if (set.has("npm:@observablehq/sqlite")) implicits.add("npm:sql.js");
if (set.has("npm:@observablehq/tex")) implicits.add("npm:katex");
if (set.has("observablehq:stdlib/sqlite")) implicits.add("npm:sql.js");
if (set.has("observablehq:stdlib/xlsx")) implicits.add("npm:exceljs");
if (set.has("observablehq:stdlib/zip")) implicits.add("npm:jszip");
if (set.has("observablehq:stdlib/vega-lite")) implicits.add("npm:vega-lite-api").add("npm:vega-lite").add("npm:vega");
Expand Down
2 changes: 1 addition & 1 deletion src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export class LoaderResolver {
getOutputFileHash(name: string): string {
const info = this.getOutputInfo(name);
if (!info) throw new Error(`output file not found: ${name}`);
return info.hash;
return createHash("sha256").update(info.hash).update(String(info.mtimeMs)).digest("hex");
}

getSourceInfo(name: string): FileInfo | undefined {
Expand Down
2 changes: 2 additions & 0 deletions src/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ export async function resolveNpmImport(root: string, specifier: string): Promise
? "dist/jquery-ui.js/+esm"
: name === "deck.gl"
? "dist.min.js/+esm"
: name === "react-dom"
? "client"
: "+esm"
} = parseNpmSpecifier(specifier);
const version = await resolveNpmVersion(root, {name, range});
Expand Down
12 changes: 7 additions & 5 deletions test/build-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import assert from "node:assert";
import {existsSync, readdirSync, statSync} from "node:fs";
import {mkdir, mkdtemp, open, readFile, rename, rm, unlink, writeFile} from "node:fs/promises";
import os from "node:os";
import {extname} from "node:path/posix";
import {join, normalize, relative} from "node:path/posix";
import {PassThrough} from "node:stream";
import {ascending, difference} from "d3-array";
import {ascending, difference, sort} from "d3-array";
import type {BuildManifest} from "../src/build.js";
import {FileBuildEffects, build} from "../src/build.js";
import {normalizeConfig, readConfig, setCurrentDate} from "../src/config.js";
Expand Down Expand Up @@ -32,6 +33,7 @@ const failureTests = ["missing-file", "missing-import"];

describe("build", () => {
before(() => setCurrentDate(new Date("2024-01-10T16:00:00")));
after(() => setCurrentDate(null));
mockJsDelivr();
mockJsr();
mockDuckDB();
Expand Down Expand Up @@ -75,7 +77,10 @@ describe("build", () => {
// renumber the hashes so they are sequential. This way we don’t have to
// update the test snapshots whenever Framework’s client code changes. We
// make an exception for minisearch.json because to test the content.
for (const path of findFiles(join(outputDir, "_observablehq"))) {
for (const path of sort(
findFiles(join(outputDir, "_observablehq")),
(a, b) => ascending(extname(a) === ".css", extname(b) === ".css") || ascending(a, b)
)) {
const match = /^((.+)\.[0-9a-f]{8})\.(\w+)$/.exec(path);
if (!match) throw new Error(`no hash found: ${path}`);
const [, key, name, ext] = match;
Expand Down Expand Up @@ -233,9 +238,6 @@ class TestEffects extends FileBuildEffects {
contents = contents.replace(/^(\s*<script>\{).*(\}<\/script>)$/gm, "$1/* redacted init script */$2");
contents = contents.replace(/(registerFile\(.*,"lastModified":)\d+(,"size":\d+.*\))/gm, "$1/* ts */1706742000000$2"); // prettier-ignore
}
if (typeof contents === "string" && outputPath.endsWith(".js")) {
contents = contents.replace(/(FileAttachment\(.*,"lastModified":)\d+(,"size":\d+.*\))/gm, "$1/* ts */1706742000000$2"); // prettier-ignore
}
return super.writeFile(outputPath, contents);
}
}
Expand Down
1 change: 1 addition & 0 deletions test/config-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const DUCKDB_DEFAULTS: DuckDBConfig = {

describe("readConfig(undefined, root)", () => {
before(() => setCurrentDate(new Date("2024-01-10T16:00:00")));
after(() => setCurrentDate(null));
it("imports the config file at the specified root", async () => {
const {md, loaders, paths, normalizePath, ...config} = await readConfig(undefined, "test/input/build/config");
assert(md instanceof MarkdownIt);
Expand Down
1 change: 1 addition & 0 deletions test/deploy-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ const DEPLOY_CONFIG: DeployConfig & {projectId: string; projectSlug: string; wor

describe("deploy", () => {
before(() => setCurrentDate(new Date("2024-01-10T16:00:00")));
after(() => setCurrentDate(null));
mockObservableApi();
mockJsDelivr();

Expand Down
3 changes: 3 additions & 0 deletions test/input/build/params2/[code]/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {FileAttachment} from "npm:@observablehq/stdlib";

FileAttachment("data.json");
1 change: 1 addition & 0 deletions test/input/build/params2/[code]/data.json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
process.stdout.write(JSON.stringify({a: 1}));
5 changes: 5 additions & 0 deletions test/input/build/params2/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# test

```js
import "/code/analytics.js"
```
7 changes: 3 additions & 4 deletions test/libraries-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,7 @@ describe("getImplicitDownloads(imports)", () => {
"npm:@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js"
])
);
assert.deepStrictEqual(
getImplicitDownloads(["npm:@observablehq/sqlite"]),
new Set(["npm:sql.js/dist/sql-wasm.js", "npm:sql.js/dist/sql-wasm.wasm"])
);
assert.deepStrictEqual(getImplicitDownloads(["npm:sql.js"]), new Set(["npm:sql.js/dist/sql-wasm.wasm"]));
});
});

Expand All @@ -74,7 +71,9 @@ describe("getImplicitDependencies(imports)", () => {
assert.deepStrictEqual(getImplicitDependencies(["npm:@observablehq/duckdb"]), new Set(["npm:@duckdb/duckdb-wasm"]));
assert.deepStrictEqual(getImplicitDependencies(["npm:@observablehq/inputs"]), new Set(["npm:htl", "npm:isoformat"])); // prettier-ignore
assert.deepStrictEqual(getImplicitDependencies(["npm:@observablehq/mermaid"]), new Set(["npm:mermaid"]));
assert.deepStrictEqual(getImplicitDependencies(["npm:@observablehq/sqlite"]), new Set(["npm:sql.js"]));
assert.deepStrictEqual(getImplicitDependencies(["npm:@observablehq/tex"]), new Set(["npm:katex"]));
assert.deepStrictEqual(getImplicitDependencies(["observablehq:stdlib/sqlite"]), new Set(["npm:sql.js"]));
assert.deepStrictEqual(getImplicitDependencies(["observablehq:stdlib/xlsx"]), new Set(["npm:exceljs"]));
assert.deepStrictEqual(getImplicitDependencies(["observablehq:stdlib/zip"]), new Set(["npm:jszip"]));
assert.deepStrictEqual(getImplicitDependencies(["observablehq:stdlib/vega-lite"]), new Set(["npm:vega-lite-api", "npm:vega-lite", "npm:vega"])); // prettier-ignore
Expand Down
4 changes: 2 additions & 2 deletions test/node-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ describe("resolveNodeImport(root, spec) with top-level node_modules", () => {
before(() => rm(join(testRoot, ".observablehq/cache/_node"), {recursive: true, force: true}));
it("resolves the version of a direct dependency", async () => {
assert.deepStrictEqual(await resolveNodeImport(testRoot, "d3-array"), "/_node/[email protected]/index.js");
assert.deepStrictEqual(await resolveNodeImport(testRoot, "mime"), "/_node/[email protected].4/index.js");
assert.deepStrictEqual(await resolveNodeImport(testRoot, "mime"), "/_node/[email protected].6/index.js");
});
it("allows entry points", async () => {
assert.deepStrictEqual(await resolveNodeImport(testRoot, "mime/lite"), "/_node/[email protected].4/lite.js");
assert.deepStrictEqual(await resolveNodeImport(testRoot, "mime/lite"), "/_node/[email protected].6/lite.js");
});
it("allows non-javascript entry points", async () => {
assert.deepStrictEqual(await resolveNodeImport(testRoot, "glob/package.json"), "/_node/[email protected]/package.json");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {test} from "./test.a9a4ef0e.js";

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import {FileAttachment} from "../_observablehq/stdlib.00000003.js";

export const test = FileAttachment({"name":"../test.txt","mimeType":"text/plain","path":"../_file/test.f2ca1bb6.txt","lastModified":/* ts */1706742000000,"size":5}, import.meta.url).text();
export const test = FileAttachment({"name":"../test.txt","mimeType":"text/plain","path":"../_file/test.f2ca1bb6.txt","lastModified":1704931200000,"size":5}, import.meta.url).text();
6 changes: 3 additions & 3 deletions test/output/build/data-loaders/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
<link rel="modulepreload" href="./_observablehq/client.00000001.js">
<link rel="modulepreload" href="./_observablehq/runtime.00000002.js">
<link rel="modulepreload" href="./_observablehq/stdlib.00000003.js">
<link rel="modulepreload" href="./_import/import-test.e7269c4e.js">
<link rel="modulepreload" href="./_import/test.86a60bc6.js">
<link rel="modulepreload" href="./_import/import-test.3349a02d.js">
<link rel="modulepreload" href="./_import/test.a9a4ef0e.js">
<script type="module">

import {define} from "./_observablehq/client.00000001.js";
Expand All @@ -23,7 +23,7 @@
registerFile("./test.txt", {"name":"./test.txt","mimeType":"text/plain","path":"./_file/test.f2ca1bb6.txt","lastModified":/* ts */1706742000000,"size":5});

define({id: "05e74070", inputs: ["display"], outputs: ["test"], body: async (display) => {
const {test} = await import("./_import/import-test.e7269c4e.js");
const {test} = await import("./_import/import-test.3349a02d.js");

display(await test);
return {test};
Expand Down
Loading

0 comments on commit 7f9e657

Please sign in to comment.