Skip to content

Commit 2143147

Browse files
committed
Revamp file-based data access
- New datasource URL syntax based on ZEP 8 proposal (zarr-developers/zeps#48) - Support for ZIP archives
1 parent 5898236 commit 2143147

File tree

578 files changed

+17882
-5394
lines changed

Some content is hidden

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

578 files changed

+17882
-5394
lines changed

.github/workflows/build.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
fetch-depth: 0
3232
- uses: actions/setup-node@v4
3333
with:
34-
node-version: 20.x
34+
node-version: 22.x
3535
cache: "npm"
3636
cache-dependency-path: |
3737
package-lock.json
@@ -99,7 +99,7 @@ jobs:
9999
fetch-depth: 0
100100
- uses: actions/setup-node@v4
101101
with:
102-
node-version: 20.x
102+
node-version: 22.x
103103
- name: Set up Python ${{ matrix.python-version }}
104104
uses: actions/setup-python@v5
105105
with:
@@ -146,7 +146,7 @@ jobs:
146146
fetch-depth: 0
147147
- uses: actions/setup-node@v4
148148
with:
149-
node-version: 20.x
149+
node-version: 22.x
150150
cache: "npm"
151151
- name: Set up Python
152152
uses: actions/setup-python@v5
@@ -221,7 +221,7 @@ jobs:
221221
- name: Use Node.js
222222
uses: actions/setup-node@v4
223223
with:
224-
node-version: 20.x
224+
node-version: 22.x
225225
registry-url: "https://registry.npmjs.org"
226226
- uses: actions/download-artifact@v4
227227
with:

.github/workflows/build_preview.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
strategy:
99
matrix:
1010
node-version:
11-
- "20.x"
11+
- "22.x"
1212
runs-on: ubuntu-latest
1313

1414
steps:

.prettierignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
/templates/
22
/python/
33
/third_party/jpgjs/jpg.js
4-
/testdata/*.json
4+
/testdata/**/*.json
5+
zarr.json
56
.parcel-cache
67
dist
78
/lib
89
/docs/_build/
910
/.ruff_cache
11+
package.json
12+
package-lock.json

build_tools/build-package.ts

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ async function buildPackage(options: {
7171
outbase: srcDir,
7272
bundle: false,
7373
outdir: libDir,
74+
target: "es2022",
7475
});
7576

7677
let compilerOptionsFromConfigFile: ts.CompilerOptions = {};

build_tools/update-conditions.ts

+84-80
Original file line numberDiff line numberDiff line change
@@ -17,70 +17,100 @@ const imports: Record<string, any> = {};
1717
imports["#src/third_party/jpgjs/jpg.js"] = "./src/third_party/jpgjs/jpg.js";
1818
imports["#src/*.js"] = "./src/*.ts";
1919
imports["#src/*"] = "./src/*";
20+
imports["#tests/fixtures/msw"] = {
21+
node: "./tests/fixtures/msw_node.ts",
22+
default: "./tests/fixtures/msw_browser.ts",
23+
};
24+
imports["#tests/fixtures/gl"] = {
25+
node: "./tests/fixtures/gl_node.ts",
26+
default: "./tests/fixtures/gl_browser.ts",
27+
};
28+
imports["#tests/*.js"] = "./tests/*.ts";
2029
imports["#testdata/*"] = "./testdata/*";
2130

22-
const datasourceDir = path.resolve(rootDir, "src", "datasource");
23-
const layerDir = path.resolve(rootDir, "src", "layer");
24-
25-
const datasources = (
26-
await fs.promises.readdir(datasourceDir, { withFileTypes: true })
27-
)
28-
.filter((e) => e.isDirectory())
29-
.map((e) => e.name);
30-
31-
const layers = (await fs.promises.readdir(layerDir, { withFileTypes: true }))
32-
.filter((e) => e.isDirectory())
33-
.map((e) => e.name);
34-
35-
const datasourceKeys = {
36-
backend: "backend",
37-
async_computation: "async_computation",
38-
register_default: "frontend",
39-
register_credentials_provider: "frontend",
40-
} as const;
31+
async function listSubdirs(dir: string): Promise<string[]> {
32+
return (await fs.promises.readdir(dir, { withFileTypes: true }))
33+
.filter((e) => e.isDirectory())
34+
.map((e) => e.name);
35+
}
4136

42-
const datasourceModules = Object.fromEntries(
43-
Object.values(datasourceKeys).map((key) => [key, new Array<string>()]),
44-
);
37+
async function writeModule(modulePath: string, imports: string[]) {
38+
await fs.promises.writeFile(
39+
modulePath,
40+
"// DO NOT EDIT: Generated by config/update_conditions.ts\n" +
41+
imports.map((name) => `import ${JSON.stringify(name)};\n`).join(""),
42+
{ encoding: "utf-8" },
43+
);
44+
}
4545

46-
for (const datasource of datasources) {
47-
for (const [filePrefix, moduleKind] of Object.entries(datasourceKeys)) {
48-
const sourcePrefix = `./src/datasource/${datasource}/${filePrefix}`;
49-
if (
50-
await fs.promises
51-
.stat(path.resolve(rootDir, `${sourcePrefix}.ts`))
52-
.catch(() => undefined)
53-
) {
54-
const source = sourcePrefix + JS_EXT;
55-
const conditions: Record<string, string> = {};
56-
if (datasource === "python") {
57-
conditions["neuroglancer/python"] = source;
58-
conditions.default = NOOP;
59-
} else {
60-
if (filePrefix === "register_credentials_provider") {
61-
conditions["neuroglancer/python"] = NOOP;
46+
async function handleDrivers(
47+
kind: string,
48+
moduleMap: Record<string, string[]>,
49+
) {
50+
const driverDir = path.resolve(rootDir, "src", kind);
51+
const drivers = await listSubdirs(driverDir);
52+
const modules: Record<string, string[]> = {};
53+
for (const driver of drivers) {
54+
for (const [filePrefix, moduleKinds] of Object.entries(moduleMap)) {
55+
const sourcePrefix = `./src/${kind}/${driver}/${filePrefix}`;
56+
if (
57+
await fs.promises
58+
.stat(path.resolve(rootDir, `${sourcePrefix}.ts`))
59+
.catch(() => undefined)
60+
) {
61+
const source = sourcePrefix + JS_EXT;
62+
const conditions: Record<string, string> = {};
63+
if (driver === "python") {
64+
conditions["neuroglancer/python"] = source;
65+
conditions.default = NOOP;
66+
} else {
67+
if (filePrefix === "register_credentials_provider") {
68+
conditions["neuroglancer/python"] = NOOP;
69+
}
70+
conditions[`neuroglancer/${kind}/${driver}:enabled`] = source;
71+
conditions[`neuroglancer/${kind}:none_by_default`] = NOOP;
72+
conditions[`neuroglancer/${kind}/${driver}:disabled`] = NOOP;
73+
conditions.default = source;
74+
}
75+
let moduleId = `#${kind}/${driver}`;
76+
if (filePrefix !== "index") {
77+
moduleId += `/${filePrefix}`;
78+
}
79+
imports[moduleId] = conditions;
80+
for (const moduleKind of moduleKinds) {
81+
if (modules[moduleKind] === undefined) {
82+
modules[moduleKind] = [];
83+
}
84+
modules[moduleKind].push(moduleId);
6285
}
63-
conditions[`neuroglancer/datasource/${datasource}:enabled`] = source;
64-
conditions["neuroglancer/datasource:none_by_default"] = NOOP;
65-
conditions[`neuroglancer/datasource/${datasource}:disabled`] = source;
66-
conditions.default = source;
6786
}
68-
const moduleId = `#datasource/${datasource}/${filePrefix}`;
69-
imports[moduleId] = conditions;
70-
datasourceModules[moduleKind].push(moduleId);
7187
}
7288
}
89+
for (const [moduleKind, moduleIds] of Object.entries(modules)) {
90+
await writeModule(
91+
path.resolve(driverDir, `enabled_${moduleKind}_modules.ts`),
92+
moduleIds,
93+
);
94+
}
7395
}
7496

75-
for (const layer of layers) {
76-
const source = `./src/layer/${layer}/index` + JS_EXT;
77-
imports[`#layer/${layer}`] = {
78-
[`neuroglancer/layer/${layer}:enabled`]: source,
79-
"neuroglancer/layer:none_by_default": NOOP,
80-
[`neuroglancer/layer/${layer}:enabled`]: source,
81-
default: source,
82-
};
83-
}
97+
await handleDrivers("datasource", {
98+
backend: ["backend"],
99+
async_computation: ["async_computation"],
100+
register_default: ["frontend"],
101+
register_credentials_provider: ["frontend"],
102+
});
103+
104+
await handleDrivers("kvstore", {
105+
register: ["frontend", "backend"],
106+
register_frontend: ["frontend"],
107+
register_backend: ["backend"],
108+
register_credentials_provider: ["frontend"],
109+
});
110+
111+
await handleDrivers("layer", {
112+
index: ["frontend"],
113+
});
84114

85115
// main entrypoint.
86116
imports["#main"] = {
@@ -94,32 +124,6 @@ imports["#python_integration_build"] = {
94124
default: NOOP,
95125
};
96126

97-
async function writeModule(modulePath: string, imports: string[]) {
98-
await fs.promises.writeFile(
99-
modulePath,
100-
"// DO NOT EDIT: Generated by config/update_conditions.ts\n" +
101-
imports.map((name) => `import ${JSON.stringify(name)};\n`).join(""),
102-
{ encoding: "utf-8" },
103-
);
104-
}
105-
106-
for (const [moduleKind, moduleIds] of Object.entries(datasourceModules)) {
107-
await writeModule(
108-
path.resolve(
109-
rootDir,
110-
"src",
111-
"datasource",
112-
`enabled_${moduleKind}_modules.ts`,
113-
),
114-
moduleIds,
115-
);
116-
}
117-
118-
await writeModule(
119-
path.resolve(rootDir, "src", "layer", "enabled_frontend_modules.ts"),
120-
layers.map((name) => `#layer/${name}`),
121-
);
122-
123127
packageJson.imports = imports;
124128

125129
packageJson.exports = {
+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google Inc.
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
// Fake ngauth server used for tests.
18+
19+
import type { AddressInfo } from "node:net";
20+
import cookie from "cookie";
21+
import express from "express";
22+
23+
const COOKIE_NAME = "ngauth_login";
24+
const COOKIE_VALUE = "fake_login";
25+
const TOKEN_VALUE = "fake_token";
26+
27+
export interface FakeNgauthServer extends AsyncDisposable {
28+
url: string;
29+
}
30+
31+
export function startFakeNgauthServer(): Promise<FakeNgauthServer> {
32+
const app = express();
33+
app.use(express.json({ inflate: false, type: "*/*" }));
34+
app.get("/login", (req, res) => {
35+
const origin = (req.query.origin ?? "").toString();
36+
res.contentType("text/html");
37+
// Note: The real ngauth marks this cookie as http-only, but that prevents
38+
// JavaScript from clearing it.
39+
res.cookie(COOKIE_NAME, COOKIE_VALUE, { sameSite: "lax" });
40+
const jsonToken = JSON.stringify({ token: TOKEN_VALUE });
41+
const jsonOrigin = JSON.stringify(origin);
42+
res.send(`
43+
<html>
44+
<body>
45+
<script>
46+
window.opener.postMessage(${jsonToken}, ${jsonOrigin});
47+
</script>
48+
</body>
49+
</html>
50+
`);
51+
});
52+
app.post("/token", (req, res) => {
53+
const cookies = cookie.parse(req.headers.cookie ?? "");
54+
const origin = req.headers.origin ?? "";
55+
res.set("x-frame-options", "deny");
56+
res.set("access-control-allow-origin", origin);
57+
res.set("access-control-allow-credentials", "true");
58+
res.set("vary", "origin");
59+
60+
if (cookies[COOKIE_NAME] === COOKIE_VALUE) {
61+
res.contentType("text/plain");
62+
res.send(TOKEN_VALUE);
63+
} else {
64+
res.status(401);
65+
res.send();
66+
}
67+
});
68+
app.post("/gcs_token", (req, res) => {
69+
const origin = req.headers.origin ?? "";
70+
res.set("access-control-allow-origin", origin);
71+
res.set("vary", "origin");
72+
const { body } = req;
73+
if (
74+
typeof body !== "object" ||
75+
Array.isArray(body) ||
76+
body.token !== TOKEN_VALUE
77+
) {
78+
res.status(400);
79+
res.send();
80+
} else {
81+
res.json({ token: "fake_gcs_token:" + body.bucket });
82+
}
83+
});
84+
const server = app.listen(0, "localhost");
85+
return new Promise((resolve, reject) => {
86+
server.on("error", reject);
87+
server.on("listening", () => {
88+
const port = (server.address() as AddressInfo).port;
89+
resolve({
90+
url: `http://localhost:${port}`,
91+
[Symbol.asyncDispose]: () =>
92+
new Promise<void>((resolve) => server.close(() => resolve())),
93+
});
94+
});
95+
});
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { webcrypto } from "node:crypto";
2+
import type { JSDOM } from "jsdom";
3+
4+
declare let jsdom: JSDOM;
5+
6+
Object.defineProperty(globalThis, "crypto", {
7+
value: webcrypto,
8+
});
9+
10+
for (const name of [
11+
/*"DOMParser", "XPathResult", "navigator"*/
12+
] as const) {
13+
Object.defineProperty(globalThis, name, {
14+
value: jsdom.window[name],
15+
});
16+
}

build_tools/vitest/setup-crypto.ts

-7
This file was deleted.

0 commit comments

Comments
 (0)