@lithent/hmr-parser 0.2.0 → 0.2.2
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/dist/lithentHmrParser.mjs +3166 -2956
- package/dist/lithentHmrParser.mjs.map +1 -1
- package/dist/lithentHmrParser.umd.js +81 -37
- package/dist/lithentHmrParser.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/strings.ts +50 -10
- package/src/tests/mark.test.ts +30 -0
- package/src/transform/componentRegister.ts +266 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@lithent/hmr-parser",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"description": "Lithent HMR parser utilities for build tools",
|
|
6
6
|
"main": "./dist/lithentHmrParser.umd.js",
|
|
7
7
|
"module": "./dist/lithentHmrParser.mjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"vite-plugin-checker": "^0.8.0",
|
|
41
41
|
"vite-plugin-dts": "^4.2.1",
|
|
42
42
|
"vitest": "^2.1.5",
|
|
43
|
-
"lithent": "1.20.
|
|
43
|
+
"lithent": "1.20.2"
|
|
44
44
|
},
|
|
45
45
|
"volta": {
|
|
46
46
|
"node": "20.3.0",
|
package/src/strings.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const createHmrBootstrapBlock = (
|
|
|
12
12
|
const __lithentModuleId = new URL(import.meta.url).pathname;
|
|
13
13
|
const __lithentBoundaryStoreKey = \`__lithent_hmr_boundary__\${__lithentModuleId}\`;
|
|
14
14
|
const __lithentDisposeStoreKey = \`__lithent_hmr_dispose__\${__lithentModuleId}\`;
|
|
15
|
+
const __lithentInvalidateCountKey = \`__lithent_hmr_invalidate_count__\${__lithentModuleId}\`;
|
|
15
16
|
const __lithentGlobalStore =
|
|
16
17
|
typeof globalThis === 'object'
|
|
17
18
|
? globalThis
|
|
@@ -85,6 +86,44 @@ const __lithentModuleHotStore =
|
|
|
85
86
|
|
|
86
87
|
const __lithentHmrTargets = ${serializedTargets};
|
|
87
88
|
|
|
89
|
+
const __lithentGetInvalidateCount = () => {
|
|
90
|
+
return (__lithentGlobalStore && __lithentGlobalStore[__lithentInvalidateCountKey]) || 0;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const __lithentIncrementInvalidateCount = () => {
|
|
94
|
+
if (__lithentGlobalStore) {
|
|
95
|
+
const current = __lithentGetInvalidateCount();
|
|
96
|
+
__lithentGlobalStore[__lithentInvalidateCountKey] = current + 1;
|
|
97
|
+
return current + 1;
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const __lithentResetInvalidateCount = () => {
|
|
103
|
+
if (__lithentGlobalStore) {
|
|
104
|
+
__lithentGlobalStore[__lithentInvalidateCountKey] = 0;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const __lithentSafeInvalidate = (reason) => {
|
|
109
|
+
const MAX_INVALIDATE_ATTEMPTS = 3;
|
|
110
|
+
const count = __lithentIncrementInvalidateCount();
|
|
111
|
+
|
|
112
|
+
if (count >= MAX_INVALIDATE_ATTEMPTS) {
|
|
113
|
+
console.warn(
|
|
114
|
+
\`[Lithent HMR] 전체 리프레시를 \${count}번 시도했지만 계속 실패합니다. 컴포넌트가 마운트되지 않았거나 다른 문제가 있을 수 있습니다. 페이지를 수동으로 새로고침해주세요.\`,
|
|
115
|
+
reason
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.warn(
|
|
121
|
+
\`[Lithent HMR] 변경된 경계를 적용하지 못해 전체 리프레시합니다. (시도 \${count}/\${MAX_INVALIDATE_ATTEMPTS})\`,
|
|
122
|
+
reason
|
|
123
|
+
);
|
|
124
|
+
import.meta.hot?.invalidate?.();
|
|
125
|
+
};
|
|
126
|
+
|
|
88
127
|
const __lithentSetupHmrHooks = () => {
|
|
89
128
|
if (!import.meta.hot) {
|
|
90
129
|
return;
|
|
@@ -116,7 +155,9 @@ const __lithentSetupHmrHooks = () => {
|
|
|
116
155
|
}
|
|
117
156
|
}
|
|
118
157
|
|
|
119
|
-
if (
|
|
158
|
+
if (applied) {
|
|
159
|
+
__lithentResetInvalidateCount();
|
|
160
|
+
} else {
|
|
120
161
|
if (missing.length && __lithentModuleHotStore) {
|
|
121
162
|
queueMicrotask(() => {
|
|
122
163
|
let retried = false;
|
|
@@ -126,19 +167,15 @@ const __lithentSetupHmrHooks = () => {
|
|
|
126
167
|
retried = true;
|
|
127
168
|
}
|
|
128
169
|
}
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
133
|
-
import.meta.hot?.invalidate?.();
|
|
170
|
+
if (retried) {
|
|
171
|
+
__lithentResetInvalidateCount();
|
|
172
|
+
} else {
|
|
173
|
+
__lithentSafeInvalidate(\`missing components: \${missing.join(', ')}\`);
|
|
134
174
|
}
|
|
135
175
|
});
|
|
136
176
|
return;
|
|
137
177
|
}
|
|
138
|
-
|
|
139
|
-
'[Lithent HMR] 변경된 경계를 적용하지 못해 전체 리프레시합니다.'
|
|
140
|
-
);
|
|
141
|
-
import.meta.hot?.invalidate?.();
|
|
178
|
+
__lithentSafeInvalidate('no components applied');
|
|
142
179
|
}
|
|
143
180
|
});
|
|
144
181
|
|
|
@@ -155,6 +192,9 @@ const __lithentSetupHmrHooks = () => {
|
|
|
155
192
|
if (disposeApp) {
|
|
156
193
|
__lithentGlobalStore[__lithentDisposeStoreKey] = disposeApp;
|
|
157
194
|
}
|
|
195
|
+
|
|
196
|
+
// Reset invalidate count on dispose to allow fresh attempts on next load
|
|
197
|
+
__lithentResetInvalidateCount();
|
|
158
198
|
}
|
|
159
199
|
});
|
|
160
200
|
};
|
package/src/tests/mark.test.ts
CHANGED
|
@@ -157,5 +157,35 @@ if (import.meta.vitest) {
|
|
|
157
157
|
expect(namedRegisters).toHaveLength(2);
|
|
158
158
|
expect(namedUnregisters).toHaveLength(2);
|
|
159
159
|
});
|
|
160
|
+
|
|
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
|
+
});
|
|
160
190
|
});
|
|
161
191
|
}
|
|
@@ -9,8 +9,12 @@ export const stitchComponentRegistration = (
|
|
|
9
9
|
) => {
|
|
10
10
|
if (!mounts.length) return;
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const importStatements = extractImportStatements(code);
|
|
13
|
+
const needsMountCallback = !hasNamedImport(importStatements, 'mountCallback');
|
|
14
|
+
const needsGetComponentKey = !hasNamedImport(
|
|
15
|
+
importStatements,
|
|
16
|
+
'getComponentKey'
|
|
17
|
+
);
|
|
14
18
|
|
|
15
19
|
if (needsMountCallback || needsGetComponentKey) {
|
|
16
20
|
const pieces: string[] = [];
|
|
@@ -48,3 +52,263 @@ if (__lithentModuleHotStore) {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
};
|
|
55
|
+
|
|
56
|
+
type ScannerState =
|
|
57
|
+
| 'code'
|
|
58
|
+
| 'single'
|
|
59
|
+
| 'double'
|
|
60
|
+
| 'template'
|
|
61
|
+
| 'lineComment'
|
|
62
|
+
| 'blockComment';
|
|
63
|
+
|
|
64
|
+
const extractImportStatements = (source: string): string[] => {
|
|
65
|
+
const imports: string[] = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
let state: ScannerState = 'code';
|
|
68
|
+
let templateDepth = 0;
|
|
69
|
+
|
|
70
|
+
while (i < source.length) {
|
|
71
|
+
const ch = source[i];
|
|
72
|
+
const next = source[i + 1];
|
|
73
|
+
|
|
74
|
+
switch (state) {
|
|
75
|
+
case 'code':
|
|
76
|
+
if (ch === '/' && next === '/') {
|
|
77
|
+
state = 'lineComment';
|
|
78
|
+
i += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '/' && next === '*') {
|
|
82
|
+
state = 'blockComment';
|
|
83
|
+
i += 2;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (ch === "'") {
|
|
87
|
+
state = 'single';
|
|
88
|
+
i++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (ch === '"') {
|
|
92
|
+
state = 'double';
|
|
93
|
+
i++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === '`') {
|
|
97
|
+
state = 'template';
|
|
98
|
+
templateDepth = 0;
|
|
99
|
+
i++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
ch === 'i' &&
|
|
104
|
+
isImportKeyword(source, i) &&
|
|
105
|
+
!isDynamicImport(source, i)
|
|
106
|
+
) {
|
|
107
|
+
const end = findImportStatementEnd(source, i);
|
|
108
|
+
imports.push(source.slice(i, end));
|
|
109
|
+
i = end;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
i++;
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'single':
|
|
116
|
+
if (ch === '\\') {
|
|
117
|
+
i += 2;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (ch === "'") {
|
|
121
|
+
state = 'code';
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'double':
|
|
127
|
+
if (ch === '\\') {
|
|
128
|
+
i += 2;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '"') {
|
|
132
|
+
state = 'code';
|
|
133
|
+
}
|
|
134
|
+
i++;
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'template':
|
|
138
|
+
if (ch === '\\') {
|
|
139
|
+
i += 2;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (ch === '`') {
|
|
143
|
+
if (templateDepth === 0) {
|
|
144
|
+
state = 'code';
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
templateDepth--;
|
|
149
|
+
i++;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (ch === '$' && next === '{') {
|
|
153
|
+
templateDepth++;
|
|
154
|
+
i += 2;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (ch === '}' && templateDepth > 0) {
|
|
158
|
+
templateDepth--;
|
|
159
|
+
}
|
|
160
|
+
i++;
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'lineComment':
|
|
164
|
+
if (ch === '\n') {
|
|
165
|
+
state = 'code';
|
|
166
|
+
}
|
|
167
|
+
i++;
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'blockComment':
|
|
171
|
+
if (ch === '*' && next === '/') {
|
|
172
|
+
state = 'code';
|
|
173
|
+
i += 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
i++;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return imports;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const hasNamedImport = (imports: string[], name: string): boolean => {
|
|
185
|
+
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`);
|
|
186
|
+
return imports.some(statement => {
|
|
187
|
+
const [clause] = statement.split(/from\s+/i);
|
|
188
|
+
return pattern.test(clause);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const escapeRegExp = (value: string): string =>
|
|
193
|
+
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
194
|
+
|
|
195
|
+
const isImportKeyword = (source: string, index: number): boolean => {
|
|
196
|
+
if (!source.startsWith('import', index)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const prev = source[index - 1];
|
|
201
|
+
if (prev && /\w/.test(prev)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const next = source[index + 'import'.length];
|
|
206
|
+
if (next && /\w/.test(next)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const isDynamicImport = (source: string, index: number): boolean => {
|
|
214
|
+
const nextChar = source[index + 'import'.length];
|
|
215
|
+
return nextChar === '(' || nextChar === '.';
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const findImportStatementEnd = (source: string, start: number): number => {
|
|
219
|
+
let i = start;
|
|
220
|
+
let state: ScannerState = 'code';
|
|
221
|
+
|
|
222
|
+
while (i < source.length) {
|
|
223
|
+
const ch = source[i];
|
|
224
|
+
const next = source[i + 1];
|
|
225
|
+
|
|
226
|
+
switch (state) {
|
|
227
|
+
case 'code':
|
|
228
|
+
if (ch === ';') {
|
|
229
|
+
return i + 1;
|
|
230
|
+
}
|
|
231
|
+
if (ch === '/' && next === '/') {
|
|
232
|
+
state = 'lineComment';
|
|
233
|
+
i += 2;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (ch === '/' && next === '*') {
|
|
237
|
+
state = 'blockComment';
|
|
238
|
+
i += 2;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (ch === "'") {
|
|
242
|
+
state = 'single';
|
|
243
|
+
i++;
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (ch === '"') {
|
|
247
|
+
state = 'double';
|
|
248
|
+
i++;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (ch === '`') {
|
|
252
|
+
state = 'template';
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (ch === '\n' && source[i + 1] === '\n') {
|
|
257
|
+
return i + 1;
|
|
258
|
+
}
|
|
259
|
+
i++;
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'single':
|
|
263
|
+
if (ch === '\\') {
|
|
264
|
+
i += 2;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (ch === "'") {
|
|
268
|
+
state = 'code';
|
|
269
|
+
}
|
|
270
|
+
i++;
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'double':
|
|
274
|
+
if (ch === '\\') {
|
|
275
|
+
i += 2;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (ch === '"') {
|
|
279
|
+
state = 'code';
|
|
280
|
+
}
|
|
281
|
+
i++;
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case 'template':
|
|
285
|
+
if (ch === '\\') {
|
|
286
|
+
i += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ch === '`') {
|
|
290
|
+
state = 'code';
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case 'lineComment':
|
|
296
|
+
if (ch === '\n') {
|
|
297
|
+
state = 'code';
|
|
298
|
+
}
|
|
299
|
+
i++;
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 'blockComment':
|
|
303
|
+
if (ch === '*' && next === '/') {
|
|
304
|
+
state = 'code';
|
|
305
|
+
i += 2;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
i++;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return source.length;
|
|
314
|
+
};
|