@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.
@@ -1,139 +1,97 @@
1
- import type { PluginContext } from '@esportsplus/typescript/compiler';
1
+ import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
- import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
4
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
5
- import { generateCode, generateReactiveInlining } from './codegen';
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
- type AnalyzedFile = {
10
- reactiveCalls: ReactiveCallInfo[];
11
- templates: TemplateInfo[];
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
- const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
24
-
25
- const REGEX_BACKSLASH = /\\/g;
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
- function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
31
- return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
23
+ return false;
32
24
  }
33
25
 
34
26
 
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
- }
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
- if (programSourceFile) {
50
- sourceFile = programSourceFile;
51
- }
30
+ const plugin: Plugin = {
31
+ patterns: PATTERNS,
52
32
 
53
- if (!files) {
54
- files = new Map();
55
- context.set(CONTEXT_KEY, files);
56
- }
33
+ transform: (ctx: TransformContext) => {
34
+ let importsIntent: ImportIntent[] = [],
35
+ prepend: string[] = [],
36
+ replacements: ReplacementIntent[] = [],
37
+ removeImports: string[] = [];
57
38
 
58
- files.set(filename, {
59
- reactiveCalls: findReactiveCalls(sourceFile, checker),
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
- const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
65
- let code = sourceFile.getFullText(),
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
- // Try to get pre-analyzed data from context
69
- let analyzed = getAnalyzedFile(context, filename);
70
-
71
- // Fall back to inline analysis (for Vite or when context unavailable)
72
- if (!analyzed) {
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
- let checker = program.getTypeChecker(),
78
- programSourceFile = program.getSourceFile(filename)
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
- if (programSourceFile) {
83
- sourceFile = programSourceFile;
84
- }
55
+ for (let i = 0, n = reactiveCalls.length; i < n; i++) {
56
+ let call = reactiveCalls[i];
85
57
 
86
- analyzed = {
87
- reactiveCalls: findReactiveCalls(sourceFile, checker),
88
- templates: findHtmlTemplates(sourceFile, checker)
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
- let changed = false,
93
- codegenChanged = false,
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
- }
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
- if (analyzed.templates.length > 0) {
111
- let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
69
+ // Transform html`` templates
70
+ if (templates.length > 0) {
71
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker);
112
72
 
113
- if (codegenResult.changed) {
114
- changed = true;
115
- codegenChanged = true;
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
- for (let [name, alias] of existingAliases) {
125
- aliasedImports.push(`${name} as ${alias}`);
78
+ if (replacements.length === 0 && prepend.length === 0) {
79
+ return {};
126
80
  }
127
81
 
128
- result = imports.modify(result, sourceFile, PACKAGE, { add: aliasedImports });
129
- }
82
+ importsIntent.push({
83
+ namespace: COMPILER_NAMESPACE,
84
+ package: PACKAGE,
85
+ remove: removeImports
86
+ });
130
87
 
131
- if (changed) {
132
- sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
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 { analyze, transform };
97
+ export default plugin;
@@ -1,5 +1,4 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- import { analyze, transform } from '..';
1
+ import plugin from '..';
3
2
 
4
3
 
5
- export default plugin.tsc({ analyze, transform }) as ReturnType<typeof plugin.tsc>;
4
+ export default plugin;
@@ -1,10 +1,9 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { PACKAGE } from '../../constants';
3
- import { analyze, transform } from '..';
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
- transform
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
+ }