55 This script is used to publish Ember's type definitions. The basic workflow
66 is:
77
8- 1. Run `tsc` against the Ember packages which make up its public API, with
9- the output being `/types/stable`.
8+ 1. Run `tsc` against the Ember packages which make up its public API, with the
9+ output being `/types/stable`.
1010
11- 2. Wrap each emitted module in a `declare module` statement. While doing so,
12- keep track of the full list of emitted modules.
11+ 2. Wrap each emitted module in a `declare module` statement. This requires
12+ replacing all relative imports with absolute imports and removing all
13+ `declare` statements from the body of the module.
1314
14- 3. Check that each module emitted is included in `types/stable/index.d.ts`,
15- if and only if it also appears in a list of stable types modules defined
16- in this script, so that they all "show up" to end users. That list will
15+ While doing so, keep track of the full list of emitted modules for the sake
16+ of step (3).
17+
18+ 3. Check that each module emitted is included in `types/stable/index.d.ts`, if
19+ and only if it also appears in a list of stable types modules defined in
20+ this script, so that they all "show up" to end users. That list will
1721 eventually be the list of *all* modules, but this allows us to publish
1822 iteratively as we gain confidence in the stability of the types.
1923
2024 This is *not* an optimal long-term publishing strategy. We would prefer to
2125 generate per-package roll-ups, using a Rollup plugin or some such, but we are
22- currently blocked on a number of internal circular dependencies as well as
23- the difficulty of avoiding multiple definitions of the same types reused
24- across many rollups.
26+ currently blocked on a number of internal circular dependencies as well as the
27+ difficulty of avoiding multiple definitions of the same types reused across
28+ many rollups.
2529
2630 @packageDocumentation
2731 */
@@ -30,7 +34,15 @@ import glob from 'glob';
3034import { spawnSync } from 'node:child_process' ;
3135import fs from 'node:fs' ;
3236import path from 'node:path' ;
33- import MagicString from 'magic-string' ;
37+ import * as parser from 'recast/parsers/babel-ts.js' ;
38+ import {
39+ isClassDeclaration ,
40+ isStringLiteral ,
41+ isVariableDeclaration ,
42+ isTSDeclareFunction ,
43+ } from '@babel/types' ;
44+ import { builders as b , visit } from 'ast-types' ;
45+ import { parse , print } from 'recast' ;
3446
3547/**
3648 Modules we know we are not ready to expose yet, mostly because they do not
@@ -404,7 +416,7 @@ async function main() {
404416
405417 let status = 'success' ;
406418 for ( let moduleName of moduleNames ) {
407- let result = wrapInDeclareModule ( moduleName ) ;
419+ let result = processModule ( moduleName ) ;
408420 if ( result !== 'success' ) {
409421 status = result ;
410422 }
@@ -416,8 +428,16 @@ async function main() {
416428 // We need to import "package root" types as such, *not* via the actual
417429 // module which provides them, or TS does not see them correctly via the
418430 // side effect imports, so transform them accordingly:
419- // `@ember/owner/index.d.ts` -> `@ember/owner`
420- let moduleOrPackagePath = moduleName . replace ( / \/ i n d e x .d .t s $ / , '' ) ;
431+ //
432+ // `@ember/owner/index.d.ts` -> `@ember/owner`
433+ //
434+ // We also need to replace `.d.ts` entirely:
435+ //
436+ // `@ember/utils/lib/compare.d.ts` -> `@ember/utils/lib/compare`
437+ //
438+ // Otherwise, the modules won't be resolved correctly via the side-effect
439+ // imports.
440+ let moduleOrPackagePath = moduleName . replace ( / \/ i n d e x .d .t s $ / , '' ) . replace ( '.d.ts' , '' ) ;
421441
422442 // Then create a relative path *to* the path on disk so that the
423443 // side-effect import is e.g. `import './@ember/owner';`, which makes it
@@ -429,14 +449,19 @@ async function main() {
429449 let stableIndexDTsContents = BASE_INDEX_D_TS . replace ( MODULES_PLACEHOLDER , sideEffectModules ) ;
430450 fs . writeFileSync ( path . join ( TYPES_DIR , 'index.d.ts' ) , stableIndexDTsContents ) ;
431451
452+ // Make the generated types easier to read!
453+ spawnSync ( 'prettier' , [ '--write' , 'types/stable/**/*.ts' ] ) ;
454+
432455 process . exit ( status === 'success' ? 0 : 1 ) ;
433456}
434457
435458/**
436- * @param {string } moduleName
437- * @return {'success' | 'failure' }
459+ Load the module, rewrite it, and write it back to disk.
460+
461+ @param {string } moduleName
462+ @return {'success' | 'failure' }
438463 */
439- function wrapInDeclareModule ( moduleName ) {
464+ function processModule ( moduleName ) {
440465 let modulePath = path . join ( TYPES_DIR , moduleName ) ;
441466
442467 /** @type {string } */
@@ -450,19 +475,16 @@ function wrapInDeclareModule(moduleName) {
450475
451476 let moduleNameForDeclaration = moduleName . replace ( '/index.d.ts' , '' ) ;
452477
453- // This is a horrible nightmare of a hack; in a later PR I'm going to just
454- // replace all of this by going ahead and using recast or such. As annoying as
455- // that will be, it will be *way* more reliable.
456- let string = new MagicString ( contents ) ;
457- string
458- . replace ( / ^ e x p o r t d e c l a r e / gm, 'export ' ) // g for global, m for multiline
459- . replace ( / ^ d e c l a r e / gm, '' ) // g for global, m for multiline
460- . indent ( ' ' )
461- . prepend ( `declare module '${ moduleNameForDeclaration } ' {\n` )
462- . append ( '}\n' ) ;
478+ let rewrittenModule ;
479+ try {
480+ rewrittenModule = rewriteModule ( contents , moduleNameForDeclaration ) ;
481+ } catch ( e ) {
482+ console . error ( `Error rewriting ${ moduleName } ` , e ) ;
483+ return 'failure' ;
484+ }
463485
464486 try {
465- fs . writeFileSync ( modulePath , string . toString ( ) ) ;
487+ fs . writeFileSync ( modulePath , rewrittenModule ) ;
466488 } catch ( e ) {
467489 console . error ( `Error writing ${ modulePath } : ${ e } ` ) ;
468490 return 'failure' ;
@@ -471,5 +493,175 @@ function wrapInDeclareModule(moduleName) {
471493 return 'success' ;
472494}
473495
496+ /**
497+ Rewrite a given module declaration:
498+
499+ - Tranform the main body of the module into a new top-level `declare module`
500+ statement.
501+ - Remove all `declare` modifiers from items in the module itself.
502+ - Update all `import` specifiers to be absolute in terms of the package
503+ name, which means handling both `./` and `../` correctly.
504+ - Preserve existing `declare module` statements, so that anything using e.g.
505+ declaration merging continues to work correctly.
506+
507+ @param {string } code The initial code to rewrite.
508+ @param {string } moduleName The name of the module to use.
509+ @returns {string }
510+ */
511+ export function rewriteModule ( code , moduleName ) {
512+ let ast = parse ( code , { parser } ) ;
513+
514+ /** @type {Array<import("ast-types/gen/namedTypes").namedTypes.TSModuleDeclaration> } */
515+ let otherModuleDeclarations = [ ] ;
516+
517+ visit ( ast , {
518+ // We need to preserve existing `declare module { ... }` blocks so that
519+ // things which rely on declaration merging can work, but they need to be
520+ // emitted *outside* the `declare module` we are introducing.
521+ visitTSModuleDeclaration ( path ) {
522+ otherModuleDeclarations . push ( path . node ) ;
523+ path . prune ( path . node ) ;
524+ this . traverse ( path ) ;
525+ } ,
526+
527+ // Remove `declare` from `declare (let|const|var)` in the top-level module.
528+ visitVariableDeclaration ( path ) {
529+ if ( isVariableDeclaration ( path . node ) && ! hasParentModuleDeclarationBlock ( path ) ) {
530+ path . node . declare = false ;
531+ }
532+ this . traverse ( path ) ;
533+ } ,
534+
535+ // Remove `declare` from `declare class` in the top-level module.
536+ visitClassDeclaration ( path ) {
537+ if ( isClassDeclaration ( path . node ) && ! hasParentModuleDeclarationBlock ( path ) ) {
538+ path . node . declare = false ;
539+ }
540+ this . traverse ( path ) ;
541+ } ,
542+
543+ // Remove `declare` from `declare function` in the top-level module.
544+ visitTSDeclareFunction ( path ) {
545+ if ( isTSDeclareFunction ( path . node ) && ! hasParentModuleDeclarationBlock ( path ) ) {
546+ path . node . declare = false ;
547+ }
548+ this . traverse ( path ) ;
549+ } ,
550+
551+ // For any relative imports like `import { something } from './somewhere';`,
552+ // rewrite as `import { something } from '@ember/some-package/somewhere';`
553+ // since relative imports are not allowed in `declare module { }` blocks.
554+ visitImportDeclaration ( path ) {
555+ let source = path . node . source ;
556+ if ( isStringLiteral ( source ) ) {
557+ source . value = normalizeSpecifier ( moduleName , source . value ) ;
558+ }
559+ this . traverse ( path ) ;
560+ } ,
561+
562+ // Do the same for `export ... from './relative-path'`.
563+ visitExportNamedDeclaration ( path ) {
564+ let source = path . node . source ;
565+ if ( isStringLiteral ( source ) ) {
566+ if ( source . value . startsWith ( './' ) || source . value . startsWith ( '../' ) ) {
567+ source . value = normalizeSpecifier ( moduleName , source . value ) ;
568+ }
569+ }
570+ this . traverse ( path ) ;
571+ } ,
572+ } ) ;
573+
574+ let newAST = b . file (
575+ b . program ( [
576+ b . declareModule (
577+ b . identifier ( `'${ moduleName . replace ( '.d.ts' , '' ) } '` ) ,
578+ b . blockStatement ( ast . program . body )
579+ ) ,
580+ ...otherModuleDeclarations ,
581+ ] )
582+ ) ;
583+
584+ return print ( newAST ) . code ;
585+ }
586+
587+ /**
588+ Is this declaration in a `declare module { }` block?
589+
590+ @param {import('ast-types/lib/node-path').NodePath } path
591+ @return boolean
592+ */
593+ function hasParentModuleDeclarationBlock ( path ) {
594+ /** @type {import('ast-types/lib/node-path').NodePath } */
595+ let parentPath = path ;
596+ while ( ( parentPath = parentPath . parent ) ) {
597+ if ( parentPath . node . type === 'ModuleDeclaration' ) {
598+ return true ;
599+ }
600+ }
601+
602+ return false ;
603+ }
604+
605+ const TERMINAL_MODULE_RE = / \/ [ \w - _ ] + \. d \. t s $ / ;
606+ const NEIGHBOR_PATH_RE = / ^ ( \. ) \/ / ;
607+
608+ /**
609+ Given a relative path, `./` or `(../)+`, rewrite it as an absolute path.
610+
611+ @param {string } moduleName The name of the host module we are declaring.
612+ @param {string } specifier The name of the module it is importing.
613+ @return {string }
614+ */
615+ function normalizeSpecifier ( moduleName , specifier ) {
616+ if ( specifier . startsWith ( './' ) ) {
617+ let parentModuleName = moduleName . replace ( TERMINAL_MODULE_RE , '' ) ;
618+ let sansLeadingDot = specifier . replace ( NEIGHBOR_PATH_RE , '' ) ;
619+ let newImportName = `${ parentModuleName } /${ sansLeadingDot } ` ;
620+ return newImportName ;
621+ } else if ( specifier . startsWith ( '../' ) ) {
622+ // Reverse it so we can just `pop` from `parentPathChunks` as we go: walking
623+ // backward through the specifier means as soon as we hit the `..` we can
624+ // start using the chunks from the end of the hosting module.
625+ let reversedSpecifierChunks = specifier . split ( '/' ) . reverse ( ) ;
626+ let parentPathChunks = moduleName . split ( '/' ) ;
627+
628+ // To make that logic work, though, we need to drop the last item from the
629+ // chunks comprising host module, because we need to *not* treat the current
630+ // module itself as a parent. If we're not in a "root" module, we need to
631+ // do it an extra time to get rid of the terminal `foo.d.ts` as well.
632+ let terminal = parentPathChunks . pop ( ) ;
633+ if ( terminal ?. endsWith ( '.d.ts' ) ) {
634+ parentPathChunks . pop ( ) ;
635+ }
636+
637+ // Walk back from the end of the specifier, replacing `..` with chunks from
638+ // the parent paths.
639+ /** @type {string[] } */
640+ let merged = [ ] ;
641+ for ( let chunk of reversedSpecifierChunks ) {
642+ if ( chunk === '..' ) {
643+ let parent = parentPathChunks . pop ( ) ;
644+ if ( ! parent ) {
645+ throw new Error (
646+ `Could not generate a valid path for relative path specifier ${ specifier } in ${ moduleName } `
647+ ) ;
648+ }
649+ merged . push ( parent ) ;
650+ } else {
651+ merged . push ( chunk ) ;
652+ }
653+ }
654+
655+ // Reverse them again so we have the correct ordering.
656+ merged . reverse ( ) ;
657+ // Then incorporate the rest of the parent path chunks.
658+ merged . unshift ( ...parentPathChunks ) ;
659+
660+ return merged . join ( '/' ) ;
661+ } else {
662+ return specifier ;
663+ }
664+ }
665+
474666// Run it!
475667main ( ) ;
0 commit comments