@lithent/hmr-parser 0.2.0 → 0.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@lithent/hmr-parser",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
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.0"
43
+ "lithent": "1.20.2"
44
44
  },
45
45
  "volta": {
46
46
  "node": "20.3.0",
@@ -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 needsMountCallback = !/import\s+{[^}]*mountCallback/.test(code);
13
- const needsGetComponentKey = !/import\s+{[^}]*getComponentKey/.test(code);
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
+ };