@human-synthesis/norns-core 0.0.3 → 0.0.5

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-core",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Norns core — Svelte with CoffeeScript, Pug, and UnoCSS preconfigured",
5
5
  "license": "MIT",
6
6
  "author": "Human Synthesis",
@@ -25,6 +25,7 @@
25
25
  "vite": "^5.0.0 || ^6.0.0"
26
26
  },
27
27
  "dependencies": {
28
+ "@unocss/extractor-pug": "^0.65.0 || ^66.0.0",
28
29
  "acorn": "^8.14.0",
29
30
  "coffeescript": "^2.7.0",
30
31
  "magic-string": "^0.30.10",
package/src/preprocess.js CHANGED
@@ -25,78 +25,142 @@ function isRuneCall(node) {
25
25
  }
26
26
 
27
27
  /**
28
- * Fuse `var X; X = $rune(...)` patterns (CoffeeScript output) into
29
- * `let X = $rune(...)` so Svelte 5 accepts the rune in declaration position.
28
+ * Fuse `var X; X = expr` patterns (CoffeeScript output) into `let X = expr`,
29
+ * so Svelte 5 accepts runes in declaration position and doesn't warn about
30
+ * non-state variables being "updated".
30
31
  *
31
- * Also handles `var X; ({X} = $props())` (destructured props).
32
+ * Also handles `var X; ({X, ...} = $props())` (destructured props).
33
+ *
34
+ * Walks function bodies recursively so closures get the same treatment.
32
35
  */
33
36
  export function fuseRuneDeclarations(code) {
34
37
  let ast;
35
38
  try {
36
- ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
39
+ ast = parse(code, {
40
+ ecmaVersion: 'latest',
41
+ sourceType: 'module',
42
+ allowReturnOutsideFunction: true,
43
+ allowAwaitOutsideFunction: true
44
+ });
37
45
  } catch {
38
46
  return code;
39
47
  }
40
48
 
41
49
  const s = new MagicString(code);
42
- const fused = new Set();
43
- const varStmts = [];
50
+ walkBody(ast.body);
51
+
52
+ function walkBody(body) {
53
+ const fused = new Set();
54
+ const varStmts = [];
55
+ const claimed = new Set();
56
+
57
+ for (let i = 0; i < body.length; i++) {
58
+ const stmt = body[i];
59
+
60
+ if (stmt.type === 'VariableDeclaration' && stmt.kind === 'var') {
61
+ varStmts.push(stmt);
62
+ continue;
63
+ }
64
+ if (stmt.type !== 'ExpressionStatement') continue;
65
+ const e = stmt.expression;
66
+
67
+ // Pattern: X = expr (where X was declared via var earlier in this scope)
68
+ if (
69
+ e.type === 'AssignmentExpression' &&
70
+ e.operator === '=' &&
71
+ e.left.type === 'Identifier' &&
72
+ isVarDeclared(varStmts, e.left.name) &&
73
+ !claimed.has(e.left.name)
74
+ ) {
75
+ claimed.add(e.left.name);
76
+ 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};`);
80
+ continue;
81
+ }
44
82
 
45
- for (const stmt of ast.body) {
46
- if (stmt.type === 'VariableDeclaration' && stmt.kind === 'var') {
47
- varStmts.push(stmt);
48
- continue;
83
+ // Pattern: ({X, Y} = $props()) → let {X, Y} = $props()
84
+ if (
85
+ e.type === 'AssignmentExpression' &&
86
+ e.operator === '=' &&
87
+ e.left.type === 'ObjectPattern' &&
88
+ isRuneCall(e.right)
89
+ ) {
90
+ const names = collectPatternNames(e.left);
91
+ if (names.every((n) => isVarDeclared(varStmts, n) && !claimed.has(n))) {
92
+ for (const n of names) {
93
+ claimed.add(n);
94
+ fused.add(n);
95
+ }
96
+ const lhs = code.slice(e.left.start, e.left.end);
97
+ const rhs = code.slice(e.right.start, e.right.end);
98
+ s.overwrite(stmt.start, stmt.end, `let ${lhs} = ${rhs};`);
99
+ }
100
+ }
49
101
  }
50
- if (stmt.type !== 'ExpressionStatement') continue;
51
- const e = stmt.expression;
52
-
53
- // Pattern: X = $rune(...)
54
- if (
55
- e.type === 'AssignmentExpression' &&
56
- e.operator === '=' &&
57
- e.left.type === 'Identifier' &&
58
- isRuneCall(e.right)
59
- ) {
60
- fused.add(e.left.name);
61
- const init = code.slice(e.right.start, e.right.end);
62
- s.overwrite(stmt.start, stmt.end, `let ${e.left.name} = ${init};`);
63
- continue;
102
+
103
+ for (const stmt of varStmts) {
104
+ const remaining = stmt.declarations.filter(
105
+ (d) => !(d.id.type === 'Identifier' && fused.has(d.id.name))
106
+ );
107
+ if (remaining.length === 0) {
108
+ s.remove(stmt.start, stmt.end);
109
+ } else if (remaining.length < stmt.declarations.length) {
110
+ const rebuilt = remaining
111
+ .map((d) =>
112
+ d.init ? `${d.id.name} = ${code.slice(d.init.start, d.init.end)}` : d.id.name
113
+ )
114
+ .join(', ');
115
+ s.overwrite(stmt.start, stmt.end, `var ${rebuilt};`);
116
+ }
64
117
  }
65
118
 
66
- // Pattern: ({X, Y} = $props()) → let {X, Y} = $props()
67
- if (
68
- e.type === 'AssignmentExpression' &&
69
- e.operator === '=' &&
70
- e.left.type === 'ObjectPattern' &&
71
- isRuneCall(e.right)
72
- ) {
73
- for (const prop of e.left.properties) {
74
- if (prop.type === 'Property' && prop.value.type === 'Identifier') {
75
- fused.add(prop.value.name);
76
- } else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
77
- fused.add(prop.argument.name);
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
+ }
141
+ }
142
+
143
+ function isVarDeclared(varStmts, name) {
144
+ for (const stmt of varStmts) {
145
+ for (const d of stmt.declarations) {
146
+ if (d.id.type === 'Identifier' && d.id.name === name && !d.init) {
147
+ return true;
78
148
  }
79
149
  }
80
- const lhs = code.slice(e.left.start, e.left.end);
81
- const rhs = code.slice(e.right.start, e.right.end);
82
- s.overwrite(stmt.start, stmt.end, `let ${lhs} = ${rhs};`);
83
150
  }
151
+ return false;
84
152
  }
85
153
 
86
- for (const stmt of varStmts) {
87
- const remaining = stmt.declarations.filter(
88
- (d) => !(d.id.type === 'Identifier' && fused.has(d.id.name))
89
- );
90
- if (remaining.length === 0) {
91
- s.remove(stmt.start, stmt.end);
92
- } else if (remaining.length < stmt.declarations.length) {
93
- const rebuilt = remaining
94
- .map((d) =>
95
- d.init ? `${d.id.name} = ${code.slice(d.init.start, d.init.end)}` : d.id.name
96
- )
97
- .join(', ');
98
- s.overwrite(stmt.start, stmt.end, `var ${rebuilt};`);
154
+ function collectPatternNames(pat) {
155
+ const names = [];
156
+ if (pat.type === 'ObjectPattern') {
157
+ 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')
160
+ names.push(prop.argument.name);
161
+ }
99
162
  }
163
+ return names;
100
164
  }
101
165
 
102
166
  return s.toString();
@@ -110,8 +174,9 @@ function hasLangAttr(attrs) {
110
174
  }
111
175
 
112
176
  /**
113
- * For .norn files: inject lang="coffee" / lang="pug" defaults.
114
- * Also auto-wraps top-level non-script/style content in <template lang="pug">.
177
+ * For .norn files: inject lang="coffee" / lang="pug" defaults on
178
+ * <script> and <template> blocks, and auto-wrap any top-level non-script /
179
+ * non-style content in <template lang="pug">.
115
180
  */
116
181
  function nornDefaultLangs() {
117
182
  return {
@@ -121,32 +186,29 @@ function nornDefaultLangs() {
121
186
 
122
187
  let out = content;
123
188
 
124
- // Strip <script> + <style> blocks to find "outside" content
125
- const blocks = [];
126
- const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
127
- let m;
128
- while ((m = blockRe.exec(out)) !== null) {
129
- blocks.push({ start: m.index, end: m.index + m[0].length });
130
- }
131
- let outside = '';
132
- let pos = 0;
133
- for (const b of blocks) {
134
- outside += out.slice(pos, b.start);
135
- pos = b.end;
136
- }
137
- outside += out.slice(pos);
138
-
139
- // If no <template> exists and there's content outside script/style,
140
- // wrap that content in a <template lang="pug"> block.
141
- if (!TEMPLATE_TAG.test(content) && outside.trim()) {
142
- const trimmed = outside.trim();
143
- let result = `<template lang="pug">\n${trimmed}\n</template>\n`;
144
- let cursor = 0;
189
+ // If no <template> exists, scan for script/style blocks and wrap the rest.
190
+ if (!TEMPLATE_TAG.test(content)) {
191
+ const blocks = [];
192
+ const blockRe = /<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi;
193
+ let m;
194
+ while ((m = blockRe.exec(out)) !== null) {
195
+ blocks.push({ start: m.index, end: m.index + m[0].length });
196
+ }
197
+ let outside = '';
198
+ let pos = 0;
145
199
  for (const b of blocks) {
146
- result += '\n' + out.slice(b.start, b.end);
147
- cursor = b.end;
200
+ outside += out.slice(pos, b.start);
201
+ pos = b.end;
202
+ }
203
+ outside += out.slice(pos);
204
+
205
+ if (outside.trim()) {
206
+ let result = `<template lang="pug">\n${outside.trim()}\n</template>\n`;
207
+ for (const b of blocks) {
208
+ result += '\n' + out.slice(b.start, b.end);
209
+ }
210
+ out = result;
148
211
  }
149
- out = result;
150
212
  }
151
213
 
152
214
  // Inject lang="coffee" on <script> tags missing lang=
@@ -181,16 +243,14 @@ function nornCoffeeRuneFusion() {
181
243
  * - `.norn` files default `<script>` to CoffeeScript and `<template>` to Pug
182
244
  * (and auto-wrap top-level content in `<template lang="pug">` if no
183
245
  * template block is present).
184
- * - CoffeeScript output is post-processed to fuse `var X; X = $state(...)`
185
- * declarations into `let X = $state(...)` so Svelte 5 runes work without
186
- * backtick-embedded JS.
246
+ * - CoffeeScript output is post-processed: `var X; X = expr` patterns become
247
+ * `let X = expr` so Svelte 5 runes work without backtick-embedded JS and
248
+ * normal variables/functions don't trigger non-reactive warnings.
187
249
  *
188
250
  * @param {import('svelte-preprocess').AutoPreprocessOptions} [options]
189
251
  */
190
252
  export function nornsPreprocess(options = {}) {
191
253
  return [
192
- // Order matters: norn defaults run first, then svelte-preprocess
193
- // compiles Coffee/Pug, then we fuse rune declarations in the JS output.
194
254
  nornDefaultLangs(),
195
255
  sveltePreprocess({
196
256
  coffeescript: { bare: true },
package/src/uno.js CHANGED
@@ -7,17 +7,30 @@ import {
7
7
  transformerDirectives,
8
8
  transformerVariantGroup
9
9
  } from 'unocss';
10
+ import extractorPug from '@unocss/extractor-pug';
11
+
12
+ const NORN_PIPELINE = [
13
+ /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
14
+ /\.(norn|coffee)($|\?)/
15
+ ];
10
16
 
11
17
  /**
12
- * Norns UnoCSS Vite plugin with a sensible preset stack.
18
+ * Norns UnoCSS Vite plugin.
13
19
  *
14
- * Includes transformerDirectives (so `@apply` works in `<style>` blocks)
15
- * and transformerVariantGroup (so `class="hover:(text-blue underline)"`
16
- * shorthand works).
20
+ * Defaults baked in for the Norns stack:
21
+ * - presets: Uno + Attributify + Icons + Typography
22
+ * - transformers: Directives (`@apply`) + VariantGroup
23
+ * - extractor: extractor-pug, so Pug class shorthand inside `.svelte` and
24
+ * `.norn` files is recognized
25
+ * - content pipeline includes `.norn` and `.coffee`
26
+ * - hmrTopLevelAwait: false to avoid a TDZ error in WebKit/Safari when
27
+ * `virtual:uno.css` is imported from a SvelteKit route module
17
28
  *
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.
29
+ * **Naming caveat for shortcuts:** avoid names starting with a CSS
30
+ * pseudo-class prefix (`hover-`, `focus-`, `link-`, `active-`, `visited-`,
31
+ * `disabled-`, `checked-`, etc.) UnoCSS parses those as variant + utility,
32
+ * not as a shortcut name. Use `nav-link`, `lk-muted`, etc. instead of
33
+ * `link-muted`.
21
34
  *
22
35
  * @param {import('unocss/vite').VitePluginConfig} [options]
23
36
  */
@@ -25,6 +38,8 @@ export function nornsUno(options = {}) {
25
38
  const {
26
39
  presets = [],
27
40
  transformers = [],
41
+ extractors = [],
42
+ content = {},
28
43
  hmrTopLevelAwait = false,
29
44
  ...rest
30
45
  } = options;
@@ -37,6 +52,14 @@ export function nornsUno(options = {}) {
37
52
  ...presets
38
53
  ],
39
54
  transformers: [transformerDirectives(), transformerVariantGroup(), ...transformers],
55
+ extractors: [extractorPug(), ...extractors],
56
+ content: {
57
+ pipeline: {
58
+ include: NORN_PIPELINE,
59
+ ...(content.pipeline ?? {})
60
+ },
61
+ ...content
62
+ },
40
63
  hmrTopLevelAwait,
41
64
  ...rest
42
65
  });