mez-easyjs 0.1.0
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/LICENSE +21 -0
- package/README.md +40 -0
- package/bin/create-easy-app.js +60 -0
- package/bin/easy.js +7 -0
- package/compiler.d.ts +37 -0
- package/package.json +74 -0
- package/runtime.d.ts +103 -0
- package/src/commands/build.js +284 -0
- package/src/commands/create.js +476 -0
- package/src/commands/dev.js +434 -0
- package/src/commands/generate.js +104 -0
- package/src/commands/ws.js +157 -0
- package/src/compiler/codegen.js +1334 -0
- package/src/compiler/css.js +117 -0
- package/src/compiler/errors.js +65 -0
- package/src/compiler/expr.js +142 -0
- package/src/compiler/index.js +105 -0
- package/src/compiler/parse.js +115 -0
- package/src/compiler/strip-ts.js +39 -0
- package/src/compiler/template.js +419 -0
- package/src/index.js +128 -0
- package/src/runtime/app.js +115 -0
- package/src/runtime/component.js +222 -0
- package/src/runtime/css.js +35 -0
- package/src/runtime/errors.js +121 -0
- package/src/runtime/escape.js +62 -0
- package/src/runtime/events.js +151 -0
- package/src/runtime/hmr.js +157 -0
- package/src/runtime/index.js +40 -0
- package/src/runtime/overlay.js +95 -0
- package/src/runtime/reactive.js +172 -0
- package/src/runtime/router.js +423 -0
- package/src/runtime/scheduler.js +39 -0
- package/src/runtime/store.js +42 -0
- package/src/transform.js +106 -0
- package/templates/minimal/README.md +21 -0
- package/templates/minimal/easy.config.js +7 -0
- package/templates/minimal/index.html +13 -0
- package/templates/minimal/package.json +14 -0
- package/templates/minimal/src/App.easy +24 -0
- package/templates/minimal/src/main.js +4 -0
- package/templates/minimal/src/styles.css +14 -0
- package/templates/starter/README.md +18 -0
- package/templates/starter/easy.config.js +7 -0
- package/templates/starter/index.html +13 -0
- package/templates/starter/package.json +14 -0
- package/templates/starter/src/App.easy +69 -0
- package/templates/starter/src/components/Counter.easy +76 -0
- package/templates/starter/src/components/Greeting.easy +24 -0
- package/templates/starter/src/main.js +11 -0
- package/templates/starter/src/pages/About.easy +20 -0
- package/templates/starter/src/pages/Home.easy +69 -0
- package/templates/starter/src/styles.css +14 -0
- package/templates/tailwind/README.md +23 -0
- package/templates/tailwind/easy.config.js +7 -0
- package/templates/tailwind/index.html +19 -0
- package/templates/tailwind/package.json +14 -0
- package/templates/tailwind/src/App.easy +53 -0
- package/templates/tailwind/src/main.js +4 -0
- package/templates/tailwind/src/styles.css +4 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope CSS selectors with [scopeId] attribute selectors.
|
|
3
|
+
* Handles basic CSS: rules, media queries, keyframes (left alone).
|
|
4
|
+
*/
|
|
5
|
+
export function scopeCSS(css, scopeId) {
|
|
6
|
+
if (!css || !css.trim()) return '';
|
|
7
|
+
|
|
8
|
+
const attr = `[${scopeId}]`;
|
|
9
|
+
let output = '';
|
|
10
|
+
let i = 0;
|
|
11
|
+
const src = css;
|
|
12
|
+
|
|
13
|
+
while (i < src.length) {
|
|
14
|
+
// Skip comments
|
|
15
|
+
if (src[i] === '/' && src[i + 1] === '*') {
|
|
16
|
+
const end = src.indexOf('*/', i + 2);
|
|
17
|
+
output += src.slice(i, end === -1 ? src.length : end + 2);
|
|
18
|
+
i = end === -1 ? src.length : end + 2;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// @keyframes / @font-face — copy block as-is
|
|
23
|
+
if (src[i] === '@') {
|
|
24
|
+
const rest = src.slice(i);
|
|
25
|
+
const atMatch = rest.match(/^@(keyframes|font-face|import|charset)\b/i);
|
|
26
|
+
if (atMatch) {
|
|
27
|
+
const block = readAtBlock(src, i);
|
|
28
|
+
output += block.text;
|
|
29
|
+
i = block.end;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// @media / @supports — scope inner rules
|
|
34
|
+
const nestMatch = rest.match(/^@(media|supports|layer|container)[^{]*\{/i);
|
|
35
|
+
if (nestMatch) {
|
|
36
|
+
const headerEnd = src.indexOf('{', i);
|
|
37
|
+
output += src.slice(i, headerEnd + 1);
|
|
38
|
+
i = headerEnd + 1;
|
|
39
|
+
const inner = extractBalanced(src, i - 1);
|
|
40
|
+
output += scopeCSS(inner.content, scopeId);
|
|
41
|
+
output += '}';
|
|
42
|
+
i = inner.end;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Regular rule: selectors { body }
|
|
48
|
+
const brace = src.indexOf('{', i);
|
|
49
|
+
if (brace === -1) {
|
|
50
|
+
output += src.slice(i);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const selectors = src.slice(i, brace).trim();
|
|
55
|
+
const body = extractBalanced(src, brace);
|
|
56
|
+
|
|
57
|
+
if (selectors) {
|
|
58
|
+
const scoped = selectors
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((sel) => scopeSelector(sel.trim(), attr))
|
|
61
|
+
.join(', ');
|
|
62
|
+
output += `${scoped} {${body.content}}`;
|
|
63
|
+
}
|
|
64
|
+
i = body.end;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scopeSelector(selector, attr) {
|
|
71
|
+
if (!selector) return selector;
|
|
72
|
+
// :global(.foo) → .foo
|
|
73
|
+
if (selector.startsWith(':global(')) {
|
|
74
|
+
const inner = selector.slice(8, -1);
|
|
75
|
+
return inner;
|
|
76
|
+
}
|
|
77
|
+
// Don't scope keyframe percentages / from / to
|
|
78
|
+
if (/^(from|to|\d+%)$/i.test(selector)) return selector;
|
|
79
|
+
|
|
80
|
+
// Insert attribute before pseudo-elements/classes on the last compound selector
|
|
81
|
+
// e.g. ".foo .bar:hover" → ".foo .bar[e-xxx]:hover"
|
|
82
|
+
const parts = selector.split(/\s+/);
|
|
83
|
+
const last = parts[parts.length - 1];
|
|
84
|
+
const m = last.match(/^([^\::]+)?(::?[a-z-]+(?:\([^)]*\))?)*$/i);
|
|
85
|
+
if (m) {
|
|
86
|
+
const base = m[1] || '*';
|
|
87
|
+
const pseudos = last.slice(base.length);
|
|
88
|
+
parts[parts.length - 1] = `${base}${attr}${pseudos}`;
|
|
89
|
+
return parts.join(' ');
|
|
90
|
+
}
|
|
91
|
+
return `${selector}${attr}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function extractBalanced(src, openIdx) {
|
|
95
|
+
let depth = 0;
|
|
96
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
97
|
+
if (src[i] === '{') depth++;
|
|
98
|
+
else if (src[i] === '}') {
|
|
99
|
+
depth--;
|
|
100
|
+
if (depth === 0) {
|
|
101
|
+
return { content: src.slice(openIdx + 1, i), end: i + 1 };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { content: src.slice(openIdx + 1), end: src.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readAtBlock(src, start) {
|
|
109
|
+
const brace = src.indexOf('{', start);
|
|
110
|
+
if (brace === -1) {
|
|
111
|
+
const semi = src.indexOf(';', start);
|
|
112
|
+
const end = semi === -1 ? src.length : semi + 1;
|
|
113
|
+
return { text: src.slice(start, end), end };
|
|
114
|
+
}
|
|
115
|
+
const body = extractBalanced(src, brace);
|
|
116
|
+
return { text: src.slice(start, body.end), end: body.end };
|
|
117
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time errors with filename, line/column, snippet, and fix hint.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class CompileError extends Error {
|
|
6
|
+
constructor({ message, filename, line, column, source, hint, code }) {
|
|
7
|
+
super(formatCompileError({ message, filename, line, column, source, hint }));
|
|
8
|
+
this.name = 'CompileError';
|
|
9
|
+
this.filename = filename;
|
|
10
|
+
this.line = line;
|
|
11
|
+
this.column = column;
|
|
12
|
+
this.hint = hint;
|
|
13
|
+
this.code = code || 'E_COMPILE';
|
|
14
|
+
this.frame = buildFrame(source, line, column);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatCompileError({ message, filename, line, column, source, hint }) {
|
|
19
|
+
const loc =
|
|
20
|
+
filename != null && line != null
|
|
21
|
+
? `${filename}:${line}:${column || 1}`
|
|
22
|
+
: filename || 'unknown';
|
|
23
|
+
const frame = buildFrame(source, line, column);
|
|
24
|
+
const hintLine = hint ? `\n\nHint: ${hint}` : '';
|
|
25
|
+
return `[easy] ${loc}\n${message}${frame ? `\n\n${frame}` : ''}${hintLine}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildFrame(source, line, column) {
|
|
29
|
+
if (!source || line == null || line < 1) return '';
|
|
30
|
+
const lines = String(source).split(/\r?\n/);
|
|
31
|
+
const idx = line - 1;
|
|
32
|
+
if (idx < 0 || idx >= lines.length) return '';
|
|
33
|
+
|
|
34
|
+
const start = Math.max(0, idx - 2);
|
|
35
|
+
const end = Math.min(lines.length, idx + 3);
|
|
36
|
+
const width = String(end).length;
|
|
37
|
+
const out = [];
|
|
38
|
+
|
|
39
|
+
for (let i = start; i < end; i++) {
|
|
40
|
+
const num = String(i + 1).padStart(width, ' ');
|
|
41
|
+
const mark = i === idx ? '>' : ' ';
|
|
42
|
+
out.push(`${mark} ${num} | ${lines[i]}`);
|
|
43
|
+
if (i === idx && column != null && column > 0) {
|
|
44
|
+
out.push(` ${' '.repeat(width)} | ${' '.repeat(column - 1)}^`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Locate line/column of a substring or index in source. */
|
|
51
|
+
export function locate(source, indexOrSearch) {
|
|
52
|
+
const src = String(source || '');
|
|
53
|
+
let index =
|
|
54
|
+
typeof indexOrSearch === 'number'
|
|
55
|
+
? indexOrSearch
|
|
56
|
+
: src.indexOf(String(indexOrSearch));
|
|
57
|
+
if (index < 0) index = 0;
|
|
58
|
+
const before = src.slice(0, index);
|
|
59
|
+
const lines = before.split(/\r?\n/);
|
|
60
|
+
return { line: lines.length, column: lines[lines.length - 1].length + 1 };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assert(condition, opts) {
|
|
64
|
+
if (!condition) throw new CompileError(opts);
|
|
65
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile template expressions to CSP-safe JS (no eval / new Function).
|
|
3
|
+
* Rewrites bare identifiers to state/methods/props/local lookups.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const KEYWORDS = new Set([
|
|
7
|
+
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
8
|
+
'typeof', 'instanceof', 'new', 'void', 'in', 'of',
|
|
9
|
+
'return', 'if', 'else', 'var', 'let', 'const', 'function',
|
|
10
|
+
'this', 'window', 'document', 'Math', 'Date', 'JSON',
|
|
11
|
+
'Object', 'Array', 'String', 'Number', 'Boolean', 'RegExp',
|
|
12
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite', 'encodeURIComponent',
|
|
13
|
+
'decodeURIComponent'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} expression
|
|
18
|
+
* @param {{ locals?: string[] }} options
|
|
19
|
+
* @returns {{ code: string, keys: string[] }}
|
|
20
|
+
*/
|
|
21
|
+
export function compileExpression(expression, options = {}) {
|
|
22
|
+
const locals = new Set(options.locals || []);
|
|
23
|
+
const keys = new Set();
|
|
24
|
+
const src = String(expression || '').trim();
|
|
25
|
+
if (!src) return { code: 'undefined', keys: [] };
|
|
26
|
+
|
|
27
|
+
let i = 0;
|
|
28
|
+
let out = '';
|
|
29
|
+
|
|
30
|
+
while (i < src.length) {
|
|
31
|
+
const ch = src[i];
|
|
32
|
+
|
|
33
|
+
// strings
|
|
34
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
35
|
+
const { text, end } = readString(src, i);
|
|
36
|
+
out += text;
|
|
37
|
+
i = end;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// numbers
|
|
42
|
+
if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(src[i + 1] || ''))) {
|
|
43
|
+
const start = i;
|
|
44
|
+
i++;
|
|
45
|
+
while (i < src.length && /[0-9._xXoa-fA-F]/.test(src[i])) i++;
|
|
46
|
+
out += src.slice(start, i);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// identifiers / keywords
|
|
51
|
+
if (/[A-Za-z_$]/.test(ch)) {
|
|
52
|
+
const start = i;
|
|
53
|
+
i++;
|
|
54
|
+
while (i < src.length && /[\w$]/.test(src[i])) i++;
|
|
55
|
+
const id = src.slice(start, i);
|
|
56
|
+
|
|
57
|
+
// skip property names after .
|
|
58
|
+
const prev = previousNonSpace(out);
|
|
59
|
+
if (prev === '.') {
|
|
60
|
+
out += id;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (KEYWORDS.has(id) || locals.has(id)) {
|
|
65
|
+
out += id;
|
|
66
|
+
if (locals.has(id)) {
|
|
67
|
+
// local already in scope as const
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// method call: ident(
|
|
73
|
+
const next = nextNonSpace(src, i);
|
|
74
|
+
if (next === '(') {
|
|
75
|
+
out += `__m[${JSON.stringify(id)}]`;
|
|
76
|
+
keys.add(id);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// props.xxx already handled if id is props
|
|
81
|
+
if (id === 'props') {
|
|
82
|
+
out += '__p';
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
keys.add(id);
|
|
87
|
+
out += `__s[${JSON.stringify(id)}]`;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
out += ch;
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { code: out, keys: [...keys] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Emit a reader arrow used in updaters.
|
|
100
|
+
* Locals (e-for) are assumed declared in outer scope.
|
|
101
|
+
*/
|
|
102
|
+
export function emitReader(expression, options = {}) {
|
|
103
|
+
const { code, keys } = compileExpression(expression, options);
|
|
104
|
+
return {
|
|
105
|
+
keys,
|
|
106
|
+
// __s = state, __m = methods, __p = props
|
|
107
|
+
code: `(function(){ const __s = state; const __m = methods; const __p = props; return (${code}); })()`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readString(src, start) {
|
|
112
|
+
const quote = src[start];
|
|
113
|
+
let i = start + 1;
|
|
114
|
+
let text = quote;
|
|
115
|
+
while (i < src.length) {
|
|
116
|
+
const ch = src[i];
|
|
117
|
+
text += ch;
|
|
118
|
+
if (ch === '\\') {
|
|
119
|
+
i++;
|
|
120
|
+
if (i < src.length) text += src[i];
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (ch === quote) {
|
|
125
|
+
return { text, end: i + 1 };
|
|
126
|
+
}
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
return { text, end: src.length };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function previousNonSpace(s) {
|
|
133
|
+
for (let i = s.length - 1; i >= 0; i--) {
|
|
134
|
+
if (!/\s/.test(s[i])) return s[i];
|
|
135
|
+
}
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function nextNonSpace(src, i) {
|
|
140
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
141
|
+
return src[i] || '';
|
|
142
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseSFC } from './parse.js';
|
|
2
|
+
import { parseTemplate } from './template.js';
|
|
3
|
+
import { scopeCSS } from './css.js';
|
|
4
|
+
import { generate } from './codegen.js';
|
|
5
|
+
import { stripTypeScript } from './strip-ts.js';
|
|
6
|
+
import { CompileError, locate } from './errors.js';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compile an Easy.js Single File Component (.easy).
|
|
11
|
+
* Svelte-like: optional <template>, {expr}, onclick={handler}, let/function script.
|
|
12
|
+
*/
|
|
13
|
+
export function compile(source, options = {}) {
|
|
14
|
+
const filename = options.filename || 'anonymous.easy';
|
|
15
|
+
const scopeId = options.id || 'e-' + hash(filename + '\0' + source).slice(0, 8);
|
|
16
|
+
|
|
17
|
+
let sfc;
|
|
18
|
+
try {
|
|
19
|
+
sfc = parseSFC(source, filename);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof CompileError) throw err;
|
|
22
|
+
const loc = locate(source, 0);
|
|
23
|
+
throw new CompileError({
|
|
24
|
+
message: err.message || String(err),
|
|
25
|
+
filename,
|
|
26
|
+
...loc,
|
|
27
|
+
source,
|
|
28
|
+
hint: 'Check <script>, markup, and <style> are well-formed (template wrapper is optional).'
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!sfc.template || !sfc.template.content.trim()) {
|
|
33
|
+
throw new CompileError({
|
|
34
|
+
message: 'No markup found',
|
|
35
|
+
filename,
|
|
36
|
+
line: 1,
|
|
37
|
+
column: 1,
|
|
38
|
+
source,
|
|
39
|
+
hint: 'Put HTML after <script> (Svelte style), or use <template>...</template>.'
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let ast;
|
|
44
|
+
try {
|
|
45
|
+
ast = parseTemplate(sfc.template.content, { filename });
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new CompileError({
|
|
48
|
+
message: err.message || String(err),
|
|
49
|
+
filename,
|
|
50
|
+
...locate(source, sfc.template.content.slice(0, 12)),
|
|
51
|
+
source,
|
|
52
|
+
hint: 'Check tags, {expressions}, onclick={handler}, {#if}/{#each}, or e-if/e-for.'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scopedStyles = sfc.styles
|
|
57
|
+
.map((s) => (s.scoped !== false ? scopeCSS(s.content, scopeId) : s.content))
|
|
58
|
+
.join('\n');
|
|
59
|
+
|
|
60
|
+
let scriptContent = sfc.script ? sfc.script.content : '';
|
|
61
|
+
const lang = (sfc.script?.lang || 'js').toLowerCase();
|
|
62
|
+
if (lang === 'ts' || lang === 'typescript') {
|
|
63
|
+
scriptContent = stripTypeScript(scriptContent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let result;
|
|
67
|
+
try {
|
|
68
|
+
result = generate(ast, {
|
|
69
|
+
script: scriptContent,
|
|
70
|
+
styles: scopedStyles,
|
|
71
|
+
scopeId,
|
|
72
|
+
filename,
|
|
73
|
+
source,
|
|
74
|
+
lang
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof CompileError) throw err;
|
|
78
|
+
throw new CompileError({
|
|
79
|
+
message: err.message || String(err),
|
|
80
|
+
filename,
|
|
81
|
+
...locate(source, 0),
|
|
82
|
+
source,
|
|
83
|
+
hint: 'Review markup expressions and <script> (let/function or export default).'
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
code: result.code,
|
|
89
|
+
map: null,
|
|
90
|
+
scopeId,
|
|
91
|
+
styles: scopedStyles,
|
|
92
|
+
dependencies: result.dependencies
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { parseSFC } from './parse.js';
|
|
97
|
+
export { parseTemplate } from './template.js';
|
|
98
|
+
export { scopeCSS } from './css.js';
|
|
99
|
+
export { CompileError, formatCompileError, buildFrame } from './errors.js';
|
|
100
|
+
export { compileExpression } from './expr.js';
|
|
101
|
+
export { stripTypeScript } from './strip-ts.js';
|
|
102
|
+
|
|
103
|
+
function hash(str) {
|
|
104
|
+
return createHash('sha256').update(str).digest('hex');
|
|
105
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse Easy.js SFC — Svelte-like by default.
|
|
3
|
+
*
|
|
4
|
+
* Preferred:
|
|
5
|
+
* <script>...</script>
|
|
6
|
+
* <markup/>
|
|
7
|
+
* <style>...</style>
|
|
8
|
+
*
|
|
9
|
+
* Optional <template> still supported for compatibility.
|
|
10
|
+
*/
|
|
11
|
+
import { CompileError, locate } from './errors.js';
|
|
12
|
+
|
|
13
|
+
export function parseSFC(source, filename = 'component.easy') {
|
|
14
|
+
const result = {
|
|
15
|
+
filename,
|
|
16
|
+
template: null,
|
|
17
|
+
script: null,
|
|
18
|
+
styles: [],
|
|
19
|
+
markupMode: 'svelte' // or 'template'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const blocks = [];
|
|
23
|
+
const blockRe = /<(template|script|style)(\s[^>]*)?>([\s\S]*?)<\/\1>/gi;
|
|
24
|
+
let match;
|
|
25
|
+
|
|
26
|
+
while ((match = blockRe.exec(source)) !== null) {
|
|
27
|
+
const tag = match[1].toLowerCase();
|
|
28
|
+
const attrs = parseAttrs(match[2] || '');
|
|
29
|
+
const content = match[3];
|
|
30
|
+
blocks.push({
|
|
31
|
+
tag,
|
|
32
|
+
attrs,
|
|
33
|
+
content,
|
|
34
|
+
start: match.index,
|
|
35
|
+
end: match.index + match[0].length,
|
|
36
|
+
full: match[0]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const block of blocks) {
|
|
41
|
+
if (block.tag === 'template') {
|
|
42
|
+
if (result.template) {
|
|
43
|
+
throw new CompileError({
|
|
44
|
+
message: 'Multiple <template> blocks are not supported',
|
|
45
|
+
filename,
|
|
46
|
+
...locate(source, block.start),
|
|
47
|
+
source,
|
|
48
|
+
hint: 'Use one <template>, or omit it and put markup between <script> and <style>.'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
result.template = { content: block.content.trim(), attrs: block.attrs };
|
|
52
|
+
result.markupMode = 'template';
|
|
53
|
+
} else if (block.tag === 'script') {
|
|
54
|
+
if (result.script) {
|
|
55
|
+
throw new CompileError({
|
|
56
|
+
message: 'Multiple <script> blocks are not supported',
|
|
57
|
+
filename,
|
|
58
|
+
...locate(source, block.start),
|
|
59
|
+
source,
|
|
60
|
+
hint: 'Combine scripts into a single <script> block.'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
result.script = {
|
|
64
|
+
content: block.content.trim(),
|
|
65
|
+
attrs: block.attrs,
|
|
66
|
+
lang: String(block.attrs.lang || 'js').toLowerCase()
|
|
67
|
+
};
|
|
68
|
+
} else if (block.tag === 'style') {
|
|
69
|
+
result.styles.push({
|
|
70
|
+
content: block.content.trim(),
|
|
71
|
+
attrs: block.attrs,
|
|
72
|
+
scoped: block.attrs.scoped !== undefined ? block.attrs.scoped !== 'false' : true,
|
|
73
|
+
lang: block.attrs.lang || 'css'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Svelte-like: markup is everything outside <script>/<style>/<template>
|
|
79
|
+
if (!result.template) {
|
|
80
|
+
let markup = source;
|
|
81
|
+
// Remove blocks from last to first so indices stay valid
|
|
82
|
+
const ordered = [...blocks].sort((a, b) => b.start - a.start);
|
|
83
|
+
for (const b of ordered) {
|
|
84
|
+
markup = markup.slice(0, b.start) + markup.slice(b.end);
|
|
85
|
+
}
|
|
86
|
+
markup = markup.trim();
|
|
87
|
+
if (!markup) {
|
|
88
|
+
throw new CompileError({
|
|
89
|
+
message: 'No markup found',
|
|
90
|
+
filename,
|
|
91
|
+
line: 1,
|
|
92
|
+
column: 1,
|
|
93
|
+
source,
|
|
94
|
+
hint: 'Add HTML between <script> and <style>, or wrap markup in <template>...</template>.'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
result.template = { content: markup, attrs: {} };
|
|
98
|
+
result.markupMode = 'svelte';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseAttrs(str) {
|
|
105
|
+
const attrs = {};
|
|
106
|
+
const re = /([:@]?[\w:-]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
107
|
+
let m;
|
|
108
|
+
while ((m = re.exec(str)) !== null) {
|
|
109
|
+
const name = m[1];
|
|
110
|
+
const value =
|
|
111
|
+
m[2] !== undefined ? m[2] : m[3] !== undefined ? m[3] : m[4] !== undefined ? m[4] : true;
|
|
112
|
+
attrs[name] = value;
|
|
113
|
+
}
|
|
114
|
+
return attrs;
|
|
115
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight strip of TypeScript syntax for lang="ts" scripts.
|
|
3
|
+
* Not a full TS compiler — enough for demos (types, interfaces, satisfies, as).
|
|
4
|
+
*/
|
|
5
|
+
export function stripTypeScript(code) {
|
|
6
|
+
let src = String(code || '');
|
|
7
|
+
|
|
8
|
+
// Remove import type / export type
|
|
9
|
+
src = src.replace(/^\s*import\s+type\s+[\s\S]*?;?\s*$/gm, '');
|
|
10
|
+
src = src.replace(/^\s*export\s+type\s+[\s\S]*?;?\s*$/gm, '');
|
|
11
|
+
|
|
12
|
+
// Remove interface / type alias blocks
|
|
13
|
+
src = src.replace(/^\s*(?:export\s+)?interface\s+\w+[\s\S]*?\{[\s\S]*?\}\s*/gm, '');
|
|
14
|
+
src = src.replace(/^\s*(?:export\s+)?type\s+\w+\s*=\s*[^;]+;\s*/gm, '');
|
|
15
|
+
|
|
16
|
+
// Strip `as Type` / `satisfies Type`
|
|
17
|
+
src = src.replace(/\s+as\s+const\b/g, '');
|
|
18
|
+
src = src.replace(/\s+as\s+[A-Za-z0-9_$.<>,\s|&[\]]+/g, '');
|
|
19
|
+
src = src.replace(/\s+satisfies\s+[A-Za-z0-9_$.<>,\s|&[\]]+/g, '');
|
|
20
|
+
|
|
21
|
+
// Strip parameter / variable type annotations in common positions
|
|
22
|
+
// function foo(a: number, b: string): void
|
|
23
|
+
src = src.replace(/:\s*[A-Za-z0-9_$.<>,\s|&[\]]+(?=\s*[,)=])/g, '');
|
|
24
|
+
// return types ) : Type {
|
|
25
|
+
src = src.replace(/\)\s*:\s*[A-Za-z0-9_$.<>,\s|&[\]]+(?=\s*\{)/g, ')');
|
|
26
|
+
// let x: number =
|
|
27
|
+
src = src.replace(
|
|
28
|
+
/\b(let|const|var)\s+([A-Za-z_$][\w$]*)\s*:\s*[A-Za-z0-9_$.<>,\s|&[\]]+(?=\s*=)/g,
|
|
29
|
+
'$1 $2'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Non-null assertions foo!
|
|
33
|
+
src = src.replace(/([A-Za-z0-9_)\]])\!(?=\s*[,.;)\]]|$)/g, '$1');
|
|
34
|
+
|
|
35
|
+
// Angle generics on calls: foo<Bar>( → foo( (conservative)
|
|
36
|
+
src = src.replace(/\b([A-Za-z_$][\w$]*)\s*<\s*[A-Za-z_$][\w$.,\s|[&\]<>]*\s*>\s*\(/g, '$1(');
|
|
37
|
+
|
|
38
|
+
return src;
|
|
39
|
+
}
|