Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/architecture/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,22 @@ Two constraints bound every choice:
| parsers | `sax` | 1.6.1 | Streaming XML for the read-only XLSX reader. Checked 2026-08-04: BlueOak-1.0.0, **zero dependencies**, 80 KB unpacked, no native code, no post-install script, published 2026-07-24, 85.5M weekly downloads. It is here because **no maintained XLSX library met the requirements**: the capable one is unmaintained and ships a permanent advisory, the maintained one cannot return formula text. Streaming matters twice over: it is what keeps the 500,000-row envelope at 85 MB rather than 297 MB, and refusing a `DOCTYPE` at the parser is the entity-expansion defence section 20.9 asks for. The reasoning and the measurements are in [`adr-xlsx-parser.md`](./adr-xlsx-parser.md). |
| runtime | `hono` | 4.12.33 | The HTTP surface. Chosen in architecture 8.5 because the same route handlers run on Node and on a Worker, which is what makes the Phase 6 projection a configuration change rather than a rewrite. Checked 2026-08-03: MIT, zero runtime dependencies, 1.36 MB unpacked, no native code, no post-install script, last release three days earlier. The zero-dependency part matters most: an HTTP framework is the usual place a transitive tree arrives. |

| deploy-cloudflare (**dev only**) | `wrangler`, `miniflare` | 4.119.0, 5.20260801.0-alpha | Running a Worker with D1 and R2 bindings **locally**, so the Phase 6 projection is built and tested without a Cloudflare account. Checked 2026-08-06: MIT/Apache-2.0. Dev-only is the whole justification: invariant 7 constrains what a *user* installs, and neither ever reaches a published package. `workerd` is native and ships a post-install script, declined in `pnpm-workspace.yaml` exactly as esbuild's is, because its binary arrives through `@cloudflare/workerd-<platform>` optional dependencies instead; verified by starting a Worker with live bindings with the script refused. Admitting these is what narrowed `check:no-native` to the published closure (#256), so `packages/deploy-cloudflare/test/emulator.test.ts` exists to prove the thing that bought is real. Miniflare's alpha is the version pairing with this wrangler; it is a test harness, so an alpha costs a broken test rather than a broken build. |

`node:sqlite`, `node:crypto` and `node:zlib` are used directly and are deliberately not
dependencies. That is the reason the supported Node floor is 24.15: see
[`docs/compatibility/sqlite-fts5.md`](../compatibility/sqlite-fts5.md).

## Adding one

Before adding a dependency, check and record: last release, open CVEs, licence, install
weight, whether it ships native code, and whether it runs a post-install script. Then add a
weight, whether it ships native code, and whether it runs a post-install script.

**Native code is judged by who installs it.** `check:no-native` guards the production closure of
the published packages, which is what a user gets. A dev-only tool that ships a binary is
allowed, and a published package that takes one is not, whatever it is for. The guard's own
tests are in `tools/arch/test/no-native.test.ts`, and the assertion that matters there is the
mutation: declare a native dependency on a published package and watch it fail. Then add a
row above with the capability it provides, not the problem it solves in general. If an
existing dependency or a Node builtin can do the job, that is the answer.

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@types/node": "24.13.3",
"typescript": "7.0.2",
"vitest": "4.1.10",
"wrangler": "4.119.0",
"zod": "4.4.3"
}
}
6 changes: 5 additions & 1 deletion packages/deploy-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
],
"scripts": {
"build": "tsc -b",
"clean": "rm -rf dist *.tsbuildinfo"
"clean": "rm -rf dist *.tsbuildinfo",
"test": "vitest run"
},
"dependencies": {
"@lorepack/core": "workspace:*"
Expand All @@ -29,5 +30,8 @@
"type": "git",
"url": "git+https://github.com/burakdede/lorepack.git",
"directory": "packages/deploy-cloudflare"
},
"devDependencies": {
"miniflare": "5.20260801.0-alpha"
}
}
67 changes: 67 additions & 0 deletions packages/deploy-cloudflare/test/emulator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Miniflare } from 'miniflare';
import { describe, expect, it } from 'vitest';

/**
* The emulator this phase is built against actually runs (#86).
*
* Phase 6 projects a build onto a Worker with D1 and R2 bindings, and every later ticket assumes
* that can be exercised locally. This is the one test that proves the assumption rather than
* inheriting it, so a broken environment says so plainly instead of surfacing as a confusing
* failure inside a projection test.
*
* It is deliberately about the *environment* rather than about Lorepack, and it is the reason
* the emulator is allowed to be a dependency at all: `check:no-native` was narrowed to the
* published closure for this (#256), so the thing it bought had better be verified.
*/

describe('the Cloudflare emulator', () => {
it('runs a Worker with a working D1 binding and R2 bucket', async () => {
const mf = new Miniflare({
modules: true,
script: `export default { async fetch(request, env) {
await env.DB.exec("CREATE TABLE IF NOT EXISTS probe (value TEXT)");
await env.DB.prepare("INSERT INTO probe (value) VALUES (?)").bind("from-d1").run();
const row = await env.DB.prepare("SELECT value FROM probe LIMIT 1").first();
await env.BUCKET.put("probe", "from-r2");
const object = await env.BUCKET.get("probe");
return Response.json({ d1: row.value, r2: await object.text() });
}}`,
d1Databases: { DB: 'probe-db' },
r2Buckets: { BUCKET: 'probe-bucket' },
});

try {
const response = await mf.dispatchFetch('http://localhost/');
// Both bindings in one request: a Worker that starts and cannot reach its storage is not
// an environment this phase can be built on.
expect(await response.json()).toEqual({ d1: 'from-d1', r2: 'from-r2' });
} finally {
await mf.dispose();
}
}, 120_000);

it('gives each database its own storage, so a candidate cannot leak into the active build', async () => {
const script = `export default { async fetch(request, env) {
await env.DB.exec("CREATE TABLE IF NOT EXISTS t (v TEXT)");
const url = new URL(request.url);
if (url.searchParams.has('write')) {
await env.DB.prepare("INSERT INTO t (v) VALUES (?)").bind("x").run();
}
const row = await env.DB.prepare("SELECT count(*) AS n FROM t").first();
return Response.json({ n: row.n });
}}`;

const one = new Miniflare({ modules: true, script, d1Databases: { DB: 'one' } });
const two = new Miniflare({ modules: true, script, d1Databases: { DB: 'two' } });
try {
await one.dispatchFetch('http://localhost/?write=1');
expect(await (await one.dispatchFetch('http://localhost/')).json()).toEqual({ n: 1 });
// The isolation that makes testing a *candidate* projection meaningful rather than
// incidental: writing one database must be invisible to the other.
expect(await (await two.dispatchFetch('http://localhost/')).json()).toEqual({ n: 0 });
} finally {
await one.dispose();
await two.dispose();
}
}, 120_000);
});
5 changes: 5 additions & 0 deletions packages/deploy-cloudflare/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: { name: 'deploy-cloudflare', include: ['test/**/*.test.ts'] },
});
Loading
Loading