@human-synthesis/norns 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  **AI-driven software architecture and development framework, based on Svelte.**
4
4
 
5
- SvelteKit with Pug, CoffeeScript, and `.n` / `.c` files — preconfigured.
5
+ SvelteKit with **Pug + Civet** and the `.n` / `.c` file extensions — preconfigured. The `.c` extension is recognised as an alias for `.civet`; both compile through Civet.
6
+
7
+ Includes a small runtime layer: feature-folder modularity, a DI container, route/page wrappers with valibot validation, and a migrations CLI.
6
8
 
7
9
  ## Stack
8
10
 
9
11
  - [Svelte 5](https://svelte.dev) — components and runes
10
12
  - [SvelteKit 2](https://kit.svelte.dev) — file-system routing, SSR, endpoints
11
13
  - [Pug](https://pugjs.org) — templates
12
- - [CoffeeScript 2](https://coffeescript.org) — script
13
- - [Tailwind CSS v4](https://tailwindcss.com) — recommended styling
14
+ - [Civet](https://civet.dev) — script (TypeScript-flavored, indented)
15
+ - [Tailwind CSS v4](https://tailwindcss.com) — recommended styling (consumer-installed)
14
16
  - [Vite](https://vitejs.dev) — bundler
15
17
  - [bun](https://bun.sh) — runtime / package manager
16
18
 
@@ -39,10 +41,10 @@ export default nornsConfig({
39
41
  ```js
40
42
  import { defineConfig } from 'vite';
41
43
  import { sveltekit } from '@sveltejs/kit/vite';
42
- import { nornsCoffeePlugin } from '@human-synthesis/norns/vite';
44
+ import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
43
45
 
44
46
  export default defineConfig({
45
- plugins: [nornsCoffeePlugin(), sveltekit()]
47
+ plugins: [nornsCivetPlugin(), sveltekit()]
46
48
  });
47
49
  ```
48
50
 
@@ -53,11 +55,189 @@ export default defineConfig({
53
55
  "scripts": {
54
56
  "dev": "norns dev",
55
57
  "build": "norns build",
56
- "preview": "norns preview"
58
+ "preview": "norns preview",
59
+ "migrate": "norns migrate"
57
60
  }
58
61
  }
59
62
  ```
60
63
 
64
+ ## Auto-imports
65
+
66
+ `nornsAutoImport()` returns an object that's both a Svelte preprocessor (for `.n` / `.svelte` files) and a Vite plugin (for standalone `.c` / `.civet` modules). The same instance has all four resolvers: framework helpers, project components, project utilities, and library presets. Wire it in both places — Svelte's compiler ignores the Vite hooks, Vite ignores the Svelte hooks:
67
+
68
+ ```js
69
+ // svelte.config.js
70
+ import { nornsConfig } from '@human-synthesis/norns/config';
71
+ import { nornsPreprocess } from '@human-synthesis/norns/preprocess';
72
+ import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
73
+
74
+ export default nornsConfig({
75
+ preprocess: [
76
+ ...nornsPreprocess(),
77
+ nornsAutoImport({
78
+ componentDirs: ['src/lib/components', 'src/routes'],
79
+ exportDirs: ['src/lib', 'src/routes']
80
+ })
81
+ ]
82
+ });
83
+ ```
84
+
85
+ ```js
86
+ // vite.config.js
87
+ import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
88
+ import { nornsAutoImport } from '@human-synthesis/norns/auto-import';
89
+
90
+ export default {
91
+ plugins: [
92
+ nornsCivetPlugin(),
93
+ nornsAutoImport({ exportDirs: ['src/lib', 'src/routes'] }),
94
+ sveltekit()
95
+ ]
96
+ };
97
+ ```
98
+
99
+ ### What gets auto-imported
100
+
101
+ | Layer | Resolves | Examples |
102
+ |-------|----------|----------|
103
+ | Helpers | Hardcoded module-name lists, optionally path-gated | `onMount` from `svelte`, `redirect` from `@sveltejs/kit`, `page` from `$app/state` (client) or `@human-synthesis/norns/server` (server) |
104
+ | Components (dir scan) | Capitalised basenames in `componentDirs` | `<Card>` → `$lib/components/Card.svelte`; `<Game>` → `./Game.n` (route-colocated, importer-relative) |
105
+ | Components (preset map) | Bare-specifier `Record<name, importPath>` from a UI library | `<Btn>` → `'@human-synthesis/norns-ui/components/Btn.n'` (used verbatim) |
106
+ | Project utilities | Named exports (`export const X`, `export X := …`, `export { a, b }`) discovered in `exportDirs` | `notes` from `$lib/notes/server/public`; `scheduleAiMove` from `./ai` (sibling) |
107
+
108
+ Resolution priority is **helpers → component dir → component preset → exports**. A name picked up earlier shadows a later match silently — first-match-wins lets you override a library preset by dropping a file under your own `componentDirs`.
109
+
110
+ Path emission:
111
+
112
+ - Files inside `$lib` emit `$lib/...` paths (portable, friendly to the dts file).
113
+ - Files outside `$lib` emit a path relative to the importer.
114
+ - Project-utility paths are stripped of their file extension to match Norns/SvelteKit convention (`'$lib/notes/server/public'`, not `…/public.c`); the configured `extensions` array does the rest.
115
+
116
+ Files without a `<script>` block get one prepended automatically when a known component is referenced from markup. Runes (`$state`, `$derived`, `$effect`, `$props`) are Svelte compiler globals — no import needed; the plugin doesn't touch them.
117
+
118
+ ### Defaults
119
+
120
+ - **Helpers**: `svelte`, `svelte/store`, `@sveltejs/kit`, `$app/state` (non-server paths), `@human-synthesis/norns/server` (server paths only).
121
+ - **Component dirs**: `['src/lib/components']`.
122
+ - **Component extensions**: `['.svelte', '.n']`.
123
+ - **Export dirs**: `false` (off) — opt in. SvelteKit route/hook files (`+*.{c,svelte,n}`, `hooks.*`) are excluded from the export scan since their named exports (`load`, `actions`, `handle`, …) are framework-consumed.
124
+ - **Export extensions**: `['.c', '.civet', '.js']`. `.ts` is excluded by default — regex-based scanning can't reliably tell value exports from type-only ones under `verbatimModuleSyntax`.
125
+
126
+ ### UI library presets
127
+
128
+ A preset is a function returning a config slice — typically a `components` map. Compose it with your own config:
129
+
130
+ ```js
131
+ // vite.config.js
132
+ import { presetUI } from '@human-synthesis/norns-ui/auto-import';
133
+
134
+ const ui = presetUI();
135
+
136
+ export default {
137
+ plugins: [
138
+ nornsCivetPlugin(),
139
+ nornsAutoImport({
140
+ exportDirs: ['src/lib', 'src/routes'],
141
+ components: ui.components // { Btn: '@human-synthesis/norns-ui/components/Btn.n', … }
142
+ }),
143
+ sveltekit()
144
+ ]
145
+ };
146
+ ```
147
+
148
+ Drop `src/lib/components/Btn.n` in your project and it shadows the preset's `Btn` silently — `componentDirs` resolves first.
149
+
150
+ > **Roadmap.** Helpers from a preset (e.g. `toast()` from a UI library) currently can't merge with the defaults — passing `helpers` to `nornsAutoImport` _replaces_ the default list. A `presets` (or `additionalHelpers`) option to extend without replacing is a planned follow-up; for now, presets only deliver components.
151
+
152
+ ### Full options reference
153
+
154
+ | Option | Default | Notes |
155
+ |--------|---------|-------|
156
+ | `helpers` | `DEFAULT_HELPERS` (5 modules) | Pass `false` to disable. Each entry: `{ from, imports[], match? }` where `match` is a regex tested against the filename. |
157
+ | `componentDirs` | `['src/lib/components']` | `false` or `[]` to disable. |
158
+ | `componentExtensions` | `['.svelte', '.n']` | |
159
+ | `components` | `null` | `Record<name, importPath>` — bare-specifier preset map. |
160
+ | `exportDirs` | `false` | Off by default. Opt in with e.g. `['src/lib', 'src/routes']`. |
161
+ | `exportExtensions` | `['.c', '.civet', '.js']` | |
162
+ | `libRoot` | `'src/lib'` | Project-relative root that `libAlias` maps to. |
163
+ | `libAlias` | `'$lib'` | Alias prefix emitted in import paths. |
164
+ | `root` | `process.cwd()` | Project root. |
165
+
166
+ ## Runtime — feature folders + DI
167
+
168
+ Wire your hooks once:
169
+
170
+ ```civet
171
+ # src/hooks.server.c
172
+ import { boot } from '@human-synthesis/norns/server'
173
+
174
+ features := import.meta.glob './lib/*/server/module.c', { eager: true }
175
+ app := await boot { features }
176
+
177
+ { handle, handleError } := app
178
+ export { handle, handleError }
179
+ ```
180
+
181
+ Each feature is a folder under `src/lib/<feature>/`:
182
+
183
+ ```
184
+ src/lib/notes/
185
+ server/
186
+ module.c # registers DI bindings + migrations
187
+ repo.c # SQL / data access
188
+ service.c # business logic
189
+ public.c # the ONLY file other features may import
190
+ shared/
191
+ schema.c # valibot validation schemas
192
+ ```
193
+
194
+ Routes use thin wrappers from `@human-synthesis/norns/server`:
195
+
196
+ ```civet
197
+ # src/routes/notes/+page.server.c
198
+ import { page } from '@human-synthesis/norns/server'
199
+ import { notes } from '$lib/notes/server/public'
200
+ import { createNoteSchema } from '$lib/notes/shared/schema'
201
+
202
+ export load := page.load
203
+ handler: ({ container }) =>
204
+ notes: notes(container).list()
205
+
206
+ export actions := page.actions
207
+ create:
208
+ input: createNoteSchema
209
+ run: ({ input, container }) =>
210
+ id := notes(container).create input
211
+ throw redirect 303, `/notes/${id}`
212
+ ```
213
+
214
+ The wrappers handle: input parsing, [valibot](https://valibot.dev) validation, container resolution, and consistent error mapping.
215
+
216
+ ## Migrations
217
+
218
+ ```sh
219
+ bun run migrate create notes/add_pinned # scaffold migrations/notes/<ts>_add_pinned.sql
220
+ bun run migrate up # apply pending migrations
221
+ bun run migrate status # list applied + pending
222
+ ```
223
+
224
+ Migration files live at `<project>/migrations/<feature>/*.sql`. The CLI tracks applied migrations in a `norns_migrations` table.
225
+
226
+ v1 supports SQLite via `better-sqlite3`. For Cloudflare D1 use `wrangler d1 migrations apply`. Postgres / libSQL via the CLI are planned.
227
+
228
+ ## Drivers
229
+
230
+ The `db` helpers wire Drizzle across multiple targets:
231
+
232
+ ```civet
233
+ # module.c — Node + better-sqlite3 in dev
234
+ import { betterSqlite } from '@human-synthesis/norns/server'
235
+ db := await betterSqlite 'data/app.db', { pragma: ['journal_mode = WAL'] }
236
+ app.single 'db', => db
237
+ ```
238
+
239
+ D1, libSQL, and Postgres factories ship in the same module; the driver packages are user-installed (peer-style).
240
+
61
241
  ## License
62
242
 
63
243
  MIT © Daniel Teodoroiu / [Human Synthesis](https://humansynthesis.ai). Built on top of [SvelteKit](https://github.com/sveltejs/kit) and [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
package/bin/norns.js CHANGED
@@ -1,8 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
- import { watch, realpathSync, readFileSync } from 'node:fs';
3
+ import { watch, realpathSync, readFileSync, lstatSync, readdirSync, rmSync } from 'node:fs';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { createRequire } from 'node:module';
6
+ import {
7
+ listMigrations,
8
+ resolveDatabaseUrl,
9
+ openSqliteDb,
10
+ getApplied,
11
+ applyMigrations,
12
+ createMigration
13
+ } from '../src/migrate.js';
6
14
 
7
15
  const FRAMEWORK_PKGS = ['@human-synthesis/norns-core', '@human-synthesis/norns'];
8
16
 
@@ -21,6 +29,66 @@ function resolveWorkspaceFrameworkSrcs(root) {
21
29
  return out;
22
30
  }
23
31
 
32
+ /**
33
+ * In workspace mode (a parent node_modules has framework packages as symlinks),
34
+ * a `bun add <pkg>` from the consumer dir often drops the *published* version
35
+ * of @human-synthesis/* into the local node_modules, which then shadows the
36
+ * workspace symlinks. The shadow is the npm-published code, not the local
37
+ * source — silently breaks dev. This detects the shadow and removes it.
38
+ *
39
+ * Only acts when both conditions hold:
40
+ * 1. some ancestor node_modules has the framework package as a symlink
41
+ * (proves we're in workspace mode)
42
+ * 2. the cwd-local node_modules has the same package as a real directory
43
+ * (the shadow that's overriding the symlink)
44
+ *
45
+ * No-op for normal installs (no symlinked ancestor → nothing to shadow).
46
+ *
47
+ * @param {string} cwd
48
+ * @returns {string[]} package names that were cleaned
49
+ */
50
+ function cleanShadowedFrameworkPkgs(cwd) {
51
+ // Walk up from cwd looking for a parent with a framework package as symlink.
52
+ let workspaceMode = false;
53
+ let dir = dirname(cwd);
54
+ while (dir !== dirname(dir)) {
55
+ for (const pkg of FRAMEWORK_PKGS) {
56
+ try {
57
+ const stat = lstatSync(join(dir, 'node_modules', ...pkg.split('/')));
58
+ if (stat.isSymbolicLink()) {
59
+ workspaceMode = true;
60
+ break;
61
+ }
62
+ } catch {}
63
+ }
64
+ if (workspaceMode) break;
65
+ dir = dirname(dir);
66
+ }
67
+ if (!workspaceMode) return [];
68
+
69
+ const removed = [];
70
+ for (const pkg of FRAMEWORK_PKGS) {
71
+ const shadowPath = join(cwd, 'node_modules', ...pkg.split('/'));
72
+ try {
73
+ const stat = lstatSync(shadowPath);
74
+ // lstat doesn't follow symlinks — a symlinked dir reports
75
+ // isDirectory() === false, so this only matches real dirs.
76
+ if (stat.isDirectory()) {
77
+ rmSync(shadowPath, { recursive: true, force: true });
78
+ removed.push(pkg);
79
+ }
80
+ } catch {}
81
+ }
82
+
83
+ // Tidy up an emptied @human-synthesis/ scope dir if it has no other content.
84
+ const scopeDir = join(cwd, 'node_modules', '@human-synthesis');
85
+ try {
86
+ if (readdirSync(scopeDir).length === 0) rmSync(scopeDir, { recursive: true, force: true });
87
+ } catch {}
88
+
89
+ return removed;
90
+ }
91
+
24
92
  function findViteBin(root) {
25
93
  const require = createRequire(join(root, 'package.json'));
26
94
  const pkgPath = require.resolve('vite/package.json');
@@ -32,6 +100,13 @@ function findViteBin(root) {
32
100
 
33
101
  function devCommand(passthrough) {
34
102
  const cwd = process.cwd();
103
+ const cleaned = cleanShadowedFrameworkPkgs(cwd);
104
+ if (cleaned.length > 0) {
105
+ console.log(
106
+ `[norns] removed shadowed framework packages from local node_modules: ${cleaned.join(', ')} ` +
107
+ `— the workspace symlinks at the parent will be used instead.`
108
+ );
109
+ }
35
110
  const viteBin = findViteBin(cwd);
36
111
  const watchSrcs = resolveWorkspaceFrameworkSrcs(cwd);
37
112
 
@@ -102,6 +177,12 @@ function devCommand(passthrough) {
102
177
 
103
178
  function passthroughCommand(name, passthrough) {
104
179
  const cwd = process.cwd();
180
+ const cleaned = cleanShadowedFrameworkPkgs(cwd);
181
+ if (cleaned.length > 0) {
182
+ console.log(
183
+ `[norns] removed shadowed framework packages from local node_modules: ${cleaned.join(', ')}`
184
+ );
185
+ }
105
186
  const viteBin = findViteBin(cwd);
106
187
  const child = spawn(process.execPath, [viteBin, name, ...passthrough], {
107
188
  cwd,
@@ -111,6 +192,85 @@ function passthroughCommand(name, passthrough) {
111
192
  child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
112
193
  }
113
194
 
195
+ function migrateCommand(rest) {
196
+ const sub = rest[0] || 'status';
197
+ const cwd = process.cwd();
198
+ const cleaned = cleanShadowedFrameworkPkgs(cwd);
199
+ if (cleaned.length > 0) {
200
+ console.log(
201
+ `[norns] removed shadowed framework packages from local node_modules: ${cleaned.join(', ')}`
202
+ );
203
+ }
204
+ try {
205
+ switch (sub) {
206
+ case 'status':
207
+ return runMigrateStatus(cwd);
208
+ case 'up':
209
+ return runMigrateUp(cwd);
210
+ case 'create': {
211
+ const file = createMigration(cwd, rest[1]);
212
+ console.log(`Created ${file}`);
213
+ return;
214
+ }
215
+ default:
216
+ console.error(`norns migrate: unknown subcommand "${sub}"`);
217
+ console.error('Usage: norns migrate <status|up|create <feature>/<name>>');
218
+ process.exit(1);
219
+ }
220
+ } catch (err) {
221
+ console.error(err.message);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ function runMigrateStatus(cwd) {
227
+ const all = listMigrations(cwd);
228
+ if (all.length === 0) {
229
+ console.log('No migrations found.');
230
+ return;
231
+ }
232
+ const db = openTargetDb(cwd);
233
+ const applied = getApplied(db);
234
+ console.log(`Found ${all.length} migration(s):`);
235
+ for (const m of all) {
236
+ const tag = applied.has(m.id) ? '[applied]' : '[pending]';
237
+ console.log(` ${tag} ${m.id}`);
238
+ }
239
+ const pending = all.filter((m) => !applied.has(m.id)).length;
240
+ console.log(`\n${pending} pending, ${all.length - pending} applied.`);
241
+ }
242
+
243
+ function runMigrateUp(cwd) {
244
+ const all = listMigrations(cwd);
245
+ if (all.length === 0) {
246
+ console.log('No migrations found.');
247
+ return;
248
+ }
249
+ const db = openTargetDb(cwd);
250
+ const applied = getApplied(db);
251
+ const pending = all.filter((m) => !applied.has(m.id));
252
+ if (pending.length === 0) {
253
+ console.log('Nothing to apply — all migrations are up to date.');
254
+ return;
255
+ }
256
+ console.log(`Applying ${pending.length} migration(s)...`);
257
+ for (const m of pending) {
258
+ try {
259
+ applyMigrations(db, [m]);
260
+ console.log(` [ok] ${m.id}`);
261
+ } catch (err) {
262
+ console.error(` [fail] ${m.id}: ${err.message}`);
263
+ process.exit(1);
264
+ }
265
+ }
266
+ console.log('Done.');
267
+ }
268
+
269
+ function openTargetDb(cwd) {
270
+ const target = resolveDatabaseUrl(cwd);
271
+ return openSqliteDb(cwd, target.path);
272
+ }
273
+
114
274
  const [, , cmd = 'dev', ...rest] = process.argv;
115
275
 
116
276
  switch (cmd) {
@@ -121,14 +281,23 @@ switch (cmd) {
121
281
  case 'preview':
122
282
  passthroughCommand(cmd, rest);
123
283
  break;
284
+ case 'migrate':
285
+ migrateCommand(rest);
286
+ break;
124
287
  case '-h':
125
288
  case '--help':
126
289
  console.log(`norns <command>
127
290
 
128
291
  Commands:
129
- dev start vite dev with framework-source watching (default)
130
- build run vite build
131
- preview run vite preview
292
+ dev start vite dev with framework-source watching (default)
293
+ build run vite build
294
+ preview run vite preview
295
+ migrate status list applied + pending migrations
296
+ migrate up apply pending migrations
297
+ migrate create <feature>/<name> scaffold a new SQL migration
298
+
299
+ Migration db is read from \$DATABASE_URL (default: file:./data/app.db).
300
+ Only file: (better-sqlite3) is supported in v1; for D1 use \`wrangler d1 migrations apply\`.
132
301
  `);
133
302
  break;
134
303
  default:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns",
3
- "version": "0.0.6",
4
- "description": "Norns — SvelteKit with CoffeeScript, Pug, and the .n / .c file extensions",
3
+ "version": "0.0.8",
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)",
7
7
  "type": "module",
@@ -17,11 +17,16 @@
17
17
  "bin": {
18
18
  "norns": "./bin/norns.js"
19
19
  },
20
+ "scripts": {
21
+ "test": "bun test"
22
+ },
20
23
  "exports": {
21
24
  ".": "./src/index.js",
25
+ "./auto-import": "./src/auto-import.js",
22
26
  "./config": "./src/config.js",
23
27
  "./vite": "./src/vite.js",
24
28
  "./preprocess": "./src/preprocess.js",
29
+ "./server": "./src/server/index.js",
25
30
  "./package.json": "./package.json"
26
31
  },
27
32
  "peerDependencies": {
@@ -30,8 +35,8 @@
30
35
  "vite": "^5.0.0 || ^6.0.0"
31
36
  },
32
37
  "dependencies": {
33
- "@human-synthesis/norns-core": "^0.0.6",
34
- "coffeescript": "^2.7.0"
38
+ "@danielx/civet": "^0.11.0",
39
+ "@human-synthesis/norns-core": "^0.0.7"
35
40
  },
36
41
  "engines": {
37
42
  "node": ">=18"