Skip to content

Commit 3a5de29

Browse files
committed
Merge branch 'main' into toph/onramp
2 parents 27128bb + 27bc864 commit 3a5de29

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+888
-120
lines changed

docs/config.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,13 @@ footer: ({path}) => `<a href="https://github.com/example/test/blob/main/src${pat
197197

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

200-
## cleanUrls <a href="https://github.com/observablehq/framework/releases/tag/v1.3.0" class="observablehq-version-badge" data-version="^1.3.0" title="Added in 1.3.0"></a>
200+
## preserveIndex <a href="https://github.com/observablehq/framework/pulls/1784" class="observablehq-version-badge" data-version="prerelease" title="Added in #1784"></a>
201201

202-
Whether page links should be “clean”, _i.e._, formatted without a `.html` extension. Defaults to true. If true, a link to `config.html` will be formatted as `config`. Regardless of this setting, a link to an index page will drop the implied `index.html`; for example `foo/index.html` will be formatted as `foo/`.
202+
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.
203+
204+
## preserveExtension <a href="https://github.com/observablehq/framework/pulls/1784" class="observablehq-version-badge" data-version="prerelease" title="Added in #1784"></a>
205+
206+
Whether page links should preserve the `.html` extension. Defaults to false. If true, a link to `/foo` will be formatted as `/foo.html`.
203207

204208
## toc
205209

@@ -297,6 +301,45 @@ export default {
297301
};
298302
```
299303

