@human-synthesis/norns 0.0.11 → 0.0.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Norns — SvelteKit with Civet, Pug, and the .n / .civet / .c file extensions",
5
5
  "license": "MIT",
6
6
  "author": "Daniel Teodoroiu (https://humansynthesis.ai)",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@danielx/civet": "^0.11.0",
39
- "@human-synthesis/norns-core": "^0.0.9"
39
+ "@human-synthesis/norns-core": "^0.0.10"
40
40
  },
41
41
  "engines": {
42
42
  "node": ">=18"
package/src/migrate.js CHANGED
@@ -10,9 +10,11 @@
10
10
  * tooling (this CLI, wrangler for D1) reads. Keeping them out of `src/lib`
11
11
  * also keeps SvelteKit's bundler from ever trying to ship them.
12
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.
13
+ * v1 supports SQLite only. Backend selection is runtime-detected:
14
+ * - Under Bun: built-in `bun:sqlite` (no native build, works on Alpine).
15
+ * - Under Node: `better-sqlite3` (must be installed in the consumer app).
16
+ * Postgres/libSQL come later; Cloudflare D1 is intentionally out of scope
17
+ * here — use `wrangler d1 migrations apply <db>` for D1 deploys.
16
18
  */
17
19
 
18
20
  import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
@@ -115,11 +117,21 @@ export function resolveDatabaseUrl(cwd) {
115
117
  }
116
118
 
117
119
  /**
118
- * Open a better-sqlite3 db at `path` and ensure the migration tracking table
119
- * exists.
120
+ * Open a SQLite db at `path` and ensure the migration tracking table exists.
121
+ *
122
+ * Backend is runtime-selected: `bun:sqlite` under Bun (no native build,
123
+ * works on Alpine where `better-sqlite3`'s N-API binding fails to load
124
+ * against Bun's V8 compat layer), `better-sqlite3` under Node.
125
+ *
126
+ * The returned object exposes the better-sqlite3 surface used by the
127
+ * migration code (`pragma`, `exec`, `prepare(...).all/get/run`,
128
+ * `transaction`, `close`). Under Bun a minimal `pragma()` shim is grafted
129
+ * on — bun:sqlite has no built-in `pragma` method, but `exec('PRAGMA …')`
130
+ * is equivalent for the writes the migrate code performs.
120
131
  *
121
132
  * @param {string} cwd directory whose `package.json` is used to resolve
122
133
  * better-sqlite3 from the consumer's node_modules
134
+ * (only relevant under Node)
123
135
  * @param {string} path SQLite file path
124
136
  * @param {{ requireFrom?: string | URL }} [opts] override the require base
125
137
  * (used by tests)
@@ -127,6 +139,32 @@ export function resolveDatabaseUrl(cwd) {
127
139
  */
128
140
  export function openSqliteDb(cwd, path, opts = {}) {
129
141
  mkdirSync(dirname(path), { recursive: true });
142
+ const db = openRawSqlite(cwd, path, opts);
143
+ db.pragma('journal_mode = WAL');
144
+ db.exec(`CREATE TABLE IF NOT EXISTS ${MIGRATION_TABLE} (
145
+ id TEXT PRIMARY KEY,
146
+ applied_at INTEGER NOT NULL
147
+ )`);
148
+ return db;
149
+ }
150
+
151
+ /**
152
+ * @param {string} cwd
153
+ * @param {string} path
154
+ * @param {{ requireFrom?: string | URL }} opts
155
+ * @returns {any}
156
+ */
157
+ function openRawSqlite(cwd, path, opts) {
158
+ if (typeof Bun !== 'undefined') {
159
+ const r = createRequire(import.meta.url);
160
+ const { Database } = r('bun:sqlite');
161
+ const db = new Database(path);
162
+ // bun:sqlite has no `pragma()` method. Norns calls it only with
163
+ // write-style statements (`'journal_mode = WAL'`, `'foreign_keys = ON'`),
164
+ // for which `exec('PRAGMA …')` is the documented equivalent.
165
+ db.pragma = (stmt) => db.exec('PRAGMA ' + stmt);
166
+ return db;
167
+ }
130
168
  const require = createRequire(opts.requireFrom ?? join(cwd, 'package.json'));
131
169
  let Database;
132
170
  try {
@@ -136,13 +174,7 @@ export function openSqliteDb(cwd, path, opts = {}) {
136
174
  'norns migrate: `better-sqlite3` is not installed in this app. Run: bun add better-sqlite3'
137
175
  );
138
176
  }
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;
177
+ return new Database(path);
146
178
  }
147
179
 
148
180
  /**
package/src/server/db.js CHANGED
@@ -56,13 +56,31 @@ function importDynamic(mod) {
56
56
  */
57
57
 
58
58
  /**
59
- * Open a Drizzle instance backed by `better-sqlite3`.
59
+ * Open a Drizzle instance backed by SQLite.
60
+ *
61
+ * Backend is runtime-selected: `bun:sqlite` + `drizzle-orm/bun-sqlite` under
62
+ * Bun (built-in, no native build, works on Alpine), `better-sqlite3` +
63
+ * `drizzle-orm/better-sqlite3` under Node. The function name keeps the
64
+ * `betterSqlite` alias for backward compatibility — what actually gets
65
+ * loaded depends on the runtime.
60
66
  *
61
67
  * @param {string} path SQLite file path (e.g. `data/notes.db`)
62
68
  * @param {BetterSqliteOptions} [opts]
63
69
  * @returns {Promise<any>}
64
70
  */
65
71
  export async function betterSqlite(path, opts = {}) {
72
+ if (typeof Bun !== 'undefined') {
73
+ const [{ Database }, { drizzle }] = await Promise.all([
74
+ importDynamic('bun:sqlite'),
75
+ importDynamic('drizzle-orm/bun-sqlite')
76
+ ]);
77
+ const sqlite = new Database(path, opts.connection);
78
+ if (opts.pragma) {
79
+ // bun:sqlite has no `pragma()` method — use `exec('PRAGMA …')`.
80
+ for (const p of opts.pragma) sqlite.exec('PRAGMA ' + p);
81
+ }
82
+ return drizzle(sqlite, opts.drizzle);
83
+ }
66
84
  const [{ default: Database }, { drizzle }] = await Promise.all([
67
85
  importDynamic('better-sqlite3'),
68
86
  importDynamic('drizzle-orm/better-sqlite3')
package/src/vite.js CHANGED
@@ -2,6 +2,7 @@ import { readFile, stat, realpath, readdir, mkdir, writeFile } from 'node:fs/pro
2
2
  import { dirname, join } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
4
  import { compile as compileCivet } from '@danielx/civet';
5
+ import { extractPugClasses } from '@human-synthesis/norns-core/preprocess';
5
6
 
6
7
  const DEFAULT_EXTENSIONS = ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'];
7
8
  const NORNS_EXTENSIONS = ['.svelte', '.n', '.civet', '.c'];
@@ -143,53 +144,34 @@ export function nornsCivetPlugin() {
143
144
 
144
145
  /* === pugTailwindExtract ==================================================
145
146
  *
146
- * Tailwind v4's content extractor tokenizes candidates against a fixed
147
- * non-class alphabet. Pug class-shorthand chains followed directly by an
148
- * attribute paren `.grid.gap-6(class="…")` — fail that tokenizer: the
149
- * substring `.gap-6(` reads as one non-class token, so `gap-6` never
150
- * reaches the candidate set and the CSS rule is never generated.
147
+ * Tailwind v4's content scanner only picks up utility candidates from
148
+ * space-separated string contexts (`class="…"`, JS strings, etc.). It does
149
+ * NOT understand Pug's chained-class shorthand: `.flex.items-center.p-4`
150
+ * reads as one dotted token, doesn't match any utility, and gets dropped.
151
+ * Same for chains followed by an attribute paren `.grid.gap-6(class="…")`.
151
152
  *
152
- * Because the `tag.cls.cls(attrs)` form is idiomatic Pug, asking authors
153
- * to either move every utility into `class="…"` or hand-maintain a
154
- * safelist is a paper cut on every page they touch.
153
+ * Net effect without help: the HTML emitted by `.n` files has the class
154
+ * names, but Tailwind never generates rules for them. Pages render with
155
+ * silently-missing layout (no error, no warning, just visually wrong).
155
156
  *
156
- * This plugin walks every `.n` file under `root`, extracts each `.cls`
157
- * segment via a permissive regex, and writes the deduplicated set into a
158
- * single sidecar HTML file. Consumers reference the file from their CSS
159
- * via `@source "./.tailwind-pug-classes.html";` so Tailwind picks it up
160
- * like any other content source.
157
+ * This plugin walks every `.n` file under `root`, extracts each shorthand
158
+ * class via `extractPugClasses` from `@human-synthesis/norns-core`, and
159
+ * writes the deduplicated set into a sidecar HTML file. Consumers
160
+ * reference the file from their CSS via
161
+ * `@source "./.tailwind-pug-classes.html";`
162
+ * so Tailwind picks it up like any other content source.
161
163
  *
162
- * The regex is permissive on purpose it captures every `.candidate`
163
- * segment in the source, including occasional false positives like
164
- * `.svelte` inside template text. Those are free: Tailwind's own
165
- * candidate-validation step drops anything that isn't a real utility, so
166
- * the only cost is a few extra bytes in the sidecar.
164
+ * Extraction skips `<script>` and `<style>` blocks, mixin calls (`+if`),
165
+ * text-emit lines (`|`), raw HTML lines (`<`), and Pug comments (`//`).
166
+ * It also pulls tokens out of any `class="…"` / `class!="…"` attribute on
167
+ * the same line redundant with Tailwind's own scan in most cases, free
168
+ * defence in depth.
167
169
  *
168
170
  * Runs with `enforce: 'pre'` so the scan sees the raw Pug source, not the
169
171
  * Svelte output that the rest of the chain emits.
170
172
  * ========================================================================
171
173
  */
172
174
 
173
- /**
174
- * Match every `.candidate` segment. Class names may contain Tailwind's
175
- * full alphabet — letters, digits, `-`, `_`, `:`, `/`, and arbitrary-value
176
- * brackets `[...]`. Pug shorthand never contains a `.` inside a class
177
- * (the dot is the delimiter), so `text-[1.5rem]`-style values never appear
178
- * in shorthand — those always live inside `class="…"`, which Tailwind
179
- * extracts directly.
180
- */
181
- const SEGMENT_RE = /\.([A-Za-z][\w\-:/]*(?:\[[^\]]*\])?)/g;
182
-
183
- function extractPugClasses(source) {
184
- const out = new Set();
185
- let m;
186
- SEGMENT_RE.lastIndex = 0;
187
- while ((m = SEGMENT_RE.exec(source))) {
188
- if (m[1]) out.add(m[1]);
189
- }
190
- return out;
191
- }
192
-
193
175
  async function walkNFiles(dir, ext, out = []) {
194
176
  let entries;
195
177
  try {