@human-synthesis/norns-core 0.0.2 → 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 +3 -1
- package/src/index.js +1 -1
- package/src/preprocess.js +254 -7
- package/src/uno.js +19 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@human-synthesis/norns-core",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
"vite": "^5.0.0 || ^6.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"acorn": "^8.14.0",
|
|
28
29
|
"coffeescript": "^2.7.0",
|
|
30
|
+
"magic-string": "^0.30.10",
|
|
29
31
|
"pug": "^3.0.3",
|
|
30
32
|
"svelte-preprocess": "^6.0.3"
|
|
31
33
|
},
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { nornsPreprocess } from './preprocess.js';
|
|
1
|
+
export { nornsPreprocess, fuseRuneDeclarations } from './preprocess.js';
|
|
2
2
|
export { nornsUno } from './uno.js';
|
package/src/preprocess.js
CHANGED
|
@@ -1,15 +1,262 @@
|
|
|
1
1
|
import { sveltePreprocess } from 'svelte-preprocess';
|
|
2
|
+
import { parse } from 'acorn';
|
|
3
|
+
import MagicString from 'magic-string';
|
|
4
|
+
|
|
5
|
+
const RUNES = new Set([
|
|
6
|
+
'$state',
|
|
7
|
+
'$state.raw',
|
|
8
|
+
'$derived',
|
|
9
|
+
'$derived.by',
|
|
10
|
+
'$effect',
|
|
11
|
+
'$effect.pre',
|
|
12
|
+
'$effect.root',
|
|
13
|
+
'$props',
|
|
14
|
+
'$bindable'
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function isRuneCall(node) {
|
|
18
|
+
if (!node || node.type !== 'CallExpression') return false;
|
|
19
|
+
const c = node.callee;
|
|
20
|
+
if (c.type === 'Identifier') return RUNES.has(c.name);
|
|
21
|
+
if (c.type === 'MemberExpression' && c.object.type === 'Identifier') {
|
|
22
|
+
return RUNES.has(`${c.object.name}.${c.property.name}`);
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
2
26
|
|
|
3
27
|
/**
|
|
4
|
-
*
|
|
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".
|
|
31
|
+
*
|
|
32
|
+
* Also handles `var X; ({X, ...} = $props())` (destructured props).
|
|
33
|
+
*
|
|
34
|
+
* Walks function bodies recursively so closures get the same treatment.
|
|
35
|
+
*/
|
|
36
|
+
export function fuseRuneDeclarations(code) {
|
|
37
|
+
let ast;
|
|
38
|
+
try {
|
|
39
|
+
ast = parse(code, {
|
|
40
|
+
ecmaVersion: 'latest',
|
|
41
|
+
sourceType: 'module',
|
|
42
|
+
allowReturnOutsideFunction: true,
|
|
43
|
+
allowAwaitOutsideFunction: true
|
|
44
|
+
});
|
|
45
|
+
} catch {
|
|
46
|
+
return code;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const s = new MagicString(code);
|
|
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
|
+
}
|
|
82
|
+
|
|
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
|
+
}
|
|
101
|
+
}
|
|
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
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
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;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
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
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return names;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return s.toString();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const SCRIPT_TAG = /<script\b([^>]*)>/i;
|
|
170
|
+
const TEMPLATE_TAG = /<template\b([^>]*)>/i;
|
|
171
|
+
|
|
172
|
+
function hasLangAttr(attrs) {
|
|
173
|
+
return /\blang\s*=/.test(attrs || '');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
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">.
|
|
180
|
+
*/
|
|
181
|
+
function nornDefaultLangs() {
|
|
182
|
+
return {
|
|
183
|
+
name: 'norns-default-langs',
|
|
184
|
+
markup({ content, filename }) {
|
|
185
|
+
if (!filename || !filename.endsWith('.norn')) return null;
|
|
186
|
+
|
|
187
|
+
let out = content;
|
|
188
|
+
|
|
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;
|
|
199
|
+
for (const b of blocks) {
|
|
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;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Inject lang="coffee" on <script> tags missing lang=
|
|
215
|
+
out = out.replace(SCRIPT_TAG, (full, attrs) =>
|
|
216
|
+
hasLangAttr(attrs) ? full : `<script lang="coffee"${attrs}>`
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// Inject lang="pug" on <template> tags missing lang=
|
|
220
|
+
out = out.replace(TEMPLATE_TAG, (full, attrs) =>
|
|
221
|
+
hasLangAttr(attrs) ? full : `<template lang="pug"${attrs}>`
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return { code: out };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function nornCoffeeRuneFusion() {
|
|
230
|
+
return {
|
|
231
|
+
name: 'norns-coffee-rune-fusion',
|
|
232
|
+
script({ content, attributes }) {
|
|
233
|
+
if (attributes.lang !== 'coffee' && attributes.lang !== 'coffeescript') return null;
|
|
234
|
+
const code = fuseRuneDeclarations(content);
|
|
235
|
+
return code === content ? null : { code };
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Norns preprocessor stack.
|
|
242
|
+
*
|
|
243
|
+
* - `.norn` files default `<script>` to CoffeeScript and `<template>` to Pug
|
|
244
|
+
* (and auto-wrap top-level content in `<template lang="pug">` if no
|
|
245
|
+
* template block is present).
|
|
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.
|
|
5
249
|
*
|
|
6
250
|
* @param {import('svelte-preprocess').AutoPreprocessOptions} [options]
|
|
7
|
-
* @returns {import('svelte/compiler').PreprocessorGroup}
|
|
8
251
|
*/
|
|
9
252
|
export function nornsPreprocess(options = {}) {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
253
|
+
return [
|
|
254
|
+
nornDefaultLangs(),
|
|
255
|
+
sveltePreprocess({
|
|
256
|
+
coffeescript: { bare: true },
|
|
257
|
+
pug: {},
|
|
258
|
+
...options
|
|
259
|
+
}),
|
|
260
|
+
nornCoffeeRuneFusion()
|
|
261
|
+
];
|
|
15
262
|
}
|
package/src/uno.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import UnoCSS from 'unocss/vite';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
presetUno,
|
|
4
|
+
presetAttributify,
|
|
5
|
+
presetIcons,
|
|
6
|
+
presetTypography,
|
|
7
|
+
transformerDirectives,
|
|
8
|
+
transformerVariantGroup
|
|
9
|
+
} from 'unocss';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Norns UnoCSS Vite plugin with a sensible preset stack.
|
|
6
13
|
*
|
|
14
|
+
* Includes transformerDirectives (so `@apply` works in `<style>` blocks)
|
|
15
|
+
* and transformerVariantGroup (so `class="hover:(text-blue underline)"`
|
|
16
|
+
* shorthand works).
|
|
17
|
+
*
|
|
7
18
|
* Defaults `hmrTopLevelAwait: false` to avoid a TDZ
|
|
8
19
|
* "Cannot access 'component' before initialization" error in WebKit/Safari
|
|
9
20
|
* when importing 'virtual:uno.css' from a SvelteKit layout/page module.
|
|
10
|
-
* Pass `hmrTopLevelAwait: true` if you've verified your stack handles it.
|
|
11
21
|
*
|
|
12
22
|
* @param {import('unocss/vite').VitePluginConfig} [options]
|
|
13
23
|
*/
|
|
14
24
|
export function nornsUno(options = {}) {
|
|
15
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
presets = [],
|
|
27
|
+
transformers = [],
|
|
28
|
+
hmrTopLevelAwait = false,
|
|
29
|
+
...rest
|
|
30
|
+
} = options;
|
|
16
31
|
return UnoCSS({
|
|
17
32
|
presets: [
|
|
18
33
|
presetUno(),
|
|
@@ -21,6 +36,7 @@ export function nornsUno(options = {}) {
|
|
|
21
36
|
presetTypography(),
|
|
22
37
|
...presets
|
|
23
38
|
],
|
|
39
|
+
transformers: [transformerDirectives(), transformerVariantGroup(), ...transformers],
|
|
24
40
|
hmrTopLevelAwait,
|
|
25
41
|
...rest
|
|
26
42
|
});
|