@human-synthesis/norns-core 0.0.4 → 0.0.6

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Human Synthesis
3
+ Copyright (c) 2026 Daniel Teodoroiu / Human Synthesis
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,13 +1,25 @@
1
- # @human-synthesis/norns-core
1
+ # Norns Core
2
2
 
3
- Svelte with CoffeeScript, Pug, and UnoCSS preconfigured.
3
+ **AI-driven software architecture and development framework, based on Svelte.**
4
+
5
+ Svelte preprocessor for the Norns stack: Pug + CoffeeScript in `.n` files, with the small set of fixes needed to make Svelte 5 runes feel native in Coffee.
6
+
7
+ ## Stack
8
+
9
+ - [Svelte 5](https://svelte.dev) — components and runes
10
+ - [Pug](https://pugjs.org) — templates
11
+ - [CoffeeScript 2](https://coffeescript.org) — script
12
+ - [Vite](https://vitejs.dev) — bundler
13
+ - [bun](https://bun.sh) — runtime / package manager
4
14
 
5
15
  ## Install
6
16
 
7
17
  ```sh
8
- pnpm add -D @human-synthesis/norns-core svelte unocss vite
18
+ bun add -D @human-synthesis/norns-core svelte
9
19
  ```
10
20
 
21
+ Most users want the umbrella package [`@human-synthesis/norns`](https://github.com/human-synthesis/norns) instead.
22
+
11
23
  ## Usage
12
24
 
13
25
  `svelte.config.js`:
@@ -16,47 +28,11 @@ pnpm add -D @human-synthesis/norns-core svelte unocss vite
16
28
  import { nornsPreprocess } from '@human-synthesis/norns-core/preprocess';
17
29
 
18
30
  export default {
31
+ extensions: ['.svelte', '.n'],
19
32
  preprocess: nornsPreprocess()
20
33
  };
21
34
  ```
22
35
 
23
- `vite.config.js`:
24
-
25
- ```js
26
- import { defineConfig } from 'vite';
27
- import { svelte } from '@sveltejs/vite-plugin-svelte';
28
- import { nornsUno } from '@human-synthesis/norns-core/uno';
29
-
30
- export default defineConfig({
31
- plugins: [nornsUno(), svelte()]
32
- });
33
- ```
34
-
35
- ## Writing components
36
-
37
- ```svelte
38
- <template lang="pug">
39
- h1.text-3xl.font-bold Hello {name}
40
- button(on:click="{increment}") count is {count}
41
- </template>
42
-
43
- <script lang="coffee">
44
- export let name = 'world'
45
- count = 0
46
- increment = -> count++
47
- </script>
48
- ```
49
-
50
- ## Why this exists
51
-
52
- Norns wraps Svelte with the language and styling defaults we like:
53
-
54
- - **CoffeeScript** in `<script lang="coffee">` blocks
55
- - **Pug** in `<template lang="pug">` blocks
56
- - **UnoCSS** with `presetUno`, `presetAttributify`, `presetIcons`, `presetTypography` enabled
57
-
58
- The wrapper is intentionally thin — under the hood it's just `svelte-preprocess` and `unocss/vite` with reasonable defaults. Override anything by passing options.
59
-
60
- ## License & attribution
36
+ ## License
61
37
 
62
- MIT © Human Synthesis. Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
38
+ MIT © Daniel Teodoroiu / [Human Synthesis](https://humansynthesis.ai). Built on top of [Svelte](https://github.com/sveltejs/svelte) © Svelte Contributors, MIT licensed.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@human-synthesis/norns-core",
3
- "version": "0.0.4",
4
- "description": "Norns core — Svelte with CoffeeScript, Pug, and UnoCSS preconfigured",
3
+ "version": "0.0.6",
4
+ "description": "Norns core — Svelte preprocessor for CoffeeScript, Pug, and .n files",
5
5
  "license": "MIT",
6
- "author": "Human Synthesis",
6
+ "author": "Daniel Teodoroiu (https://humansynthesis.ai)",
7
7
  "type": "module",
8
8
  "repository": {
9
9
  "type": "git",
@@ -13,16 +13,16 @@
13
13
  "src",
14
14
  "README.md"
15
15
  ],
16
+ "scripts": {
17
+ "test": "bun test"
18
+ },
16
19
  "exports": {
17
20
  ".": "./src/index.js",
18
21
  "./preprocess": "./src/preprocess.js",
19
- "./uno": "./src/uno.js",
20
22
  "./package.json": "./package.json"
21
23
  },
22
24
  "peerDependencies": {
23
- "svelte": "^5.0.0",
24
- "unocss": "^0.65.0",
25
- "vite": "^5.0.0 || ^6.0.0"
25
+ "svelte": "^5.0.0"
26
26
  },
27
27
  "dependencies": {
28
28
  "acorn": "^8.14.0",
package/src/index.js CHANGED
@@ -1,2 +1 @@
1
1
  export { nornsPreprocess, fuseRuneDeclarations } from './preprocess.js';
2
- export { nornsUno } from './uno.js';
package/src/preprocess.js CHANGED
@@ -33,6 +33,8 @@ function isRuneCall(node) {
33
33
  *
34
34
  * Walks function bodies recursively so closures get the same treatment.
35
35
  */
36
+ export { transformIfChains, rewritePugClasses };
37
+
36
38
  export function fuseRuneDeclarations(code) {
37
39
  let ast;
38
40
  try {
@@ -64,7 +66,17 @@ export function fuseRuneDeclarations(code) {
64
66
  if (stmt.type !== 'ExpressionStatement') continue;
65
67
  const e = stmt.expression;
66
68
 
67
- // Pattern: X = expr (where X was declared via var earlier in this scope)
69
+ // Pattern: X = expr (where X is var-declared earlier in this scope).
70
+ // We fuse to `let X = expr` for any expression. Coffee always emits
71
+ // `var X; X = ...` for top-level assignments, and Svelte 5 reports
72
+ // `non_reactive_update` warnings for any `var` that's reassigned —
73
+ // even if the user only wrote a single function declaration. We
74
+ // limit to TOP LEVEL only (no recursion into nested function bodies)
75
+ // to avoid MagicString chunk conflicts.
76
+ //
77
+ // Special-case: `$derived(IIFE())` — Coffee compiles `$derived do ->`
78
+ // to `$derived((function(){...})())`, which evaluates ONCE at
79
+ // definition time instead of reactively. Rewrite to `$derived.by(fn)`.
68
80
  if (
69
81
  e.type === 'AssignmentExpression' &&
70
82
  e.operator === '=' &&
@@ -74,9 +86,17 @@ export function fuseRuneDeclarations(code) {
74
86
  ) {
75
87
  claimed.add(e.left.name);
76
88
  fused.add(e.left.name);
77
- const init = code.slice(e.right.start, e.right.end);
78
- const keyword = isRuneCall(e.right) ? 'let' : 'let';
79
- s.overwrite(stmt.start, stmt.end, `${keyword} ${e.left.name} = ${init};`);
89
+
90
+ const rewrite = rewriteDerivedIIFE(e.right, code);
91
+ if (rewrite) {
92
+ s.overwrite(
93
+ stmt.start,
94
+ stmt.end,
95
+ `let ${e.left.name} = $derived.by(${rewrite});`
96
+ );
97
+ } else {
98
+ s.appendLeft(stmt.start, 'let ');
99
+ }
80
100
  continue;
81
101
  }
82
102
 
@@ -116,28 +136,13 @@ export function fuseRuneDeclarations(code) {
116
136
  }
117
137
  }
118
138
 
119
- // Recurse into nested function bodies (closures)
120
- for (const stmt of body) {
121
- recurseInto(stmt);
122
- }
123
- }
124
-
125
- function recurseInto(node) {
126
- if (!node || typeof node !== 'object') return;
127
- if (Array.isArray(node)) {
128
- node.forEach(recurseInto);
129
- return;
130
- }
131
- if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
132
- if (node.body && node.body.type === 'BlockStatement') {
133
- walkBody(node.body.body);
134
- return;
135
- }
136
- }
137
- for (const key of Object.keys(node)) {
138
- if (key === 'parent' || key === 'loc' || key === 'range') continue;
139
- recurseInto(node[key]);
140
- }
139
+ // Note: we deliberately do NOT recurse into nested function bodies.
140
+ // Svelte runes ($state, $derived, etc.) only apply at component
141
+ // top-level. Inner function bodies (event handlers, callbacks) have
142
+ // `var X; X = ...` patterns from CoffeeScript output too, but they
143
+ // don't trigger Svelte warnings and don't need fusion. Recursing
144
+ // also creates MagicString chunk conflicts when an outer assignment
145
+ // is being modified at the same time as something inside its body.
141
146
  }
142
147
 
143
148
  function isVarDeclared(varStmts, name) {
@@ -151,13 +156,35 @@ export function fuseRuneDeclarations(code) {
151
156
  return false;
152
157
  }
153
158
 
159
+ /**
160
+ * If `node` is `$derived((function(){...})())` (the result of Coffee's
161
+ * `$derived do ->`), return the source text of the inner FunctionExpression
162
+ * so the caller can rewrite to `$derived.by(<fn>)`. Otherwise null.
163
+ */
164
+ function rewriteDerivedIIFE(node, src) {
165
+ if (!node || node.type !== 'CallExpression') return null;
166
+ if (node.callee.type !== 'Identifier' || node.callee.name !== '$derived') return null;
167
+ if (node.arguments.length !== 1) return null;
168
+ const arg = node.arguments[0];
169
+ if (arg.type !== 'CallExpression') return null;
170
+ if (arg.arguments.length !== 0) return null;
171
+ const fn = arg.callee;
172
+ if (fn.type !== 'FunctionExpression' && fn.type !== 'ArrowFunctionExpression') return null;
173
+ return src.slice(fn.start, fn.end);
174
+ }
175
+
154
176
  function collectPatternNames(pat) {
155
177
  const names = [];
156
178
  if (pat.type === 'ObjectPattern') {
157
179
  for (const prop of pat.properties) {
158
- if (prop.type === 'Property' && prop.value.type === 'Identifier') names.push(prop.value.name);
159
- else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier')
180
+ if (prop.type === 'Property') {
181
+ let v = prop.value;
182
+ // Unwrap default value: `{ x = 1 }` has Property → AssignmentPattern → Identifier
183
+ if (v.type === 'AssignmentPattern') v = v.left;
184
+ if (v.type === 'Identifier') names.push(v.name);
185
+ } else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
160
186
  names.push(prop.argument.name);
187
+ }
161
188
  }
162
189
  }
163
190
  return names;
@@ -178,16 +205,305 @@ function hasLangAttr(attrs) {
178
205
  * <script> and <template> blocks, and auto-wrap any top-level non-script /
179
206
  * non-style content in <template lang="pug">.
180
207
  */
208
+ /**
209
+ * If a .n file ends with an opening <script> or <style> tag but no matching
210
+ * close before EOF, append the closing tag. Lets users skip the boilerplate
211
+ * when the block is the very last thing in the file.
212
+ */
213
+ function autoCloseTrailingBlock(content) {
214
+ let out = content;
215
+ for (const tag of ['script', 'style']) {
216
+ const opens = (out.match(new RegExp(`<${tag}\\b[^>]*>`, 'gi')) ?? []).length;
217
+ const closes = (out.match(new RegExp(`</${tag}>`, 'gi')) ?? []).length;
218
+ if (opens > closes) {
219
+ out = out.replace(/\s*$/, `\n</${tag}>\n`);
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+
225
+ const IF_RE = /^(\s*)\+if\s*\((.+)\)\s*$/;
226
+ const ELSEIF_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+elseif\\s*\\((.+)\\)\\s*$`);
227
+ const ELSE_RE_TPL = (ind) => new RegExp(`^${escapeRegex(ind)}\\+else\\s*$`);
228
+
229
+ function escapeRegex(s) {
230
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
231
+ }
232
+
233
+ function stripQuotes(s) {
234
+ s = s.trim();
235
+ if (s.length >= 2) {
236
+ const first = s[0];
237
+ const last = s[s.length - 1];
238
+ if ((first === "'" || first === '"') && first === last) return s.slice(1, -1);
239
+ }
240
+ return s;
241
+ }
242
+
243
+ function detectIndentDiff(lines, fromIdx, parentIndent) {
244
+ for (let j = fromIdx; j < lines.length; j++) {
245
+ const line = lines[j];
246
+ if (line.trim() === '') continue;
247
+ const m = line.match(/^(\s*)/);
248
+ if (m && m[1].length > parentIndent.length) {
249
+ return m[1].slice(parentIndent.length);
250
+ }
251
+ break;
252
+ }
253
+ return '\t';
254
+ }
255
+
256
+ /**
257
+ * Find the position of the matching `)` for an `(` at `start`, respecting
258
+ * nested parens and quoted strings.
259
+ */
260
+ function findMatchingParen(str, start) {
261
+ if (str[start] !== '(') return -1;
262
+ let depth = 1;
263
+ let inSingle = false;
264
+ let inDouble = false;
265
+ for (let i = start + 1; i < str.length; i++) {
266
+ const c = str[i];
267
+ if (inSingle) {
268
+ if (c === "'" && str[i - 1] !== '\\') inSingle = false;
269
+ } else if (inDouble) {
270
+ if (c === '"' && str[i - 1] !== '\\') inDouble = false;
271
+ } else {
272
+ if (c === "'") inSingle = true;
273
+ else if (c === '"') inDouble = true;
274
+ else if (c === '(') depth++;
275
+ else if (c === ')') {
276
+ depth--;
277
+ if (depth === 0) return i;
278
+ }
279
+ }
280
+ }
281
+ return -1;
282
+ }
283
+
284
+ const PUG_CLASS_SPECIAL = /[:.\/]/;
285
+
286
+ /**
287
+ * Rewrite Pug element lines whose class shorthand contains characters Pug's
288
+ * lexer rejects (`:`, `/`) or that Pug already mis-parses (fractional `.\d+`
289
+ * continuations like `.gap-2.5`). Route those classes from shorthand into the
290
+ * `(class="...")` attribute. Pug then sees only safe class shorthand.
291
+ *
292
+ * Examples:
293
+ * `.text-blue.hover:bg-red(href="/")`
294
+ * → `.text-blue(class="hover:bg-red" href="/")`
295
+ * `.gap-2.5.flex`
296
+ * → `.flex(class="gap-2.5")`
297
+ * `.bg-white/40.text-4xl(class="static")`
298
+ * → `.text-4xl(class="bg-white/40 static")`
299
+ *
300
+ * Skips lines inside `<script>` / `<style>` blocks.
301
+ */
302
+ function rewritePugClasses(content) {
303
+ const blockRanges = [];
304
+ const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
305
+ let m;
306
+ while ((m = blockRe.exec(content)) !== null) {
307
+ blockRanges.push([m.index, m.index + m[0].length]);
308
+ }
309
+
310
+ const lines = content.split('\n');
311
+ let offset = 0;
312
+ const out = lines.map((line) => {
313
+ const lineEnd = offset + line.length;
314
+ const inBlock = blockRanges.some(([s, e]) => offset < e && lineEnd > s);
315
+ const result = inBlock ? line : rewritePugLine(line);
316
+ offset = lineEnd + 1; // +1 for the newline
317
+ return result;
318
+ });
319
+ return out.join('\n');
320
+ }
321
+
322
+ function rewritePugLine(line) {
323
+ const trimmed = line.trimStart();
324
+ if (!trimmed) return line;
325
+ const first = trimmed[0];
326
+ if (first === '|' || first === '<') return line; // text emit / raw HTML
327
+ if (trimmed.startsWith('//')) return line; // pug comment
328
+ if (first === '+' || first === ':') return line; // mixin call / pug filter
329
+
330
+ let i = 0;
331
+ while (i < line.length && /\s/.test(line[i])) i++;
332
+ const indent = line.slice(0, i);
333
+
334
+ let tag = '';
335
+ if (i < line.length && /[a-zA-Z]/.test(line[i])) {
336
+ let j = i;
337
+ while (j < line.length && /[\w-]/.test(line[j])) j++;
338
+ tag = line.slice(i, j);
339
+ i = j;
340
+ }
341
+
342
+ const segs = [];
343
+ while (i < line.length && (line[i] === '.' || line[i] === '#')) {
344
+ const sep = line[i];
345
+ let j = i + 1;
346
+ if (sep === '#') {
347
+ while (j < line.length && /[\w-]/.test(line[j])) j++;
348
+ } else {
349
+ // class — extend chars to allow `/`, `:`, plus fractional `.\d+` suffixes
350
+ while (j < line.length && /[\w/:-]/.test(line[j])) j++;
351
+ while (j < line.length && line[j] === '.' && /\d/.test(line[j + 1] || '')) {
352
+ j++;
353
+ while (j < line.length && /\d/.test(line[j])) j++;
354
+ }
355
+ }
356
+ if (j === i + 1) break; // empty token, abort
357
+ segs.push(line.slice(i, j));
358
+ i = j;
359
+ }
360
+
361
+ if (segs.length === 0) return line;
362
+
363
+ let attrs = '';
364
+ if (line[i] === '(') {
365
+ const close = findMatchingParen(line, i);
366
+ if (close !== -1) {
367
+ attrs = line.slice(i, close + 1);
368
+ i = close + 1;
369
+ }
370
+ }
371
+ const rest = line.slice(i);
372
+
373
+ const safe = [];
374
+ const routed = [];
375
+ for (const seg of segs) {
376
+ if (seg[0] === '#') {
377
+ safe.push(seg);
378
+ } else {
379
+ const cls = seg.slice(1);
380
+ if (PUG_CLASS_SPECIAL.test(cls)) routed.push(cls);
381
+ else safe.push(seg);
382
+ }
383
+ }
384
+
385
+ if (routed.length === 0) return line;
386
+
387
+ const newAttrs = mergeClassIntoAttrs(attrs, routed);
388
+ return `${indent}${tag}${safe.join('')}${newAttrs}${rest}`;
389
+ }
390
+
391
+ function mergeClassIntoAttrs(attrsStr, classesToAdd) {
392
+ const classStr = classesToAdd.join(' ');
393
+ if (!attrsStr) return `(class="${classStr}")`;
394
+ const inner = attrsStr.slice(1, -1);
395
+
396
+ // Existing static `class="..."` → prepend our routed classes.
397
+ const staticRe = /((?:^|\s)class\s*=\s*)"([^"]*)"/;
398
+ if (staticRe.test(inner)) {
399
+ return `(${inner.replace(staticRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
400
+ }
401
+ // Existing dynamic `class!="{expr}"` → prepend static text. Svelte parses
402
+ // the resulting `class="static {expr}"` as text + interpolation.
403
+ const dynamicRe = /((?:^|\s)class\s*!=\s*)"([^"]*)"/;
404
+ if (dynamicRe.test(inner)) {
405
+ return `(${inner.replace(dynamicRe, (_, prefix, val) => `${prefix}"${classStr} ${val}"`)})`;
406
+ }
407
+
408
+ // No class= attribute exists — insert one.
409
+ return `(class="${classStr}" ${inner})`;
410
+ }
411
+
412
+ /**
413
+ * Rewrite Pug `+if('expr') / +elseif('expr') / +else` chains to raw Svelte
414
+ * block syntax emitted via Pug `|` text. Bypasses svelte-preprocess's `+if`
415
+ * mixin (which doesn't support chaining).
416
+ *
417
+ * Input:
418
+ * +if('a')
419
+ * div one
420
+ * +elseif('b')
421
+ * div two
422
+ * +else
423
+ * div three
424
+ *
425
+ * Output:
426
+ * | {#if a}
427
+ * div one
428
+ * | {:else if b}
429
+ * div two
430
+ * | {:else}
431
+ * div three
432
+ * | {/if}
433
+ */
434
+ function transformIfChains(content) {
435
+ const lines = content.split('\n');
436
+ const out = [];
437
+ let i = 0;
438
+
439
+ while (i < lines.length) {
440
+ const m = lines[i].match(IF_RE);
441
+ if (!m) {
442
+ out.push(lines[i]);
443
+ i++;
444
+ continue;
445
+ }
446
+
447
+ const chainIndent = m[1];
448
+ const ifExpr = stripQuotes(m[2]);
449
+ const indentDiff = detectIndentDiff(lines, i + 1, chainIndent);
450
+ const elseIfRe = ELSEIF_RE_TPL(chainIndent);
451
+ const elseRe = ELSE_RE_TPL(chainIndent);
452
+
453
+ out.push(`${chainIndent}| {#if ${ifExpr}}`);
454
+ i++;
455
+
456
+ while (i < lines.length) {
457
+ const cur = lines[i];
458
+
459
+ const eIfM = cur.match(elseIfRe);
460
+ if (eIfM) {
461
+ out.push(`${chainIndent}| {:else if ${stripQuotes(eIfM[1])}}`);
462
+ i++;
463
+ continue;
464
+ }
465
+ const eM = cur.match(elseRe);
466
+ if (eM) {
467
+ out.push(`${chainIndent}| {:else}`);
468
+ i++;
469
+ continue;
470
+ }
471
+
472
+ if (cur.trim() === '') {
473
+ out.push(cur);
474
+ i++;
475
+ continue;
476
+ }
477
+
478
+ const lineIndent = cur.match(/^(\s*)/)[1];
479
+ if (lineIndent.length > chainIndent.length) {
480
+ // Body line — de-indent by one level so it sits at the chain's level.
481
+ out.push(cur.startsWith(indentDiff) ? cur.slice(indentDiff.length) : cur);
482
+ i++;
483
+ continue;
484
+ }
485
+
486
+ break;
487
+ }
488
+
489
+ out.push(`${chainIndent}| {/if}`);
490
+ }
491
+
492
+ return out.join('\n');
493
+ }
494
+
181
495
  function nornDefaultLangs() {
182
496
  return {
183
497
  name: 'norns-default-langs',
184
498
  markup({ content, filename }) {
185
- if (!filename || !filename.endsWith('.norn')) return null;
499
+ if (!filename || !filename.endsWith('.n')) return null;
186
500
 
187
- let out = content;
501
+ let out = autoCloseTrailingBlock(content);
502
+ out = transformIfChains(out);
503
+ out = rewritePugClasses(out);
188
504
 
189
505
  // If no <template> exists, scan for script/style blocks and wrap the rest.
190
- if (!TEMPLATE_TAG.test(content)) {
506
+ if (!TEMPLATE_TAG.test(out)) {
191
507
  const blocks = [];
192
508
  const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
193
509
  let m;
@@ -237,6 +553,60 @@ function nornCoffeeRuneFusion() {
237
553
  };
238
554
  }
239
555
 
556
+ /**
557
+ * Lift `import` statements to the top of a JS module.
558
+ *
559
+ * CoffeeScript 2 hoists all variables to a single `var x, y, z;` line at the
560
+ * very top of the file, then emits `import` statements *after* it. JS modules
561
+ * require imports above all other top-level statements, and svelte-preprocess
562
+ * + Svelte's MagicString-based pipeline don't handle this gracefully when
563
+ * there are also nested var/assignment patterns inside script function bodies.
564
+ *
565
+ * This pass parses the JS, finds all `ImportDeclaration` nodes, and rewrites
566
+ * them in source order at the very top of the module.
567
+ */
568
+ export function liftImports(code) {
569
+ let ast;
570
+ try {
571
+ ast = parse(code, {
572
+ ecmaVersion: 'latest',
573
+ sourceType: 'module',
574
+ allowReturnOutsideFunction: true,
575
+ allowAwaitOutsideFunction: true
576
+ });
577
+ } catch {
578
+ return code;
579
+ }
580
+
581
+ const imports = ast.body.filter((s) => s.type === 'ImportDeclaration');
582
+ if (imports.length === 0) return code;
583
+
584
+ const firstNonImportIdx = ast.body.findIndex((s) => s.type !== 'ImportDeclaration');
585
+ if (firstNonImportIdx === -1) return code; // already all imports
586
+
587
+ const lateImports = imports.filter((i) => ast.body.indexOf(i) > firstNonImportIdx);
588
+ if (lateImports.length === 0) return code; // already in the right order
589
+
590
+ const s = new MagicString(code);
591
+ const importTexts = imports.map((i) => code.slice(i.start, i.end));
592
+ for (const i of imports) {
593
+ s.remove(i.start, i.end);
594
+ }
595
+ s.prependLeft(0, importTexts.join('\n') + '\n');
596
+ return s.toString();
597
+ }
598
+
599
+ function nornsCoffeeImportLift() {
600
+ return {
601
+ name: 'norns-coffee-import-lift',
602
+ script({ content, attributes }) {
603
+ if (attributes.lang !== 'coffee' && attributes.lang !== 'coffeescript') return null;
604
+ const code = liftImports(content);
605
+ return code === content ? null : { code };
606
+ }
607
+ };
608
+ }
609
+
240
610
  /**
241
611
  * Norns preprocessor stack.
242
612
  *
@@ -255,8 +625,22 @@ export function nornsPreprocess(options = {}) {
255
625
  sveltePreprocess({
256
626
  coffeescript: { bare: true },
257
627
  pug: {},
628
+ typescript: {
629
+ compilerOptions: {
630
+ // Silence TS 6.x's deprecation warning for older moduleResolution
631
+ // values (node10) that some toolchains still default to.
632
+ ignoreDeprecations: '6.0',
633
+ // Preserve value imports (Svelte component imports look "unused"
634
+ // to the TS transpiler since their usage lives in the template,
635
+ // but they MUST be emitted). verbatimModuleSyntax keeps any
636
+ // non-`import type` imports verbatim.
637
+ verbatimModuleSyntax: true,
638
+ isolatedModules: true
639
+ }
640
+ },
258
641
  ...options
259
642
  }),
643
+ nornsCoffeeImportLift(),
260
644
  nornCoffeeRuneFusion()
261
645
  ];
262
646
  }
package/src/uno.js DELETED
@@ -1,43 +0,0 @@
1
- import UnoCSS from 'unocss/vite';
2
- import {
3
- presetUno,
4
- presetAttributify,
5
- presetIcons,
6
- presetTypography,
7
- transformerDirectives,
8
- transformerVariantGroup
9
- } from 'unocss';
10
-
11
- /**
12
- * Norns UnoCSS Vite plugin with a sensible preset stack.
13
- *
14
- * Includes transformerDirectives (so `@apply` works in `<style>` blocks)
15
- * and transformerVariantGroup (so `class="hover:(text-blue underline)"`
16
- * shorthand works).
17
- *
18
- * Defaults `hmrTopLevelAwait: false` to avoid a TDZ
19
- * "Cannot access 'component' before initialization" error in WebKit/Safari
20
- * when importing 'virtual:uno.css' from a SvelteKit layout/page module.
21
- *
22
- * @param {import('unocss/vite').VitePluginConfig} [options]
23
- */
24
- export function nornsUno(options = {}) {
25
- const {
26
- presets = [],
27
- transformers = [],
28
- hmrTopLevelAwait = false,
29
- ...rest
30
- } = options;
31
- return UnoCSS({
32
- presets: [
33
- presetUno(),
34
- presetAttributify(),
35
- presetIcons(),
36
- presetTypography(),
37
- ...presets
38
- ],
39
- transformers: [transformerDirectives(), transformerVariantGroup(), ...transformers],
40
- hmrTopLevelAwait,
41
- ...rest
42
- });
43
- }