@lithent/hmr-parser 0.2.2 → 0.2.3

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 (42) hide show
  1. package/dist/helper/calculator.d.cts +8 -0
  2. package/dist/helper/calculator.d.ts +1 -1
  3. package/dist/index.d.cts +8 -0
  4. package/dist/index.d.ts +5 -5
  5. package/dist/lithentHmrParser.mjs +4214 -4370
  6. package/dist/lithentHmrParser.mjs.map +1 -1
  7. package/dist/lithentHmrParser.umd.js +137 -216
  8. package/dist/lithentHmrParser.umd.js.map +1 -1
  9. package/dist/markerParse.d.cts +2 -0
  10. package/dist/markerParse.d.ts +1 -1
  11. package/dist/noMarkerParse.d.cts +2 -0
  12. package/dist/noMarkerParse.d.ts +1 -1
  13. package/dist/shared.d.cts +4 -0
  14. package/dist/shared.d.ts +1 -1
  15. package/dist/strings.d.cts +1 -0
  16. package/dist/strings.d.ts +1 -1
  17. package/dist/tests/bootstrap.test.d.cts +1 -0
  18. package/dist/tests/bootstrap.test.d.ts +1 -0
  19. package/dist/tests/mark.test.d.cts +1 -0
  20. package/dist/tests/nomark.test.d.cts +1 -0
  21. package/dist/transform/componentRegister.d.cts +3 -0
  22. package/dist/transform/componentRegister.d.ts +2 -2
  23. package/dist/transform/exportSnapshot.d.cts +3 -0
  24. package/dist/transform/exportSnapshot.d.ts +3 -0
  25. package/dist/transform/renderGuard.d.cts +3 -0
  26. package/dist/transform/renderGuard.d.ts +1 -1
  27. package/dist/types.d.cts +28 -0
  28. package/dist/utils/ast/componentCollector.d.cts +9 -0
  29. package/dist/utils/ast/componentCollector.d.ts +5 -4
  30. package/package.json +2 -2
  31. package/src/helper/calculator.ts +27 -7
  32. package/src/markerParse.ts +30 -14
  33. package/src/noMarkerParse.ts +16 -4
  34. package/src/shared.ts +8 -2
  35. package/src/strings.ts +105 -177
  36. package/src/tests/bootstrap.test.ts +312 -0
  37. package/src/tests/mark.test.ts +61 -181
  38. package/src/tests/nomark.test.tsx +110 -89
  39. package/src/transform/componentRegister.ts +13 -304
  40. package/src/transform/exportSnapshot.ts +77 -0
  41. package/src/transform/renderGuard.ts +59 -49
  42. package/src/utils/ast/componentCollector.ts +195 -96
@@ -1,191 +1,71 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parse } from '@babel/parser';
1
3
  import { transformWithMarker } from '../markerParse';
2
4
 
3
- const createDefaultMarkerRegex = () =>
4
- new RegExp(
5
- '/\\*\\s*lithent:hmr-boundary(?:\\s+([A-Za-z0-9_,\\s]+))?\\s*\\*/'
6
- );
5
+ const transform = (code: string) =>
6
+ transformWithMarker({
7
+ code,
8
+ markerRegex: /\/\*\s*lithent:hmr-boundary(?:\s+([A-Za-z0-9_,\s]+))?\s*\*\//,
9
+ boundaryImportSpecifier: 'lithent/devHelper',
10
+ tagFunctionImportSpecifier: 'lithent',
11
+ });
7
12
 
8
- const markerSourceDefault = `
13
+ describe('explicit component markers', () => {
14
+ it('wraps local and exported components independently and guards render calls', () => {
15
+ const result = transform(`
9
16
  'use client';
10
- import { render, mount } from 'lithent';
11
-
17
+ import {mount, render} from 'lithent';
12
18
  /* lithent:hmr-boundary default */
13
-
14
- const App = mount((renew, props: { title: string }) => {
15
- return () => <div>{props.title}</div>;
16
- });
17
-
18
- const Secondary = mount((renew, props: { subtitle: string }) => {
19
- return () => <p>{props.subtitle}</p>;
20
- });
21
-
19
+ const App = mount(() => {return () => <Secondary/>;});
20
+ const Secondary = () => <span/>;
21
+ render(<App/>, document.body);
22
22
  export default App;
