@santi020k/lumen 1.6.0 → 1.7.0
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/README.md +51 -13
- package/dist/cli.js +17 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +32 -2
- package/dist/integration-diagnostics.d.ts.map +1 -1
- package/dist/integration-diagnostics.js +8 -2
- package/dist/native-audit.d.ts +43 -2
- package/dist/native-audit.d.ts.map +1 -1
- package/dist/native-audit.js +274 -6
- package/dist/registry-data.d.ts +50 -10
- package/dist/registry-data.d.ts.map +1 -1
- package/dist/registry-data.js +117 -17
- package/dist/registry-path.d.ts +3 -0
- package/dist/registry-path.d.ts.map +1 -0
- package/dist/registry-path.js +15 -0
- package/dist/registry-types.d.ts +1 -0
- package/dist/registry-types.d.ts.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +76 -14
- package/dist/v2-migration.d.ts +29 -0
- package/dist/v2-migration.d.ts.map +1 -0
- package/dist/v2-migration.js +729 -0
- package/native-components.json +861 -0
- package/package.json +8 -3
- package/release-manifest.json +15 -14
- package/styles/critical.css +2054 -0
- package/styles.css +191 -38
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, relative, resolve } from 'node:path';
|
|
3
|
+
// cspell:words swiftpm
|
|
4
|
+
const ASTRO_PACKAGE = '@santi020k/lumen-astro';
|
|
5
|
+
const ASTRO_RUNTIME_PACKAGE = '@santi020k/lumen-astro/runtime';
|
|
6
|
+
const REACT_PACKAGE = '@santi020k/lumen-react';
|
|
7
|
+
const REACT_NATIVE_PACKAGE = '@santi020k/lumen-react-native';
|
|
8
|
+
const REACT_NATIVE_DATETIME_PACKAGE = '@santi020k/lumen-react-native/datetime';
|
|
9
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
10
|
+
'.astro',
|
|
11
|
+
'.htm',
|
|
12
|
+
'.html',
|
|
13
|
+
'.js',
|
|
14
|
+
'.jsx',
|
|
15
|
+
'.ts',
|
|
16
|
+
'.tsx'
|
|
17
|
+
]);
|
|
18
|
+
const IGNORED_DIRECTORIES = new Set([
|
|
19
|
+
'.astro',
|
|
20
|
+
'.build',
|
|
21
|
+
'.cache',
|
|
22
|
+
'.expo',
|
|
23
|
+
'.git',
|
|
24
|
+
'.next',
|
|
25
|
+
'.output',
|
|
26
|
+
'.swiftpm',
|
|
27
|
+
'.turbo',
|
|
28
|
+
'.vercel',
|
|
29
|
+
'.wrangler',
|
|
30
|
+
'DerivedData',
|
|
31
|
+
'Pods',
|
|
32
|
+
'build',
|
|
33
|
+
'coverage',
|
|
34
|
+
'dist',
|
|
35
|
+
'node_modules',
|
|
36
|
+
'vendor'
|
|
37
|
+
]);
|
|
38
|
+
const VISUAL_SIZE_ALIASES = new Set(['default', 'lg', 'sm']);
|
|
39
|
+
const isIdentifierCharacter = (value) => Boolean(value && /[\w$]/u.test(value));
|
|
40
|
+
const isQuote = (value) => value === '\'' || value === '"' || value === '`';
|
|
41
|
+
const hasKeywordBoundary = (source, start, keyword) => !isIdentifierCharacter(source[start - 1]) &&
|
|
42
|
+
!isIdentifierCharacter(source[start + keyword.length]);
|
|
43
|
+
const isSafeImportClause = (clause) => !clause.includes(';') &&
|
|
44
|
+
!clause.includes('\'') &&
|
|
45
|
+
!clause.includes('"') &&
|
|
46
|
+
!/\bexport\b/u.test(clause);
|
|
47
|
+
const isImportContext = (source, importStart, fromStart, moduleStart) => {
|
|
48
|
+
if (fromStart < 0 || importStart < 0)
|
|
49
|
+
return false;
|
|
50
|
+
if (!hasKeywordBoundary(source, importStart, 'import'))
|
|
51
|
+
return false;
|
|
52
|
+
if (!hasKeywordBoundary(source, fromStart, 'from'))
|
|
53
|
+
return false;
|
|
54
|
+
const lineStart = source.lastIndexOf('\n', importStart) + 1;
|
|
55
|
+
if (source.slice(lineStart, importStart).trim() !== '')
|
|
56
|
+
return false;
|
|
57
|
+
const clause = source.slice(importStart + 'import'.length, fromStart);
|
|
58
|
+
if (!isSafeImportClause(clause))
|
|
59
|
+
return false;
|
|
60
|
+
return source.slice(fromStart + 'from'.length, moduleStart).trim() === '';
|
|
61
|
+
};
|
|
62
|
+
const getLocation = (source, index) => {
|
|
63
|
+
let line = 1;
|
|
64
|
+
let lineStart = 0;
|
|
65
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
66
|
+
if (source[cursor] === '\n') {
|
|
67
|
+
line += 1;
|
|
68
|
+
lineStart = cursor + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { column: index - lineStart + 1, line };
|
|
72
|
+
};
|
|
73
|
+
const createFinding = (source, file, index, kind, message) => ({
|
|
74
|
+
...getLocation(source, index),
|
|
75
|
+
file,
|
|
76
|
+
kind,
|
|
77
|
+
message
|
|
78
|
+
});
|
|
79
|
+
const findImportStatements = (source, module) => {
|
|
80
|
+
const statements = [];
|
|
81
|
+
for (const quote of ['\'', '"']) {
|
|
82
|
+
const needle = `${quote}${module}${quote}`;
|
|
83
|
+
let moduleStart = source.indexOf(needle);
|
|
84
|
+
while (moduleStart >= 0) {
|
|
85
|
+
const fromStart = source.lastIndexOf('from', moduleStart);
|
|
86
|
+
const importStart = fromStart >= 0 ? source.lastIndexOf('import', fromStart) : -1;
|
|
87
|
+
if (isImportContext(source, importStart, fromStart, moduleStart)) {
|
|
88
|
+
const afterModule = moduleStart + needle.length;
|
|
89
|
+
const semicolon = source[afterModule] === ';';
|
|
90
|
+
statements.push({
|
|
91
|
+
clause: source.slice(importStart + 'import'.length, fromStart).trim(),
|
|
92
|
+
end: afterModule + (semicolon ? 1 : 0),
|
|
93
|
+
module,
|
|
94
|
+
quote,
|
|
95
|
+
semicolon,
|
|
96
|
+
start: importStart
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
moduleStart = source.indexOf(needle, moduleStart + needle.length);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return statements.sort((left, right) => left.start - right.start);
|
|
103
|
+
};
|
|
104
|
+
const parseNamedImports = (clause) => {
|
|
105
|
+
const open = clause.indexOf('{');
|
|
106
|
+
const close = clause.lastIndexOf('}');
|
|
107
|
+
if (open < 0 || close <= open)
|
|
108
|
+
return undefined;
|
|
109
|
+
const sources = clause
|
|
110
|
+
.slice(open + 1, close)
|
|
111
|
+
.split(',')
|
|
112
|
+
.map(source => source.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
const imports = sources.flatMap(source => {
|
|
115
|
+
const match = /^(type\s+)?([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/u.exec(source);
|
|
116
|
+
return match?.[2] ?
|
|
117
|
+
[
|
|
118
|
+
{
|
|
119
|
+
imported: match[2],
|
|
120
|
+
local: match[3] ?? match[2],
|
|
121
|
+
source,
|
|
122
|
+
typeOnly: Boolean(match[1])
|
|
123
|
+
}
|
|
124
|
+
] :
|
|
125
|
+
[];
|
|
126
|
+
});
|
|
127
|
+
return { close, complete: imports.length === sources.length, imports, open };
|
|
128
|
+
};
|
|
129
|
+
const getDefaultImport = (clause) => {
|
|
130
|
+
const beforeNamed = clause.split('{', 1)[0]?.trim().replace(/,$/u, '').trim();
|
|
131
|
+
return beforeNamed && /^[A-Za-z_$][\w$]*$/u.test(beforeNamed) ?
|
|
132
|
+
beforeNamed :
|
|
133
|
+
undefined;
|
|
134
|
+
};
|
|
135
|
+
const formatImport = (clause, statement) => `import ${clause} from ${statement.quote}${statement.module}${statement.quote}${statement.semicolon ? ';' : ''}`;
|
|
136
|
+
const applyEdits = (source, edits) => edits
|
|
137
|
+
.sort((left, right) => right.start - left.start)
|
|
138
|
+
.reduce((result, edit) => `${result.slice(0, edit.start)}${edit.replacement}${result.slice(edit.end)}`, source);
|
|
139
|
+
const findBalancedEnd = (source, start, opening, closing) => {
|
|
140
|
+
let depth = 0;
|
|
141
|
+
let quote;
|
|
142
|
+
for (let index = start; index < source.length; index += 1) {
|
|
143
|
+
const character = source[index];
|
|
144
|
+
if (quote) {
|
|
145
|
+
if (character === '\\') {
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
else if (character === quote) {
|
|
149
|
+
quote = undefined;
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (isQuote(character)) {
|
|
154
|
+
quote = character;
|
|
155
|
+
}
|
|
156
|
+
else if (character === opening) {
|
|
157
|
+
depth += 1;
|
|
158
|
+
}
|
|
159
|
+
else if (character === closing) {
|
|
160
|
+
depth -= 1;
|
|
161
|
+
if (depth === 0)
|
|
162
|
+
return index + 1;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return source.length;
|
|
166
|
+
};
|
|
167
|
+
const skipWhitespace = (source, start, end) => {
|
|
168
|
+
let cursor = start;
|
|
169
|
+
while (cursor < end && /\s/u.test(source[cursor] ?? ''))
|
|
170
|
+
cursor += 1;
|
|
171
|
+
return cursor;
|
|
172
|
+
};
|
|
173
|
+
const findQuotedEnd = (source, start, end, quote) => {
|
|
174
|
+
let cursor = start;
|
|
175
|
+
while (cursor < end && source[cursor] !== quote)
|
|
176
|
+
cursor += source[cursor] === '\\' ? 2 : 1;
|
|
177
|
+
return cursor;
|
|
178
|
+
};
|
|
179
|
+
const createAttribute = (start, nameEnd, name, end, value, valueKind) => ({
|
|
180
|
+
end,
|
|
181
|
+
name,
|
|
182
|
+
nameEnd,
|
|
183
|
+
nameStart: start,
|
|
184
|
+
start,
|
|
185
|
+
value,
|
|
186
|
+
valueKind
|
|
187
|
+
});
|
|
188
|
+
const parseMarkupAttributeValue = (source, attributeStart, nameEnd, name, valueStart, end) => {
|
|
189
|
+
const character = source[valueStart];
|
|
190
|
+
if (character === '\'' || character === '"') {
|
|
191
|
+
const contentStart = valueStart + 1;
|
|
192
|
+
const contentEnd = findQuotedEnd(source, contentStart, end, character);
|
|
193
|
+
const cursor = Math.min(contentEnd + 1, end);
|
|
194
|
+
return {
|
|
195
|
+
attribute: createAttribute(attributeStart, nameEnd, name, cursor, source.slice(contentStart, contentEnd), 'literal'),
|
|
196
|
+
cursor
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (character === '{') {
|
|
200
|
+
const cursor = findBalancedEnd(source, valueStart, '{', '}');
|
|
201
|
+
return {
|
|
202
|
+
attribute: createAttribute(attributeStart, nameEnd, name, cursor, source.slice(valueStart + 1, cursor - 1).trim(), 'expression'),
|
|
203
|
+
cursor
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
let cursor = valueStart;
|
|
207
|
+
while (cursor < end && /[^\s>]/u.test(source[cursor] ?? ''))
|
|
208
|
+
cursor += 1;
|
|
209
|
+
return {
|
|
210
|
+
attribute: createAttribute(attributeStart, nameEnd, name, cursor, source.slice(valueStart, cursor), 'literal'),
|
|
211
|
+
cursor
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const parseMarkupAttribute = (source, start, end) => {
|
|
215
|
+
if (source[start] === '{') {
|
|
216
|
+
const cursor = findBalancedEnd(source, start, '{', '}');
|
|
217
|
+
const expression = source.slice(start + 1, cursor - 1).trim();
|
|
218
|
+
return expression === 'size' ?
|
|
219
|
+
{
|
|
220
|
+
attribute: createAttribute(start, start + 1, 'size', cursor, expression, 'expression'),
|
|
221
|
+
cursor
|
|
222
|
+
} :
|
|
223
|
+
{ cursor };
|
|
224
|
+
}
|
|
225
|
+
let nameEnd = start;
|
|
226
|
+
while (nameEnd < end && /[^\s=/>]/u.test(source[nameEnd] ?? ''))
|
|
227
|
+
nameEnd += 1;
|
|
228
|
+
const name = source.slice(start, nameEnd);
|
|
229
|
+
const equalsStart = skipWhitespace(source, nameEnd, end);
|
|
230
|
+
if (source[equalsStart] !== '=') {
|
|
231
|
+
return {
|
|
232
|
+
attribute: createAttribute(start, nameEnd, name, equalsStart, undefined, 'boolean'),
|
|
233
|
+
cursor: equalsStart
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const valueStart = skipWhitespace(source, equalsStart + 1, end);
|
|
237
|
+
return parseMarkupAttributeValue(source, start, nameEnd, name, valueStart, end);
|
|
238
|
+
};
|
|
239
|
+
const parseMarkupAttributes = (source, start, end) => {
|
|
240
|
+
const attributes = [];
|
|
241
|
+
let cursor = start;
|
|
242
|
+
while (cursor < end) {
|
|
243
|
+
cursor = skipWhitespace(source, cursor, end);
|
|
244
|
+
if (cursor >= end || source[cursor] === '/' || source[cursor] === '>')
|
|
245
|
+
break;
|
|
246
|
+
const parsed = parseMarkupAttribute(source, cursor, end);
|
|
247
|
+
if (parsed.attribute)
|
|
248
|
+
attributes.push(parsed.attribute);
|
|
249
|
+
cursor = parsed.cursor;
|
|
250
|
+
}
|
|
251
|
+
return attributes;
|
|
252
|
+
};
|
|
253
|
+
const isMarkupQuoteStart = (character, braceDepth) => isQuote(character) && (character !== '`' || braceDepth > 0);
|
|
254
|
+
const advanceQuotedSource = (character, quote) => ({
|
|
255
|
+
close: character === quote,
|
|
256
|
+
skip: character === '\\'
|
|
257
|
+
});
|
|
258
|
+
const findMarkupTagEnd = (source, start) => {
|
|
259
|
+
let braceDepth = 0;
|
|
260
|
+
let quote;
|
|
261
|
+
for (let index = start; index < source.length; index += 1) {
|
|
262
|
+
const character = source[index];
|
|
263
|
+
if (quote) {
|
|
264
|
+
const quoted = advanceQuotedSource(character, quote);
|
|
265
|
+
if (quoted.skip)
|
|
266
|
+
index += 1;
|
|
267
|
+
if (quoted.close)
|
|
268
|
+
quote = undefined;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (isMarkupQuoteStart(character, braceDepth))
|
|
272
|
+
quote = character;
|
|
273
|
+
else if (character === '{')
|
|
274
|
+
braceDepth += 1;
|
|
275
|
+
else if (character === '}')
|
|
276
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
277
|
+
else if (character === '>' && braceDepth === 0)
|
|
278
|
+
return index;
|
|
279
|
+
}
|
|
280
|
+
return source.length;
|
|
281
|
+
};
|
|
282
|
+
const collectAstroComponentNames = (source) => {
|
|
283
|
+
const componentNames = new Map();
|
|
284
|
+
for (const statement of findImportStatements(source, ASTRO_PACKAGE)) {
|
|
285
|
+
const named = parseNamedImports(statement.clause);
|
|
286
|
+
for (const imported of named?.imports ?? []) {
|
|
287
|
+
if (imported.imported === 'Input' ||
|
|
288
|
+
imported.imported === 'NativeSelect') {
|
|
289
|
+
componentNames.set(imported.local, imported.imported);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return componentNames;
|
|
294
|
+
};
|
|
295
|
+
const createRuntimeReplacement = (statement, named, runtimeSpecifier, runtimeAlreadyImported) => {
|
|
296
|
+
const remaining = named.imports.filter(item => item.imported !== 'UIPrimitives');
|
|
297
|
+
const prefix = statement.clause
|
|
298
|
+
.slice(0, named.open)
|
|
299
|
+
.trim()
|
|
300
|
+
.replace(/,$/u, '')
|
|
301
|
+
.trim();
|
|
302
|
+
const suffix = statement.clause.slice(named.close + 1).trim();
|
|
303
|
+
const separator = prefix && remaining.length ? ', ' : ' ';
|
|
304
|
+
const remainingClause = [
|
|
305
|
+
prefix,
|
|
306
|
+
remaining.length ?
|
|
307
|
+
`{ ${remaining.map(item => item.source).join(', ')} }` :
|
|
308
|
+
'',
|
|
309
|
+
suffix
|
|
310
|
+
]
|
|
311
|
+
.filter(Boolean)
|
|
312
|
+
.join(separator);
|
|
313
|
+
const runtimeStatement = runtimeAlreadyImported ?
|
|
314
|
+
'' :
|
|
315
|
+
`import ${runtimeSpecifier.local} from ${statement.quote}${ASTRO_RUNTIME_PACKAGE}${statement.quote}${statement.semicolon ? ';' : ''}`;
|
|
316
|
+
const rootStatement = remainingClause ?
|
|
317
|
+
formatImport(remainingClause, statement) :
|
|
318
|
+
'';
|
|
319
|
+
return [rootStatement, runtimeStatement].filter(Boolean).join('\n');
|
|
320
|
+
};
|
|
321
|
+
const createRuntimeReview = (source, file, statement, message) => createFinding(source, file, statement.start, 'astro-runtime-subpath', message);
|
|
322
|
+
const migrateRuntimeImports = (source, file) => {
|
|
323
|
+
const changes = [];
|
|
324
|
+
const manualReview = [];
|
|
325
|
+
const edits = [];
|
|
326
|
+
const runtimeImports = findImportStatements(source, ASTRO_RUNTIME_PACKAGE);
|
|
327
|
+
let runtimeLocal = runtimeImports
|
|
328
|
+
.map(statement => getDefaultImport(statement.clause))
|
|
329
|
+
.find(Boolean);
|
|
330
|
+
for (const statement of findImportStatements(source, ASTRO_PACKAGE)) {
|
|
331
|
+
const named = parseNamedImports(statement.clause);
|
|
332
|
+
if (!named) {
|
|
333
|
+
if (statement.clause.includes('UIPrimitives')) {
|
|
334
|
+
manualReview.push(createRuntimeReview(source, file, statement, 'UIPrimitives is part of an import that cannot be rewritten safely.'));
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const runtimeSpecifier = named.imports.find(item => item.imported === 'UIPrimitives');
|
|
339
|
+
if (!runtimeSpecifier) {
|
|
340
|
+
if (statement.clause
|
|
341
|
+
.slice(named.open + 1, named.close)
|
|
342
|
+
.includes('UIPrimitives')) {
|
|
343
|
+
manualReview.push(createRuntimeReview(source, file, statement, 'UIPrimitives is part of an import that cannot be rewritten safely.'));
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!named.complete || runtimeSpecifier.typeOnly) {
|
|
348
|
+
manualReview.push(createRuntimeReview(source, file, statement, 'UIPrimitives is part of an import that cannot be rewritten safely.'));
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (runtimeLocal && runtimeLocal !== runtimeSpecifier.local) {
|
|
352
|
+
manualReview.push(createRuntimeReview(source, file, statement, `UIPrimitives uses the local name ${runtimeSpecifier.local}, but the runtime subpath is already imported as ${runtimeLocal}.`));
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const replacement = createRuntimeReplacement(statement, named, runtimeSpecifier, Boolean(runtimeLocal));
|
|
356
|
+
edits.push({ end: statement.end, replacement, start: statement.start });
|
|
357
|
+
changes.push(createFinding(source, file, statement.start, 'astro-runtime-subpath', `Move ${runtimeSpecifier.local} to the @santi020k/lumen-astro/runtime default import.`));
|
|
358
|
+
runtimeLocal = runtimeSpecifier.local;
|
|
359
|
+
}
|
|
360
|
+
return { changes, manualReview, source: applyEdits(source, edits) };
|
|
361
|
+
};
|
|
362
|
+
const getTagNameEnd = (source, start) => {
|
|
363
|
+
let end = start;
|
|
364
|
+
while (/[\w:.-]/u.test(source[end] ?? ''))
|
|
365
|
+
end += 1;
|
|
366
|
+
return end;
|
|
367
|
+
};
|
|
368
|
+
const getVisualSizeAttributeName = (tagName, astroComponents) => {
|
|
369
|
+
if (astroComponents.has(tagName))
|
|
370
|
+
return 'visualSize';
|
|
371
|
+
if (tagName === 'lumen-input' || tagName === 'lumen-native-select')
|
|
372
|
+
return 'visual-size';
|
|
373
|
+
return undefined;
|
|
374
|
+
};
|
|
375
|
+
const inspectVisualSize = (source, file, tagName, replacementName, attributes) => {
|
|
376
|
+
const size = attributes.find(attribute => attribute.name === 'size');
|
|
377
|
+
if (!size)
|
|
378
|
+
return {};
|
|
379
|
+
const existingReplacement = attributes.some(attribute => attribute.name === replacementName);
|
|
380
|
+
const literalAlias = size.valueKind === 'literal' &&
|
|
381
|
+
size.value !== undefined &&
|
|
382
|
+
VISUAL_SIZE_ALIASES.has(size.value);
|
|
383
|
+
const numericValue = size.value !== undefined && /^\d+$/u.test(size.value);
|
|
384
|
+
if (literalAlias && !existingReplacement) {
|
|
385
|
+
return {
|
|
386
|
+
change: createFinding(source, file, size.nameStart, 'visual-size-alias-removal', `Rename ${tagName} size=${JSON.stringify(size.value)} to ${replacementName}.`),
|
|
387
|
+
edit: {
|
|
388
|
+
end: size.nameEnd,
|
|
389
|
+
replacement: replacementName,
|
|
390
|
+
start: size.nameStart
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (numericValue)
|
|
395
|
+
return {};
|
|
396
|
+
const message = existingReplacement ?
|
|
397
|
+
`${tagName} declares both size and ${replacementName}; choose the intended native and visual sizes manually.` :
|
|
398
|
+
`${tagName} has a non-numeric, non-migratable size value; review it manually.`;
|
|
399
|
+
return {
|
|
400
|
+
review: createFinding(source, file, size.start, 'visual-size-alias-removal', message)
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const getMarkupStart = (source, file) => {
|
|
404
|
+
if (!file.endsWith('.astro'))
|
|
405
|
+
return 0;
|
|
406
|
+
const opening = /^(?:\uFEFF)?\s*---\s*(?:\r?\n|$)/u.exec(source);
|
|
407
|
+
if (!opening)
|
|
408
|
+
return 0;
|
|
409
|
+
const closing = source.indexOf('\n---', opening[0].length);
|
|
410
|
+
return closing < 0 ? source.length : closing + '\n---'.length;
|
|
411
|
+
};
|
|
412
|
+
const appendVisualInspection = (inspection, edits, changes, manualReview) => {
|
|
413
|
+
if (inspection.edit)
|
|
414
|
+
edits.push(inspection.edit);
|
|
415
|
+
if (inspection.change)
|
|
416
|
+
changes.push(inspection.change);
|
|
417
|
+
if (inspection.review)
|
|
418
|
+
manualReview.push(inspection.review);
|
|
419
|
+
};
|
|
420
|
+
const migrateVisualSizes = (source, file, astroComponents) => {
|
|
421
|
+
const changes = [];
|
|
422
|
+
const manualReview = [];
|
|
423
|
+
const edits = [];
|
|
424
|
+
let cursor = getMarkupStart(source, file);
|
|
425
|
+
while (cursor < source.length) {
|
|
426
|
+
const tagStart = source.indexOf('<', cursor);
|
|
427
|
+
if (tagStart < 0)
|
|
428
|
+
break;
|
|
429
|
+
const nameStart = tagStart + 1;
|
|
430
|
+
if (source.startsWith('!--', nameStart)) {
|
|
431
|
+
const commentEnd = source.indexOf('-->', nameStart + 3);
|
|
432
|
+
cursor = commentEnd < 0 ? source.length : commentEnd + 3;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (source[nameStart] === '/') {
|
|
436
|
+
cursor = nameStart + 1;
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const nameEnd = getTagNameEnd(source, nameStart);
|
|
440
|
+
const tagName = source.slice(nameStart, nameEnd);
|
|
441
|
+
const replacementName = getVisualSizeAttributeName(tagName, astroComponents);
|
|
442
|
+
if (!replacementName) {
|
|
443
|
+
cursor = nameEnd || nameStart;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const tagEnd = findMarkupTagEnd(source, nameEnd);
|
|
447
|
+
const attributes = parseMarkupAttributes(source, nameEnd, tagEnd);
|
|
448
|
+
const inspection = inspectVisualSize(source, file, tagName, replacementName, attributes);
|
|
449
|
+
appendVisualInspection(inspection, edits, changes, manualReview);
|
|
450
|
+
cursor = Math.min(tagEnd + 1, source.length);
|
|
451
|
+
}
|
|
452
|
+
return { changes, manualReview, source: applyEdits(source, edits) };
|
|
453
|
+
};
|
|
454
|
+
const formatNamedImport = (item, imported) => {
|
|
455
|
+
const prefix = item.typeOnly ? 'type ' : '';
|
|
456
|
+
const alias = item.local === imported ? '' : ` as ${item.local}`;
|
|
457
|
+
return `${prefix}${imported}${alias}`;
|
|
458
|
+
};
|
|
459
|
+
const getSonnerReplacement = (imported) => {
|
|
460
|
+
if (imported === 'Sonner')
|
|
461
|
+
return 'ToastViewport';
|
|
462
|
+
if (imported === 'SonnerProps')
|
|
463
|
+
return 'ToastViewportProps';
|
|
464
|
+
return undefined;
|
|
465
|
+
};
|
|
466
|
+
const createNamedImportReplacement = (statement, named) => {
|
|
467
|
+
const prefix = statement.clause
|
|
468
|
+
.slice(0, named.open)
|
|
469
|
+
.trim()
|
|
470
|
+
.replace(/,$/u, '')
|
|
471
|
+
.trim();
|
|
472
|
+
const suffix = statement.clause.slice(named.close + 1).trim();
|
|
473
|
+
const imports = named.imports
|
|
474
|
+
.map(item => formatNamedImport(item, getSonnerReplacement(item.imported) ?? item.imported))
|
|
475
|
+
.sort((left, right) => left.localeCompare(right));
|
|
476
|
+
const separator = prefix && imports.length ? ', ' : ' ';
|
|
477
|
+
const clause = [prefix, `{ ${imports.join(', ')} }`, suffix]
|
|
478
|
+
.filter(Boolean)
|
|
479
|
+
.join(separator);
|
|
480
|
+
return formatImport(clause, statement);
|
|
481
|
+
};
|
|
482
|
+
const migrateSonnerImports = (source, file) => {
|
|
483
|
+
const changes = [];
|
|
484
|
+
const manualReview = [];
|
|
485
|
+
const edits = [];
|
|
486
|
+
for (const packageName of [ASTRO_PACKAGE, REACT_PACKAGE]) {
|
|
487
|
+
for (const statement of findImportStatements(source, packageName)) {
|
|
488
|
+
const named = parseNamedImports(statement.clause);
|
|
489
|
+
const mentionsSonner = statement.clause.includes('Sonner');
|
|
490
|
+
if (!mentionsSonner)
|
|
491
|
+
continue;
|
|
492
|
+
const replacements = named?.imports.flatMap(item => {
|
|
493
|
+
const replacement = getSonnerReplacement(item.imported);
|
|
494
|
+
return replacement ? [{ item, replacement }] : [];
|
|
495
|
+
}) ?? [];
|
|
496
|
+
if (!named?.complete) {
|
|
497
|
+
manualReview.push(createFinding(source, file, statement.start, 'sonner-alias-removal', 'Sonner is part of an import that cannot be rewritten safely.'));
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (replacements.length === 0)
|
|
501
|
+
continue;
|
|
502
|
+
edits.push({
|
|
503
|
+
end: statement.end,
|
|
504
|
+
replacement: createNamedImportReplacement(statement, named),
|
|
505
|
+
start: statement.start
|
|
506
|
+
});
|
|
507
|
+
for (const { item, replacement } of replacements) {
|
|
508
|
+
changes.push(createFinding(source, file, statement.start, 'sonner-alias-removal', `Replace ${item.imported} with ${replacement} while preserving the local name ${item.local}.`));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return { changes, manualReview, source: applyEdits(source, edits) };
|
|
513
|
+
};
|
|
514
|
+
const REACT_NATIVE_DATETIME_EXPORTS = new Set([
|
|
515
|
+
'LumenDateField',
|
|
516
|
+
'LumenDateFieldProps',
|
|
517
|
+
'LumenDateRangeField',
|
|
518
|
+
'LumenDateRangeFieldProps',
|
|
519
|
+
'LumenDateRangeValue'
|
|
520
|
+
]);
|
|
521
|
+
const createReactNativeDatetimeReview = (source, file, statement, message) => createFinding(source, file, statement.start, 'react-native-datetime-subpath', message);
|
|
522
|
+
const isSafeReactNativeDatetimeClause = (named, prefix, suffix) => Boolean(named?.complete && (prefix === '' || prefix === 'type') && suffix === '');
|
|
523
|
+
const planReactNativeDatetimeImport = (statement) => {
|
|
524
|
+
const mentionsDatetime = [...REACT_NATIVE_DATETIME_EXPORTS].some(exportName => statement.clause.includes(exportName));
|
|
525
|
+
if (!mentionsDatetime)
|
|
526
|
+
return { kind: 'irrelevant' };
|
|
527
|
+
const named = parseNamedImports(statement.clause);
|
|
528
|
+
const prefix = named ?
|
|
529
|
+
statement.clause.slice(0, named.open).trim().replace(/,$/u, '').trim() :
|
|
530
|
+
'';
|
|
531
|
+
const suffix = named ? statement.clause.slice(named.close + 1).trim() : '';
|
|
532
|
+
if (!isSafeReactNativeDatetimeClause(named, prefix, suffix))
|
|
533
|
+
return { kind: 'unsafe' };
|
|
534
|
+
const datetimeImports = named.imports.filter(item => REACT_NATIVE_DATETIME_EXPORTS.has(item.imported));
|
|
535
|
+
if (datetimeImports.length === 0)
|
|
536
|
+
return { kind: 'irrelevant' };
|
|
537
|
+
return {
|
|
538
|
+
datetimeImports,
|
|
539
|
+
kind: 'migrate',
|
|
540
|
+
remaining: named.imports.filter(item => !REACT_NATIVE_DATETIME_EXPORTS.has(item.imported)),
|
|
541
|
+
typePrefix: prefix === 'type' ? 'type ' : ''
|
|
542
|
+
};
|
|
543
|
+
};
|
|
544
|
+
const migrateReactNativeDatetimeStatement = (source, file, statement, datetimeSubpathImported) => {
|
|
545
|
+
const plan = planReactNativeDatetimeImport(statement);
|
|
546
|
+
if (plan.kind === 'irrelevant')
|
|
547
|
+
return { changes: [] };
|
|
548
|
+
if (plan.kind === 'unsafe') {
|
|
549
|
+
return {
|
|
550
|
+
changes: [],
|
|
551
|
+
review: createReactNativeDatetimeReview(source, file, statement, 'React Native datetime exports are part of an import that cannot be rewritten safely.')
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (datetimeSubpathImported) {
|
|
555
|
+
return {
|
|
556
|
+
changes: [],
|
|
557
|
+
review: createReactNativeDatetimeReview(source, file, statement, 'The datetime subpath is already imported; merge the remaining root datetime exports manually.')
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const rootStatement = plan.remaining.length > 0 ?
|
|
561
|
+
`import ${plan.typePrefix}{ ${plan.remaining.map(item => item.source).join(', ')} } from ${statement.quote}${REACT_NATIVE_PACKAGE}${statement.quote}${statement.semicolon ? ';' : ''}` :
|
|
562
|
+
'';
|
|
563
|
+
const datetimeStatement = `import ${plan.typePrefix}{ ${plan.datetimeImports.map(item => item.source).join(', ')} } from ${statement.quote}${REACT_NATIVE_DATETIME_PACKAGE}${statement.quote}${statement.semicolon ? ';' : ''}`;
|
|
564
|
+
return {
|
|
565
|
+
changes: plan.datetimeImports.map(item => createFinding(source, file, statement.start, 'react-native-datetime-subpath', `Move ${item.imported} to ${REACT_NATIVE_DATETIME_PACKAGE} while preserving the local name ${item.local}.`)),
|
|
566
|
+
edit: {
|
|
567
|
+
end: statement.end,
|
|
568
|
+
replacement: [rootStatement, datetimeStatement]
|
|
569
|
+
.filter(Boolean)
|
|
570
|
+
.join('\n'),
|
|
571
|
+
start: statement.start
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
const migrateReactNativeDatetimeImports = (source, file) => {
|
|
576
|
+
const changes = [];
|
|
577
|
+
const manualReview = [];
|
|
578
|
+
const edits = [];
|
|
579
|
+
const existingDatetimeImports = findImportStatements(source, REACT_NATIVE_DATETIME_PACKAGE);
|
|
580
|
+
for (const statement of findImportStatements(source, REACT_NATIVE_PACKAGE)) {
|
|
581
|
+
const result = migrateReactNativeDatetimeStatement(source, file, statement, existingDatetimeImports.length > 0);
|
|
582
|
+
changes.push(...result.changes);
|
|
583
|
+
if (result.edit)
|
|
584
|
+
edits.push(result.edit);
|
|
585
|
+
if (result.review)
|
|
586
|
+
manualReview.push(result.review);
|
|
587
|
+
}
|
|
588
|
+
return { changes, manualReview, source: applyEdits(source, edits) };
|
|
589
|
+
};
|
|
590
|
+
const getRawElementEnd = (source, tagName, start) => {
|
|
591
|
+
if (tagName !== 'script' && tagName !== 'style')
|
|
592
|
+
return start;
|
|
593
|
+
const closing = source.toLowerCase().indexOf(`</${tagName}`, start);
|
|
594
|
+
return closing < 0 ? source.length : closing;
|
|
595
|
+
};
|
|
596
|
+
const migrateSonnerElements = (source, file) => {
|
|
597
|
+
const changes = [];
|
|
598
|
+
const edits = [];
|
|
599
|
+
let cursor = getMarkupStart(source, file);
|
|
600
|
+
while (cursor < source.length) {
|
|
601
|
+
const tagStart = source.indexOf('<', cursor);
|
|
602
|
+
if (tagStart < 0)
|
|
603
|
+
break;
|
|
604
|
+
const closing = source[tagStart + 1] === '/';
|
|
605
|
+
const nameStart = tagStart + (closing ? 2 : 1);
|
|
606
|
+
if (source.startsWith('!--', tagStart + 1)) {
|
|
607
|
+
const commentEnd = source.indexOf('-->', tagStart + 4);
|
|
608
|
+
cursor = commentEnd < 0 ? source.length : commentEnd + 3;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const nameEnd = getTagNameEnd(source, nameStart);
|
|
612
|
+
const tagName = source.slice(nameStart, nameEnd);
|
|
613
|
+
const rawElementEnd = closing ?
|
|
614
|
+
nameEnd :
|
|
615
|
+
getRawElementEnd(source, tagName.toLowerCase(), nameEnd);
|
|
616
|
+
if (rawElementEnd !== nameEnd) {
|
|
617
|
+
cursor = rawElementEnd;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (tagName === 'lumen-sonner') {
|
|
621
|
+
edits.push({
|
|
622
|
+
end: nameEnd,
|
|
623
|
+
replacement: 'lumen-toast-viewport',
|
|
624
|
+
start: nameStart
|
|
625
|
+
});
|
|
626
|
+
changes.push(createFinding(source, file, nameStart, 'sonner-alias-removal', 'Rename lumen-sonner to lumen-toast-viewport.'));
|
|
627
|
+
}
|
|
628
|
+
cursor = nameEnd || nameStart;
|
|
629
|
+
}
|
|
630
|
+
return { changes, manualReview: [], source: applyEdits(source, edits) };
|
|
631
|
+
};
|
|
632
|
+
export const migrateLumenV2Source = (source, file = '<source>') => {
|
|
633
|
+
const astro = file.endsWith('.astro');
|
|
634
|
+
const markup = astro || file.endsWith('.htm') || file.endsWith('.html');
|
|
635
|
+
const astroComponents = astro ?
|
|
636
|
+
collectAstroComponentNames(source) :
|
|
637
|
+
new Map();
|
|
638
|
+
const runtime = astro ?
|
|
639
|
+
migrateRuntimeImports(source, file) :
|
|
640
|
+
{ changes: [], manualReview: [], source };
|
|
641
|
+
const sonnerImports = migrateSonnerImports(runtime.source, file);
|
|
642
|
+
const reactNativeDatetime = migrateReactNativeDatetimeImports(sonnerImports.source, file);
|
|
643
|
+
if (!markup) {
|
|
644
|
+
return {
|
|
645
|
+
changes: [
|
|
646
|
+
...runtime.changes,
|
|
647
|
+
...sonnerImports.changes,
|
|
648
|
+
...reactNativeDatetime.changes
|
|
649
|
+
],
|
|
650
|
+
manualReview: [
|
|
651
|
+
...runtime.manualReview,
|
|
652
|
+
...sonnerImports.manualReview,
|
|
653
|
+
...reactNativeDatetime.manualReview
|
|
654
|
+
],
|
|
655
|
+
source: reactNativeDatetime.source
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const visualSizes = migrateVisualSizes(reactNativeDatetime.source, file, astroComponents);
|
|
659
|
+
const sonnerElements = migrateSonnerElements(visualSizes.source, file);
|
|
660
|
+
return {
|
|
661
|
+
changes: [
|
|
662
|
+
...runtime.changes,
|
|
663
|
+
...sonnerImports.changes,
|
|
664
|
+
...reactNativeDatetime.changes,
|
|
665
|
+
...visualSizes.changes,
|
|
666
|
+
...sonnerElements.changes
|
|
667
|
+
],
|
|
668
|
+
manualReview: [
|
|
669
|
+
...runtime.manualReview,
|
|
670
|
+
...sonnerImports.manualReview,
|
|
671
|
+
...reactNativeDatetime.manualReview,
|
|
672
|
+
...visualSizes.manualReview,
|
|
673
|
+
...sonnerElements.manualReview
|
|
674
|
+
],
|
|
675
|
+
source: sonnerElements.source
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
const discoverSourceFiles = async (path) => {
|
|
679
|
+
const details = await stat(path);
|
|
680
|
+
if (details.isFile())
|
|
681
|
+
return SOURCE_EXTENSIONS.has(extname(path)) ? [path] : [];
|
|
682
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
683
|
+
const nested = await Promise.all(entries
|
|
684
|
+
.filter(entry => !entry.isDirectory() || !IGNORED_DIRECTORIES.has(entry.name))
|
|
685
|
+
.map(entry => discoverSourceFiles(resolve(path, entry.name))));
|
|
686
|
+
return nested.flat().sort();
|
|
687
|
+
};
|
|
688
|
+
export const migrateLumenV2 = async (options = {}) => {
|
|
689
|
+
const root = resolve(options.cwd ?? process.cwd());
|
|
690
|
+
const files = await discoverSourceFiles(root);
|
|
691
|
+
const changedFiles = [];
|
|
692
|
+
const changes = [];
|
|
693
|
+
const manualReview = [];
|
|
694
|
+
for (const absoluteFile of files) {
|
|
695
|
+
const file = relative(root, absoluteFile) || absoluteFile;
|
|
696
|
+
const source = await readFile(absoluteFile, 'utf8');
|
|
697
|
+
const migration = migrateLumenV2Source(source, file);
|
|
698
|
+
changes.push(...migration.changes);
|
|
699
|
+
manualReview.push(...migration.manualReview);
|
|
700
|
+
if (migration.source !== source) {
|
|
701
|
+
changedFiles.push(file);
|
|
702
|
+
if (options.apply)
|
|
703
|
+
await writeFile(absoluteFile, migration.source, 'utf8');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
applied: options.apply === true,
|
|
708
|
+
changedFiles,
|
|
709
|
+
changes,
|
|
710
|
+
filesScanned: files.length,
|
|
711
|
+
manualReview,
|
|
712
|
+
root
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
export const formatLumenV2Migration = (report) => {
|
|
716
|
+
const action = report.applied ? 'Applied' : 'Would apply';
|
|
717
|
+
const lines = [
|
|
718
|
+
`Lumen v2 migration ${report.applied ? 'apply' : 'dry run'}: ${report.root}`,
|
|
719
|
+
`Scanned ${report.filesScanned} source file${report.filesScanned === 1 ? '' : 's'}.`,
|
|
720
|
+
`${action} ${report.changes.length} change${report.changes.length === 1 ? '' : 's'} in ${report.changedFiles.length} file${report.changedFiles.length === 1 ? '' : 's'}.`,
|
|
721
|
+
...report.changes.map(finding => `- ${finding.file}:${finding.line}:${finding.column} [${finding.kind}] ${finding.message}`)
|
|
722
|
+
];
|
|
723
|
+
if (report.manualReview.length) {
|
|
724
|
+
lines.push('', `Manual review required for ${report.manualReview.length} finding${report.manualReview.length === 1 ? '' : 's'}:`, ...report.manualReview.map(finding => `- ${finding.file}:${finding.line}:${finding.column} [${finding.kind}] ${finding.message}`));
|
|
725
|
+
}
|
|
726
|
+
if (!report.applied && report.changes.length)
|
|
727
|
+
lines.push('', 'Run lumen migrate v2 --apply to write these changes.');
|
|
728
|
+
return lines.join('\n');
|
|
729
|
+
};
|