@human-synthesis/norns-core 0.0.3 → 0.0.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/preprocess.js +143 -83
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.4",
4
4
  "description": "Norns core — Svelte with CoffeeScript, Pug, and UnoCSS preconfigured",
5
5
  "license": "MIT",
6
6
  "author": "Human Synthesis",
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 },