304+
## duckdb <a href="https://github.com/observablehq/framework/pull/1734" class="observablehq-version-badge" data-version="prerelease" title="Added in #1734"></a>
305+
306+
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:
307+
308+
```js run=false
309+
export default {
310+
duckdb: {
311+
extensions: ["spatial", "h3"]
312+
}
313+
};
314+
```
315+
316+
The **extensions** option can either be an array of extension names, or an object whose keys are extension names and whose values are configuration options for the given extension, including its **source** repository (defaulting to the keyword _core_ for core extensions, and otherwise _community_; can also be a custom repository URL), whether to **load** it immediately (defaulting to true, except for known extensions that support autoloading), and whether to **install** it (_i.e._ to self-host, defaulting to true). As additional shorthand, you can specify `[name]: true` to install and load the named extension from the default (_core_ or _community_) source repository, or `[name]: string` to install and load the named extension from the given source repository.
317+
318+
The configuration above is equivalent to:
319+
320+
```js run=false
321+
export default {
322+
duckdb: {
323+
extensions: {
324+
spatial: {
325+
source: "https://extensions.duckdb.org/",
326+
install: true,
327+
load: true
328+
},
329+
h3: {
330+
source: "https://community-extensions.duckdb.org/",
331+
install: true,
332+
load: true
333+
}
334+
}
335+
}
336+
};
337+
```
338+
339+
The `json` and `parquet` are configured (and therefore self-hosted) by default. To expressly disable self-hosting of extension, you can set its **install** property to false, or equivalently pass null as the extension configuration object.
340+
341+
For more, see [DuckDB extensions](./lib/duckdb#extensions).
342+
300343
## markdownIt <a href="https://github.com/observablehq/framework/releases/tag/v1.1.0" class="observablehq-version-badge" data-version="^1.1.0" title="Added in v1.1.0"></a>
301344

302345
A hook for registering additional [markdown-it](https://github.com/markdown-it/markdown-it) plugins. For example, to use [markdown-it-footnote](https://github.com/markdown-it/markdown-it-footnote), first install the plugin with either `npm add markdown-it-footnote` or `yarn add markdown-it-footnote`, then register it like so:

docs/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ The <code>build</code> command generates the `dist` directory; you can then copy
535535

536536
<pre data-copy>npx http-server dist</pre>
537537

538-
<div class="tip">By default, Framework generates “clean” URLs by dropping the `.html` extension from page links. Not all webhosts support this; some need the <a href="./config#clean-urls"><b>cleanUrls</b> config option</a> set to false.</div>
538+
<div class="tip">By default, Framework generates “clean” URLs by dropping the `.html` extension from page links. Not all webhosts support this; some need the <a href="./config#preserve-extension"><b>preserveExtension</b> config option</a> set to true.</div>
539539

540540
<div class="tip">When deploying to GitHub Pages without using GitHub’s related actions (<a href="https://github.com/actions/configure-pages">configure-pages</a>,
541541
<a href="https://github.com/actions/deploy-pages">deploy-pages</a>, and

docs/lib/duckdb.md

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ const db2 = await DuckDBClient.of({base: FileAttachment("quakes.db")});
6565
db2.queryRow(`SELECT COUNT() FROM base.events`)
6666
```
6767

68-
For externally-hosted data, you can create an empty `DuckDBClient` and load a table from a SQL query, say using [`read_parquet`](https://duckdb.org/docs/guides/import/parquet_import) or [`read_csv`](https://duckdb.org/docs/guides/import/csv_import). DuckDB offers many affordances to make this easier (in many cases it detects the file format and uses the correct loader automatically).
68+
For externally-hosted data, you can create an empty `DuckDBClient` and load a table from a SQL query, say using [`read_parquet`](https://duckdb.org/docs/guides/import/parquet_import) or [`read_csv`](https://duckdb.org/docs/guides/import/csv_import). DuckDB offers many affordances to make this easier. (In many cases it detects the file format and uses the correct loader automatically.)
6969

7070
```js run=false
7171
const db = await DuckDBClient.of();
@@ -105,3 +105,96 @@ const sql = DuckDBClient.sql({quakes: `https://earthquake.usgs.gov/earthquakes/f
105105
```sql echo
106106
SELECT * FROM quakes ORDER BY updated DESC;
107107
```
108+
109+
## Extensions <a href="https://github.com/observablehq/framework/pull/1734" class="observablehq-version-badge" data-version="prerelease" title="Added in #1734"></a>
110+
111+
[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:
112+
113+
```sql echo
114+
SELECT bbox FROM read_json('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson');
115+
```
116+
117+
To read a local file (or data loader), use `FileAttachment` and interpolation `${…}`:
118+
119+
```sql echo
120+
SELECT bbox FROM read_json(${FileAttachment("../quakes.json").href});
121+
```
122+
123+
For convenience, Framework configures the `json` and `parquet` extensions by default. Some other [core extensions](https://duckdb.org/docs/extensions/core_extensions.html) also autoload, meaning that you don’t need to explicitly enable them; however, Framework will only [self-host extensions](#self-hosting-of-extensions) if you explicitly configure them, and therefore we recommend that you always use the [**duckdb** config option](../config#duckdb) to configure DuckDB extensions. Any configured extensions will be automatically [installed and loaded](https://duckdb.org/docs/extensions/overview#explicit-install-and-load), making them available in SQL code blocks as well as the `sql` and `DuckDBClient` built-ins.
124+
125+
For example, to configure the [`spatial` extension](https://duckdb.org/docs/extensions/spatial/overview.html):
126+
127+
```js run=false
128+
export default {
129+
duckdb: {
130+
extensions: ["spatial"]
131+
}
132+
};
133+
```
134+
135+
You can then use the `ST_Area` function to compute the area of a polygon:
136+
137+
```sql echo run=false
138+
SELECT ST_Area('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'::GEOMETRY) as area;
139+
```
140+
141+
To tell which extensions have been loaded, you can run the following query:
142+
143+
```sql echo
144+
FROM duckdb_extensions() WHERE loaded;
145+
```
146+
147+
<div class="warning">
148+
149+
If the `duckdb_extensions()` function runs before DuckDB autoloads a core extension (such as `json`), it might not be included in the returned set.
150+
151+
</div>
152+
153+
### Self-hosting of extensions
154+
155+
As with [npm imports](../imports#self-hosting-of-npm-imports), configured DuckDB extensions are self-hosted, improving performance, stability, & security, and allowing you to develop offline. Extensions are downloaded to the DuckDB cache folder, which lives in <code>.observablehq/<wbr>cache/<wbr>\_duckdb</code> within the source root (typically `src`). You can clear the cache and restart the preview server to re-fetch the latest versions of any DuckDB extensions. If you use an [autoloading core extension](https://duckdb.org/docs/extensions/core_extensions.html#list-of-core-extensions) that is not configured, DuckDB-Wasm [will load it](https://duckdb.org/docs/api/wasm/extensions.html#fetching-duckdb-wasm-extensions) from the default extension repository, `extensions.duckdb.org`, at runtime.
156+
157+
## Configuring
158+
159+
The second argument to `DuckDBClient.of` and `DuckDBClient.sql` is a [`DuckDBConfig`](https://shell.duckdb.org/docs/interfaces/index.DuckDBConfig.html) object which configures the behavior of DuckDB-Wasm. By default, Framework sets the `castBigIntToDouble` and `castTimestampToDate` query options to true. To instead use [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt):
160+
161+
```js run=false
162+
const bigdb = DuckDBClient.of({}, {query: {castBigIntToDouble: false}});
163+
```
164+
165+
By default, `DuckDBClient.of` and `DuckDBClient.sql` automatically load all [configured extensions](#extensions). To change the loaded extensions for a particular `DuckDBClient`, use the **extensions** config option. For example, pass an empty array to instantiate a DuckDBClient with no loaded extensions (even if your configuration lists several):
166+
167+
```js echo run=false
168+
const simpledb = DuckDBClient.of({}, {extensions: []});
169+
```
170+
171+
Alternatively, you can configure extensions to be self-hosted but not load by default using the **duckdb** config option and the `load: false` shorthand:
172+
173+
```js run=false
174+
export default {
175+
duckdb: {
176+
extensions: {
177+
spatial: false,
178+
h3: false
179+
}
180+
}
181+
};
182+
```
183+
184+
You can then selectively load extensions as needed like so:
185+
186+
```js echo run=false
187+
const geosql = DuckDBClient.sql({}, {extensions: ["spatial", "h3"]});
188+
```
189+
190+
In the future, we’d like to allow DuckDB to be configured globally (beyond just [extensions](#extensions)) via the [**duckdb** config option](../config#duckdb); please upvote [#1791](https://github.com/observablehq/framework/issues/1791) if you are interested in this feature.
191+
192+
## Versioning
193+
194+
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:
195+
196+
```js run=false
197+
import * as duckdb from "npm:@duckdb/[email protected]";
198+
```
199+
200+
However, you will not be able to change the version of DuckDB-Wasm used by SQL code blocks or the `sql` or `DuckDBClient` built-ins, nor can you use Framework’s support for self-hosting extensions with a different version of DuckDB-Wasm.

docs/project-structure.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ For this site, routes map to files as:
9999
/hello → dist/hello.html → src/hello.md
100100
```
101101

102-
This assumes [“clean URLs”](./config#clean-urls) as supported by most static site servers; `/hello` can also be accessed as `/hello.html`, and `/` can be accessed as `/index` and `/index.html`. (Some static site servers automatically redirect to clean URLs, but we recommend being consistent when linking to your site.)
102+
This assumes [“clean URLs”](./config#preserve-extension) as supported by most static site servers; `/hello` can also be accessed as `/hello.html`, and `/` can be accessed as `/index` and `/index.html`. (Some static site servers automatically redirect to clean URLs, but we recommend being consistent when linking to your site.)
103103

104104
Apps should always have a top-level `index.md` in the source root; this is your app’s home page, and it’s what people visit by default.
105105

docs/sql.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ sql:
2929

3030
<div class="tip">For performance and reliability, we recommend using local files rather than loading data from external servers at runtime. You can use a <a href="./data-loaders">data loader</a> to take a snapshot of a remote data during build if needed.</div>
3131

32-
You can also register tables via code (say to have sources that are defined dynamically via user input) by defining the `sql` symbol with [DuckDBClient.sql](./lib/duckdb).
32+
You can also register tables via code (say to have sources that are defined dynamically via user input) by defining the `sql` symbol with [DuckDBClient.sql](./lib/duckdb). To register [DuckDB extensions](./lib/duckdb#extensions), use the [**duckdb** config option](./config#duckdb).
3333

3434
## SQL code blocks
3535

package.json

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@
2424
"docs:deploy": "tsx --no-warnings=ExperimentalWarning ./src/bin/observable.ts deploy",
2525
"build": "rimraf dist && node build.js --outdir=dist --outbase=src \"src/**/*.{ts,js,css}\" --ignore \"**/*.d.ts\"",
2626
"test": "concurrently npm:test:mocha npm:test:tsc npm:test:lint npm:test:prettier",
27-
"test:coverage": "c8 --check-coverage --lines 80 --per-file yarn test:mocha",
28-
"test:build": "rimraf test/build && cross-env npm_package_version=1.0.0-test node build.js --sourcemap --outdir=test/build \"{src,test}/**/*.{ts,js,css}\" --ignore \"test/input/**\" --ignore \"test/output/**\" --ignore \"test/preview/dashboard/**\" --ignore \"**/*.d.ts\" && cp -r templates test/build",
29-
"test:mocha": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 -p \"test/build/test/**/*-test.js\" && yarn test:annotate",
30-
"test:mocha:serial": "yarn test:build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/*-test.js\" && yarn test:annotate",
31-
"test:annotate": "yarn test:build && cross-env OBSERVABLE_ANNOTATE_FILES=true TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/annotate.js\"",
27+
"test:coverage": "c8 --check-coverage --lines 80 --per-file yarn test:mocha:all",
28+
"test:build": "rimraf test/build && rimraf --glob test/.observablehq/cache test/input/build/*/.observablehq/cache && cross-env npm_package_version=1.0.0-test node build.js --sourcemap --outdir=test/build \"{src,test}/**/*.{ts,js,css}\" --ignore \"test/input/**\" --ignore \"test/output/**\" --ignore \"test/preview/dashboard/**\" --ignore \"**/*.d.ts\" && cp -r templates test/build",
29+
"test:mocha": "yarn test:mocha:serial -p",
30+
"test:mocha:serial": "yarn test:build && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/*-test.js\"",
31+
"test:mocha:annotate": "yarn test:build && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 OBSERVABLE_ANNOTATE_FILES=true TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/annotate.js\"",
32+
"test:mocha:all": "yarn test:mocha && cross-env OBSERVABLE_TELEMETRY_DISABLE=1 OBSERVABLE_ANNOTATE_FILES=true TZ=America/Los_Angeles mocha --timeout 30000 \"test/build/test/**/annotate.js\"",
3233
"test:lint": "eslint src test --max-warnings=0",
3334
"test:prettier": "prettier --check src test",
3435
"test:tsc": "tsc --noEmit",
@@ -56,7 +57,8 @@
5657
"dependencies": {
5758
"@clack/prompts": "^0.7.0",
5859
"@observablehq/inputs": "^0.12.0",
59-
"@observablehq/runtime": "^5.9.4",
60+
"@observablehq/inspector": "^5.0.1",
61+
"@observablehq/runtime": "^6.0.0-rc.1",
6062
"@rollup/plugin-commonjs": "^25.0.7",
6163
"@rollup/plugin-json": "^6.1.0",
6264
"@rollup/plugin-node-resolve": "^15.2.3",

src/build.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {existsSync} from "node:fs";
33
import {copyFile, readFile, rm, stat, writeFile} from "node:fs/promises";
44
import {basename, dirname, extname, join} from "node:path/posix";
55
import type {Config} from "./config.js";
6+
import {getDuckDBManifest} from "./duckdb.js";
67
import {CliError} from "./error.js";
78
import {getClientPath, prepareOutput} from "./files.js";
89
import {findModule, getModuleHash, readJavaScript} from "./javascript/module.js";
@@ -53,7 +54,7 @@ export async function build(
5354
{config}: BuildOptions,
5455
effects: BuildEffects = new FileBuildEffects(config.output, join(config.root, ".observablehq", "cache"))
5556
): Promise<void> {
56-
const {root, loaders} = config;
57+
const {root, loaders, duckdb} = config;
5758
Telemetry.record({event: "build", step: "start"});
5859

5960
// Prepare for build (such as by emptying the existing output root).
@@ -140,6 +141,21 @@ export async function build(
140141
effects.logger.log(cachePath);
141142
}
142143

144+
// Copy over the DuckDB extensions, initializing aliases that are needed to
145+
// construct the DuckDB manifest.
146+
for (const path of globalImports) {
147+
if (path.startsWith("/_duckdb/")) {
148+
const sourcePath = join(cacheRoot, path);
149+
effects.output.write(`${faint("build")} ${path} ${faint("→")} `);
150+
const contents = await readFile(sourcePath);
151+
const hash = createHash("sha256").update(contents).digest("hex").slice(0, 8);
152+
const [, , , version, bundle, name] = path.split("/");
153+
const alias = join("/_duckdb/", `${basename(name, ".duckdb_extension.wasm")}-${hash}`, version, bundle, name);
154+
aliases.set(path, alias);
155+
await effects.writeFile(alias, contents);
156+
}
157+
}
158+
143159
// Generate the client bundles. These are initially generated into the cache
144160
// because we need to rewrite any npm and node imports to be hashed; this is
145161
// handled generally for all global imports below.
@@ -149,6 +165,7 @@ export async function build(
149165
effects.output.write(`${faint("bundle")} ${path} ${faint("→")} `);
150166
const clientPath = getClientPath(path === "/_observablehq/client.js" ? "index.js" : path.slice("/_observablehq/".length)); // prettier-ignore
151167
const define: {[key: string]: string} = {};
168+
if (path === "/_observablehq/stdlib/duckdb.js") define["DUCKDB_MANIFEST"] = JSON.stringify(await getDuckDBManifest(duckdb, {root, aliases})); // prettier-ignore
152169
const contents = await rollupClient(clientPath, root, path, {minify: true, keepNames: true, define});
153170
await prepareOutput(cachePath);
154171
await writeFile(cachePath, contents);
@@ -202,9 +219,10 @@ export async function build(
202219

203220
// Copy over global assets (e.g., minisearch.json, DuckDB’s WebAssembly).
204221
// Anything in _observablehq also needs a content hash, but anything in _npm
205-
// or _node does not (because they are already necessarily immutable).
222+
// or _node does not (because they are already necessarily immutable). We’re
223+
// skipping DuckDB’s extensions because they were previously copied above.
206224
for (const path of globalImports) {
207-
if (path.endsWith(".js")) continue;
225+
if (path.endsWith(".js") || path.startsWith("/_duckdb/")) continue;
208226
const sourcePath = join(cacheRoot, path);
209227
effects.output.write(`${faint("build")} ${path} ${faint("→")} `);
210228
if (path.startsWith("/_observablehq/")) {

src/client/runtime.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export {Inspector, Runtime, RuntimeError} from "@observablehq/runtime";
1+
export {Inspector} from "@observablehq/inspector";
2+
export {Runtime, RuntimeError} from "@observablehq/runtime";

src/client/stdlib.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,3 @@ export {AbstractFile, FileAttachment, registerFile} from "./stdlib/fileAttachmen
22
export * as Generators from "./stdlib/generators/index.js";
33
export {Mutable} from "./stdlib/mutable.js";
44
export {resize} from "./stdlib/resize.js";
5-
export class Library {} // TODO remove @observablehq/runtime dependency
6-
export const FileAttachments = undefined; // TODO remove @observablehq/runtime dependency

0 commit comments

Comments
 (0)