@esportsplus/template 0.37.0 → 0.38.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.
- package/build/compiler/codegen.d.ts +7 -6
- package/build/compiler/codegen.js +140 -105
- package/build/compiler/index.d.ts +3 -10
- package/build/compiler/index.js +54 -73
- package/build/compiler/plugins/tsc.d.ts +2 -3
- package/build/compiler/plugins/tsc.js +2 -3
- package/build/compiler/plugins/vite.js +2 -3
- package/build/constants.d.ts +2 -1
- package/build/constants.js +3 -1
- package/package.json +3 -3
- package/src/compiler/codegen.ts +201 -150
- package/src/compiler/index.ts +65 -107
- package/src/compiler/plugins/tsc.ts +2 -3
- package/src/compiler/plugins/vite.ts +3 -4
- package/src/constants.ts +6 -1
- package/test/integration/combined.ts +90 -0
- package/test/integration/tsconfig.json +17 -0
package/src/compiler/index.ts
CHANGED
|
@@ -1,139 +1,97 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { findHtmlTemplates, findReactiveCalls, type ReactiveCallInfo, type TemplateInfo } from './ts-parser';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
|
|
4
|
+
import { generateCode } from './codegen';
|
|
5
|
+
import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
|
|
7
6
|
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type TransformResult = {
|
|
15
|
-
changed: boolean;
|
|
16
|
-
code: string;
|
|
17
|
-
sourceFile: ts.SourceFile;
|
|
18
|
-
};
|
|
19
|
-
|
|
8
|
+
const PATTERNS = [
|
|
9
|
+
`${COMPILER_ENTRYPOINT}\``,
|
|
10
|
+
`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
|
|
11
|
+
];
|
|
20
12
|
|
|
21
|
-
const CONTEXT_KEY = 'template:analyzed';
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const REGEX_FORWARD_SLASH = /\//g;
|
|
14
|
+
function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
|
|
15
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
16
|
+
let range = ranges[i];
|
|
28
17
|
|
|
18
|
+
if (start >= range.start && end <= range.end) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
29
22
|
|
|
30
|
-
|
|
31
|
-
return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
|
|
23
|
+
return false;
|
|
32
24
|
}
|
|
33
25
|
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
let code = sourceFile.getFullText();
|
|
37
|
-
|
|
38
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
27
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
41
28
|
|
|
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, '\\'));
|
|
48
29
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
30
|
+
const plugin: Plugin = {
|
|
31
|
+
patterns: PATTERNS,
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
33
|
+
transform: (ctx: TransformContext) => {
|
|
34
|
+
let importsIntent: ImportIntent[] = [],
|
|
35
|
+
prepend: string[] = [],
|
|
36
|
+
replacements: ReplacementIntent[] = [],
|
|
37
|
+
removeImports: string[] = [];
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
templates: findHtmlTemplates(sourceFile, checker)
|
|
61
|
-
});
|
|
62
|
-
};
|
|
39
|
+
// Find templates first to build exclusion ranges
|
|
40
|
+
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
filename = sourceFile.fileName;
|
|
42
|
+
// Build ranges for all template nodes - reactive calls inside these are handled by template codegen
|
|
43
|
+
let templateRanges: { end: number; start: number }[] = [];
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
74
|
-
return { changed: false, code, sourceFile };
|
|
45
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
46
|
+
templateRanges.push({
|
|
47
|
+
end: templates[i].end,
|
|
48
|
+
start: templates[i].start
|
|
49
|
+
});
|
|
75
50
|
}
|
|
76
51
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
80
|
-
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
52
|
+
// Transform standalone html.reactive() calls (exclude those inside templates)
|
|
53
|
+
let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
|
|
81
54
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
55
|
+
for (let i = 0, n = reactiveCalls.length; i < n; i++) {
|
|
56
|
+
let call = reactiveCalls[i];
|
|
85
57
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
58
|
+
// Skip reactive calls that are inside template expressions - handled by template codegen
|
|
59
|
+
if (isInRange(templateRanges, call.start, call.end)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
91
62
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
63
|
+
replacements.push({
|
|
64
|
+
generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
65
|
+
node: call.node
|
|
66
|
+
});
|
|
67
|
+
}
|
|
109
68
|
|
|
110
|
-
|
|
111
|
-
|
|
69
|
+
// Transform html`` templates
|
|
70
|
+
if (templates.length > 0) {
|
|
71
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker);
|
|
112
72
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
result = codegenResult.code;
|
|
73
|
+
prepend.push(...result.prepend);
|
|
74
|
+
replacements.push(...result.replacements);
|
|
75
|
+
removeImports.push(COMPILER_ENTRYPOINT);
|
|
117
76
|
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
|
|
121
|
-
if (existingAliases.size > 0 && !codegenChanged) {
|
|
122
|
-
let aliasedImports: string[] = [];
|
|
123
77
|
|
|
124
|
-
|
|
125
|
-
|
|
78
|
+
if (replacements.length === 0 && prepend.length === 0) {
|
|
79
|
+
return {};
|
|
126
80
|
}
|
|
127
81
|
|
|
128
|
-
|
|
129
|
-
|
|
82
|
+
importsIntent.push({
|
|
83
|
+
namespace: COMPILER_NAMESPACE,
|
|
84
|
+
package: PACKAGE,
|
|
85
|
+
remove: removeImports
|
|
86
|
+
});
|
|
130
87
|
|
|
131
|
-
|
|
132
|
-
|
|
88
|
+
return {
|
|
89
|
+
imports: importsIntent,
|
|
90
|
+
prepend,
|
|
91
|
+
replacements
|
|
92
|
+
};
|
|
133
93
|
}
|
|
134
|
-
|
|
135
|
-
return { changed, code: result, sourceFile };
|
|
136
94
|
};
|
|
137
95
|
|
|
138
96
|
|
|
139
|
-
export
|
|
97
|
+
export default plugin;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { PACKAGE } from '
|
|
3
|
-
import
|
|
2
|
+
import { PACKAGE } from '~/constants';
|
|
3
|
+
import templatePlugin from '..';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export default plugin.vite({
|
|
7
|
-
analyze,
|
|
8
7
|
name: PACKAGE,
|
|
9
|
-
|
|
8
|
+
plugins: [templatePlugin]
|
|
10
9
|
});
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
const ARRAY_SLOT = Symbol('template.array.slot');
|
|
2
5
|
|
|
3
6
|
const CLEANUP = Symbol('template.cleanup');
|
|
@@ -6,6 +9,8 @@ const COMPILER_ENTRYPOINT = 'html';
|
|
|
6
9
|
|
|
7
10
|
const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
|
|
8
11
|
|
|
12
|
+
const COMPILER_NAMESPACE = uid('template');
|
|
13
|
+
|
|
9
14
|
const enum COMPILER_TYPES {
|
|
10
15
|
ArraySlot = 'array-slot',
|
|
11
16
|
Attributes = 'attributes',
|
|
@@ -48,7 +53,7 @@ const STORE = Symbol('template.store');
|
|
|
48
53
|
export {
|
|
49
54
|
ARRAY_SLOT,
|
|
50
55
|
CLEANUP,
|
|
51
|
-
COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES,
|
|
56
|
+
COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES,
|
|
52
57
|
DIRECT_ATTACH_EVENTS,
|
|
53
58
|
LIFECYCLE_EVENTS,
|
|
54
59
|
PACKAGE,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: Combined reactivity + template plugins
|
|
3
|
+
*
|
|
4
|
+
* This file tests that both plugins work together correctly:
|
|
5
|
+
* 1. reactive() from @esportsplus/reactivity - creates reactive objects/arrays
|
|
6
|
+
* 2. html`` from @esportsplus/template - compiles templates
|
|
7
|
+
* 3. html.reactive() from @esportsplus/template - renders reactive arrays
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { reactive } from '@esportsplus/reactivity';
|
|
11
|
+
import { html } from '@esportsplus/template';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Test 1: Basic reactive object with template
|
|
15
|
+
const state = reactive({
|
|
16
|
+
count: 0,
|
|
17
|
+
name: 'World'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const basicTemplate = () => html`
|
|
21
|
+
<div class="counter">
|
|
22
|
+
<span>Hello ${() => state.name}!</span>
|
|
23
|
+
<span>Count: ${() => state.count}</span>
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// Test 2: Reactive array with html.reactive()
|
|
29
|
+
const items = reactive([
|
|
30
|
+
{ id: 1, text: 'First' },
|
|
31
|
+
{ id: 2, text: 'Second' },
|
|
32
|
+
{ id: 3, text: 'Third' }
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const reactiveList = () => html`
|
|
36
|
+
<ul class="list">
|
|
37
|
+
${html.reactive(items, (item) => html`
|
|
38
|
+
<li data-id="${item.id}">${item.text}</li>
|
|
39
|
+
`)}
|
|
40
|
+
</ul>
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Test 3: Combined - reactive object + reactive array in same template
|
|
45
|
+
type Todo = { id: number; done: boolean; text: string };
|
|
46
|
+
|
|
47
|
+
const todos = reactive<Todo[]>([
|
|
48
|
+
{ id: 1, done: false, text: 'Learn TypeScript' },
|
|
49
|
+
{ id: 2, done: true, text: 'Build compiler' }
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const app = reactive({
|
|
53
|
+
title: 'Todo App'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const combinedTemplate = () => html`
|
|
57
|
+
<div class="app">
|
|
58
|
+
<h1>${() => app.title}</h1>
|
|
59
|
+
<ul>
|
|
60
|
+
${html.reactive(todos, (todo) => html`
|
|
61
|
+
<li class="${() => todo.done ? 'done' : ''}">
|
|
62
|
+
<input type="checkbox" checked="${() => todo.done}" />
|
|
63
|
+
<span>${todo.text}</span>
|
|
64
|
+
</li>
|
|
65
|
+
`)}
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Test 4: Static template (no reactive expressions)
|
|
72
|
+
export const staticTemplate = () => html`
|
|
73
|
+
<footer>
|
|
74
|
+
<p>Static content - no transformations needed</p>
|
|
75
|
+
</footer>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// Test 5: Nested templates with effects
|
|
80
|
+
export const nestedTemplate = () => html`
|
|
81
|
+
<div class="wrapper">
|
|
82
|
+
${html`<header>Header</header>`}
|
|
83
|
+
<main>
|
|
84
|
+
${() => state.count > 5
|
|
85
|
+
? html`<span>High count!</span>`
|
|
86
|
+
: html`<span>Low count</span>`
|
|
87
|
+
}
|
|
88
|
+
</main>
|
|
89
|
+
</div>
|
|
90
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@esportsplus/typescript/tsconfig.package.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"outDir": "./build",
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationDir": null,
|
|
8
|
+
"noUnusedLocals": false,
|
|
9
|
+
"plugins": [
|
|
10
|
+
{ "transform": "@esportsplus/reactivity/compiler/tsc" },
|
|
11
|
+
{ "transform": "../../build/compiler/plugins/tsc.js" }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"./**/*.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|