23
- `;
24
-
25
- const markerSourceNamed = `
26
- "use client";
27
- import type { TagFunction } from 'lithent';
28
- import { createBoundary } from 'lithent/devHelper';
29
- import { render, mount } from 'lithent';
30
-
31
- /* lithent:hmr-boundary Counter, Another */
32
-
33
- export const Counter = mount((renew, props) => {
34
- return () => <span>{props.value}</span>;
35
- });
36
-
37
- export const Another = mount((renew, props) => {
38
- return () => <span>{props.value}</span>;
39
- });
40
- `;
41
-
42
- const defaultResult = transformWithMarker({
43
- code: markerSourceDefault,
44
- markerRegex: createDefaultMarkerRegex(),
45
- boundaryImportSpecifier: 'lithent/devHelper',
46
- tagFunctionImportSpecifier: 'lithent',
47
- });
48
-
49
- const namedResult = transformWithMarker({
50
- code: markerSourceNamed,
51
- markerRegex: createDefaultMarkerRegex(),
52
- boundaryImportSpecifier: 'lithent/devHelper',
53
- tagFunctionImportSpecifier: 'lithent',
54
- });
55
-
56
- console.log('[marker] transformed default:', defaultResult.transformed);
57
- console.log('[marker] transformed named:', namedResult.transformed);
58
- console.log('[marker] code default:', defaultResult.code);
59
- console.log('[marker] code named:', namedResult.code);
60
-
61
- if (!import.meta.vitest) {
62
- const defaultPre = document?.getElementById('marker-default');
63
- if (defaultPre) {
64
- defaultPre.textContent = defaultResult.transformed
65
- ? defaultResult.code
66
- : 'transform skipped';
67
- }
68
-
69
- const namedPre = document?.getElementById('marker-named');
70
- if (namedPre) {
71
- namedPre.textContent = namedResult.transformed
72
- ? namedResult.code
73
- : 'transform skipped';
74
- }
75
- }
76
-
77
- if (import.meta.vitest) {
78
- const { describe, it, expect } = import.meta.vitest;
79
-
80
- describe('transformWithMarker', () => {
81
- it('replaces marker comment with HMR bootstrap and injects helpers', () => {
82
- expect(defaultResult.transformed).toBe(true);
83
- expect(defaultResult.code).not.toMatch(/lithent:hmr-boundary/);
84
- expect(defaultResult.code).toContain(
85
- "import { createBoundary } from 'lithent/devHelper';"
86
- );
87
- expect(defaultResult.code).toContain('import.meta.hot');
88
- expect(defaultResult.code).toContain(
89
- "import { mountCallback, getComponentKey } from 'lithent';"
90
- );
91
- expect(defaultResult.code).toContain(
92
- 'const __lithentModuleId = new URL(import.meta.url).pathname;'
93
- );
94
- expect(defaultResult.code).toContain(
95
- 'const __lithentHmrTargets = ["App","Secondary"];'
96
- );
97
- expect(defaultResult.code).toContain('const knownNames = new Set([');
98
- expect(defaultResult.code).toContain('...["App","Secondary"]');
99
- expect(defaultResult.code).toContain('...__lithentHmrTargets');
100
- expect(defaultResult.code).toContain(
101
- 'const __lithentHotComponent_App = App;'
102
- );
103
- expect(defaultResult.code).toContain(
104
- '__lithentModuleHotStore["App"] = __lithentHotComponent_App;'
105
- );
106
- expect(defaultResult.code).toContain(
107
- 'const __lithentHotComponent_Secondary = Secondary;'
108
- );
109
- expect(defaultResult.code).toContain(
110
- '__lithentModuleHotStore["Secondary"] = __lithentHotComponent_Secondary;'
111
- );
112
- expect(defaultResult.code).toContain(
113
- 'const __lithentRenderOnce = (factory)'
114
- );
115
- const registerMatches =
116
- defaultResult.code.match(/counterBoundary\.register\(compKey\)/g) ?? [];
117
- const unregisterMatches =
118
- defaultResult.code.match(
119
- /mountCallback\(\(\) => \(\) => unregister\(\)\)/g
120
- ) ?? [];
121
- expect(registerMatches).toHaveLength(2);
122
- expect(unregisterMatches).toHaveLength(2);
123
- });
23
+ `);
24
+ expect(result.transformed).toBe(true);
25
+ expect(result.code).not.toContain('/* lithent:hmr-boundary');
26
+ expect(result.code).toContain('__lithentWrapComponent("App", mount(');
27
+ expect(result.code).toContain('__lithentWrapComponent("Secondary",');
28
+ expect(result.code).toContain('__lithentRenderOnce(0, () => render(');
29
+ expect(() =>
30
+ parse(result.code, {
31
+ sourceType: 'module',
32
+ plugins: ['typescript', 'jsx'],
33
+ })
34
+ ).not.toThrow();
35
+ });
124
36
 
