@human-synthesis/norns 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { nornsPreprocess } from '@human-synthesis/norns-core/preprocess';
2
4
 
3
5
  /**
@@ -5,7 +7,13 @@ import { nornsPreprocess } from '@human-synthesis/norns-core/preprocess';
5
7
  *
6
8
  * Defaults:
7
9
  * - `extensions: ['.svelte', '.n']` — both vanilla and Norns components
8
- * - `kit.moduleExtensions: ['.js', '.ts', '.c']` — Kit special files (`+page.c` etc.)
10
+ * - `kit.moduleExtensions: ['.js', '.ts', '.c', '.civet']` — Kit special files
11
+ * (`+page.c`, `+page.civet`, etc.)
12
+ * - `kit.files.hooks.server` — set to `src/hooks.server.{c,civet}` if either
13
+ * file exists. SvelteKit's upstream `resolve_entry` only searches `.js` /
14
+ * `.ts` for hooks (it doesn't honor `moduleExtensions`), so the explicit
15
+ * path is the non-invasive way to make `.c`/`.civet` hooks discoverable.
16
+ * Same for the client and universal counterparts.
9
17
  * - `preprocess: nornsPreprocess()` — Coffee + Pug + rune fusion + auto-close
10
18
  *
11
19
  * Spread your own overrides at the call site to extend or replace defaults.
@@ -20,13 +28,50 @@ export function nornsConfig(overrides = {}) {
20
28
  extensions: extensionsOverride,
21
29
  ...rest
22
30
  } = overrides;
31
+
32
+ const cwd = process.cwd();
33
+ const userFiles = kitOverrides.files ?? {};
34
+ const userHooks = userFiles.hooks ?? {};
35
+
36
+ const hooks = {
37
+ ...userHooks,
38
+ server: userHooks.server ?? findHook(cwd, ['src/hooks.server.c', 'src/hooks.server.civet']),
39
+ client: userHooks.client ?? findHook(cwd, ['src/hooks.client.c', 'src/hooks.client.civet']),
40
+ universal: userHooks.universal ?? findHook(cwd, ['src/hooks.c', 'src/hooks.civet'])
41
+ };
42
+ // Drop keys whose value is undefined so SvelteKit applies its defaults
43
+ for (const k of /** @type {const} */ (['server', 'client', 'universal'])) {
44
+ if (hooks[k] === undefined) delete hooks[k];
45
+ }
46
+
47
+ const { files: _ignoredUserFiles, ...kitRest } = kitOverrides;
48
+
23
49
  return {
24
50
  extensions: extensionsOverride ?? ['.svelte', '.n'],
25
51
  preprocess: preprocessOverride ?? nornsPreprocess(),
26
52
  kit: {
27
- moduleExtensions: ['.js', '.ts', '.c'],
28
- ...kitOverrides
53
+ moduleExtensions: ['.js', '.ts', '.c', '.civet'],
54
+ ...kitRest,
55
+ files: {
56
+ ...userFiles,
57
+ hooks
58
+ }
29
59
  },
30
60
  ...rest
31
61
  };
32
62
  }
63
+
64
+ /**
65
+ * Return the first relative path whose file exists, or `undefined` so
66
+ * SvelteKit falls back to its default `.js` / `.ts` resolution.
67
+ *
68
+ * @param {string} cwd
69
+ * @param {string[]} candidates
70
+ * @returns {string | undefined}
71
+ */
72
+ function findHook(cwd, candidates) {
73
+ for (const rel of candidates) {
74
+ if (existsSync(join(cwd, rel))) return rel;
75
+ }
76
+ return undefined;
77
+ }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { nornsAutoImport } from './auto-import.js';
1
2
  export { nornsConfig } from './config.js';
2
- export { nornsCoffeePlugin } from './vite.js';
3
+ export { nornsCivetPlugin } from './vite.js';
3
4
  export { nornsPreprocess } from './preprocess.js';
