2121// end-appended list, the tail-appended definitions and the old array's tail are
2222// the lit controls — each MUST conflict, or a clean result would be equally
2323// explained by a harness that cannot see a conflict at all.
24+ //
25+ // [#29987 codemod] Since the codemod split (`scripts/codemod-split-conversions-registry.mjs`)
26+ // each conversion is DEFINED in a module of its own, `src/conversions/entries/18/<id>.ts`,
27+ // which a retirement adds as a new file and which therefore never conflicts. What a
28+ // retirement still edits in `registry.ts` is two lines: the import of its conversion and
29+ // its entry in the list. The imports stand in the order the definitions stood, so the
30+ // placement rule reads them where it read the definitions: an import is a definition's
31+ // place in this file, and the module it names is read for the definition itself.
2432
2533import { afterAll , beforeAll , describe , expect , it } from 'vitest' ;
2634import { spawnSync } from 'node:child_process' ;
@@ -46,10 +54,14 @@ const CLOSE = '];\n';
4654const ENTRY = / \{ c o n v e r s i o n : ( [ A - Z a - z _ $ ] [ \w $ ] * ) , o r d e r : ( \d + (?: \. \d + ) ? ) \} , \n / y;
4755/** A blank line — which is what a comment between entries is once `maskComments` has blanked it; it belongs to the entry below it. */
4856const BLANK = / [ \t ] * \n / y;
49- /** A conversion definition and its id, which every definition in the file spells on the next line. */
50- const DEFINITION = / ^ (?: e x p o r t ) ? c o n s t ( [ A - Z a - z _ $ ] [ \w $ ] * ) : M e t a d a t a C o n v e r s i o n = \{ \n i d : ' ( [ ^ ' \n ] + ) ' , $ / gm;
51- /** Where a conversion that sorts last is defined: directly above this declaration's doc comment. */
52- const TAIL_ANCHOR = '\ninterface OrderedConversion {\n' ;
57+ /**
58+ * A conversion's place in the file: its import, from the module named for its id
59+ * (`./entries/<toMajor>/<id>.js`). The module itself is read for the definition
60+ * ({@link definitionInModule}).
61+ */
62+ const DEFINITION = / ^ i m p o r t \{ ( [ A - Z a - z _ $ ] [ \w $ ] * ) \} f r o m ' \. \/ e n t r i e s \/ 1 8 \/ ( [ ^ ' \n ] + ) \. j s ' ; $ / gm;
63+ /** The module a conversion is defined in, from its id. */
64+ const MODULE_PATH = ( id : string ) : string => path . resolve ( HERE , `../src/conversions/entries/18/${ id } .ts` ) ;
5365/** How `CONVERSIONS_BY_MAJOR` reads the entries (the annotation: see that table's docblock). */
5466const WIRING = ' 18: /* @__PURE__ */ inApplicationOrder(MAJOR_18_CONVERSIONS),\n' ;
5567
@@ -128,11 +140,26 @@ function entriesOf(source: string): { entries: Entry[]; end: number } {
128140 return { entries, end : at } ;
129141}
130142
131- /** Every conversion definition in the file, in file order — a commented-out one is not a definition . */
143+ /** Every conversion's import in the file, in file order — a commented-out one is not an import . */
132144function definitionsOf ( source : string ) : { ident : string ; id : string ; at : number } [ ] {
133145 return [ ...maskOnce ( source ) . matchAll ( DEFINITION ) ] . map ( ( m ) => ( { ident : m [ 1 ] ! , id : m [ 2 ] ! , at : m . index ! } ) ) ;
134146}
135147
148+ /** Where a conversion that sorts last is imported: directly after every other conversion's import. */
149+ function definitionsTail ( source : string ) : number {
150+ const defs = definitionsOf ( source ) ;
151+ expect ( defs . length , 'the conversions\' imports' ) . toBeGreaterThan ( 0 ) ;
152+ const last = defs . at ( - 1 ) ! ;
153+ return source . indexOf ( '\n' , last . at ) + 1 ;
154+ }
155+
156+ /** The definition in a conversion's module: `const IDENT: MetadataConversion = {` with its id on the next line, exported by name. */
157+ function definitionInModule ( ident : string , id : string ) : boolean {
158+ const text = maskComments ( readFileSync ( MODULE_PATH ( id ) , 'utf8' ) ) ;
159+ return text . includes ( `\nconst ${ ident } : MetadataConversion = {\n id: '${ id } ',\n toMajor: 18,\n` )
160+ && text . endsWith ( `\nexport { ${ ident } };\n` ) ;
161+ }
162+
136163/** Rule 1, as findings: the entries are sorted strictly by identifier. */
137164function sortFindings ( source : string ) : string [ ] {
138165 const idents = entriesOf ( source ) . entries . map ( ( e ) => e . ident ) ;
@@ -147,13 +174,13 @@ function placementFindings(source: string): string[] {
147174 return entries . flatMap ( ( e , i ) => {
148175 if ( PLACED_BEFORE_THE_RULE . has ( e . ident ) ) return [ ] ;
149176 const at = index . get ( e . ident ) ;
150- if ( at === undefined ) return [ `${ e . ident } : no \`const ${ e . ident } : MetadataConversion \` definition in the file` ] ;
177+ if ( at === undefined ) return [ `${ e . ident } : no import of \` ${ e . ident } \` from its module in the file` ] ;
151178 const want = entries [ i + 1 ] ?. ident ;
152179 const next = defs [ at + 1 ] ?. ident ;
153180 if ( next === want ) return [ ] ;
154181 return [
155- `${ e . ident } is defined above ${ next ?? 'nothing (it is the last conversion defined )' } ; define it directly above `
156- + ( want ? `${ want } 's definition , the entry that follows it` : 'OrderedConversion , after every other conversion' ) ,
182+ `${ e . ident } is imported above ${ next ?? 'nothing (it is the last conversion imported )' } ; import it directly above `
183+ + ( want ? `${ want } 's import , the entry that follows it` : 'nothing , after every other conversion\'s import ' ) ,
157184 ] ;
158185 } ) ;
159186}
@@ -165,17 +192,20 @@ function docStart(source: string, offset: number): number {
165192}
166193
167194const kebab = ( ident : string ) : string => `synthetic-${ ident . replace ( / [ A - Z ] / g, ( c ) => `-${ c . toLowerCase ( ) } ` ) } ` ;
168- const definition = ( ident : string ) : string => `/** A synthetic major-18 retirement. */\nconst ${ ident } : MetadataConversion = {\n`
195+ /** A retirement's line in `registry.ts`: the import of its conversion (the module is a new file, so it never conflicts). */
196+ const definition = ( ident : string ) : string => `import { ${ ident } } from './entries/18/${ kebab ( ident ) } .js';\n` ;
197+ /** The pre-split shape, for the old-shape control only: the definition itself, in the one file. */
198+ const legacyDefinition = ( ident : string ) : string => `/** A synthetic major-18 retirement. */\nconst ${ ident } : MetadataConversion = {\n`
169199 + ` id: '${ kebab ( ident ) } ',\n toMajor: 18,\n surface: 'synthetic.key',\n summary: 'synthetic',\n`
170200 + ' apply: (stack) => stack,\n fixture: { before: {}, after: {}, expectedNotices: 0 },\n};\n\n' ;
171201const entry = ( ident : string , order : number ) : string => ` { conversion: ${ ident } , order: ${ order } },\n` ;
172202
173- /** Offset of the definition the doc comment asks for: above the next entry's, or above `OrderedConversion` . */
203+ /** Offset of the import the doc comment asks for: above the next entry's, or after every other conversion's . */
174204function definitionSite ( source : string , ident : string ) : number {
175205 const next = entriesOf ( source ) . entries . find ( ( e ) => e . ident > ident ) ;
176- if ( ! next ) return docStart ( source , source . indexOf ( TAIL_ANCHOR ) + 1 ) ;
206+ if ( ! next ) return definitionsTail ( source ) ;
177207 const def = definitionsOf ( source ) . find ( ( d ) => d . ident === next . ident ) ;
178- expect ( def , `${ next . ident } 's definition ` ) . toBeDefined ( ) ;
208+ expect ( def , `${ next . ident } 's import ` ) . toBeDefined ( ) ;
179209 return docStart ( source , def ! . at ) ;
180210}
181211
@@ -198,9 +228,9 @@ function retireAtListEnd(source: string, ident: string, order: number): string {
198228 return insert ( withDefinition , entriesOf ( withDefinition ) . end , entry ( ident , order ) ) ;
199229}
200230
201- /** The definition half done the old way: defined after every other conversion (the entry still sorted). */
231+ /** The definition half done the old way: imported after every other conversion (the entry still sorted). */
202232function retireAtDefinitionsEnd ( source : string , ident : string , order : number ) : string {
203- const tail = docStart ( source , source . indexOf ( TAIL_ANCHOR ) + 1 ) ;
233+ const tail = definitionsTail ( source ) ;
204234 return sortedEntry ( insert ( source , tail , definition ( ident ) ) , ident , order ) ;
205235}
206236
@@ -302,9 +332,10 @@ describe('major 18 in the conversions registry — the shape the merge needs (#2
302332 ) . toEqual ( [ ] ) ;
303333 } ) ;
304334
305- it ( 'each entry is a major-18 conversion defined in this file , and the list is all of them' , ( ) => {
335+ it ( 'each entry is a major-18 conversion imported from the module that defines it , and the list is all of them' , ( ) => {
306336 const ids = new Map ( definitionsOf ( SOURCE ) . map ( ( d ) => [ d . ident , d . id ] ) ) ;
307337 expect ( IDENTS . filter ( ( i ) => ! ids . has ( i ) ) ) . toEqual ( [ ] ) ;
338+ expect ( IDENTS . filter ( ( i ) => ! definitionInModule ( i , ids . get ( i ) ! ) ) , 'each module defines its conversion under its id' ) . toEqual ( [ ] ) ;
308339 expect ( new Set ( IDENTS . map ( ( i ) => ids . get ( i ) ) ) ) . toEqual ( new Set ( APPLIED ) ) ;
309340 expect ( APPLIED ) . toHaveLength ( IDENTS . length ) ;
310341 expect ( CONVERSIONS_BY_MAJOR [ 18 ] ! . every ( ( c ) => c . toMajor === 18 ) ) . toBe ( true ) ;
@@ -321,7 +352,7 @@ describe('major 18 in the conversions registry — the shape the merge needs (#2
321352 expect ( MIGRATIONS_BY_MAJOR [ 18 ] ! . conversionIds ) . toEqual ( order ) ;
322353 } ) ;
323354
324- it ( 'a conversion added after the rule is defined directly above the entry that follows it' , ( ) => {
355+ it ( 'a conversion added after the rule is imported directly above the entry that follows it' , ( ) => {
325356 expect ( PLACED_BEFORE_THE_RULE . size , '⛔ the exemption is closed: place a new conversion by the rule' ) . toBe ( 46 ) ;
326357 expect ( placementFindings ( SOURCE ) ) . toEqual ( [ ] ) ;
327358 } ) ;
@@ -376,9 +407,9 @@ describe('major 18 in the conversions registry — two retirements, no merge dri
376407 expect ( placementFindings ( retireAtDefinitionsEnd ( SOURCE , A , NEXT_ORDER ) ) ) . toEqual ( [
377408 ...( PLACED_BEFORE_THE_RULE . has ( neighbour )
378409 ? [ ]
379- : [ `${ neighbour } is defined above ${ IDENTS [ k ] } ; define it directly above ${ A } 's definition , the entry that follows it` ] ) ,
380- `${ A } is defined above nothing (it is the last conversion defined ); define it directly above ${ IDENTS [ k ] } 's `
381- + 'definition , the entry that follows it' ,
410+ : [ `${ neighbour } is imported above ${ IDENTS [ k ] } ; import it directly above ${ A } 's import , the entry that follows it` ] ) ,
411+ `${ A } is imported above nothing (it is the last conversion imported ); import it directly above ${ IDENTS [ k ] } 's `
412+ + 'import , the entry that follows it' ,
382413 ] ) ;
383414 } ) ;
384415
@@ -408,7 +439,7 @@ describe('major 18 in the conversions registry — two retirements, no merge dri
408439 expect ( m . conflicted ) . toEqual ( [ REL ] ) ;
409440 } ) ;
410441
411- it ( 'the same two conversions defined after every other conversion conflict — the definitions have a tail too' , ( ) => {
442+ it ( 'the same two conversions imported after every other conversion conflict — the imports have a tail too' , ( ) => {
412443 const m = repo . merge ( 'definitions-end-a' , 'definitions-end-b' ) ;
413444 expect ( m . status ) . toBe ( 1 ) ;
414445 expect ( m . conflicted ) . toEqual ( [ REL ] ) ;
@@ -419,7 +450,7 @@ describe('major 18 in the conversions registry — two retirements, no merge dri
419450 + " id: 'view-list-tabs-removed',\n};\n\n"
420451 + 'export const CONVERSIONS_BY_MAJOR = {\n 18: [\n viewListTabsRemoved,\n ],\n};\n' ;
421452 const append = ( ident : string ) => old
422- . replace ( 'export const CONVERSIONS_BY_MAJOR' , `${ definition ( ident ) } export const CONVERSIONS_BY_MAJOR` )
453+ . replace ( 'export const CONVERSIONS_BY_MAJOR' , `${ legacyDefinition ( ident ) } export const CONVERSIONS_BY_MAJOR` )
423454 . replace ( ' viewListTabsRemoved,\n' , ` viewListTabsRemoved,\n ${ ident } ,\n` ) ;
424455 const legacy = new FileRepo ( old ) ;
425456 try {
0 commit comments