@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.
- package/package.json +1 -1
- package/src/preprocess.js +143 -83
package/package.json
CHANGED
package/src/preprocess.js
CHANGED
|
@@ -25,78 +25,142 @@ function isRuneCall(node) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* Fuse `var X; X =
|
|
29
|
-
*
|
|
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, {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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 },
|