@human-synthesis/norns-core 0.0.2 → 0.0.3
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 +194 -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.3",
|
|
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,202 @@
|
|
|
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
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fuse `var X; X = $rune(...)` patterns (CoffeeScript output) into
|
|
29
|
+
* `let X = $rune(...)` so Svelte 5 accepts the rune in declaration position.
|
|
30
|
+
*
|
|
31
|
+
* Also handles `var X; ({X} = $props())` (destructured props).
|
|
32
|
+
*/
|
|
33
|
+
export function fuseRuneDeclarations(code) {
|
|
34
|
+
let ast;
|
|
35
|
+
try {
|
|
36
|
+
ast = parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
|
|
37
|
+
} catch {
|
|
38
|
+
return code;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const s = new MagicString(code);
|
|
42
|
+
const fused = new Set();
|
|
43
|
+
const varStmts = [];
|
|
44
|
+
|
|
45
|
+
for (const stmt of ast.body) {
|
|
46
|
+
if (stmt.type === 'VariableDeclaration' && stmt.kind === 'var') {
|
|
47
|
+
varStmts.push(stmt);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
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;
|
|
64
|
+
}
|
|
65
|
+
|
|
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);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
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
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
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};`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return s.toString();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const SCRIPT_TAG = /<script\b([^>]*)>/i;
|
|
106
|
+
const TEMPLATE_TAG = /<template\b([^>]*)>/i;
|
|
107
|
+
|
|
108
|
+
function hasLangAttr(attrs) {
|
|
109
|
+
return /\blang\s*=/.test(attrs || '');
|
|
110
|
+
}
|
|
2
111
|
|
|
3
112
|
/**
|
|
4
|
-
*
|
|
113
|
+
* For .norn files: inject lang="coffee" / lang="pug" defaults.
|
|
114
|
+
* Also auto-wraps top-level non-script/style content in <template lang="pug">.
|
|
115
|
+
*/
|
|
116
|
+
function nornDefaultLangs() {
|
|
117
|
+
return {
|
|
118
|
+
name: 'norns-default-langs',
|
|
119
|
+
markup({ content, filename }) {
|
|
120
|
+
if (!filename || !filename.endsWith('.norn')) return null;
|
|
121
|
+
|
|
122
|
+
let out = content;
|
|
123
|
+
|
|
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;
|
|
145
|
+
for (const b of blocks) {
|
|
146
|
+
result += '\n' + out.slice(b.start, b.end);
|
|
147
|
+
cursor = b.end;
|
|
148
|
+
}
|
|
149
|
+
out = result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Inject lang="coffee" on <script> tags missing lang=
|
|
153
|
+
out = out.replace(SCRIPT_TAG, (full, attrs) =>
|
|
154
|
+
hasLangAttr(attrs) ? full : `<script lang="coffee"${attrs}>`
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Inject lang="pug" on <template> tags missing lang=
|
|
158
|
+
out = out.replace(TEMPLATE_TAG, (full, attrs) =>
|
|
159
|
+
hasLangAttr(attrs) ? full : `<template lang="pug"${attrs}>`
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return { code: out };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function nornCoffeeRuneFusion() {
|
|
168
|
+
return {
|
|
169
|
+
name: 'norns-coffee-rune-fusion',
|
|
170
|
+
script({ content, attributes }) {
|
|
171
|
+
if (attributes.lang !== 'coffee' && attributes.lang !== 'coffeescript') return null;
|
|
172
|
+
const code = fuseRuneDeclarations(content);
|
|
173
|
+
return code === content ? null : { code };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Norns preprocessor stack.
|
|
180
|
+
*
|
|
181
|
+
* - `.norn` files default `<script>` to CoffeeScript and `<template>` to Pug
|
|
182
|
+
* (and auto-wrap top-level content in `<template lang="pug">` if no
|
|
183
|
+
* 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.
|
|
5
187
|
*
|
|
6
188
|
* @param {import('svelte-preprocess').AutoPreprocessOptions} [options]
|
|
7
|
-
* @returns {import('svelte/compiler').PreprocessorGroup}
|
|
8
189
|
*/
|
|
9
190
|
export function nornsPreprocess(options = {}) {
|
|
10
|
-
return
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
191
|
+
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
|
+
nornDefaultLangs(),
|
|
195
|
+
sveltePreprocess({
|
|
196
|
+
coffeescript: { bare: true },
|
|
197
|
+
pug: {},
|
|
198
|
+
...options
|
|
199
|
+
}),
|
|
200
|
+
nornCoffeeRuneFusion()
|
|
201
|
+
];
|
|
15
202
|
}
|
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
|
});
|