package/src/migrate.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Migration discovery + applier for the `norns migrate` CLI. Pure functions —
3
+ * `bin/norns.js` is a thin shell over these.
4
+ *
5
+ * Layout convention:
6
+ * <project>/migrations/<feature>/<timestamp>_<slug>.sql
7
+ *
8
+ * Migrations live at the project root, OUTSIDE `src/lib/`, organised per
9
+ * feature. They aren't application code — they're operational artifacts that
10
+ * tooling (this CLI, wrangler for D1) reads. Keeping them out of `src/lib`
11
+ * also keeps SvelteKit's bundler from ever trying to ship them.
12
+ *
13
+ * v1 supports SQLite via `better-sqlite3`. Postgres/libSQL come later;
14
+ * Cloudflare D1 is intentionally out of scope here — use
15
+ * `wrangler d1 migrations apply <db>` for D1 deploys.
16
+ */
17
+
18
+ import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { createRequire } from 'node:module';
21
+
22
+ /**
23
+ * Reserved names under `src/lib/`. These are treated as utility folders, not
24
+ * features. Anything starting with `_` is also reserved.
25
+ */
26
+ export const RESERVED_LIB_DIRS = new Set(['server', 'components', 'utils']);
27
+
28
+ export const MIGRATION_TABLE = 'norns_migrations';
29
+
30
+ /**
31
+ * List feature folder names under `src/lib/`. A folder is a feature iff it
32
+ * contains `server/module.c`.
33
+ *
34
+ * @param {string} libDir
35
+ * @returns {string[]}
36
+ */
37
+ export function listFeatures(libDir) {
38
+ if (!existsSync(libDir)) return [];
39
+ const out = [];
40
+ for (const name of readdirSync(libDir)) {
41
+ if (name.startsWith('_')) continue;
42
+ if (RESERVED_LIB_DIRS.has(name)) continue;
43
+ const featureDir = join(libDir, name);
44
+ try {
45
+ if (!statSync(featureDir).isDirectory()) continue;
46
+ } catch {
47
+ continue;
48
+ }
49
+ const modulePath = join(featureDir, 'server', 'module.c');
50
+ if (existsSync(modulePath)) out.push(name);
51
+ }
52
+ out.sort();
53
+ return out;
54
+ }
55
+
56
+ /** @typedef {{ feature: string, file: string, path: string, id: string }} Migration */
57
+
58
+ /** @param {string} cwd */
59
+ export function migrationsRoot(cwd) {
60
+ return join(cwd, 'migrations');
61
+ }
62
+
63
+ /**
64
+ * Scan `<cwd>/migrations/<feature>/*.sql` across all features. Sorted by
65
+ * filename first (so timestamp-prefixed files apply chronologically across
66
+ * features) and feature name as a tiebreaker.
67
+ *
68
+ * @param {string} cwd
69
+ * @returns {Migration[]}
70
+ */
71
+ export function listMigrations(cwd) {
72
+ const root = migrationsRoot(cwd);
73
+ if (!existsSync(root)) return [];
74
+ const out = [];
75
+ for (const feature of readdirSync(root)) {
76
+ const featDir = join(root, feature);
77
+ try {
78
+ if (!statSync(featDir).isDirectory()) continue;
79
+ } catch {
80
+ continue;
81
+ }
82
+ for (const file of readdirSync(featDir)) {
83
+ if (!file.endsWith('.sql')) continue;
84
+ out.push({
85
+ feature,
86
+ file,
87
+ path: join(featDir, file),
88
+ id: `${feature}/${file.replace(/\.sql$/, '')}`
89
+ });
90
+ }
91
+ }
92
+ out.sort((a, b) => {
93
+ const c = a.file.localeCompare(b.file);
94
+ return c !== 0 ? c : a.feature.localeCompare(b.feature);
95
+ });
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * Resolve `DATABASE_URL` to a connection. v1: only `file:` (SQLite via
101
+ * better-sqlite3). Defaults to `file:./data/app.db` if unset.
102
+ *
103
+ * @param {string} cwd
104
+ * @returns {{ kind: 'sqlite', path: string }}
105
+ */
106
+ export function resolveDatabaseUrl(cwd) {
107
+ const url = process.env.DATABASE_URL || `file:${join(cwd, 'data', 'app.db')}`;
108
+ if (url.startsWith('file:')) return { kind: 'sqlite', path: url.slice(5) };
109
+ const scheme = url.split('://')[0];
110
+ throw new Error(
111
+ `norns migrate: only SQLite (file:...) is supported in v1; got "${scheme}://...".\n` +
112
+ ' For Cloudflare D1, use `wrangler d1 migrations apply <db>`.\n' +
113
+ ' Postgres/libSQL via the CLI are not yet wired.'
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Open a better-sqlite3 db at `path` and ensure the migration tracking table
119
+ * exists.
120
+ *
121
+ * @param {string} cwd directory whose `package.json` is used to resolve
122
+ * better-sqlite3 from the consumer's node_modules
123
+ * @param {string} path SQLite file path
124
+ * @param {{ requireFrom?: string | URL }} [opts] override the require base
125
+ * (used by tests)
126
+ * @returns {any}
127
+ */
128
+ export function openSqliteDb(cwd, path, opts = {}) {
129
+ mkdirSync(dirname(path), { recursive: true });
130
+ const require = createRequire(opts.requireFrom ?? join(cwd, 'package.json'));
131
+ let Database;
132
+ try {
133
+ Database = require('better-sqlite3');
134
+ } catch {
135
+ throw new Error(
136
+ 'norns migrate: `better-sqlite3` is not installed in this app. Run: bun add better-sqlite3'
137
+ );
138
+ }
139
+ const db = new Database(path);
140
+ db.pragma('journal_mode = WAL');
141
+ db.exec(`CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
142
+ id TEXT PRIMARY KEY,
143
+ applied_at INTEGER NOT NULL
144
+ )`);
145
+ return db;
146
+ }
147
+
148
+ /**
149
+ * @param {any} db
150
+ * @returns {Set<string>}
151
+ */
152
+ export function getApplied(db) {
153
+ const rows = db.prepare(`SELECT id FROM ${MIGRATION_TABLE}`).all();
154
+ return new Set(rows.map((r) => r.id));
155
+ }
156
+
157
+ /**
158
+ * Apply pending migrations to `db`. Returns the list of ids applied.
159
+ *
160
+ * @param {any} db
161
+ * @param {Migration[]} pending
162
+ * @returns {string[]}
163
+ */
164
+ export function applyMigrations(db, pending) {
165
+ const insert = db.prepare(`INSERT INTO ${MIGRATION_TABLE} (id, applied_at) VALUES (?, ?)`);
166
+ const applied = [];
167
+ for (const m of pending) {
168
+ const sql = readFileSync(m.path, 'utf8');
169
+ const tx = db.transaction(() => {
170
+ db.exec(sql);
171
+ insert.run(m.id, Date.now());
172
+ });
173
+ tx();
174
+ applied.push(m.id);
175
+ }
176
+ return applied;
177
+ }
178
+
179
+ /**
180
+ * Scaffold a new migration file at `<cwd>/migrations/<feature>/<ts>_<slug>.sql`.
181
+ *
182
+ * No filesystem check on the feature name — both `src/lib/<feature>/` (the
183
+ * default convention) and nested layouts like `src/lib/<group>/<feature>/`
184
+ * (used by demos that mirror multiple variants side by side) are valid.
185
+ * Typo'd feature names produce orphan folders that are easy to spot under
186
+ * `migrations/`.
187
+ *
188
+ * @param {string} cwd
189
+ * @param {string} arg `<feature>/<name>` form
190
+ * @returns {string} the path of the created file
191
+ */
192
+ export function createMigration(cwd, arg) {
193
+ if (!arg || !arg.includes('/')) {
194
+ throw new Error(
195
+ 'Usage: norns migrate create <feature>/<name>\nExample: norns migrate create notes/add_pinned_column'
196
+ );
197
+ }
198
+ const [feature, ...rest] = arg.split('/');
199
+ const slug = rest.join('/').replace(/[^a-zA-Z0-9_]+/g, '_');
200
+ const migDir = join(migrationsRoot(cwd), feature);
201
+ mkdirSync(migDir, { recursive: true });
202
+ const ts = new Date()
203
+ .toISOString()
204
+ .replace(/[-:T]/g, '')
205
+ .replace(/\..+$/, '')
206
+ .slice(0, 14);
207
+ const file = join(migDir, `${ts}_${slug}.sql`);
208
+ writeFileSync(file, `-- ${feature}: ${slug}\n-- Created: ${new Date().toISOString()}\n\n`, {
209
+ flag: 'wx'
210
+ });
211
+ return file;
212
+ }
@@ -0,0 +1,74 @@
1
+ import { sequence } from '@sveltejs/kit/hooks';
2
+ import { Container } from './container.js';
3
+ import { contextHandle } from './handle/context.js';
4
+ import { errorHandle } from './handle/error.js';
5
+
6
+ /**
7
+ * Create a fresh root container with no features registered. Useful for tests
8
+ * that want full control over what's bound.
9
+ *
10
+ * @returns {Container}
11
+ */
12
+ export function createApp() {
13
+ return new Container();
14
+ }
15
+
16
+ /** @typedef {(app: Container) => void | Promise<void>} ModuleRegister */
17
+ /** @typedef {{ default?: ModuleRegister } | ModuleRegister} FeatureModule */
18
+
19
+ /**
20
+ * Boot a Norns app: builds the root container, runs every feature's
21
+ * `module.c` registration, and returns the SvelteKit hooks ready to wire into
22
+ * `src/hooks.server.c`.
23
+ *
24
+ * Typical use in a consumer app:
25
+ *
26
+ * import { boot } from '@human-synthesis/norns/server';
27
+ *
28
+ * const app = await boot({
29
+ * features: import.meta.glob('./lib/*\/server/module.c', { eager: true })
30
+ * });
31
+ * export const { handle, handleError, container } = app;
32
+ *
33
+ * Each `module.c` must default-export a function `(app) -> ...` that calls
34
+ * `app.bind(...)` / `app.single(...)` / `app.migrations(...)`.
35
+ *
36
+ * @param {{
37
+ * features?: Record<string, FeatureModule>,
38
+ * extraHandle?: import('@sveltejs/kit').Handle | import('@sveltejs/kit').Handle[],
39
+ * handleError?: import('@sveltejs/kit').HandleServerError
40
+ * }} [opts]
41
+ * @returns {Promise<{
42
+ * container: Container,
43
+ * handle: import('@sveltejs/kit').Handle,
44
+ * handleError: import('@sveltejs/kit').HandleServerError
45
+ * }>}
46
+ */
47
+ export async function boot(opts = {}) {
48
+ const container = createApp();
49
+
50
+ if (opts.features) {
51
+ for (const [path, mod] of Object.entries(opts.features)) {
52
+ const register = /** @type {ModuleRegister | undefined} */ (
53
+ typeof mod === 'function' ? mod : mod?.default
54
+ );
55
+ if (typeof register !== 'function') {
56
+ throw new Error(
57
+ `Norns: ${path} must default-export a function (app) -> ... — got ${typeof register}`
58
+ );
59
+ }
60
+ await register(container);
61
+ }
62
+ }
63
+
64
+ const extras = opts.extraHandle
65
+ ? Array.isArray(opts.extraHandle)
66
+ ? opts.extraHandle
67
+ : [opts.extraHandle]
68
+ : [];
69
+
70
+ const handle = sequence(contextHandle(container), ...extras);
71
+ const handleError = opts.handleError ?? errorHandle();
72
+
73
+ return { container, handle, handleError };
74
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Norns DI container.
3
+ *
4
+ * Tokens are namespaced strings (e.g. `notes.repo`, `db`). Bindings register a
5
+ * factory that produces an instance; `single` bindings memoize at the level
6
+ * where they are declared, `bind` bindings re-run the factory on every resolve.
7
+ *
8
+ * A child scope (created via `scope()`) inherits its parent's bindings but
9
+ * tracks its own overrides and request-scoped singletons. Request scopes are
10
+ * the natural fit for things like `db` (a transactional handle for one
11
+ * request) or `user` (the authenticated principal).
12
+ *
13
+ * Overrides take precedence over bindings at every level — used in tests to
14
+ * swap a real service for a fake without rebinding the production module.
15
+ *
16
+ * Migration directories are tracked at the root container only; child scopes
17
+ * inherit visibility through the root walk.
18
+ */
19
+
20
+ /** @typedef {(c: Container) => any} Factory */
21
+ /** @typedef {{ factory: Factory, lifetime: 'transient' | 'singleton' }} Binding */
22
+
23
+ export class Container {
24
+ /** @param {Container | null} [parent] */
25
+ constructor(parent = null) {
26
+ /** @type {Container | null} */
27
+ this.parent = parent;
28
+ /** @type {Map<string, Binding>} */
29
+ this.bindings = new Map();
30
+ /** @type {Map<string, any>} */
31
+ this.singletons = new Map();
32
+ /** @type {Map<string, Factory>} */
33
+ this.overrides = new Map();
34
+ /** @type {string[]} */
35
+ this.migrationDirs = [];
36
+ }
37
+
38
+ /**
39
+ * Bind a transient factory — called on every `resolve(token)`.
40
+ *
41
+ * @param {string} token
42
+ * @param {Factory} factory
43
+ * @returns {this}
44
+ */
45
+ bind(token, factory) {
46
+ this.bindings.set(token, { factory, lifetime: 'transient' });
47
+ return this;
48
+ }
49
+
50
+ /**
51
+ * Bind a singleton factory — called once per container scope.
52
+ *
53
+ * @param {string} token
54
+ * @param {Factory} factory
55
+ * @returns {this}
56
+ */
57
+ single(token, factory) {
58
+ this.bindings.set(token, { factory, lifetime: 'singleton' });
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ * Override a token at this scope. Wins over any binding in this or any
64
+ * parent scope. Useful in tests.
65
+ *
66
+ * @param {string} token
67
+ * @param {Factory} factory
68
+ * @returns {this}
69
+ */
70
+ override(token, factory) {
71
+ this.overrides.set(token, factory);
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Register a migration directory. Tracked at the root container.
77
+ *
78
+ * @param {string} dir absolute path to a `migrations/` directory containing
79
+ * `*.sql` files
80
+ * @returns {this}
81
+ */
82
+ migrations(dir) {
83
+ let root = /** @type {Container} */ (this);
84
+ while (root.parent) root = root.parent;
85
+ root.migrationDirs.push(dir);
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Get all registered migration directories. Walks to root.
91
+ *
92
+ * @returns {string[]}
93
+ */
94
+ getMigrationDirs() {
95
+ let root = /** @type {Container} */ (this);
96
+ while (root.parent) root = root.parent;
97
+ return [...root.migrationDirs];
98
+ }
99
+
100
+ /**
101
+ * Resolve a token to a value. Walks the scope chain.
102
+ *
103
+ * Resolution order:
104
+ * 1. nearest override (any scope)
105
+ * 2. nearest binding (any scope) — singletons cache at that scope
106
+ * 3. throw
107
+ *
108
+ * The factory is called with the leaf scope (the one `resolve` was called
109
+ * on), so factories can resolve other tokens at the same level.
110
+ *
111
+ * @param {string} token
112
+ * @returns {any}
113
+ */
114
+ resolve(token) {
115
+ let node = /** @type {Container | null} */ (this);
116
+ while (node) {
117
+ if (node.overrides.has(token)) {
118
+ const factory = /** @type {Factory} */ (node.overrides.get(token));
119
+ return factory(this);
120
+ }
121
+ node = node.parent;
122
+ }
123
+
124
+ node = this;
125
+ while (node) {
126
+ const binding = node.bindings.get(token);
127
+ if (binding) {
128
+ if (binding.lifetime === 'singleton') {
129
+ if (!node.singletons.has(token)) {
130
+ node.singletons.set(token, binding.factory(this));
131
+ }
132
+ return node.singletons.get(token);
133
+ }
134
+ return binding.factory(this);
135
+ }
136
+ node = node.parent;
137
+ }
138
+
139
+ throw new Error(`Container: no binding for token "${String(token)}"`);
140
+ }
141
+
142
+ /**
143
+ * Whether a binding or override is reachable for the given token.
144
+ *
145
+ * @param {string} token
146
+ * @returns {boolean}
147
+ */
148
+ has(token) {
149
+ let node = /** @type {Container | null} */ (this);
150
+ while (node) {
151
+ if (node.overrides.has(token) || node.bindings.has(token)) return true;
152
+ node = node.parent;
153
+ }
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * Create a child scope. The child inherits bindings via the parent walk;
159
+ * its own overrides and singletons are isolated.
160
+ *
161
+ * @returns {Container}
162
+ */
163
+ scope() {
164
+ return new Container(this);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Create a fresh root container.
170
+ *
171
+ * @returns {Container}
172
+ */
173
+ export function createContainer() {
174
+ return new Container();
175
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Drizzle driver factories + a portable transaction helper.
3
+ *
4
+ * Drivers are imported lazily so consumers only need to install the ones they
5
+ * actually use. Drizzle and its driver packages are user-installed (not
6
+ * bundled in norns) — the framework just provides the assembly recipe.
7
+ *
8
+ * The dynamic `import()` paths are routed through a variable + `@vite-ignore`
9
+ * so Rollup doesn't try to resolve them at build time when the consumer
10
+ * hasn't installed the corresponding driver.
11
+ *
12
+ * Bind the result as `db` in your feature's `module.c`:
13
+ *
14
+ * import { betterSqlite } from '@human-synthesis/norns/server';
15
+ * module.exports = (app) ->
16
+ * dbInstance = await betterSqlite('data/notes.db', pragma: ['journal_mode = WAL'])
17
+ * app.single 'db', -> dbInstance
18
+ *
19
+ * For Cloudflare Workers, switch to `d1(env.DB)`. Same module.c shape.
20
+ */
21
+
22
+ /**
23
+ * Pass-through dynamic import that hides the path from Rollup's static
24
+ * analyzer. Without this, building an app that doesn't have (say)
25
+ * `drizzle-orm/d1` installed fails even if the app never calls `d1()`.
26
+ *
27
+ * @param {string} mod
28
+ * @returns {Promise<any>}
29
+ */
30
+ function importDynamic(mod) {
31
+ return import(/* @vite-ignore */ mod);
32
+ }
33
+
34
+ /**
35
+ * @typedef {Object} BetterSqliteOptions
36
+ * @property {Object} [connection] passed to `new Database(path, opts)`
37
+ * @property {string[]} [pragma] PRAGMA statements to run after open
38
+ * @property {Object} [drizzle] passed to `drizzle(sqlite, opts)`
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} D1Options
43
+ * @property {Object} [drizzle] passed to `drizzle(binding, opts)`
44
+ */
45
+
46
+ /**
47
+ * @typedef {Object} LibsqlOptions
48
+ * @property {Object} [client] passed to `createClient({ url, ...client })`
49
+ * @property {Object} [drizzle] passed to `drizzle(client, opts)`
50
+ */
51
+
52
+ /**
53
+ * @typedef {Object} PostgresOptions
54
+ * @property {Object} [pool] passed to `new Pool({ connectionString: url, ...pool })`
55
+ * @property {Object} [drizzle] passed to `drizzle(pool, opts)`
56
+ */
57
+
58
+ /**
59
+ * Open a Drizzle instance backed by `better-sqlite3`.
60
+ *
61
+ * @param {string} path SQLite file path (e.g. `data/notes.db`)
62
+ * @param {BetterSqliteOptions} [opts]
63
+ * @returns {Promise<any>}
64
+ */
65
+ export async function betterSqlite(path, opts = {}) {
66
+ const [{ default: Database }, { drizzle }] = await Promise.all([
67
+ importDynamic('better-sqlite3'),
68
+ importDynamic('drizzle-orm/better-sqlite3')
69
+ ]);
70
+ const sqlite = new Database(path, opts.connection);
71
+ if (opts.pragma) {
72
+ for (const p of opts.pragma) sqlite.pragma(p);
73
+ }
74
+ return drizzle(sqlite, opts.drizzle);
75
+ }
76
+
77
+ /**
78
+ * Open a Drizzle instance backed by Cloudflare D1.
79
+ *
80
+ * @param {any} binding D1 binding from `event.platform.env`
81
+ * @param {D1Options} [opts]
82
+ * @returns {Promise<any>}
83
+ */
84
+ export async function d1(binding, opts = {}) {
85
+ const { drizzle } = await importDynamic('drizzle-orm/d1');
86
+ return drizzle(binding, opts.drizzle);
87
+ }
88
+
89
+ /**
90
+ * Open a Drizzle instance backed by libSQL (Turso, sqld).
91
+ *
92
+ * @param {string} url
93
+ * @param {LibsqlOptions} [opts]
94
+ * @returns {Promise<any>}
95
+ */
96
+ export async function libsql(url, opts = {}) {
97
+ const [{ createClient }, { drizzle }] = await Promise.all([
98
+ importDynamic('@libsql/client'),
99
+ importDynamic('drizzle-orm/libsql')
100
+ ]);
101
+ const client = createClient({ url, ...(opts.client ?? {}) });
102
+ return drizzle(client, opts.drizzle);
103
+ }
104
+
105
+ /**
106
+ * Open a Drizzle instance backed by node-postgres.
107
+ *
108
+ * @param {string} url
109
+ * @param {PostgresOptions} [opts]
110
+ * @returns {Promise<any>}
111
+ */
112
+ export async function postgres(url, opts = {}) {
113
+ const [{ default: pgModule }, { drizzle }] = await Promise.all([
114
+ importDynamic('pg'),
115
+ importDynamic('drizzle-orm/node-postgres')
116
+ ]);
117
+ const pool = new pgModule.Pool({ connectionString: url, ...(opts.pool ?? {}) });
118
+ return drizzle(pool, opts.drizzle);
119
+ }
120
+
121
+ /**
122
+ * Run `fn` inside a Drizzle transaction. Uniform across drivers.
123
+ *
124
+ * @template T
125
+ * @param {any} db Drizzle instance
126
+ * @param {(tx: any) => T | Promise<T>} fn
127
+ * @returns {Promise<T>}
128
+ */
129
+ export function withTransaction(db, fn) {
130
+ return db.transaction(fn);
131
+ }
@@ -0,0 +1,24 @@
1
+ import { withScope } from '../scope.js';
2
+
3
+ /** @typedef {import('../container.js').Container} Container */
4
+ /** @typedef {import('@sveltejs/kit').Handle} Handle */
5
+
6
+ /**
7
+ * Per-request middleware: creates a child scope of the root container and
8
+ * attaches it to `event.locals.container`. Anything resolved through
9
+ * `event.locals.container` for the lifetime of the request hits this scope —
10
+ * overrides and request-scoped singletons (like `db`) live here.
11
+ *
12
+ * Also runs the rest of the pipeline inside `withScope()` so AsyncLocalStorage
13
+ * makes the scope available to callees that don't get `event` directly.
14
+ *
15
+ * @param {Container} app the root container produced by `createApp()`/`boot()`
16
+ * @returns {Handle}
17
+ */
18
+ export function contextHandle(app) {
19
+ return async ({ event, resolve }) => {
20
+ const scope = app.scope();
21
+ event.locals.container = scope;
22
+ return withScope({ container: scope, event }, () => resolve(event));
23
+ };
24
+ }