@sprlab/wccompiler 0.0.1

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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * CSS Scoper — prefixes CSS selectors with the component tag name.
3
+ *
4
+ * Handles:
5
+ * - Simple selectors (.class, #id, element)
6
+ * - Comma-separated selectors
7
+ * - At-rules (@media, @keyframes) — preserved without prefixing
8
+ * - Nested selectors inside at-rules — still prefixed
9
+ */
10
+
11
+ /**
12
+ * Scope CSS by prefixing each selector with the component tag name.
13
+ *
14
+ * @param {string} css - Raw CSS string
15
+ * @param {string} tagName - Component tag name (e.g. "wcc-hi")
16
+ * @returns {string} Scoped CSS string
17
+ */
18
+ export function scopeCSS(css, tagName) {
19
+ if (!css || !css.trim()) return '';
20
+
21
+ const result = [];
22
+ let i = 0;
23
+
24
+ while (i < css.length) {
25
+ // Skip whitespace
26
+ if (/\s/.test(css[i])) {
27
+ result.push(css[i]);
28
+ i++;
29
+ continue;
30
+ }
31
+
32
+ // Detect at-rules
33
+ if (css[i] === '@') {
34
+ const atResult = consumeAtRule(css, i, tagName);
35
+ result.push(atResult.text);
36
+ i = atResult.end;
37
+ continue;
38
+ }
39
+
40
+ // Detect closing brace (end of a block)
41
+ if (css[i] === '}') {
42
+ result.push('}');
43
+ i++;
44
+ continue;
45
+ }
46
+
47
+ // Otherwise, it's a selector — read until '{'
48
+ const selectorEnd = css.indexOf('{', i);
49
+ if (selectorEnd === -1) {
50
+ // No more blocks, append remaining text as-is
51
+ result.push(css.slice(i));
52
+ break;
53
+ }
54
+
55
+ const rawSelector = css.slice(i, selectorEnd);
56
+ const scopedSelector = prefixSelectors(rawSelector, tagName);
57
+ result.push(scopedSelector);
58
+
59
+ // Now consume the declaration block (until matching '}')
60
+ const blockResult = consumeBlock(css, selectorEnd);
61
+ result.push(blockResult.text);
62
+ i = blockResult.end;
63
+ }
64
+
65
+ return result.join('');
66
+ }
67
+
68
+
69
+ /**
70
+ * Prefix comma-separated selectors with the tag name.
71
+ * e.g. ".foo, .bar" → "tag .foo, tag .bar"
72
+ */
73
+ function prefixSelectors(raw, tagName) {
74
+ return raw
75
+ .split(',')
76
+ .map(s => {
77
+ const trimmed = s.trim();
78
+ if (!trimmed) return s;
79
+ // Preserve the leading whitespace from the original
80
+ const leadingWs = s.match(/^(\s*)/)[1];
81
+ return `${leadingWs}${tagName} ${trimmed}`;
82
+ })
83
+ .join(',');
84
+ }
85
+
86
+ /**
87
+ * Consume a { ... } block starting at the opening brace.
88
+ * Returns the text (including braces) and the position after the closing brace.
89
+ */
90
+ function consumeBlock(css, start) {
91
+ let depth = 0;
92
+ let i = start;
93
+ const chars = [];
94
+
95
+ while (i < css.length) {
96
+ chars.push(css[i]);
97
+ if (css[i] === '{') depth++;
98
+ if (css[i] === '}') {
99
+ depth--;
100
+ if (depth === 0) {
101
+ return { text: chars.join(''), end: i + 1 };
102
+ }
103
+ }
104
+ i++;
105
+ }
106
+
107
+ // Unclosed block — return what we have
108
+ return { text: chars.join(''), end: i };
109
+ }
110
+
111
+ /**
112
+ * Consume an at-rule starting at '@'.
113
+ * Handles both block at-rules (@media { ... }) and statement at-rules (@import ...).
114
+ * For block at-rules, recursively scopes selectors inside.
115
+ */
116
+ function consumeAtRule(css, start, tagName) {
117
+ // Read the at-rule prelude (everything up to '{' or ';')
118
+ let i = start;
119
+ const prelude = [];
120
+
121
+ while (i < css.length && css[i] !== '{' && css[i] !== ';') {
122
+ prelude.push(css[i]);
123
+ i++;
124
+ }
125
+
126
+ if (i >= css.length) {
127
+ // Unterminated at-rule
128
+ return { text: prelude.join(''), end: i };
129
+ }
130
+
131
+ if (css[i] === ';') {
132
+ // Statement at-rule (e.g. @import, @charset)
133
+ prelude.push(';');
134
+ return { text: prelude.join(''), end: i + 1 };
135
+ }
136
+
137
+ // Block at-rule — css[i] === '{'
138
+ const preludeStr = prelude.join('');
139
+ const atName = preludeStr.trim().split(/\s/)[0]; // e.g. "@media", "@keyframes"
140
+
141
+ // @keyframes: don't scope inner selectors (they're keyframe stops, not CSS selectors)
142
+ if (atName === '@keyframes' || atName === '@-webkit-keyframes') {
143
+ const block = consumeBlock(css, i);
144
+ return { text: preludeStr + block.text, end: block.end };
145
+ }
146
+
147
+ // For @media and other nesting at-rules: scope the inner selectors
148
+ // We need to parse the inner content recursively
149
+ const innerStart = i + 1; // after '{'
150
+ let depth = 1;
151
+ let j = innerStart;
152
+
153
+ // Find the matching closing brace
154
+ while (j < css.length && depth > 0) {
155
+ if (css[j] === '{') depth++;
156
+ if (css[j] === '}') depth--;
157
+ if (depth > 0) j++;
158
+ }
159
+
160
+ const innerCSS = css.slice(innerStart, j);
161
+ const scopedInner = scopeCSS(innerCSS, tagName);
162
+
163
+ return {
164
+ text: `${preludeStr}{${scopedInner}}`,
165
+ end: j + 1,
166
+ };
167
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Dev Server — static HTTP server with polling-based live-reload.
3
+ */
4
+
5
+ import { createServer } from 'node:http';
6
+ import { readFileSync, watch, existsSync } from 'node:fs';
7
+ import { resolve, extname } from 'node:path';
8
+
9
+ const MIME_TYPES = {
10
+ '.html': 'text/html; charset=utf-8',
11
+ '.js': 'text/javascript; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.json': 'application/json; charset=utf-8',
14
+ '.png': 'image/png',
15
+ '.jpg': 'image/jpeg',
16
+ '.svg': 'image/svg+xml',
17
+ '.ico': 'image/x-icon',
18
+ };
19
+
20
+ const POLL_SNIPPET = `<script>
21
+ (function() {
22
+ var t = 0, ready = false;
23
+ setInterval(function() {
24
+ fetch('/__poll').then(function(r) { return r.json(); }).then(function(d) {
25
+ if (!ready) { t = d.t; ready = true; return; }
26
+ if (d.t > t) { t = d.t; location.reload(); }
27
+ }).catch(function() {});
28
+ }, 500);
29
+ })();
30
+ </script>`;
31
+
32
+ export function startDevServer({ port, root, outputDir }) {
33
+ let changeTs = Date.now();
34
+
35
+ const server = createServer((req, res) => {
36
+ const url = req.url.split('?')[0];
37
+
38
+ // Poll endpoint
39
+ if (url === '/__poll') {
40
+ const body = JSON.stringify({ t: changeTs });
41
+ const buf = Buffer.from(body);
42
+ res.writeHead(200, {
43
+ 'Content-Type': 'application/json',
44
+ 'Content-Length': buf.byteLength,
45
+ 'Cache-Control': 'no-cache',
46
+ });
47
+ res.end(buf);
48
+ return;
49
+ }
50
+
51
+ // Static files
52
+ const filePath = url === '/' ? '/index.html' : url;
53
+ const fullPath = resolve(root, '.' + filePath);
54
+
55
+ try {
56
+ let buf = readFileSync(fullPath);
57
+ const ext = extname(fullPath);
58
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
59
+
60
+ // Inject poll snippet into HTML
61
+ if (ext === '.html') {
62
+ let html = buf.toString('utf-8');
63
+ if (html.includes('</body>')) {
64
+ html = html.replace('</body>', POLL_SNIPPET + '\n</body>');
65
+ } else {
66
+ html += '\n' + POLL_SNIPPET;
67
+ }
68
+ buf = Buffer.from(html, 'utf-8');
69
+ }
70
+
71
+ res.writeHead(200, {
72
+ 'Content-Type': mime,
73
+ 'Content-Length': buf.byteLength,
74
+ });
75
+ res.end(buf);
76
+ } catch {
77
+ const msg = 'Not Found';
78
+ res.writeHead(404, {
79
+ 'Content-Type': 'text/plain',
80
+ 'Content-Length': Buffer.byteLength(msg),
81
+ });
82
+ res.end(msg);
83
+ }
84
+ });
85
+
86
+ // Watch output dir — update timestamp on changes (debounced)
87
+ let watcher = null;
88
+ if (outputDir && existsSync(outputDir)) {
89
+ let timer = null;
90
+ watcher = watch(outputDir, { recursive: true }, () => {
91
+ if (timer) clearTimeout(timer);
92
+ timer = setTimeout(() => { changeTs = Date.now(); }, 200);
93
+ });
94
+ }
95
+
96
+ server.listen(port, () => {
97
+ console.log(`Dev server running at http://localhost:${port}`);
98
+ });
99
+
100
+ return {
101
+ server,
102
+ close() {
103
+ if (watcher) watcher.close();
104
+ server.close();
105
+ },
106
+ };
107
+ }
package/lib/parser.js ADDED
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Parser for .html source files with <template>, <script>, <style> blocks.
3
+ *
4
+ * Extracts:
5
+ * - Block contents (template, script, style)
6
+ * - defineProps declarations
7
+ * - Root-level reactive variables
8
+ * - Computed properties
9
+ * - Watchers
10
+ * - Function declarations
11
+ *
12
+ * Tree walking (bindings, events, slots, processedTemplate) is NOT handled
13
+ * here — that's the responsibility of tree-walker.js (task 3).
14
+ */
15
+
16
+ // ── Block extraction ────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Extract the content of a named block from the HTML source.
20
+ * Returns the inner content string, or null if the block is not found.
21
+ */
22
+ function extractBlock(html, blockName) {
23
+ const re = new RegExp(`<${blockName}>([\\s\\S]*?)<\\/${blockName}>`);
24
+ const m = html.match(re);
25
+ return m ? m[1] : null;
26
+ }
27
+
28
+ // ── Name derivation ─────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Derive tagName from fileName (strip .html extension if present).
32
+ * e.g. "spr-hi.html" → "spr-hi", "spr-hi" → "spr-hi"
33
+ */
34
+ function deriveTagName(fileName) {
35
+ return fileName.replace(/\.html$/, '');
36
+ }
37
+
38
+ /**
39
+ * Convert a kebab-case tag name to PascalCase class name.
40
+ * e.g. "spr-hi" → "SprHi"
41
+ */
42
+ function toClassName(tagName) {
43
+ return tagName
44
+ .split('-')
45
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
46
+ .join('');
47
+ }
48
+
49
+ // ── Props extraction ────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Extract props from defineProps([...]) call.
53
+ * Detects duplicates and throws DUPLICATE_PROPS error.
54
+ * Returns an empty array if no defineProps is found.
55
+ */
56
+ function extractProps(script, fileName) {
57
+ const m = script.match(/defineProps\(\[([^\]]*)\]\)/);
58
+ if (!m) return [];
59
+
60
+ const raw = m[1];
61
+ const props = [];
62
+ const re = /'([^']+)'/g;
63
+ let match;
64
+ while ((match = re.exec(raw)) !== null) {
65
+ props.push(match[1]);
66
+ }
67
+
68
+ // Detect duplicates
69
+ const seen = new Set();
70
+ const duplicates = new Set();
71
+ for (const p of props) {
72
+ if (seen.has(p)) duplicates.add(p);
73
+ seen.add(p);
74
+ }
75
+ if (duplicates.size > 0) {
76
+ const names = [...duplicates].join(', ');
77
+ const error = new Error(
78
+ `Error en '${fileName}': props duplicados: ${names}`
79
+ );
80
+ error.code = 'DUPLICATE_PROPS';
81
+ throw error;
82
+ }
83
+
84
+ return props;
85
+ }
86
+
87
+ // ── Root-level reactive variables ───────────────────────────────────
88
+
89
+ /**
90
+ * Extract root-level variable declarations (const/let/var with literal value).
91
+ * Tracks brace depth to skip variables inside functions or nested blocks.
92
+ * Excludes lines that use computed(...) or watch(...).
93
+ */
94
+ function extractRootVars(script) {
95
+ const vars = [];
96
+ let depth = 0;
97
+
98
+ for (const line of script.split('\n')) {
99
+ // Track brace depth
100
+ for (const ch of line) {
101
+ if (ch === '{') depth++;
102
+ if (ch === '}') depth--;
103
+ }
104
+
105
+ // Only extract at root level (depth === 0)
106
+ if (depth !== 0) continue;
107
+
108
+ // Skip computed and watch assignments
109
+ if (/computed\s*\(/.test(line) || /watch\s*\(/.test(line)) continue;
110
+
111
+ const m = line.match(/^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/);
112
+ if (m) {
113
+ vars.push({ name: m[1], value: m[2] });
114
+ }
115
+ }
116
+
117
+ return vars;
118
+ }
119
+
120
+ // ── Computed properties ─────────────────────────────────────────────
121
+
122
+ /**
123
+ * Extract computed property declarations.
124
+ * Pattern: const name = computed(() => expr)
125
+ */
126
+ function extractComputeds(script) {
127
+ const out = [];
128
+ const re = /(?:const|let|var)\s+(\w+)\s*=\s*computed\(\s*\(\)\s*=>\s*([\s\S]*?)\)/g;
129
+ let m;
130
+ while ((m = re.exec(script)) !== null) {
131
+ out.push({ name: m[1], body: m[2].trim() });
132
+ }
133
+ return out;
134
+ }
135
+
136
+ // ── Watchers ────────────────────────────────────────────────────────
137
+
138
+ /**
139
+ * Extract watcher declarations.
140
+ * Pattern: watch('target', (newParam, oldParam) => { body })
141
+ */
142
+ function extractWatchers(script) {
143
+ const out = [];
144
+ const re = /watch\(\s*'(\w+)'\s*,\s*\((\w+)\s*,\s*(\w+)\)\s*=>\s*\{([\s\S]*?)\}\s*\)/g;
145
+ let m;
146
+ while ((m = re.exec(script)) !== null) {
147
+ out.push({
148
+ target: m[1],
149
+ newParam: m[2],
150
+ oldParam: m[3],
151
+ body: m[4].trim(),
152
+ });
153
+ }
154
+ return out;
155
+ }
156
+
157
+ // ── Function declarations ───────────────────────────────────────────
158
+
159
+ /**
160
+ * Extract top-level function declarations.
161
+ * Pattern: function name(params) { body }
162
+ * Uses brace tracking to capture the full function body.
163
+ */
164
+ function extractFunctions(script) {
165
+ const functions = [];
166
+ const lines = script.split('\n');
167
+ let i = 0;
168
+
169
+ while (i < lines.length) {
170
+ const line = lines[i];
171
+ const m = line.match(/^\s*function\s+(\w+)\s*\(([^)]*)\)\s*\{/);
172
+ if (m) {
173
+ const name = m[1];
174
+ const params = m[2].trim();
175
+ // Collect body by tracking brace depth
176
+ let depth = 0;
177
+ let bodyLines = [];
178
+ let started = false;
179
+
180
+ for (let j = i; j < lines.length; j++) {
181
+ const l = lines[j];
182
+ for (const ch of l) {
183
+ if (ch === '{') {
184
+ if (started) depth++;
185
+ else { depth = 1; started = true; }
186
+ }
187
+ if (ch === '}') depth--;
188
+ }
189
+
190
+ if (j === i) {
191
+ // First line: capture everything after the opening brace
192
+ const afterBrace = l.substring(l.indexOf('{') + 1);
193
+ if (afterBrace.trim()) bodyLines.push(afterBrace);
194
+ } else if (depth <= 0) {
195
+ // Last line: capture everything before the closing brace
196
+ const lastBraceIdx = l.lastIndexOf('}');
197
+ const before = l.substring(0, lastBraceIdx);
198
+ if (before.trim()) bodyLines.push(before);
199
+ i = j;
200
+ break;
201
+ } else {
202
+ bodyLines.push(l);
203
+ }
204
+ }
205
+
206
+ functions.push({
207
+ name,
208
+ params,
209
+ body: bodyLines.join('\n').trim(),
210
+ });
211
+ }
212
+ i++;
213
+ }
214
+
215
+ return functions;
216
+ }
217
+
218
+ // ── Main parse function ─────────────────────────────────────────────
219
+
220
+ /**
221
+ * Parse an HTML source string into a ParseResult IR.
222
+ *
223
+ * @param {string} html - The full HTML source content
224
+ * @param {string} fileName - The file name (e.g. "spr-hi.html" or "spr-hi")
225
+ * @returns {ParseResult}
226
+ */
227
+ export function parse(html, fileName) {
228
+ const tagName = deriveTagName(fileName);
229
+ const className = toClassName(tagName);
230
+
231
+ // Extract blocks
232
+ const template = extractBlock(html, 'template');
233
+ if (template === null) {
234
+ const error = new Error(
235
+ `Error en '${fileName}': el bloque <template> es obligatorio`
236
+ );
237
+ error.code = 'MISSING_TEMPLATE';
238
+ throw error;
239
+ }
240
+
241
+ const script = extractBlock(html, 'script') ?? '';
242
+ const style = extractBlock(html, 'style') ?? '';
243
+ const trimmedScript = script.trim();
244
+
245
+ // Extract script-level constructs
246
+ const props = extractProps(trimmedScript, fileName);
247
+ const reactiveVars = extractRootVars(trimmedScript);
248
+ const computeds = extractComputeds(trimmedScript);
249
+ const watchers = extractWatchers(trimmedScript);
250
+ const methods = extractFunctions(trimmedScript);
251
+
252
+ return {
253
+ tagName,
254
+ className,
255
+ template,
256
+ script: trimmedScript,
257
+ style: style.trim(),
258
+ props,
259
+ reactiveVars,
260
+ computeds,
261
+ watchers,
262
+ methods,
263
+ // Tree walker fields — populated by tree-walker.js (task 3)
264
+ bindings: [],
265
+ events: [],
266
+ slots: [],
267
+ processedTemplate: null,
268
+ };
269
+ }
package/lib/printer.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Pretty Printer — reconstructs .html source from a ParseResult IR.
3
+ *
4
+ * Produces a valid source file with <template>, <script>, and <style> blocks
5
+ * that preserves the order and semantics of all IR elements.
6
+ */
7
+
8
+ /**
9
+ * Reconstruct the <template> block from the IR.
10
+ * Uses the original template string (before tree-walking) since it
11
+ * already contains {{var}} bindings, @event attributes, and <slot> elements.
12
+ *
13
+ * @param {import('./parser.js').ParseResult} ir
14
+ * @returns {string}
15
+ */
16
+ function reconstructTemplate(ir) {
17
+ return ir.template;
18
+ }
19
+
20
+ /**
21
+ * Reconstruct the <script> block from the IR's extracted constructs.
22
+ *
23
+ * @param {import('./parser.js').ParseResult} ir
24
+ * @returns {string}
25
+ */
26
+ function reconstructScript(ir) {
27
+ const lines = [];
28
+
29
+ // defineProps
30
+ if (ir.props.length > 0) {
31
+ const propsList = ir.props.map(p => `'${p}'`).join(', ');
32
+ lines.push(` defineProps([${propsList}])`);
33
+ lines.push('');
34
+ }
35
+
36
+ // Reactive variables
37
+ for (const v of ir.reactiveVars) {
38
+ lines.push(` const ${v.name} = ${v.value}`);
39
+ }
40
+ if (ir.reactiveVars.length > 0) lines.push('');
41
+
42
+ // Computed properties
43
+ for (const c of ir.computeds) {
44
+ lines.push(` const ${c.name} = computed(() => ${c.body})`);
45
+ }
46
+ if (ir.computeds.length > 0) lines.push('');
47
+
48
+ // Watchers
49
+ for (const w of ir.watchers) {
50
+ lines.push(` watch('${w.target}', (${w.newParam}, ${w.oldParam}) => {`);
51
+ // Indent the body lines
52
+ for (const bodyLine of w.body.split('\n')) {
53
+ lines.push(` ${bodyLine}`);
54
+ }
55
+ lines.push(' })');
56
+ }
57
+ if (ir.watchers.length > 0) lines.push('');
58
+
59
+ // Functions
60
+ for (const m of ir.methods) {
61
+ lines.push(` function ${m.name}(${m.params}) {`);
62
+ for (const bodyLine of m.body.split('\n')) {
63
+ lines.push(` ${bodyLine}`);
64
+ }
65
+ lines.push(' }');
66
+ }
67
+
68
+ return lines.join('\n');
69
+ }
70
+
71
+ /**
72
+ * Pretty-print a ParseResult IR back to .html source format.
73
+ *
74
+ * @param {import('./parser.js').ParseResult} ir - The intermediate representation
75
+ * @returns {string} Reconstructed .html source
76
+ */
77
+ export function prettyPrint(ir) {
78
+ const parts = [];
79
+
80
+ // <template> block
81
+ parts.push('<template>');
82
+ parts.push(reconstructTemplate(ir));
83
+ parts.push('</template>');
84
+
85
+ // <style> block (if present)
86
+ if (ir.style) {
87
+ parts.push('');
88
+ parts.push('<style>');
89
+ parts.push(ir.style);
90
+ parts.push('</style>');
91
+ }
92
+
93
+ // <script> block (if there's any script content)
94
+ const scriptContent = reconstructScript(ir);
95
+ if (scriptContent.trim()) {
96
+ parts.push('');
97
+ parts.push('<script>');
98
+ parts.push(scriptContent);
99
+ parts.push('</script>');
100
+ }
101
+
102
+ parts.push('');
103
+ return parts.join('\n');
104
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Template string containing the mini reactive runtime (~40 lines).
3
+ * This gets inlined at the top of each compiled component so the output
4
+ * is fully self-contained with zero imports.
5
+ *
6
+ * Implements:
7
+ * - __signal(initialValue): getter/setter function with subscriber tracking
8
+ * - __computed(fn): cached derived value that auto-invalidates
9
+ * - __effect(fn): runs fn immediately and re-runs when dependencies change
10
+ *
11
+ * Dependency tracking uses a global stack (__currentEffect).
12
+ */
13
+ export const reactiveRuntime = `
14
+ let __currentEffect = null;
15
+
16
+ function __signal(initial) {
17
+ let _value = initial;
18
+ const _subs = new Set();
19
+ return (...args) => {
20
+ if (args.length === 0) {
21
+ if (__currentEffect) _subs.add(__currentEffect);
22
+ return _value;
23
+ }
24
+ const old = _value;
25
+ _value = args[0];
26
+ if (old !== _value) {
27
+ for (const fn of [..._subs]) fn();
28
+ }
29
+ };
30
+ }
31
+
32
+ function __computed(fn) {
33
+ let _cached, _dirty = true;
34
+ const _subs = new Set();
35
+ const recompute = () => {
36
+ _dirty = true;
37
+ for (const fn of [..._subs]) fn();
38
+ };
39
+ return () => {
40
+ if (__currentEffect) _subs.add(__currentEffect);
41
+ if (_dirty) {
42
+ const prev = __currentEffect;
43
+ __currentEffect = recompute;
44
+ _cached = fn();
45
+ __currentEffect = prev;
46
+ _dirty = false;
47
+ }
48
+ return _cached;
49
+ };
50
+ }
51
+
52
+ function __effect(fn) {
53
+ const run = () => {
54
+ const prev = __currentEffect;
55
+ __currentEffect = run;
56
+ fn();
57
+ __currentEffect = prev;
58
+ };
59
+ run();
60
+ }
61
+ `;