@esportsplus/template 0.37.0 → 0.38.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/build/compiler/codegen.d.ts +8 -6
- package/build/compiler/codegen.js +64 -78
- package/build/compiler/index.d.ts +5 -10
- package/build/compiler/index.js +30 -74
- package/build/compiler/plugins/tsc.js +2 -2
- package/build/compiler/plugins/vite.js +2 -3
- package/build/compiler/reactive-inlining.d.ts +5 -0
- package/build/compiler/reactive-inlining.js +75 -0
- package/package.json +3 -3
- package/src/compiler/codegen.ts +98 -113
- package/src/compiler/index.ts +32 -115
- package/src/compiler/plugins/tsc.ts +2 -2
- package/src/compiler/plugins/vite.ts +2 -3
- package/src/compiler/reactive-inlining.ts +116 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
1
2
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import type {
|
|
3
|
+
import type { TemplateInfo } from './ts-parser.js';
|
|
3
4
|
type CodegenResult = {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
imports: Map<string, string>;
|
|
6
|
+
prepend: string[];
|
|
7
|
+
replacements: ReplacementIntent[];
|
|
8
|
+
templates: Map<string, string>;
|
|
6
9
|
};
|
|
7
|
-
declare const generateCode: (templates: TemplateInfo[],
|
|
8
|
-
|
|
9
|
-
export { generateCode, generateReactiveInlining };
|
|
10
|
+
declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>) => CodegenResult;
|
|
11
|
+
export { generateCode };
|
|
10
12
|
export type { CodegenResult };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { ast,
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS
|
|
2
|
+
import { ast, uid } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
|
|
4
4
|
import { analyze } from './analyzer.js';
|
|
5
5
|
import parser from './parser.js';
|
|
6
6
|
const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
@@ -13,6 +13,18 @@ function addImport(ctx, name) {
|
|
|
13
13
|
}
|
|
14
14
|
return alias;
|
|
15
15
|
}
|
|
16
|
+
function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
|
|
17
|
+
if (isNestedHtmlTemplate(node)) {
|
|
18
|
+
replacements.push({
|
|
19
|
+
end: node.end - exprStart,
|
|
20
|
+
newText: generateNestedTemplateCode(ctx, node),
|
|
21
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
16
28
|
function generateAttributeBinding(ctx, element, name, expr, staticValue) {
|
|
17
29
|
if (name.startsWith('on') && name.length > 2) {
|
|
18
30
|
let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
@@ -32,18 +44,6 @@ function generateAttributeBinding(ctx, element, name, expr, staticValue) {
|
|
|
32
44
|
}
|
|
33
45
|
return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
|
|
34
46
|
}
|
|
35
|
-
function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
|
|
36
|
-
if (isNestedHtmlTemplate(node)) {
|
|
37
|
-
replacements.push({
|
|
38
|
-
end: node.end - exprStart,
|
|
39
|
-
newText: generateNestedTemplateCode(ctx, node),
|
|
40
|
-
start: node.getStart() - exprStart
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
47
|
function generateNestedTemplateCode(ctx, node) {
|
|
48
48
|
let expressions = [], exprTexts = [], literals = [], template = node.template;
|
|
49
49
|
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
@@ -151,6 +151,14 @@ function getOrCreateTemplateId(ctx, html) {
|
|
|
151
151
|
function isNestedHtmlTemplate(expr) {
|
|
152
152
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
153
153
|
}
|
|
154
|
+
function replaceReverse(text, replacements) {
|
|
155
|
+
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
156
|
+
for (let i = 0, n = sorted.length; i < n; i++) {
|
|
157
|
+
let r = sorted[i];
|
|
158
|
+
text = text.slice(0, r.start) + r.newText + text.slice(r.end);
|
|
159
|
+
}
|
|
160
|
+
return text;
|
|
161
|
+
}
|
|
154
162
|
function rewriteExpression(ctx, expr) {
|
|
155
163
|
if (isNestedHtmlTemplate(expr)) {
|
|
156
164
|
return generateNestedTemplateCode(ctx, expr);
|
|
@@ -159,87 +167,65 @@ function rewriteExpression(ctx, expr) {
|
|
|
159
167
|
return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
|
|
160
168
|
}
|
|
161
169
|
let replacements = [];
|
|
162
|
-
collectNestedTemplateReplacements(ctx, expr, expr.getStart(), replacements);
|
|
163
|
-
return
|
|
170
|
+
collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
171
|
+
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
164
172
|
}
|
|
165
|
-
const generateCode = (templates,
|
|
173
|
+
const generateCode = (templates, sourceFile, checker, existingAliases) => {
|
|
174
|
+
let result = {
|
|
175
|
+
imports: existingAliases ?? new Map(),
|
|
176
|
+
prepend: [],
|
|
177
|
+
replacements: [],
|
|
178
|
+
templates: new Map()
|
|
179
|
+
};
|
|
166
180
|
if (templates.length === 0) {
|
|
167
|
-
return
|
|
181
|
+
return result;
|
|
168
182
|
}
|
|
169
183
|
let ranges = [];
|
|
170
184
|
for (let i = 0, n = templates.length; i < n; i++) {
|
|
171
185
|
let exprs = templates[i].expressions;
|
|
172
186
|
for (let j = 0, m = exprs.length; j < m; j++) {
|
|
173
|
-
ranges.push({ end: exprs[j].end, start: exprs[j].getStart() });
|
|
187
|
+
ranges.push({ end: exprs[j].end, start: exprs[j].getStart(sourceFile) });
|
|
174
188
|
}
|
|
175
189
|
}
|
|
176
|
-
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.
|
|
190
|
+
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.node.getStart(sourceFile), t.node.end));
|
|
177
191
|
if (rootTemplates.length === 0) {
|
|
178
|
-
return
|
|
192
|
+
return result;
|
|
179
193
|
}
|
|
180
194
|
let ctx = {
|
|
181
195
|
checker,
|
|
182
|
-
imports:
|
|
196
|
+
imports: result.imports,
|
|
183
197
|
printer,
|
|
184
198
|
sourceFile,
|
|
185
|
-
templates:
|
|
186
|
-
},
|
|
199
|
+
templates: result.templates
|
|
200
|
+
}, templateAlias = addImport(ctx, 'template');
|
|
187
201
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let changed = replacements.length > 0, code = c.replaceReverse(originalCode, replacements);
|
|
211
|
-
if (changed && ctx.templates.size > 0) {
|
|
212
|
-
let aliasedImports = [], factories = [], updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
|
|
213
|
-
for (let [name, alias] of ctx.imports) {
|
|
214
|
-
aliasedImports.push(`${name} as ${alias}`);
|
|
215
|
-
}
|
|
216
|
-
for (let [html, id] of ctx.templates) {
|
|
217
|
-
factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
218
|
-
}
|
|
219
|
-
code = imports.modify(code, updatedSourceFile, PACKAGE, {
|
|
220
|
-
add: new Set(aliasedImports),
|
|
221
|
-
remove: [COMPILER_ENTRYPOINT]
|
|
202
|
+
let template = rootTemplates[i];
|
|
203
|
+
result.replacements.push({
|
|
204
|
+
generate: (sf) => {
|
|
205
|
+
let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
|
|
206
|
+
checker,
|
|
207
|
+
imports: ctx.imports,
|
|
208
|
+
printer,
|
|
209
|
+
sourceFile: sf,
|
|
210
|
+
templates: ctx.templates
|
|
211
|
+
}, parsed = parser.parse(template.literals);
|
|
212
|
+
for (let j = 0, m = template.expressions.length; j < m; j++) {
|
|
213
|
+
exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
|
|
214
|
+
}
|
|
215
|
+
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
216
|
+
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
217
|
+
if (arrowMatch) {
|
|
218
|
+
return getOrCreateTemplateId(localCtx, parsed.html);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return generateTemplateCode(localCtx, parsed, exprTexts, template.expressions, isArrowBody);
|
|
222
|
+
},
|
|
223
|
+
node: template.node
|
|
222
224
|
});
|
|
223
|
-
code = factories.join('\n') + '\n\n' + code;
|
|
224
225
|
}
|
|
225
|
-
|
|
226
|
-
};
|
|
227
|
-
const generateReactiveInlining = (calls, code, sourceFile, arraySlotAlias) => {
|
|
228
|
-
if (calls.length === 0) {
|
|
229
|
-
return code;
|
|
230
|
-
}
|
|
231
|
-
let replacements = [];
|
|
232
|
-
for (let i = 0, n = calls.length; i < n; i++) {
|
|
233
|
-
let call = calls[i];
|
|
234
|
-
replacements.push({
|
|
235
|
-
end: call.end,
|
|
236
|
-
newText: `new ${arraySlotAlias}(
|
|
237
|
-
${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
|
|
238
|
-
${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
|
|
239
|
-
)`,
|
|
240
|
-
start: call.start
|
|
241
|
-
});
|
|
226
|
+
for (let [html, id] of ctx.templates) {
|
|
227
|
+
result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
242
228
|
}
|
|
243
|
-
return
|
|
229
|
+
return result;
|
|
244
230
|
};
|
|
245
|
-
export { generateCode
|
|
231
|
+
export { generateCode };
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
sourceFile: ts.SourceFile;
|
|
7
|
-
};
|
|
8
|
-
declare const analyze: (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext) => void;
|
|
9
|
-
declare const transform: (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
|
|
10
|
-
export { analyze, transform };
|
|
1
|
+
import type { Plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import reactiveInliningPlugin from './reactive-inlining.js';
|
|
3
|
+
declare const templatePlugin: Plugin;
|
|
4
|
+
export default templatePlugin;
|
|
5
|
+
export { reactiveInliningPlugin, templatePlugin };
|
package/build/compiler/index.js
CHANGED
|
@@ -1,82 +1,38 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
const analyze = (sourceFile, program, context) => {
|
|
14
|
-
let code = sourceFile.getFullText();
|
|
15
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
let checker = program.getTypeChecker(), filename = sourceFile.fileName, files = context.get(CONTEXT_KEY), programSourceFile = program.getSourceFile(filename)
|
|
19
|
-
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
20
|
-
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
21
|
-
if (programSourceFile) {
|
|
22
|
-
sourceFile = programSourceFile;
|
|
23
|
-
}
|
|
24
|
-
if (!files) {
|
|
25
|
-
files = new Map();
|
|
26
|
-
context.set(CONTEXT_KEY, files);
|
|
27
|
-
}
|
|
28
|
-
files.set(filename, {
|
|
29
|
-
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
30
|
-
templates: findHtmlTemplates(sourceFile, checker)
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
const transform = (sourceFile, program, context) => {
|
|
34
|
-
let code = sourceFile.getFullText(), filename = sourceFile.fileName;
|
|
35
|
-
let analyzed = getAnalyzedFile(context, filename);
|
|
36
|
-
if (!analyzed) {
|
|
37
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
38
|
-
return { changed: false, code, sourceFile };
|
|
1
|
+
import { COMPILER_ENTRYPOINT, PACKAGE } from '../constants.js';
|
|
2
|
+
import { generateCode } from './codegen.js';
|
|
3
|
+
import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining.js';
|
|
4
|
+
import { findHtmlTemplates } from './ts-parser.js';
|
|
5
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
|
|
6
|
+
const templatePlugin = {
|
|
7
|
+
patterns: PATTERNS,
|
|
8
|
+
transform: (ctx) => {
|
|
9
|
+
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
10
|
+
if (templates.length === 0) {
|
|
11
|
+
return {};
|
|
39
12
|
}
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (programSourceFile) {
|
|
44
|
-
sourceFile = programSourceFile;
|
|
13
|
+
let existingAliases = new Map(), existingArraySlotAlias = ctx.shared.get(SHARED_KEY);
|
|
14
|
+
if (existingArraySlotAlias) {
|
|
15
|
+
existingAliases.set('ArraySlot', existingArraySlotAlias);
|
|
45
16
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
let changed = false, codegenChanged = false, existingAliases = new Map(), result = code;
|
|
52
|
-
if (analyzed.reactiveCalls.length > 0) {
|
|
53
|
-
changed = true;
|
|
54
|
-
existingAliases.set('ArraySlot', uid('ArraySlot'));
|
|
55
|
-
result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot'));
|
|
56
|
-
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
57
|
-
analyzed = {
|
|
58
|
-
reactiveCalls: [],
|
|
59
|
-
templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (analyzed.templates.length > 0) {
|
|
63
|
-
let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
|
|
64
|
-
if (codegenResult.changed) {
|
|
65
|
-
changed = true;
|
|
66
|
-
codegenChanged = true;
|
|
67
|
-
result = codegenResult.code;
|
|
17
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
|
|
18
|
+
if (result.replacements.length === 0) {
|
|
19
|
+
return {};
|
|
68
20
|
}
|
|
69
|
-
}
|
|
70
|
-
if (existingAliases.size > 0 && !codegenChanged) {
|
|
71
21
|
let aliasedImports = [];
|
|
72
|
-
for (let [name, alias] of
|
|
22
|
+
for (let [name, alias] of result.imports) {
|
|
73
23
|
aliasedImports.push(`${name} as ${alias}`);
|
|
74
24
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
25
|
+
let imports = [{
|
|
26
|
+
add: aliasedImports,
|
|
27
|
+
package: PACKAGE,
|
|
28
|
+
remove: [COMPILER_ENTRYPOINT]
|
|
29
|
+
}];
|
|
30
|
+
return {
|
|
31
|
+
imports,
|
|
32
|
+
prepend: result.prepend,
|
|
33
|
+
replacements: result.replacements
|
|
34
|
+
};
|
|
79
35
|
}
|
|
80
|
-
return { changed, code: result, sourceFile };
|
|
81
36
|
};
|
|
82
|
-
export
|
|
37
|
+
export default templatePlugin;
|
|
38
|
+
export { reactiveInliningPlugin, templatePlugin };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import {
|
|
3
|
-
export default plugin.tsc(
|
|
2
|
+
import { reactiveInliningPlugin, templatePlugin } from '../index.js';
|
|
3
|
+
export default plugin.tsc([reactiveInliningPlugin, templatePlugin]);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants.js';
|
|
3
|
-
import {
|
|
3
|
+
import { reactiveInliningPlugin, templatePlugin } from '../index.js';
|
|
4
4
|
export default plugin.vite({
|
|
5
|
-
analyze,
|
|
6
5
|
name: PACKAGE,
|
|
7
|
-
|
|
6
|
+
plugins: [reactiveInliningPlugin, templatePlugin]
|
|
8
7
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ts } from '@esportsplus/typescript';
|
|
2
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
|
|
4
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
5
|
+
const SHARED_KEY = 'template:arraySlotAlias';
|
|
6
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
7
|
+
function isHtmlFromPackage(node, checker) {
|
|
8
|
+
if (node.text !== COMPILER_ENTRYPOINT) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (!checker) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
15
|
+
if (!symbol) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
19
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
20
|
+
}
|
|
21
|
+
let declarations = symbol.getDeclarations();
|
|
22
|
+
if (!declarations || declarations.length === 0) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
26
|
+
let filename = declarations[i].getSourceFile().fileName;
|
|
27
|
+
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function visit(node, calls, checker) {
|
|
34
|
+
if (ts.isCallExpression(node) &&
|
|
35
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
36
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
37
|
+
node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
|
|
38
|
+
node.arguments.length === 2 &&
|
|
39
|
+
isHtmlFromPackage(node.expression.expression, checker)) {
|
|
40
|
+
calls.push({
|
|
41
|
+
arrayArg: node.arguments[0],
|
|
42
|
+
callbackArg: node.arguments[1],
|
|
43
|
+
node
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
ts.forEachChild(node, child => visit(child, calls, checker));
|
|
47
|
+
}
|
|
48
|
+
const plugin = {
|
|
49
|
+
patterns: PATTERNS,
|
|
50
|
+
transform: (ctx) => {
|
|
51
|
+
let calls = [];
|
|
52
|
+
visit(ctx.sourceFile, calls, ctx.checker);
|
|
53
|
+
if (calls.length === 0) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
let arraySlotAlias = uid('ArraySlot'), replacements = [];
|
|
57
|
+
ctx.shared.set(SHARED_KEY, arraySlotAlias);
|
|
58
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
59
|
+
let call = calls[i];
|
|
60
|
+
replacements.push({
|
|
61
|
+
generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
62
|
+
node: call.node
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
imports: [{
|
|
67
|
+
add: [`ArraySlot as ${arraySlotAlias}`],
|
|
68
|
+
package: PACKAGE
|
|
69
|
+
}],
|
|
70
|
+
replacements
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
export default plugin;
|
|
75
|
+
export { SHARED_KEY };
|
package/package.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"author": "ICJR",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@esportsplus/queue": "^0.2.0",
|
|
5
|
-
"@esportsplus/reactivity": "^0.
|
|
6
|
-
"@esportsplus/typescript": "^0.
|
|
5
|
+
"@esportsplus/reactivity": "^0.28.0",
|
|
6
|
+
"@esportsplus/typescript": "^0.25.0",
|
|
7
7
|
"@esportsplus/utilities": "^0.27.2",
|
|
8
8
|
"serve": "^14.2.5"
|
|
9
9
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"type": "module",
|
|
37
37
|
"types": "./build/index.d.ts",
|
|
38
|
-
"version": "0.
|
|
38
|
+
"version": "0.38.0",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc",
|
|
41
41
|
"build:test": "vite build --config test/vite.config.ts",
|
package/src/compiler/codegen.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
1
2
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { ast,
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS
|
|
4
|
-
import type {
|
|
3
|
+
import { ast, uid } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
|
|
5
|
+
import type { TemplateInfo } from './ts-parser';
|
|
5
6
|
import { analyze } from './analyzer';
|
|
6
7
|
import parser from './parser';
|
|
7
8
|
|
|
@@ -24,8 +25,10 @@ type CodegenContext = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
type CodegenResult = {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
imports: Map<string, string>;
|
|
29
|
+
prepend: string[];
|
|
30
|
+
replacements: ReplacementIntent[];
|
|
31
|
+
templates: Map<string, string>;
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
type Node = {
|
|
@@ -38,6 +41,12 @@ type ParseResult = {
|
|
|
38
41
|
slots: (Attribute | Node)[] | null;
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
type Replacement = {
|
|
45
|
+
end: number;
|
|
46
|
+
newText: string;
|
|
47
|
+
start: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
41
50
|
|
|
42
51
|
const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
43
52
|
|
|
@@ -56,6 +65,24 @@ function addImport(ctx: CodegenContext, name: string): string {
|
|
|
56
65
|
return alias;
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
function collectNestedTemplateReplacements(
|
|
69
|
+
ctx: CodegenContext,
|
|
70
|
+
node: ts.Node,
|
|
71
|
+
exprStart: number,
|
|
72
|
+
replacements: Replacement[]
|
|
73
|
+
): void {
|
|
74
|
+
if (isNestedHtmlTemplate(node as ts.Expression)) {
|
|
75
|
+
replacements.push({
|
|
76
|
+
end: node.end - exprStart,
|
|
77
|
+
newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
|
|
78
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
|
|
60
87
|
if (name.startsWith('on') && name.length > 2) {
|
|
61
88
|
let event = name.slice(2).toLowerCase(),
|
|
@@ -83,24 +110,6 @@ function generateAttributeBinding(ctx: CodegenContext, element: string, name: st
|
|
|
83
110
|
return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
|
|
84
111
|
}
|
|
85
112
|
|
|
86
|
-
function collectNestedTemplateReplacements(
|
|
87
|
-
ctx: CodegenContext,
|
|
88
|
-
node: ts.Node,
|
|
89
|
-
exprStart: number,
|
|
90
|
-
replacements: Replacement[]
|
|
91
|
-
): void {
|
|
92
|
-
if (isNestedHtmlTemplate(node as ts.Expression)) {
|
|
93
|
-
replacements.push({
|
|
94
|
-
end: node.end - exprStart,
|
|
95
|
-
newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
|
|
96
|
-
start: node.getStart() - exprStart
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
113
|
function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
|
|
105
114
|
let expressions: ts.Expression[] = [],
|
|
106
115
|
exprTexts: string[] = [],
|
|
@@ -211,7 +220,6 @@ function generateTemplateCode(
|
|
|
211
220
|
segments = path.slice(start),
|
|
212
221
|
value = `${ancestor}.${segments.join('!.')}`;
|
|
213
222
|
|
|
214
|
-
// Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
|
|
215
223
|
if (ancestor === root && segments[0] === 'firstChild') {
|
|
216
224
|
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
|
|
217
225
|
}
|
|
@@ -285,6 +293,18 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
|
|
|
285
293
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
286
294
|
}
|
|
287
295
|
|
|
296
|
+
function replaceReverse(text: string, replacements: Replacement[]): string {
|
|
297
|
+
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
298
|
+
|
|
299
|
+
for (let i = 0, n = sorted.length; i < n; i++) {
|
|
300
|
+
let r = sorted[i];
|
|
301
|
+
|
|
302
|
+
text = text.slice(0, r.start) + r.newText + text.slice(r.end);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return text;
|
|
306
|
+
}
|
|
307
|
+
|
|
288
308
|
function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
289
309
|
if (isNestedHtmlTemplate(expr)) {
|
|
290
310
|
return generateNestedTemplateCode(ctx, expr);
|
|
@@ -296,132 +316,97 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
|
296
316
|
|
|
297
317
|
let replacements: Replacement[] = [];
|
|
298
318
|
|
|
299
|
-
collectNestedTemplateReplacements(ctx, expr, expr.getStart(), replacements);
|
|
319
|
+
collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
300
320
|
|
|
301
|
-
return
|
|
321
|
+
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
302
322
|
}
|
|
303
323
|
|
|
304
324
|
|
|
305
|
-
const generateCode = (templates: TemplateInfo[],
|
|
325
|
+
const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
|
|
326
|
+
let result: CodegenResult = {
|
|
327
|
+
imports: existingAliases ?? new Map(),
|
|
328
|
+
prepend: [],
|
|
329
|
+
replacements: [],
|
|
330
|
+
templates: new Map()
|
|
331
|
+
};
|
|
332
|
+
|
|
306
333
|
if (templates.length === 0) {
|
|
307
|
-
return
|
|
334
|
+
return result;
|
|
308
335
|
}
|
|
309
336
|
|
|
310
|
-
// Precompute expression ranges for nested template detection
|
|
311
337
|
let ranges: { end: number; start: number }[] = [];
|
|
312
338
|
|
|
313
339
|
for (let i = 0, n = templates.length; i < n; i++) {
|
|
314
340
|
let exprs = templates[i].expressions;
|
|
315
341
|
|
|
316
342
|
for (let j = 0, m = exprs.length; j < m; j++) {
|
|
317
|
-
ranges.push({ end: exprs[j].end, start: exprs[j].getStart() });
|
|
343
|
+
ranges.push({ end: exprs[j].end, start: exprs[j].getStart(sourceFile) });
|
|
318
344
|
}
|
|
319
345
|
}
|
|
320
346
|
|
|
321
|
-
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.
|
|
347
|
+
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.node.getStart(sourceFile), t.node.end));
|
|
322
348
|
|
|
323
349
|
if (rootTemplates.length === 0) {
|
|
324
|
-
return
|
|
350
|
+
return result;
|
|
325
351
|
}
|
|
326
352
|
|
|
327
353
|
let ctx: CodegenContext = {
|
|
328
354
|
checker,
|
|
329
|
-
imports:
|
|
355
|
+
imports: result.imports,
|
|
330
356
|
printer,
|
|
331
357
|
sourceFile,
|
|
332
|
-
templates:
|
|
358
|
+
templates: result.templates
|
|
333
359
|
},
|
|
334
|
-
replacements: Replacement[] = [],
|
|
335
360
|
templateAlias = addImport(ctx, 'template');
|
|
336
361
|
|
|
337
362
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
338
|
-
let
|
|
339
|
-
|
|
363
|
+
let template = rootTemplates[i];
|
|
364
|
+
|
|
365
|
+
result.replacements.push({
|
|
366
|
+
generate: (sf) => {
|
|
367
|
+
let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
|
|
368
|
+
exprTexts: string[] = [],
|
|
369
|
+
isArrowBody = codeBefore.trimEnd().endsWith('=>'),
|
|
370
|
+
localCtx: CodegenContext = {
|
|
371
|
+
checker,
|
|
372
|
+
imports: ctx.imports,
|
|
373
|
+
printer,
|
|
374
|
+
sourceFile: sf,
|
|
375
|
+
templates: ctx.templates
|
|
376
|
+
},
|
|
377
|
+
parsed = parser.parse(template.literals) as ParseResult;
|
|
378
|
+
|
|
379
|
+
for (let j = 0, m = template.expressions.length; j < m; j++) {
|
|
380
|
+
exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
|
|
381
|
+
}
|
|
340
382
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
383
|
+
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
384
|
+
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
344
385
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Optimize: when template has no slots and is `() => template`, use template directly
|
|
350
|
-
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
351
|
-
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
352
|
-
|
|
353
|
-
if (arrowMatch) {
|
|
354
|
-
replacements.push({
|
|
355
|
-
end: template.end,
|
|
356
|
-
newText: getOrCreateTemplateId(ctx, parsed.html),
|
|
357
|
-
start: template.start - arrowMatch[0].length
|
|
358
|
-
});
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
386
|
+
if (arrowMatch) {
|
|
387
|
+
return getOrCreateTemplateId(localCtx, parsed.html);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
362
390
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
start: template.start
|
|
391
|
+
return generateTemplateCode(
|
|
392
|
+
localCtx,
|
|
393
|
+
parsed,
|
|
394
|
+
exprTexts,
|
|
395
|
+
template.expressions,
|
|
396
|
+
isArrowBody
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
node: template.node
|
|
373
400
|
});
|
|
374
401
|
}
|
|
375
402
|
|
|
376
|
-
let
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (changed && ctx.templates.size > 0) {
|
|
380
|
-
let aliasedImports: string[] = [],
|
|
381
|
-
factories: string[] = [],
|
|
382
|
-
updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
|
|
383
|
-
|
|
384
|
-
for (let [name, alias] of ctx.imports) {
|
|
385
|
-
aliasedImports.push(`${name} as ${alias}`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
for (let [html, id] of ctx.templates) {
|
|
389
|
-
factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Remove html entrypoint and add aliased imports
|
|
393
|
-
code = imports.modify(code, updatedSourceFile, PACKAGE, {
|
|
394
|
-
add: new Set(aliasedImports),
|
|
395
|
-
remove: [COMPILER_ENTRYPOINT]
|
|
396
|
-
});
|
|
397
|
-
code = factories.join('\n') + '\n\n' + code;
|
|
403
|
+
for (let [html, id] of ctx.templates) {
|
|
404
|
+
result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
398
405
|
}
|
|
399
406
|
|
|
400
|
-
return
|
|
407
|
+
return result;
|
|
401
408
|
};
|
|
402
409
|
|
|
403
|
-
const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
|
|
404
|
-
if (calls.length === 0) {
|
|
405
|
-
return code;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let replacements: Replacement[] = [];
|
|
409
|
-
|
|
410
|
-
for (let i = 0, n = calls.length; i < n; i++) {
|
|
411
|
-
let call = calls[i];
|
|
412
|
-
|
|
413
|
-
replacements.push({
|
|
414
|
-
end: call.end,
|
|
415
|
-
newText: `new ${arraySlotAlias}(
|
|
416
|
-
${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
|
|
417
|
-
${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
|
|
418
|
-
)`,
|
|
419
|
-
start: call.start
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return c.replaceReverse(code, replacements);
|
|
424
|
-
};
|
|
425
410
|
|
|
426
|
-
export { generateCode
|
|
411
|
+
export { generateCode };
|
|
427
412
|
export type { CodegenResult };
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,139 +1,56 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { findHtmlTemplates, findReactiveCalls, type ReactiveCallInfo, type TemplateInfo } from './ts-parser';
|
|
1
|
+
import type { ImportIntent, Plugin, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { COMPILER_ENTRYPOINT, PACKAGE } from '~/constants';
|
|
3
|
+
import { generateCode } from './codegen';
|
|
4
|
+
import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining';
|
|
5
|
+
import { findHtmlTemplates } from './ts-parser';
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
reactiveCalls: ReactiveCallInfo[];
|
|
11
|
-
templates: TemplateInfo[];
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type TransformResult = {
|
|
15
|
-
changed: boolean;
|
|
16
|
-
code: string;
|
|
17
|
-
sourceFile: ts.SourceFile;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const CONTEXT_KEY = 'template:analyzed';
|
|
22
|
-
|
|
23
|
-
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
24
|
-
|
|
25
|
-
const REGEX_BACKSLASH = /\\/g;
|
|
26
|
-
|
|
27
|
-
const REGEX_FORWARD_SLASH = /\//g;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
|
|
31
|
-
return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const analyze = (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext): void => {
|
|
36
|
-
let code = sourceFile.getFullText();
|
|
37
|
-
|
|
38
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let checker = program.getTypeChecker(),
|
|
43
|
-
filename = sourceFile.fileName,
|
|
44
|
-
files = context.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined,
|
|
45
|
-
programSourceFile = program.getSourceFile(filename)
|
|
46
|
-
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
47
|
-
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
8
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
|
|
48
9
|
|
|
49
|
-
if (programSourceFile) {
|
|
50
|
-
sourceFile = programSourceFile;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!files) {
|
|
54
|
-
files = new Map();
|
|
55
|
-
context.set(CONTEXT_KEY, files);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
files.set(filename, {
|
|
59
|
-
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
60
|
-
templates: findHtmlTemplates(sourceFile, checker)
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
10
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
filename = sourceFile.fileName;
|
|
11
|
+
const templatePlugin: Plugin = {
|
|
12
|
+
patterns: PATTERNS,
|
|
67
13
|
|
|
68
|
-
|
|
69
|
-
|
|
14
|
+
transform: (ctx: TransformContext) => {
|
|
15
|
+
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
70
16
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
74
|
-
return { changed: false, code, sourceFile };
|
|
17
|
+
if (templates.length === 0) {
|
|
18
|
+
return {};
|
|
75
19
|
}
|
|
76
20
|
|
|
77
|
-
let
|
|
78
|
-
|
|
79
|
-
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
80
|
-
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
21
|
+
let existingAliases = new Map<string, string>(),
|
|
22
|
+
existingArraySlotAlias = ctx.shared.get(SHARED_KEY) as string | undefined;
|
|
81
23
|
|
|
82
|
-
if (
|
|
83
|
-
|
|
24
|
+
if (existingArraySlotAlias) {
|
|
25
|
+
existingAliases.set('ArraySlot', existingArraySlotAlias);
|
|
84
26
|
}
|
|
85
27
|
|
|
86
|
-
|
|
87
|
-
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
88
|
-
templates: findHtmlTemplates(sourceFile, checker)
|
|
89
|
-
};
|
|
90
|
-
}
|
|
28
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
|
|
91
29
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
existingAliases = new Map<string, string>(),
|
|
95
|
-
result = code;
|
|
96
|
-
|
|
97
|
-
if (analyzed.reactiveCalls.length > 0) {
|
|
98
|
-
changed = true;
|
|
99
|
-
existingAliases.set('ArraySlot', uid('ArraySlot'));
|
|
100
|
-
result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot')!);
|
|
101
|
-
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
102
|
-
|
|
103
|
-
// Re-analyze templates after reactive inlining modifies the code
|
|
104
|
-
analyzed = {
|
|
105
|
-
reactiveCalls: [],
|
|
106
|
-
templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (analyzed.templates.length > 0) {
|
|
111
|
-
let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
|
|
112
|
-
|
|
113
|
-
if (codegenResult.changed) {
|
|
114
|
-
changed = true;
|
|
115
|
-
codegenChanged = true;
|
|
116
|
-
result = codegenResult.code;
|
|
30
|
+
if (result.replacements.length === 0) {
|
|
31
|
+
return {};
|
|
117
32
|
}
|
|
118
|
-
}
|
|
119
33
|
|
|
120
|
-
// Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
|
|
121
|
-
if (existingAliases.size > 0 && !codegenChanged) {
|
|
122
34
|
let aliasedImports: string[] = [];
|
|
123
35
|
|
|
124
|
-
for (let [name, alias] of
|
|
36
|
+
for (let [name, alias] of result.imports) {
|
|
125
37
|
aliasedImports.push(`${name} as ${alias}`);
|
|
126
38
|
}
|
|
127
39
|
|
|
128
|
-
|
|
129
|
-
|
|
40
|
+
let imports: ImportIntent[] = [{
|
|
41
|
+
add: aliasedImports,
|
|
42
|
+
package: PACKAGE,
|
|
43
|
+
remove: [COMPILER_ENTRYPOINT]
|
|
44
|
+
}];
|
|
130
45
|
|
|
131
|
-
|
|
132
|
-
|
|
46
|
+
return {
|
|
47
|
+
imports,
|
|
48
|
+
prepend: result.prepend,
|
|
49
|
+
replacements: result.replacements
|
|
50
|
+
};
|
|
133
51
|
}
|
|
134
|
-
|
|
135
|
-
return { changed, code: result, sourceFile };
|
|
136
52
|
};
|
|
137
53
|
|
|
138
54
|
|
|
139
|
-
export
|
|
55
|
+
export default templatePlugin;
|
|
56
|
+
export { reactiveInliningPlugin, templatePlugin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import {
|
|
2
|
+
import { reactiveInliningPlugin, templatePlugin } from '..';
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
export default plugin.tsc(
|
|
5
|
+
export default plugin.tsc([reactiveInliningPlugin, templatePlugin]) as ReturnType<typeof plugin.tsc>;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants';
|
|
3
|
-
import {
|
|
3
|
+
import { reactiveInliningPlugin, templatePlugin } from '..';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export default plugin.vite({
|
|
7
|
-
analyze,
|
|
8
7
|
name: PACKAGE,
|
|
9
|
-
|
|
8
|
+
plugins: [reactiveInliningPlugin, templatePlugin]
|
|
10
9
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type ReactiveCallInfo = {
|
|
8
|
+
arrayArg: ts.Expression;
|
|
9
|
+
callbackArg: ts.Expression;
|
|
10
|
+
node: ts.CallExpression;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
15
|
+
|
|
16
|
+
const SHARED_KEY = 'template:arraySlotAlias';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefined): boolean {
|
|
23
|
+
if (node.text !== COMPILER_ENTRYPOINT) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!checker) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
32
|
+
|
|
33
|
+
if (!symbol) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
38
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let declarations = symbol.getDeclarations();
|
|
42
|
+
|
|
43
|
+
if (!declarations || declarations.length === 0) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
48
|
+
let filename = declarations[i].getSourceFile().fileName;
|
|
49
|
+
|
|
50
|
+
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function visit(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
|
|
59
|
+
if (
|
|
60
|
+
ts.isCallExpression(node) &&
|
|
61
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
62
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
63
|
+
node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
|
|
64
|
+
node.arguments.length === 2 &&
|
|
65
|
+
isHtmlFromPackage(node.expression.expression, checker)
|
|
66
|
+
) {
|
|
67
|
+
calls.push({
|
|
68
|
+
arrayArg: node.arguments[0],
|
|
69
|
+
callbackArg: node.arguments[1],
|
|
70
|
+
node
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ts.forEachChild(node, child => visit(child, calls, checker));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const plugin: Plugin = {
|
|
79
|
+
patterns: PATTERNS,
|
|
80
|
+
|
|
81
|
+
transform: (ctx: TransformContext) => {
|
|
82
|
+
let calls: ReactiveCallInfo[] = [];
|
|
83
|
+
|
|
84
|
+
visit(ctx.sourceFile, calls, ctx.checker);
|
|
85
|
+
|
|
86
|
+
if (calls.length === 0) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let arraySlotAlias = uid('ArraySlot'),
|
|
91
|
+
replacements: ReplacementIntent[] = [];
|
|
92
|
+
|
|
93
|
+
ctx.shared.set(SHARED_KEY, arraySlotAlias);
|
|
94
|
+
|
|
95
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
96
|
+
let call = calls[i];
|
|
97
|
+
|
|
98
|
+
replacements.push({
|
|
99
|
+
generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
100
|
+
node: call.node
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
imports: [{
|
|
106
|
+
add: [`ArraySlot as ${arraySlotAlias}`],
|
|
107
|
+
package: PACKAGE
|
|
108
|
+
}],
|
|
109
|
+
replacements
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
export default plugin;
|
|
116
|
+
export { SHARED_KEY };
|