@santi020k/lumen 3.0.0 → 4.0.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.
Files changed (125) hide show
  1. package/README.md +207 -3
  2. package/dist/cli.js +49 -5
  3. package/dist/consumer-upgrade-audit.d.ts +10 -0
  4. package/dist/consumer-upgrade-audit.d.ts.map +1 -0
  5. package/dist/consumer-upgrade-audit.js +52 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -1
  9. package/dist/installer.d.ts.map +1 -1
  10. package/dist/installer.js +16 -0
  11. package/dist/integration-diagnostics.d.ts.map +1 -1
  12. package/dist/integration-diagnostics.js +199 -3
  13. package/dist/registry-component-name.d.ts +2 -0
  14. package/dist/registry-component-name.d.ts.map +1 -0
  15. package/dist/registry-component-name.js +53 -0
  16. package/dist/registry-data.d.ts +339 -18
  17. package/dist/registry-data.d.ts.map +1 -1
  18. package/dist/registry-data.js +789 -3
  19. package/dist/registry.d.ts.map +1 -1
  20. package/dist/registry.js +11 -9
  21. package/dist/source-script-ranges.d.ts +3 -0
  22. package/dist/source-script-ranges.d.ts.map +1 -0
  23. package/dist/source-script-ranges.js +41 -0
  24. package/dist/v2-migration.d.ts +38 -0
  25. package/dist/v2-migration.d.ts.map +1 -1
  26. package/dist/v2-migration.js +26 -14
  27. package/dist/v4-migration.d.ts +26 -0
  28. package/dist/v4-migration.d.ts.map +1 -0
  29. package/dist/v4-migration.js +272 -0
  30. package/dist/version-migration-dependencies.d.ts +8 -0
  31. package/dist/version-migration-dependencies.d.ts.map +1 -0
  32. package/dist/version-migration-dependencies.js +10 -0
  33. package/dist/version-migration-source.d.ts +4 -0
  34. package/dist/version-migration-source.d.ts.map +1 -0
  35. package/dist/version-migration-source.js +281 -0
  36. package/dist/version-migration.d.ts +34 -0
  37. package/dist/version-migration.d.ts.map +1 -0
  38. package/dist/version-migration.js +158 -0
  39. package/native-components.json +563 -16
  40. package/package.json +26 -11
  41. package/release-manifest.json +22 -17
  42. package/styles/critical.css +634 -61
  43. package/styles/motion.css +61 -0
  44. package/styles/world-map.css +30 -0
  45. package/styles.css +1750 -432
  46. package/templates/astro/advanced-fields/src/lumen/advanced-fields.astro +35 -0
  47. package/templates/astro/ai-docs/llms.txt +3 -0
  48. package/templates/astro/analytics-dashboard/src/lumen/analytics-dashboard.astro +110 -0
  49. package/templates/astro/auth-onboarding/src/lumen/auth-onboarding.astro +107 -0
  50. package/templates/astro/command-center/src/lumen/command-center.astro +34 -0
  51. package/templates/astro/commerce-dashboard/src/lumen/commerce-dashboard.astro +108 -0
  52. package/templates/astro/content-flow-actions/src/lumen/content-flow-actions.astro +10 -0
  53. package/templates/astro/content-flow-header/src/lumen/content-flow-header.astro +14 -0
  54. package/templates/astro/content-flow-list/src/lumen/content-flow-list.astro +13 -0
  55. package/templates/astro/content-flow-settings/src/lumen/content-flow-settings.astro +17 -0
  56. package/templates/astro/data-collections/src/lumen/data-collections.astro +52 -0
  57. package/templates/astro/feature-preview/src/lumen/feature-preview.astro +32 -0
  58. package/templates/astro/guided-onboarding/src/lumen/guided-onboarding.astro +35 -0
  59. package/templates/astro/interactive-pricing/src/lumen/interactive-pricing.astro +32 -0
  60. package/templates/astro/media-workspace/src/lumen/adjustment-controls.astro +89 -0
  61. package/templates/astro/media-workspace/src/lumen/media-processing.astro +47 -0
  62. package/templates/astro/media-workspace/src/lumen/media-workspace.astro +77 -0
  63. package/templates/astro/page-header/src/lumen/page-header.astro +41 -0
  64. package/templates/astro/project-workspace/src/lumen/project-workspace.astro +103 -0
  65. package/templates/astro/rich-text-editor/src/lumen/rich-text-editor.astro +21 -0
  66. package/templates/astro/saas-admin/src/lumen/saas-admin.astro +110 -0
  67. package/templates/astro/scheduler/src/lumen/scheduler.astro +36 -0
  68. package/templates/astro/section-header/src/lumen/section-header.astro +35 -0
  69. package/templates/astro/theme-builder/src/lumen/theme-builder.astro +28 -0
  70. package/templates/elements/advanced-fields/src/lumen/advanced-fields.html +25 -0
  71. package/templates/elements/analytics-dashboard/src/lumen/analytics-dashboard.html +28 -0
  72. package/templates/elements/auth-onboarding/src/lumen/auth-onboarding.html +79 -0
  73. package/templates/elements/command-center/src/lumen/command-center.html +15 -0
  74. package/templates/elements/commerce-dashboard/src/lumen/commerce-dashboard.html +24 -0
  75. package/templates/elements/content-flow-actions/src/lumen/content-flow-actions.html +11 -0
  76. package/templates/elements/content-flow-header/src/lumen/content-flow-header.html +15 -0
  77. package/templates/elements/content-flow-list/src/lumen/content-flow-list.html +14 -0
  78. package/templates/elements/content-flow-settings/src/lumen/content-flow-settings.html +18 -0
  79. package/templates/elements/data-collections/src/lumen/data-collections.html +31 -0
  80. package/templates/elements/feature-preview/src/lumen/feature-preview.html +13 -0
  81. package/templates/elements/guided-onboarding/src/lumen/guided-onboarding.html +16 -0
  82. package/templates/elements/interactive-pricing/src/lumen/interactive-pricing.html +13 -0
  83. package/templates/elements/media-workspace/src/lumen/media-workspace.html +57 -0
  84. package/templates/elements/media-workspace/src/lumen/media-workspace.ts +163 -0
  85. package/templates/elements/page-header/src/lumen/page-header.html +20 -0
  86. package/templates/elements/project-workspace/src/lumen/project-workspace.html +17 -0
  87. package/templates/elements/rich-text-editor/src/lumen/rich-text-editor.html +17 -0
  88. package/templates/elements/saas-admin/src/lumen/saas-admin.html +18 -0
  89. package/templates/elements/scheduler/src/lumen/scheduler.html +27 -0
  90. package/templates/elements/section-header/src/lumen/section-header.html +21 -0
  91. package/templates/elements/theme-builder/src/lumen/theme-builder.html +59 -0
  92. package/templates/react/advanced-fields/src/lumen/advanced-fields.tsx +37 -0
  93. package/templates/react/analytics-dashboard/src/lumen/analytics-dashboard.tsx +143 -0
  94. package/templates/react/auth-onboarding/src/lumen/auth-onboarding.tsx +129 -0
  95. package/templates/react/command-center/src/lumen/command-center.tsx +85 -0
  96. package/templates/react/commerce-dashboard/src/lumen/commerce-dashboard.tsx +148 -0
  97. package/templates/react/content-flow-actions/src/lumen/content-flow-actions.tsx +11 -0
  98. package/templates/react/content-flow-header/src/lumen/content-flow-header.tsx +14 -0
  99. package/templates/react/content-flow-list/src/lumen/content-flow-list.tsx +19 -0
  100. package/templates/react/content-flow-settings/src/lumen/content-flow-settings.tsx +26 -0
  101. package/templates/react/data-collections/src/lumen/data-collections.tsx +60 -0
  102. package/templates/react/feature-preview/src/lumen/feature-preview.tsx +42 -0
  103. package/templates/react/guided-onboarding/src/lumen/guided-onboarding.tsx +100 -0
  104. package/templates/react/import-review/src/lumen/import-review.tsx +150 -0
  105. package/templates/react/interactive-pricing/src/lumen/interactive-pricing.tsx +60 -0
  106. package/templates/react/media-workspace/src/lumen/adjustment-controls.tsx +61 -0
  107. package/templates/react/media-workspace/src/lumen/media-processing.tsx +43 -0
  108. package/templates/react/media-workspace/src/lumen/media-workspace.tsx +100 -0
  109. package/templates/react/operational-records/src/lumen/operational-records.tsx +92 -0
  110. package/templates/react/page-header/src/lumen/page-header.tsx +41 -0
  111. package/templates/react/project-workspace/src/lumen/project-workspace.tsx +112 -0
  112. package/templates/react/record-workspace/src/lumen/record-workspace.tsx +67 -0
  113. package/templates/react/review-workflow/src/lumen/review-workflow.tsx +202 -0
  114. package/templates/react/rich-text-editor/src/lumen/rich-text-editor.tsx +33 -0
  115. package/templates/react/saas-admin/src/lumen/saas-admin.tsx +119 -0
  116. package/templates/react/scheduler/src/lumen/scheduler.tsx +45 -0
  117. package/templates/react/section-header/src/lumen/section-header.tsx +33 -0
  118. package/templates/react/styles.d.ts +1 -0
  119. package/templates/react/theme-builder/src/lumen/theme-builder.tsx +57 -0
  120. package/templates/react/tsconfig.json +14 -0
  121. package/templates/react/validated-form/src/lumen/validated-form.tsx +166 -0
  122. package/templates/shared/common/src/lumen/lumen-template.css +247 -0
  123. package/templates/shared/media-workspace/src/lumen/media-workspace.css +14 -0
  124. package/templates/shared/visual-blocks/src/lumen/product-blocks.ts +224 -0
  125. package/v4-migration.json +447 -0