125
- it('respects explicit export names declared in the marker', () => {
126
- expect(namedResult.transformed).toBe(true);
127
- expect(
128
- namedResult.code.match(
129
- /import { createBoundary } from 'lithent\/devHelper';/g
130
- )
131
- ).toHaveLength(1);
132
- expect(namedResult.code).toContain(
133
- 'const __lithentHmrTargets = ["Counter","Another"];'
134
- );
135
- expect(namedResult.code).toContain('import.meta.hot');
136
- expect(namedResult.code).toContain('const knownNames = new Set([');
137
- expect(namedResult.code).toContain('...["Counter","Another"]');
138
- expect(namedResult.code).toContain('...__lithentHmrTargets');
139
- expect(namedResult.code).toContain(
140
- 'const __lithentHotComponent_Counter = Counter;'
141
- );
142
- expect(namedResult.code).toContain(
143
- '__lithentModuleHotStore["Counter"] = __lithentHotComponent_Counter;'
144
- );
145
- expect(namedResult.code).toContain(
146
- 'const __lithentHotComponent_Another = Another;'
147
- );
148
- expect(namedResult.code).toContain(
149
- '__lithentModuleHotStore["Another"] = __lithentHotComponent_Another;'
150
- );
151
- const namedRegisters =
152
- namedResult.code.match(/counterBoundary\.register\(compKey\)/g) ?? [];
153
- const namedUnregisters =
154
- namedResult.code.match(
155
- /mountCallback\(\(\) => \(\) => unregister\(\)\)/g
156
- ) ?? [];
157
- expect(namedRegisters).toHaveLength(2);
158
- expect(namedUnregisters).toHaveLength(2);
37
+ it('does not rely on unaliased lifecycle imports or snippet text', () => {
38
+ const result = transform(`
39
+ import {createBoundary} from 'lithent/devHelper';
40
+ import {mount, getComponentKey as key} from 'lithent';
41
+ const snippet = "import {mountCallback} from 'lithent'";
42
+ /* lithent:hmr-boundary Counter */
43
+ export const Counter = mount(() => {return () => <span/>;});
44
+ `);
45
+ const ast = parse(result.code, {
46
+ sourceType: 'module',
47
+ plugins: ['typescript', 'jsx'],
159
48
  });
49
+ const boundaryImports = ast.program.body.filter(
50
+ node =>
51
+ node.type === 'ImportDeclaration' &&
52
+ node.source.value === 'lithent/devHelper'
53
+ );
54
+ expect(boundaryImports).toHaveLength(2);
55
+ expect(result.code).toContain('createBoundary as __lithentCreateBoundary');
56
+ expect(result.code).toContain(
57
+ 'getComponentKey as __lithentGetComponentKey'
58
+ );
59
+ expect(result.code).toContain('mountCallback as __lithentMountCallback');
60
+ });
160
61
 
161
- it('still injects helpers when code includes snippet text mentioning mountCallback', () => {
162
- const sourceWithSnippet = [
163
- "'use client';",
164
- "import { mount } from 'lithent';",
165
- '',
166
- 'const doc = "import { h, mountCallback } from \'lithent\';";',
167
- '',
168
- '/* lithent:hmr-boundary default */',
169
- '',
170
- 'const Demo = mount(() => {',
171
- ' return () => <div>demo</div>;',
172
- '});',
173
- '',
174
- 'export default Demo;',
175
- '',
176
- ].join('\n');
177
-
178
- const snippetResult = transformWithMarker({
179
- code: sourceWithSnippet,
180
- markerRegex: createDefaultMarkerRegex(),
181
- boundaryImportSpecifier: 'lithent/devHelper',
182
- tagFunctionImportSpecifier: 'lithent',
183
- });
184
-
185
- expect(snippetResult.transformed).toBe(true);
186
- expect(snippetResult.code).toContain(
187
- "import { mountCallback, getComponentKey } from 'lithent';"
188
- );
189
- });
62
+ it('initializes wrappers before declarations even when the marker follows them', () => {
63
+ const result = transform(`
64
+ export const Badge = () => <span/>;
65
+ /* lithent:hmr-boundary Badge */
66
+ `);
67
+ expect(result.code.indexOf('const __lithentWrapComponent')).toBeLessThan(
68
+ result.code.indexOf('export const Badge')
69
+ );
190
70
  });
191
- }
71
+ });
@@ -1,98 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parse } from '@babel/parser';
1
3
  import { transformWithoutMarker } from '../noMarkerParse';
