1- #!/usr/bin/env node --stack_size=800 -r ts-node/register
1+ #!/usr/bin/env -S node -r ts-node/register
22
33import { readFileSync , writeFileSync } from 'node:fs' ;
44import path from 'node:path' ;
5- import { Project , type SourceFile } from 'ts-morph' ;
5+ import { Project , SyntaxKind , type Node } from 'ts-morph' ;
6+
7+ const startTime = Date . now ( ) ;
8+ const repoRoot = path . resolve ( __dirname , '..' ) ;
9+
10+ function logDebug ( msg : string ) {
11+ const t = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 3 ) ;
12+ console . log ( `[${ t } s] ${ msg } ` ) ;
13+ }
14+ function pretty ( p : string ) {
15+ return path . relative ( repoRoot , p ) ;
16+ }
617
7- // Get target package from command line argument
818const targetPackage = process . argv [ 2 ] ;
919if ( ! targetPackage ) {
10- console . error ( 'Usage: ./generate/reExportEverything.ts <package-name>' ) ;
11- console . error ( 'Example: ./generate/reExportEverything.ts cma-client-node' ) ;
20+ console . error ( 'Usage: ./generate/reExportEverything.light.ts <package-name>' ) ;
1221 process . exit ( 1 ) ;
1322}
1423
24+ logDebug ( `Lightweight re-export generation for "${ targetPackage } "` ) ;
25+
1526const SRC_FILE = path . resolve (
1627 __dirname ,
1728 `../packages/${ targetPackage } /src/index.ts` ,
1829) ;
19- const CMA_CLIENT_PATH = path . resolve (
20- __dirname ,
21- '../packages/cma-client/dist/types/index.d.ts' ,
22- ) ;
30+ const TYPES_DIR = path . resolve ( __dirname , '../packages/cma-client/dist/types' ) ;
31+ const ENTRY_D_TS = path . join ( TYPES_DIR , 'index.d.ts' ) ;
2332
2433const START_MARKER = '// <AUTO-GENERATED-EXPORTS>' ;
2534const END_MARKER = '// </AUTO-GENERATED-EXPORTS>' ;
2635
36+ // Keep these out of the public surface:
2737const blacklist = new Set ( [
2838 'buildClient' ,
2939 'Client' ,
@@ -37,148 +47,146 @@ const project = new Project({
3747 moduleResolution : 2 , // Node
3848 target : 99 , // ESNext
3949 } ,
50+ skipAddingFilesFromTsConfig : true ,
4051} ) ;
4152
42- // Track collected exports
43- const collected = {
44- value : new Set < string > ( ) ,
45- type : new Set < string > ( ) ,
46- } ;
47-
48- // Avoid infinite recursion
49- const visitedFiles = new Set < string > ( ) ;
53+ // Add all .d.ts under the types folder so re-exports resolve
54+ project . addSourceFilesAtPaths ( path . join ( TYPES_DIR , '**/*.d.ts' ) ) ;
5055
51- function collectExports ( file : SourceFile ) {
52- if ( visitedFiles . has ( file . getFilePath ( ) ) ) return ;
53- visitedFiles . add ( file . getFilePath ( ) ) ;
56+ const entry =
57+ project . getSourceFile ( ENTRY_D_TS ) ?? project . addSourceFileAtPath ( ENTRY_D_TS ) ;
58+ logDebug ( `Loaded declaration entry: ${ pretty ( entry . getFilePath ( ) ) } ` ) ;
5459
55- for ( const stmt of file . getExportDeclarations ( ) ) {
56- const moduleSpecifier = stmt . getModuleSpecifierSourceFile ( ) ;
60+ const exported = entry . getExportedDeclarations ( ) ;
5761
58- // Handle named exports
59- const namedExports = stmt . getNamedExports ( ) ;
60- for ( const ne of namedExports ) {
61- // Use alias if present, otherwise use original name
62- const name = ne . getAliasNode ( ) ?. getText ( ) || ne . getName ( ) ;
63- if ( blacklist . has ( name ) ) continue ;
62+ // Gather explicit type-only names from the entry file
63+ const explicitTypeOnly = new Set < string > ( ) ;
6464
65- if ( stmt . isTypeOnly ( ) ) collected . type . add ( name ) ;
66- else collected . value . add ( name ) ;
65+ for ( const ed of entry . getExportDeclarations ( ) ) {
66+ // Whole declaration is type-only
67+ if ( ed . isTypeOnly ?. ( ) ) {
68+ for ( const spec of ed . getNamedExports ( ) ) {
69+ const alias = spec . getAliasNode ( ) ?. getText ( ) ;
70+ const name = alias ?? spec . getNameNode ( ) ?. getText ( ) ?? spec . getName ( ) ;
71+ if ( name ) explicitTypeOnly . add ( name ) ;
6772 }
68-
69- // Handle export * (wildcards) - recursively collect from the target file
70- // Note: export * statements have 0 named exports and no namespace export identifier
71- const isWildcard =
72- stmt . getNamedExports ( ) . length === 0 && ! stmt . getNamespaceExport ( ) ;
73-
74- if ( isWildcard && moduleSpecifier ) {
75- // Case 1: moduleSpecifier is resolved by ts-morph
76- collectExports ( moduleSpecifier ) ;
77- } else if ( isWildcard && stmt . getModuleSpecifier ( ) ) {
78- // Case 2: relative imports that ts-morph didn't resolve
79- const moduleString = stmt . getModuleSpecifier ( ) ?. getLiteralValue ( ) ;
80- if ( moduleString ) {
81- // Try to resolve the module path relative to current file
82- const currentDir = path . dirname ( file . getFilePath ( ) ) ;
83- const resolvedPath = path . resolve ( currentDir , `${ moduleString } .d.ts` ) ;
84-
85- try {
86- const targetFile = project . addSourceFileAtPath ( resolvedPath ) ;
87- collectExports ( targetFile ) ;
88- } catch ( e ) {
89- console . warn (
90- `Could not resolve wildcard export: ${ moduleString } from ${ file . getFilePath ( ) } ` ,
91- ) ;
92- }
93- }
94- }
95-
96- // Handle "export * as NS" → collect namespace as a value export
97- const nsExport = stmt . getNamespaceExport ( ) ;
98- if ( nsExport ) {
99- const nsName = nsExport . getName ( ) ;
100- if ( nsName && ! blacklist . has ( nsName ) ) {
101- collected . value . add ( nsName ) ;
102- }
73+ }
74+ // Per-specifier `export { type Foo, Bar }`
75+ for ( const spec of ed . getNamedExports ( ) ) {
76+ if ( spec . isTypeOnly ?. ( ) ) {
77+ const alias = spec . getAliasNode ( ) ?. getText ( ) ;
78+ const name = alias ?? spec . getNameNode ( ) ?. getText ( ) ?? spec . getName ( ) ;
79+ if ( name ) explicitTypeOnly . add ( name ) ;
10380 }
10481 }
82+ }
10583
106- // Collect direct exports from functions, classes, variables
107- for ( const fn of file . getFunctions ( ) ) {
108- if ( fn . isExported ( ) && ! blacklist . has ( fn . getName ( ) || '' ) ) {
109- collected . value . add ( fn . getName ( ) || '' ) ;
110- }
84+ // Classifiers
85+ function isTypeOnlyDecl ( n : Node ) : boolean {
86+ const k = n . getKind ( ) ;
87+ if (
88+ k === SyntaxKind . InterfaceDeclaration ||
89+ k === SyntaxKind . TypeAliasDeclaration ||
90+ k === SyntaxKind . TypeParameter
91+ ) {
92+ return true ;
11193 }
94+ // Ambient namespaces in d.ts
95+ if (
96+ k === SyntaxKind . ModuleDeclaration &&
97+ n . getSourceFile ( ) . isDeclarationFile ( )
98+ ) {
99+ return true ;
100+ }
101+ return false ;
102+ }
112103
113- for ( const cls of file . getClasses ( ) ) {
114- if ( cls . isExported ( ) && ! blacklist . has ( cls . getName ( ) || '' ) ) {
115- collected . value . add ( cls . getName ( ) || '' ) ;
116- }
104+ function isValueDecl ( n : Node ) : boolean {
105+ const k = n . getKind ( ) ;
106+ return (
107+ k === SyntaxKind . EnumDeclaration ||
108+ k === SyntaxKind . ClassDeclaration ||
109+ k === SyntaxKind . FunctionDeclaration ||
110+ k === SyntaxKind . VariableDeclaration
111+ ) ;
112+ }
113+
114+ // Collect exports
115+ const valueExports = new Set < string > ( ) ;
116+ const typeExports = new Set < string > ( ) ;
117+
118+ for ( const [ name , decls ] of exported ) {
119+ if ( ! name || blacklist . has ( name ) ) continue ;
120+
121+ // Explicit type-only hint
122+ if ( explicitTypeOnly . has ( name ) ) {
123+ typeExports . add ( name ) ;
124+ continue ;
117125 }
118126
119- for ( const vs of file . getVariableStatements ( ) ) {
120- if ( vs . isExported ( ) ) {
121- for ( const decl of vs . getDeclarations ( ) ) {
122- const name = decl . getName ( ) ;
123- if ( ! blacklist . has ( name ) ) {
124- collected . value . add ( name ) ;
125- }
127+ let hasValue = false ;
128+ let hasType = false ;
129+
130+ if ( ! decls || decls . length === 0 ) {
131+ // Safer default: treat as type
132+ hasType = true ;
133+ } else {
134+ for ( const d of decls ) {
135+ if ( isValueDecl ( d ) ) hasValue = true ;
136+ else if ( isTypeOnlyDecl ( d ) ) hasType = true ;
137+ else {
138+ // Ambiguous nodes → default to type
139+ hasType = true ;
126140 }
127141 }
128142 }
129143
130- // Also collect type-only exports directly declared in this file
131- for ( const ta of file . getTypeAliases ( ) ) {
132- if ( ta . isExported ( ) && ! blacklist . has ( ta . getName ( ) ) ) {
133- collected . type . add ( ta . getName ( ) ) ;
134- }
135- }
136- for ( const i of file . getInterfaces ( ) ) {
137- if ( i . isExported ( ) && ! blacklist . has ( i . getName ( ) ) ) {
138- collected . type . add ( i . getName ( ) ) ;
139- }
140- }
141- for ( const e of file . getEnums ( ) ) {
142- if ( e . isExported ( ) && ! blacklist . has ( e . getName ( ) ) ) {
143- collected . value . add ( e . getName ( ) ) ;
144- }
145- }
144+ if ( hasValue ) valueExports . add ( name ) ;
145+ else if ( hasType ) typeExports . add ( name ) ;
146146}
147147
148- const cmaFile = project . addSourceFileAtPath ( CMA_CLIENT_PATH ) ;
149- collectExports ( cmaFile ) ;
148+ // De-dupe: if something has a value, don’t also emit as type
149+ for ( const n of valueExports ) typeExports . delete ( n ) ;
150150
151- const valueExports = Array . from ( collected . value ) . sort ( ) ;
152- const typeExports = Array . from ( collected . type ) . sort ( ) ;
151+ const values = Array . from ( valueExports ) . sort ( ) ;
152+ const types = Array . from ( typeExports ) . sort ( ) ;
153153
154- const newExportBlock = `
155- ${ START_MARKER }
156- export {
157- ${ valueExports . join ( ',\n ' ) } ,
158- } from '@datocms/cma-client';
154+ logDebug (
155+ `Collected ${ values . length } value exports and ${ types . length } type exports` ,
156+ ) ;
159157
160- export type {
161- ${ typeExports . join ( ',\n ' ) } ,
162- } from '@datocms/cma-client';
163- ${ END_MARKER }
164- ` . trim ( ) ;
158+ const lines : string [ ] = [ ] ;
159+ lines . push ( START_MARKER ) ;
160+ if ( values . length ) {
161+ lines . push (
162+ `export {\n ${ values . join ( ',\n ' ) } \n} from '@datocms/cma-client';` ,
163+ ) ;
164+ }
165+ if ( types . length ) {
166+ lines . push (
167+ `export type {\n ${ types . join ( ',\n ' ) } \n} from '@datocms/cma-client';` ,
168+ ) ;
169+ }
170+ lines . push ( END_MARKER ) ;
171+ const newBlock = lines . join ( '\n' ) ;
165172
166- // Read existing file
173+ // Read/patch target file
167174let content = '' ;
168175try {
169176 content = readFileSync ( SRC_FILE , 'utf-8' ) ;
177+ logDebug ( `Loaded target file: ${ pretty ( SRC_FILE ) } ` ) ;
170178} catch {
171- console . warn ( `File ${ SRC_FILE } not found. Creating a new one .`) ;
179+ logDebug ( `Target file ${ pretty ( SRC_FILE ) } not found; will create .`) ;
172180}
173181
174- // Replace old export block if present, otherwise prepend
175182if ( content . includes ( START_MARKER ) && content . includes ( END_MARKER ) ) {
176- const regex = new RegExp ( `${ START_MARKER } [\\s\\S]*?${ END_MARKER } ` , 'm' ) ;
177- content = content . replace ( regex , newExportBlock ) ;
183+ const re = new RegExp ( `${ START_MARKER } [\\s\\S]*?${ END_MARKER } ` , 'm' ) ;
184+ content = content . replace ( re , newBlock ) ;
185+ logDebug ( 'Replaced existing auto-generated export block.' ) ;
178186} else {
179- content = `${ newExportBlock } \n\n${ content } ` ;
187+ content = `${ newBlock } \n\n${ content } ` ;
188+ logDebug ( 'Inserted new auto-generated export block at top.' ) ;
180189}
181190
182- // Write updated file
183191writeFileSync ( SRC_FILE , content ) ;
184- console . log ( `Updated exports (including types) in ${ SRC_FILE } `) ;
192+ logDebug ( `Wrote exports to ${ pretty ( SRC_FILE ) } . Done. `) ;
0 commit comments