@@ -0,0 +1,281 @@
1
+ import { scriptRegexRanges } from './source-script-ranges.js';
2
+ import { findBalancedEnd, findImportStatements, findMarkupTagEnd, getMarkupStart, getTagNameEnd, parseMarkupAttributes, parseNamedImports } from './v2-migration.js';
3
+ import { migrateLumenV4Source } from './v4-migration.js';
4
+ const layoutGaps = { lg: 'xl', md: 'group', xl: '2xl' };
5
+ const reviewComponents = new Map([
6
+ ['Card', 'Review padding, child margins, media clipping and footer wrapping in the rendered application.'],
7
+ ['Container', 'Review responsive gutters; use --ui-container-gutter only when a fixed gutter is intentional.'],
8
+ ['Prose', 'Review reading-block margins and remove verified compensating offsets.'],
9
+ ['Typography', 'Review first/last child margins and heading spacing.'],
10
+ ['Button', 'Review direct-child selectors for .ui-button__content and loading/disabled descendants.'],
11
+ ['DatePicker', 'Associate labels with the focusable trigger; the native input ID now ends in -native.'],
12
+ ['Combobox', 'Review focus assumptions; the editable input retains focus with aria-activedescendant.'],
13
+ ['Dialog', 'Review pending-state dismissal policy and focus restoration.'],
14
+ ['DataTable', 'Use controlled sort/onSortChange and sortMode="manual" for server-paginated React results.'],
15
+ ['BarChart', 'Review stable unique X identities, label formatting and application-owned chart patches.'],
16
+ ['LineChart', 'Review stable unique X identities, label formatting and application-owned chart patches.']
17
+ ]);
18
+ const visualSizeComponents = new Set(['Select', 'PhoneInput', 'Segmented']);
19
+ const elementNames = new Map([
20
+ ['lumen-select', 'Select'],
21
+ ['lumen-phone-input', 'PhoneInput'],
22
+ ['lumen-segmented', 'Segmented'],
23
+ ['lumen-stack', 'Stack'],
24
+ ['lumen-grid', 'Grid'],
25
+ ...[...reviewComponents.keys()].map(name => [
26
+ `lumen-${name.replaceAll(/[A-Z]/gu, (letter, offset) => `${offset ? '-' : ''}${letter.toLowerCase()}`)}`,
27
+ name
28
+ ])
29
+ ]);
30
+ const lineStarts = (source) => {
31
+ const starts = [0];
32
+ for (let index = 0; index < source.length; index += 1) {
33
+ if (source[index] === '\n')
34
+ starts.push(index + 1);
35
+ }
36
+ return starts;
37
+ };
38
+ const location = (starts, offset) => {
39
+ let low = 0;
40
+ let high = starts.length;
41
+ while (low + 1 < high) {
42
+ const middle = Math.floor((low + high) / 2);
43
+ if ((starts[middle] ?? 0) <= offset)
44
+ low = middle;
45
+ else
46
+ high = middle;
47
+ }
48
+ return { column: offset - (starts[low] ?? 0) + 1, line: low + 1 };
49
+ };
50
+ const skipQuoted = (source, start) => {
51
+ const quote = source[start];
52
+ let cursor = start + 1;
53
+ while (cursor < source.length) {
54
+ if (source[cursor] === '\\')
55
+ cursor += 2;
56
+ else if (source[cursor++] === quote)
57
+ return cursor;
58
+ }
59
+ return source.length;
60
+ };
61
+ const skipNonMarkup = (source, cursor, script) => {
62
+ const comment = [['<!--', '-->'], ...(script ? [['/*', '*/'], ['//', '\n']] : [])]
63
+ .find(([opening]) => opening !== undefined && source.startsWith(opening, cursor));
64
+ const ending = comment?.[1];
65
+ if (ending) {
66
+ const end = source.indexOf(ending, cursor + 2);
67
+ return end < 0 ? source.length : end + ending.length;
68
+ }
69
+ if (script && ['"', '\'', '`'].includes(source[cursor] ?? ''))
70
+ return skipQuoted(source, cursor);
71
+ return cursor;
72
+ };
73
+ const realImportStarts = (source, exclusions) => {
74
+ const offsets = new Set();
75
+ let cursor = 0;
76
+ while (cursor < source.length) {
77
+ const next = exclusions.get(cursor) ?? skipNonMarkup(source, cursor, true);
78
+ if (next !== cursor) {
79
+ cursor = next;
80
+ continue;
81
+ }
82
+ if (source.startsWith('import', cursor))
83
+ offsets.add(cursor);
84
+ cursor += 1;
85
+ }
86
+ return offsets;
87
+ };
88
+ const collectComponents = (source, exclusions) => {
89
+ const components = new Map();
90
+ const offsets = realImportStarts(source, exclusions);
91
+ for (const module of ['@santi020k/lumen-astro', '@santi020k/lumen-react']) {
92
+ for (const statement of findImportStatements(source, module)) {
93
+ if (!offsets.has(statement.start))
94
+ continue;
95
+ const imports = parseNamedImports(statement.clause);
96
+ if (!imports?.complete)
97
+ continue;
98
+ for (const item of imports.imports) {
99
+ if (!item.typeOnly)
100
+ components.set(item.local, item.imported);
101
+ }
102
+ }
103
+ }
104
+ return components;
105
+ };
106
+ const readTag = (source, cursor) => {
107
+ if (source[cursor] !== '<' || !/[A-Za-z]/u.test(source[cursor + 1] ?? ''))
108
+ return undefined;
109
+ const nameEnd = getTagNameEnd(source, cursor + 1);
110
+ return { start: cursor, nameEnd, name: source.slice(cursor + 1, nameEnd), end: findMarkupTagEnd(source, nameEnd) };
111
+ };
112
+ const literalValue = (attribute) => {
113
+ if (attribute.valueKind === 'literal')
114
+ return attribute.value;
115
+ const expression = attribute.value;
116
+ if (expression && /^(['"])[a-z0-9-]+\1$/u.test(expression))
117
+ return expression.slice(1, -1);
118
+ return undefined;
119
+ };
120
+ const inspectGap = (context, tag) => {
121
+ const attributes = parseMarkupAttributes(context.source, tag.nameEnd, tag.end);
122
+ const gaps = attributes.filter(attribute => attribute.name === 'gap');
123
+ const gap = gaps[0];
124
+ if (!gap)
125
+ return;
126
+ const value = literalValue(gap);
127
+ const ambiguous = gaps.length > 1 || context.source.slice(tag.nameEnd, tag.end).includes('...');
128
+ if (!value || ambiguous) {
129
+ context.manualReview.push(context.finding(gap.start, 'layout-gap', `${tag.name}: review dynamic, duplicated or spread gap values; v3 md/lg/xl map to group/xl/2xl.`));
130
+ return;
131
+ }
132
+ const replacement = Object.hasOwn(layoutGaps, value) ? layoutGaps[value] : undefined;
133
+ if (!replacement)
134
+ return;
135
+ const offset = context.source.slice(gap.start, gap.end).lastIndexOf(value);
136
+ context.edits.push({ start: gap.start + offset, end: gap.start + offset + value.length, replacement });
137
+ context.changes.push(context.finding(gap.start, 'layout-gap', `${tag.name}: preserve v3 gap ${value} with ${replacement}.`));
138
+ };
139
+ const visualSizes = new Set(['default', 'lg', 'sm']);
140
+ const isNumericSize = (attribute) => {
141
+ const value = attribute.valueKind === 'expression' ? attribute.value : literalValue(attribute);
142
+ return /^\d+$/u.test(value ?? '');
143
+ };
144
+ const isKnownVisualSize = (value, component) => value !== undefined && (visualSizes.has(value) || (component === 'Select' && value === 'md'));
145
+ const reportVisualSizeReview = (context, tag, name, offset = tag.start) => {
146
+ context.manualReview.push(context.finding(offset, 'control-size', `${tag.name}: review dynamic, duplicated or spread size props; visual sizing uses ${name}, and native Select size remains numeric.`));
147
+ };
148
+ const isAmbiguousVisualSize = (attributes, count, name, spread) => spread || count !== 1 || attributes.some(attribute => attribute.name === name);
149
+ const isNativeSelectSize = (component, size, ambiguous) => component === 'Select' && isNumericSize(size) && !ambiguous;
150
+ const inspectVisualSize = (context, tag, component) => {
151
+ const attributes = parseMarkupAttributes(context.source, tag.nameEnd, tag.end);
152
+ const sizes = attributes.filter(attribute => attribute.name === 'size');
153
+ const size = sizes[0];
154
+ const spread = context.source.slice(tag.nameEnd, tag.end).includes('...');
155
+ const visualName = tag.name.startsWith('lumen-') ? 'visual-size' : 'visualSize';
156
+ if (!size) {
157
+ if (spread)
158
+ reportVisualSizeReview(context, tag, visualName);
159
+ return;
160
+ }
161
+ const ambiguous = isAmbiguousVisualSize(attributes, sizes.length, visualName, spread);
162
+ const value = literalValue(size);
163
+ if (isNativeSelectSize(component, size, ambiguous))
164
+ return;
165
+ if (ambiguous || !isKnownVisualSize(value, component)) {
166
+ reportVisualSizeReview(context, tag, visualName, size.start);
167
+ return;
168
+ }
169
+ context.edits.push({ start: size.start, end: size.start + 'size'.length, replacement: visualName });
170
+ if (value === 'md') {
171
+ const offset = context.source.slice(size.start, size.end).lastIndexOf(value);
172
+ context.edits.push({ start: size.start + offset, end: size.start + offset + value.length, replacement: 'default' });
173
+ }
174
+ context.changes.push(context.finding(size.start, 'control-size', `${tag.name}: move visual size ${value} to ${visualName}.`));
175
+ };
176
+ const inspectTag = (context, tag) => {
177
+ const component = context.components.get(tag.name) ?? elementNames.get(tag.name);
178
+ if (!component)
179
+ return;
180
+ const message = reviewComponents.get(component);
181
+ if (message)
182
+ context.manualReview.push(context.finding(tag.start, 'component-review', `${tag.name}: ${message}`));
183
+ if (component === 'Stack' || component === 'Grid')
184
+ inspectGap(context, tag);
185
+ if (visualSizeComponents.has(component))
186
+ inspectVisualSize(context, tag, component);
187
+ };
188
+ const markupSkipper = (source, exclusions, script) => {
189
+ if (!script)
190
+ return cursor => skipNonMarkup(source, cursor, false);
191
+ return cursor => exclusions.get(cursor) ?? skipNonMarkup(source, cursor, true);
192
+ };
193
+ const scanMarkup = (context, file) => {
194
+ const script = !['.astro', '.html', '.htm'].some(extension => file.endsWith(extension));
195
+ const source = context.source;
196
+ const skip = markupSkipper(source, context.regexRanges, script);
197
+ let cursor = getMarkupStart(source, file);
198
+ while (cursor < source.length) {
199
+ const next = skip(cursor);
200
+ if (next !== cursor) {
201
+ cursor = next;
202
+ continue;
203
+ }
204
+ if (!script && source[cursor] === '{') {
205
+ cursor = findBalancedEnd(source, cursor, '{', '}');
206
+ continue;
207
+ }
208
+ const tag = readTag(source, cursor);
209
+ if (!tag) {
210
+ cursor += 1;
211
+ continue;
212
+ }
213
+ cursor = tag.end + 1;
214
+ if (!script && ['script', 'style'].includes(tag.name)) {
215
+ const end = source.indexOf(`</${tag.name}`, cursor);
216
+ cursor = end < 0 ? source.length : end;
217
+ }
218
+ else if (tag.end < source.length)
219
+ inspectTag(context, tag);
220
+ }
221
+ };
222
+ const generalFindings = (source, file, version) => {
223
+ if (['.swift', '.kt', '.kts'].some(extension => file.endsWith(extension)) && source.includes('Lumen')) {
224
+ return [{ file,
225
+ line: 1,
226
+ column: 1,
227
+ kind: 'native-review',
228
+ message: version === 'v3' ?
229
+ 'Update coordinated native package pins and rebuild. Review exhaustive Swift LumenIconName switches for added cases; no web component rewrites are required.' :
230
+ 'Update coordinated native package pins and rebuild. Review sheet/control initializer signatures and exhaustive Swift LumenIconName switches.' }];
231
+ }
232
+ if (version === 'v4' && file.endsWith('.css') && source.includes('.ui-')) {
233
+ return [{ file, line: 1, column: 1, kind: 'component-review', message: 'Review internal selectors, child margins, media clipping, fixed gutters and chart patches against the v4 migration guide; CSS is not rewritten automatically.' }];
234
+ }
235
+ return [];
236
+ };
237
+ const applyEdits = (source, edits) => {
238
+ const pieces = [];
239
+ let end = 0;
240
+ for (const edit of edits) {
241
+ pieces.push(source.slice(end, edit.start), edit.replacement);
242
+ end = edit.end;
243
+ }
244
+ pieces.push(source.slice(end));
245
+ return pieces.join('');
246
+ };
247
+ const applySdkMigration = (migration, file) => {
248
+ if (!['.ts', '.js', '.mjs'].some(extension => file.endsWith(extension)))
249
+ return migration;
250
+ const sdk = migrateLumenV4Source(migration.source, file);
251
+ return {
252
+ ...migration,
253
+ changes: [...migration.changes, ...sdk.changes.map(({ column, file: sourceFile, line, message }) => ({
254
+ column, file: sourceFile, kind: 'embedded-mcp-sdk', line, message
255
+ }))],
256
+ source: sdk.source
257
+ };
258
+ };
259
+ /** Assumes v3 input for v4. Filesystem apply uses a ledger to prevent repeated spacing rewrites. */
260
+ export const migrateLumenVersionSource = (source, file, version) => {
261
+ const manualReview = generalFindings(source, file, version);
262
+ if (version === 'v3' || file.endsWith('.css'))
263
+ return { changes: [], manualReview, source };
264
+ const regexRanges = scriptRegexRanges(source, file);
265
+ if (!regexRanges) {
266
+ manualReview.push({ file, line: 1, column: 1, kind: 'component-review', message: 'The source exceeds the syntax parser nesting limit; review this file manually.' });
267
+ return { changes: [], manualReview, source };
268
+ }
269
+ const starts = lineStarts(source);
270
+ const context = {
271
+ source,
272
+ manualReview,
273
+ changes: [],
274
+ edits: [],
275
+ regexRanges,
276
+ components: collectComponents(source, regexRanges),
277
+ finding: (offset, kind, message) => ({ ...location(starts, offset), file, kind, message })
278
+ };
279
+ scanMarkup(context, file);
280
+ return applySdkMigration({ changes: context.changes, manualReview, source: applyEdits(source, context.edits) }, file);
281
+ };
@@ -0,0 +1,34 @@
1
+ import { type LumenConsumerUpgradeAudit } from './consumer-upgrade-audit.js';
2
+ import { applyLumenVersionMigrationDependencies, type LumenMigrationDependencyOptions } from './version-migration-dependencies.js';
3
+ export { migrateLumenVersionSource } from './version-migration-source.js';
4
+ export type LumenMigrationVersion = 'v3' | 'v4';
5
+ export interface LumenVersionMigrationFinding {
6
+ column: number;
7
+ file: string;
8
+ kind: 'control-size' | 'layout-gap' | 'component-review' | 'native-review' | 'embedded-mcp-sdk';
9
+ line: number;
10
+ message: string;
11
+ }
12
+ export interface LumenVersionSourceMigration {
13
+ changes: LumenVersionMigrationFinding[];
14
+ manualReview: LumenVersionMigrationFinding[];
15
+ source: string;
16
+ }
17
+ export interface LumenVersionMigrationOptions extends LumenMigrationDependencyOptions {
18
+ cwd?: string;
19
+ version: LumenMigrationVersion;
20
+ }
21
+ export interface LumenVersionMigrationReport extends Omit<LumenVersionSourceMigration, 'source'> {
22
+ consumerAudit?: LumenConsumerUpgradeAudit;
23
+ applied: boolean;
24
+ changedFiles: string[];
25
+ dependencies?: Awaited<ReturnType<typeof applyLumenVersionMigrationDependencies>>;
26
+ dependencyVersion: string;
27
+ packageVersions?: Record<string, string>;
28
+ filesScanned: number;
29
+ root: string;
30
+ version: LumenMigrationVersion;
31
+ }
32
+ export declare const migrateLumenVersion: (options: LumenVersionMigrationOptions) => Promise<LumenVersionMigrationReport>;
33
+ export declare const formatLumenVersionMigration: (report: LumenVersionMigrationReport) => string;
34
+ //# sourceMappingURL=version-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version-migration.d.ts","sourceRoot":"","sources":["../src/version-migration.ts"],"names":[],"mappings":"AAKA,OAAO,EAAgE,KAAK,yBAAyB,EAAE,MAAM,6BAA6B,CAAA;AAG1I,OAAO,EACL,sCAAsC,EACtC,KAAK,+BAA+B,EACrC,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAA;AAEzE,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAAA;AAC/C,MAAM,WAAW,4BAA4B;IAC3C,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,cAAc,GAAG,YAAY,GAAG,kBAAkB,GAAG,eAAe,GAAG,kBAAkB,CAAA;IAC/F,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;CAChB;AACD,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,4BAA4B,EAAE,CAAA;IACvC,YAAY,EAAE,4BAA4B,EAAE,CAAA;IAC5C,MAAM,EAAE,MAAM,CAAA;CACf;AACD,MAAM,WAAW,4BAA6B,SAAQ,+BAA+B;IACnF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,qBAAqB,CAAA;CAC/B;AACD,MAAM,WAAW,2BAA4B,SAAQ,IAAI,CAAC,2BAA2B,EAAE,QAAQ,CAAC;IAC9F,aAAa,CAAC,EAAE,yBAAyB,CAAA;IACzC,OAAO,EAAE,OAAO,CAAA;IAChB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,sCAAsC,CAAC,CAAC,CAAA;IACjF,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,qBAAqB,CAAA;CAC/B;AAmJD,eAAO,MAAM,mBAAmB,GAC9B,SAAS,4BAA4B,KACpC,OAAO,CAAC,2BAA2B,CA2CrC,CAAA;AAED,eAAO,MAAM,2BAA2B,GAAI,QAAQ,2BAA2B,KAAG,MAYtE,CAAA"}
@@ -0,0 +1,158 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { relative, resolve } from 'node:path';
4
+ import { formatConsumerRollout, inspectLumenConsumer } from './consumer-rollout.js';
5
+ import { formatLumenConsumerUpgradeAudit, inspectLumenConsumerUpgrade } from './consumer-upgrade-audit.js';
6
+ import { discoverSourceFiles } from './v2-migration.js';
7
+ import { migrateLumenV4 } from './v4-migration.js';
8
+ import { applyLumenVersionMigrationDependencies } from './version-migration-dependencies.js';
9
+ import { migrateLumenVersionSource } from './version-migration-source.js';
10
+ export { migrateLumenVersionSource } from './version-migration-source.js';
11
+ const extensions = new Set(['.astro', '.htm', '.html', '.js', '.jsx', '.mjs', '.ts', '.tsx', '.swift', '.kt', '.kts', '.gradle', '.css']);
12
+ const fingerprint = (source) => createHash('sha256').update(source).digest('hex');
13
+ const checkLedgerLocation = async (root) => {
14
+ for (const path of [resolve(root, '.lumen'), resolve(root, '.lumen', 'migrations-v4.json')]) {
15
+ try {
16
+ if ((await lstat(path)).isSymbolicLink())
17
+ throw new Error(`Migration ledger must not follow a symbolic link: ${path}`);
18
+ }
19
+ catch (error) {
20
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
21
+ continue;
22
+ throw error;
23
+ }
24
+ }
25
+ };
26
+ const readLedger = async (path) => {
27
+ let source;
28
+ try {
29
+ source = await readFile(path, 'utf8');
30
+ }
31
+ catch (error) {
32
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
33
+ return {};
34
+ throw error;
35
+ }
36
+ const value = JSON.parse(source);
37
+ if (!value || typeof value !== 'object' || Array.isArray(value))
38
+ throw new Error(`Invalid migration ledger: ${path}`);
39
+ const entries = Object.entries(value);
40
+ if (entries.some(([, hash]) => typeof hash !== 'string' || !/^[a-f0-9]{64}$/u.test(hash)))
41
+ throw new Error(`Invalid migration ledger: ${path}`);
42
+ return Object.fromEntries(entries.map(([file, hash]) => [file, String(hash)]));
43
+ };
44
+ const writeMigratedSource = async (absoluteFile, source, nextSource, { report, ledger, ledgerPath, file }) => {
45
+ if (await readFile(absoluteFile, 'utf8') !== source)
46
+ throw new Error(`Source changed during migration: ${file}`);
47
+ if (report.version === 'v4') {
48
+ // Write the intended fingerprint first so an interrupted apply cannot double-rewrite gaps.
49
+ await mkdir(resolve(report.root, '.lumen'), { recursive: true });
50
+ ledger[file] = fingerprint(nextSource);
51
+ await writeFile(ledgerPath, `${JSON.stringify(ledger, undefined, 2)}\n`, 'utf8');
52
+ }
53
+ if (await readFile(absoluteFile, 'utf8') !== source) {
54
+ if (report.version === 'v4') {
55
+ Reflect.deleteProperty(ledger, file);
56
+ await writeFile(ledgerPath, `${JSON.stringify(ledger, undefined, 2)}\n`, 'utf8');
57
+ }
58
+ throw new Error(`Source changed during migration: ${file}`);
59
+ }
60
+ await writeFile(absoluteFile, nextSource, 'utf8');
61
+ };
62
+ const migrateFile = async (absoluteFile, report, ledger, ledgerPath) => {
63
+ const file = relative(report.root, absoluteFile) || absoluteFile;
64
+ const source = await readFile(absoluteFile, 'utf8');
65
+ if (report.version === 'v4' && Object.hasOwn(ledger, file)) {
66
+ if (ledger[file] !== fingerprint(source))
67
+ report.manualReview.push({ file,
68
+ line: 1,
69
+ column: 1,
70
+ kind: 'layout-gap',
71
+ message: 'This file was already migrated to v4 and has since changed. Review new edits manually; repeating gap rewrites would change spacing twice.' });
72
+ return;
73
+ }
74
+ const migration = migrateLumenVersionSource(source, file, report.version);
75
+ report.changes.push(...migration.changes);
76
+ report.manualReview.push(...migration.manualReview);
77
+ if (source === migration.source)
78
+ return;
79
+ report.changedFiles.push(file);
80
+ if (!report.applied)
81
+ return;
82
+ await writeMigratedSource(absoluteFile, source, migration.source, { report, ledger, ledgerPath, file });
83
+ };
84
+ const createReport = (options, root, filesScanned) => ({
85
+ applied: options.apply === true,
86
+ changedFiles: [],
87
+ changes: [],
88
+ filesScanned,
89
+ manualReview: [],
90
+ root,
91
+ version: options.version,
92
+ dependencyVersion: options.version === 'v3' ? '3.0.1' : '4.0.0'
93
+ });
94
+ const refreshDependencyReport = async (report) => {
95
+ if (report.dependencies && report.applied) {
96
+ for (const repository of report.dependencies.repositories) {
97
+ const current = await inspectLumenConsumer(repository.repository, report.dependencyVersion);
98
+ current.warnings = current.warnings.filter(warning => !warning.startsWith('Repository has uncommitted changes'));
99
+ current.valid = current.warnings.length === 0 && current.integration.healthy;
100
+ Object.assign(repository, current);
101
+ }
102
+ }
103
+ };
104
+ const optionalConsumerAudit = async (root, version) => {
105
+ try {
106
+ await readFile(resolve(root, 'package.json'), 'utf8');
107
+ }
108
+ catch (error) {
109
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
110
+ return undefined;
111
+ throw error;
112
+ }
113
+ return inspectLumenConsumerUpgrade(root, version);
114
+ };
115
+ export const migrateLumenVersion = async (options) => {
116
+ const root = resolve(options.cwd ?? process.cwd());
117
+ if (!(await lstat(root)).isDirectory())
118
+ throw new Error('Migration --cwd must be a project directory.');
119
+ const files = await discoverSourceFiles(root, extensions);
120
+ const ledgerPath = resolve(root, '.lumen', 'migrations-v4.json');
121
+ await checkLedgerLocation(root);
122
+ const ledger = options.version === 'v4' ? await readLedger(ledgerPath) : {};
123
+ const report = createReport(options, root, files.length);
124
+ if (options.dependencies) {
125
+ report.dependencies = await applyLumenVersionMigrationDependencies(root, report.dependencyVersion, options);
126
+ if (report.dependencies.repositories.some(repository => repository.commands.some(command => !command.ok))) {
127
+ report.applied = false;
128
+ return report;
129
+ }
130
+ }
131
+ if (options.version === 'v4') {
132
+ const sdk = await migrateLumenV4({ cwd: root });
133
+ report.packageVersions = sdk.packageVersions;
134
+ report.manualReview.push(...sdk.manualReview.filter(finding => finding.rule === 'embedded-mcp-sdk-v2')
135
+ .map(({ column, file, line, message }) => ({ column, file, kind: 'embedded-mcp-sdk', line, message })));
136
+ }
137
+ for (const absoluteFile of files) {
138
+ await migrateFile(absoluteFile, report, ledger, ledgerPath);
139
+ }
140
+ await refreshDependencyReport(report);
141
+ const consumerAudit = await optionalConsumerAudit(root, report.dependencyVersion);
142
+ if (consumerAudit)
143
+ report.consumerAudit = consumerAudit;
144
+ return report;
145
+ };
146
+ export const formatLumenVersionMigration = (report) => [
147
+ `Lumen ${report.version} migration ${report.applied ? 'apply' : 'dry run'}: ${report.root}`,
148
+ `Scanned ${report.filesScanned} source files. ${report.applied ? 'Applied' : 'Would apply'} ${report.changes.length} changes in ${report.changedFiles.length} files.`,
149
+ ...report.changes.map(item => `${item.file}:${item.line}:${item.column} [${item.kind}] ${item.message}`),
150
+ ...report.manualReview.map(item => `${item.file}:${item.line}:${item.column} [manual review] ${item.message}`),
151
+ ...(report.dependencies ? [formatConsumerRollout(report.dependencies)] : []),
152
+ ...(report.consumerAudit ? [formatLumenConsumerUpgradeAudit(report.consumerAudit)] : []),
153
+ report.version === 'v3' ?
154
+ 'V3 requires coordinated package versions and a native rebuild, with no web source rewrites. Review exhaustive Swift LumenIconName switches for added cases.' :
155
+ 'Review product CSS, pending dialogs, chart identity, native initializers, exhaustive Swift icon switches and embedded MCP SDK v2 integrations. Source changes do not qualify the application for release.',
156
+ `Use --dependencies to inventory or upgrade the coordinated npm family to ${report.dependencyVersion} through the existing pnpm rollout. Native Swift/Maven pins remain application-owned.`,
157
+ ...(!report.applied && report.changes.length ? [`Run lumen migrate ${report.version} --apply to write the source changes.`] : [])
158
+ ].join('\n');