2
4
 
3
- const source = `
4
- 'use client';
5
- import { render, mount } from 'lithent';
6
-
7
- const Demo = mount((_renew, props: { message: string }) => {
8
- return () => ({ type: 'div', props: {}, children: [props.message] } as any);
9
- });
10
-
11
- const Another = mount((_renew, props: { id: number }) => {
12
- return () => ({ type: 'span', props: {}, children: [String(props.id)] } as any);
13
- });
14
-
15
- const root = document.getElementById('app');
16
- if (root) {
17
- render(Demo({ message: 'hello' }), root as any);
18
- render(Another({ id: 1 }), root as any);
19
- }
20
- `;
5
+ const transform = (code: string) =>
6
+ transformWithoutMarker({
7
+ code,
8
+ boundaryImportSpecifier: 'lithent/devHelper',
9
+ tagFunctionImportSpecifier: 'lithent',
10
+ });
21
11
 
22
- const result = transformWithoutMarker({
23
- code: source,
24
- boundaryImportSpecifier: 'lithent/devHelper',
25
- tagFunctionImportSpecifier: 'lithent',
26
- });
12
+ describe('component discovery without a marker', () => {
13
+ it.each([
14
+ [
15
+ 'expression arrow',
16
+ 'export const Badge = ({label}) => <span>{label}</span>;',
17
+ ],
18
+ [
19
+ 'block arrow',
20
+ 'export const Badge = ({label}) => { return <span>{label}</span>; };',
21
+ ],
22
+ [
23
+ 'function declaration',
24
+ 'export function Badge({label}) { return <span>{label}</span>; }',
25
+ ],
26
+ [
27
+ 'function expression',
28
+ 'export const Badge = function ({label}) { return <span>{label}</span>; };',
29
+ ],
30
+ [
31
+ 'named default',
32
+ 'export default function Badge({label}) { return <span>{label}</span>; }',
33
+ ],
34
+ [
35
+ 'lowercase named default',
36
+ 'export default function badge() { return <span/>; }',
37
+ ],
38
+ ['null output', 'export const Empty = () => null;'],
39
+ [
40
+ 'JSX list',
41
+ 'export const List = ({items}) => items.map(item => <span>{item}</span>);',
42
+ ],
43
+ [
44
+ 'returned local',
45
+ 'export const Badge = () => {const output = <span/>; return output;};',
46
+ ],
47
+ ['anonymous default', 'export default ({label}) => <span>{label}</span>;'],
48
+ [
49
+ 'anonymous default function',
50
+ 'export default function ({label}) { return <span>{label}</span>; }',
51
+ ],
52
+ ])('discovers and produces valid syntax for %s', (_name, code) => {
53
+ const result = transform(code);
54
+ expect(result.transformed).toBe(true);
55
+ expect(result.code).toContain('__lithentWrapComponent(');
56
+ expect(() =>
57
+ parse(result.code, {
58
+ sourceType: 'module',
59
+ plugins: ['typescript', 'jsx'],
60
+ })
61
+ ).not.toThrow();
62
+ });
27
63
 
28
- console.log('[no-marker] transformed:', result.transformed);
29
- console.log('[no-marker] code:', result.code);
64
+ it('discovers automatic JSX runtime calls and leaves ordinary helpers alone', () => {
65
+ const result = transform(`
66
+ import {jsx as _jsx} from 'lithent/jsx-runtime';
67
+ export const Badge = props => _jsx('span', {children: props.label});
68
+ export const Format = value => String(value);
69
+ export const Handle = () => () => _jsx('span', {});
70
+ `);
71
+ expect(result.code).toContain('__lithentWrapComponent("Badge",');
72
+ expect(result.code).not.toContain('__lithentWrapComponent("Format",');
73
+ expect(result.code).not.toContain('__lithentWrapComponent("Handle",');
74
+ });
30
75
 
31
- if (!import.meta.vitest) {
32
- const pre = document?.getElementById('output');
33
- if (pre) {
34
- pre.textContent = result.transformed ? result.code : 'transform skipped';
35
- }
36
- }
76
+ it('handles aliased mount, lmount, and multiple declarations', () => {
77
+ const result = transform(`
78
+ import {mount as make, lmount} from 'lithent';
79
+ export const A = make(function () {return () => <span/>;}), B = lmount(() => () => <div/>);
80
+ `);
81
+ expect(result.code).toContain('__lithentWrapComponent("A", make(');
82
+ expect(result.code).toContain('__lithentWrapComponent("B", lmount(');
83
+ expect(() =>
84
+ parse(result.code, {
85
+ sourceType: 'module',
86
+ plugins: ['typescript', 'jsx'],
87
+ })
88
+ ).not.toThrow();
89
+ });
37
90
 
38
- if (import.meta.vitest) {
39
- const { describe, it, expect } = import.meta.vitest;
91
+ it('recognizes namespace imports and typed expressions', () => {
92
+ const result = transform(`
93
+ import * as core from 'lithent';
94
+ import * as runtime from 'lithent/jsx-runtime';
95
+ export const A = core.mount(() => () => core.h('span', {}));
96
+ export const B = props => runtime.jsx('span', {children: props.label});
97
+ export const C = (() => null) satisfies () => null;
98
+ `);
99
+ for (const name of ['A', 'B', 'C'])
100
+ expect(result.code).toContain(`__lithentWrapComponent("${name}",`);
101
+ });
40
102
 
41
- describe('transformWithoutMarker (integration)', () => {
42
- it('should mark transform as successful and inject HMR bootstrap + boundary registration', () => {
43
- const expectedSnippet =
44
- 'const __lithentModuleId = new URL(import.meta.url).pathname;';
103
+ it('does not treat imports from other frameworks as Lithent components', () => {
104
+ const result = transform(
105
+ "import {jsx} from 'react/jsx-runtime'; export const Badge = () => jsx('span', {});"
106
+ );
107
+ expect(result.transformed).toBe(false);
108
+ });
45
109
 
46
- expect(result.transformed).toBe(true);
47
- expect(result.code.includes(expectedSnippet)).toBe(true);
48
- expect(
49
- result.code.includes(
50
- "import { createBoundary } from 'lithent/devHelper'"
51
- )
52
- ).toBe(true);
53
- expect(
54
- result.code.includes(
55
- "import { mountCallback, getComponentKey } from 'lithent'"
56
- )
57
- ).toBe(true);
58
- expect(result.code.includes('const compKey = getComponentKey();')).toBe(
59
- true
60
- );
61
- expect(
62
- result.code.includes('const __lithentHmrTargets = ["Demo","Another"];')
63
- ).toBe(true);
64
- expect(result.code.includes('const knownNames = new Set([')).toBe(true);
65
- expect(result.code.includes('...["Demo","Another"]')).toBe(true);
66
- expect(result.code.includes('...__lithentHmrTargets')).toBe(true);
67
- expect(
68
- result.code.includes('const __lithentHotComponent_Demo = Demo;')
69
- ).toBe(true);
70
- expect(
71
- result.code.includes(
72
- '__lithentModuleHotStore["Demo"] = __lithentHotComponent_Demo;'
73
- )
74
- ).toBe(true);
75
- expect(
76
- result.code.includes('const __lithentHotComponent_Another = Another;')
77
- ).toBe(true);
78
- expect(
79
- result.code.includes(
80
- '__lithentModuleHotStore["Another"] = __lithentHotComponent_Another;'
81
- )
82
- ).toBe(true);
83
- expect(
84
- result.code.includes('const __lithentRenderOnce = (factory)')
85
- ).toBe(true);
86
- expect(/__lithentRenderOnce\(\(\) =>\s*render\(/.test(result.code)).toBe(
87
- true
88
- );
89
- const registerMatches =
90
- result.code.match(/counterBoundary\.register\(compKey\)/g) ?? [];
91
- const unregisterMatches =
92
- result.code.match(/mountCallback\(\(\) => \(\) => unregister\(\)\)/g) ??
93
- [];
94
- expect(registerMatches).toHaveLength(2);
95
- expect(unregisterMatches).toHaveLength(2);
96
- });
110
+ it('does not discover JSX side effects, async functions, or generators', () => {
111
+ for (const source of [
112
+ 'export const Handle = () => {const ignored = <span/>; return 42;};',
113
+ 'export const Badge = async () => <span/>;',
114
+ 'export function* Badge() {yield <span/>;}',
115
+ ]) {
116
+ expect(transform(source).code).not.toContain('__lithentWrapComponent(');
117
+ }
97
118
  });
98
- }
119
+ });