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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/bin/create-easy-app.js +60 -0
  4. package/bin/easy.js +7 -0
  5. package/compiler.d.ts +37 -0
  6. package/package.json +74 -0
  7. package/runtime.d.ts +103 -0
  8. package/src/commands/build.js +284 -0
  9. package/src/commands/create.js +476 -0
  10. package/src/commands/dev.js +434 -0
  11. package/src/commands/generate.js +104 -0
  12. package/src/commands/ws.js +157 -0
  13. package/src/compiler/codegen.js +1334 -0
  14. package/src/compiler/css.js +117 -0
  15. package/src/compiler/errors.js +65 -0
  16. package/src/compiler/expr.js +142 -0
  17. package/src/compiler/index.js +105 -0
  18. package/src/compiler/parse.js +115 -0
  19. package/src/compiler/strip-ts.js +39 -0
  20. package/src/compiler/template.js +419 -0
  21. package/src/index.js +128 -0
  22. package/src/runtime/app.js +115 -0
  23. package/src/runtime/component.js +222 -0
  24. package/src/runtime/css.js +35 -0
  25. package/src/runtime/errors.js +121 -0
  26. package/src/runtime/escape.js +62 -0
  27. package/src/runtime/events.js +151 -0
  28. package/src/runtime/hmr.js +157 -0
  29. package/src/runtime/index.js +40 -0
  30. package/src/runtime/overlay.js +95 -0
  31. package/src/runtime/reactive.js +172 -0
  32. package/src/runtime/router.js +423 -0
  33. package/src/runtime/scheduler.js +39 -0
  34. package/src/runtime/store.js +42 -0
  35. package/src/transform.js +106 -0
  36. package/templates/minimal/README.md +21 -0
  37. package/templates/minimal/easy.config.js +7 -0
  38. package/templates/minimal/index.html +13 -0
  39. package/templates/minimal/package.json +14 -0
  40. package/templates/minimal/src/App.easy +24 -0
  41. package/templates/minimal/src/main.js +4 -0
  42. package/templates/minimal/src/styles.css +14 -0
  43. package/templates/starter/README.md +18 -0
  44. package/templates/starter/easy.config.js +7 -0
  45. package/templates/starter/index.html +13 -0
  46. package/templates/starter/package.json +14 -0
  47. package/templates/starter/src/App.easy +69 -0
  48. package/templates/starter/src/components/Counter.easy +76 -0
  49. package/templates/starter/src/components/Greeting.easy +24 -0
  50. package/templates/starter/src/main.js +11 -0
  51. package/templates/starter/src/pages/About.easy +20 -0
  52. package/templates/starter/src/pages/Home.easy +69 -0
  53. package/templates/starter/src/styles.css +14 -0
  54. package/templates/tailwind/README.md +23 -0
  55. package/templates/tailwind/easy.config.js +7 -0
  56. package/templates/tailwind/index.html +19 -0
  57. package/templates/tailwind/package.json +14 -0
  58. package/templates/tailwind/src/App.easy +53 -0
  59. package/templates/tailwind/src/main.js +4 -0
  60. 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
+ }