1- #!/usr/bin/env node --stack_size=800 - r ts-node/register
1+ #!/usr/bin/env 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' ;
66
77const startTime = Date . now ( ) ;
88const repoRoot = path . resolve ( __dirname , '..' ) ;
99
10- function logDebug ( message : string ) {
11- const elapsed = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 3 ) ;
12- console . log ( `[${ elapsed } s] ${ message } ` ) ;
10+ function logDebug ( msg : string ) {
11+ const t = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 3 ) ;
12+ console . log ( `[${ t } s] ${ msg } ` ) ;
1313}
14-
15- function prettyPath ( filePath : string ) {
16- return path . relative ( repoRoot , filePath ) ;
14+ function pretty ( p : string ) {
15+ return path . relative ( repoRoot , p ) ;
1716}
1817
19- // Get target package from command line argument
2018const targetPackage = process . argv [ 2 ] ;
2119if ( ! targetPackage ) {
22- console . error ( 'Usage: ./generate/reExportEverything.ts <package-name>' ) ;
23- console . error ( 'Example: ./generate/reExportEverything.ts cma-client-node' ) ;
20+ console . error ( 'Usage: ./generate/reExportEverything.light.ts <package-name>' ) ;
2421 process . exit ( 1 ) ;
2522}
2623
27- logDebug ( `Starting re-export generation for package "${ targetPackage } "` ) ;
24+ logDebug ( `Lightweight re-export generation for "${ targetPackage } "` ) ;
2825
2926const SRC_FILE = path . resolve (
3027 __dirname ,
3128 `../packages/${ targetPackage } /src/index.ts` ,
3229) ;
33- const CMA_CLIENT_PATH = path . resolve (
34- __dirname ,
35- '../packages/cma-client/dist/types/index.d.ts' ,
36- ) ;
37-
38- logDebug ( `Source file path resolved to ${ prettyPath ( SRC_FILE ) } ` ) ;
39- logDebug ( `CMA client types source at ${ prettyPath ( CMA_CLIENT_PATH ) } ` ) ;
30+ const TYPES_DIR = path . resolve ( __dirname , '../packages/cma-client/dist/types' ) ;
31+ const ENTRY_D_TS = path . join ( TYPES_DIR , 'index.d.ts' ) ;
4032
4133const START_MARKER = '// <AUTO-GENERATED-EXPORTS>' ;
4234const END_MARKER = '// </AUTO-GENERATED-EXPORTS>' ;
4335
36+ // Keep these out of the public surface:
4437const blacklist = new Set ( [
4538 'buildClient' ,
4639 'Client' ,
@@ -54,177 +47,146 @@ const project = new Project({
5447 moduleResolution : 2 , // Node
5548 target : 99 , // ESNext
5649 } ,
50+ skipAddingFilesFromTsConfig : true ,
5751} ) ;
5852
59- // Track collected exports
60- const collected = {
61- value : new Set < string > ( ) ,
62- type : new Set < string > ( ) ,
63- } ;
64-
65- // Avoid infinite recursion
66- 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' ) ) ;
6755
68- function collectExports ( file : SourceFile ) {
69- const currentPath = file . getFilePath ( ) ;
70- if ( visitedFiles . has ( currentPath ) ) {
71- logDebug ( `Skipping already visited file ${ prettyPath ( currentPath ) } ` ) ;
72- return ;
73- }
74- visitedFiles . add ( currentPath ) ;
75- logDebug ( `Collecting exports from ${ prettyPath ( currentPath ) } ` ) ;
76-
77- for ( const stmt of file . getExportDeclarations ( ) ) {
78- const moduleSpecifier = stmt . getModuleSpecifierSourceFile ( ) ;
56+ const entry =
57+ project . getSourceFile ( ENTRY_D_TS ) ?? project . addSourceFileAtPath ( ENTRY_D_TS ) ;
58+ logDebug ( `Loaded declaration entry: ${ pretty ( entry . getFilePath ( ) ) } ` ) ;
7959
80- // Handle named exports
81- const namedExports = stmt . getNamedExports ( ) ;
82- for ( const ne of namedExports ) {
83- // Use alias if present, otherwise use original name
84- const name = ne . getAliasNode ( ) ?. getText ( ) || ne . getName ( ) ;
85- if ( blacklist . has ( name ) ) continue ;
60+ const exported = entry . getExportedDeclarations ( ) ;
8661
87- if ( stmt . isTypeOnly ( ) ) collected . type . add ( name ) ;
88- else collected . value . add ( name ) ;
89- }
62+ // Gather explicit type-only names from the entry file
63+ const explicitTypeOnly = new Set < string > ( ) ;
9064
91- // Handle export * (wildcards) - recursively collect from the target file
92- // Note: export * statements have 0 named exports and no namespace export identifier
93- const isWildcard =
94- stmt . getNamedExports ( ) . length === 0 && ! stmt . getNamespaceExport ( ) ;
95-
96- if ( isWildcard && moduleSpecifier ) {
97- // Case 1: moduleSpecifier is resolved by ts-morph
98- if ( moduleSpecifier ) {
99- logDebug (
100- `Discovered wildcard export in ${ prettyPath (
101- currentPath ,
102- ) } → walking ${ prettyPath ( moduleSpecifier . getFilePath ( ) ) } `,
103- ) ;
104- collectExports ( moduleSpecifier ) ;
105- }
106- } else if ( isWildcard && stmt . getModuleSpecifier ( ) ) {
107- // Case 2: relative imports that ts-morph didn't resolve
108- const moduleString = stmt . getModuleSpecifier ( ) ?. getLiteralValue ( ) ;
109- if ( moduleString ) {
110- // Try to resolve the module path relative to current file
111- const currentDir = path . dirname ( file . getFilePath ( ) ) ;
112- const resolvedPath = path . resolve ( currentDir , `${ moduleString } .d.ts` ) ;
113-
114- try {
115- const targetFile = project . addSourceFileAtPath ( resolvedPath ) ;
116- logDebug (
117- `Attempting manual resolution for wildcard export in ${ prettyPath (
118- currentPath ,
119- ) } → ${ prettyPath ( resolvedPath ) } `,
120- ) ;
121- collectExports ( targetFile ) ;
122- } catch ( e ) {
123- console . warn (
124- `Could not resolve wildcard export: ${ moduleString } from ${ file . getFilePath ( ) } ` ,
125- ) ;
126- }
127- }
128- }
129-
130- // Handle "export * as NS" → collect namespace as a value export
131- const nsExport = stmt . getNamespaceExport ( ) ;
132- if ( nsExport ) {
133- const nsName = nsExport . getName ( ) ;
134- if ( nsName && ! blacklist . has ( nsName ) ) {
135- collected . value . add ( nsName ) ;
136- }
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 ) ;
13772 }
13873 }
139-
140- // Collect direct exports from functions, classes, variables
141- for ( const fn of file . getFunctions ( ) ) {
142- if ( fn . isExported ( ) && ! blacklist . has ( fn . getName ( ) || '' ) ) {
143- collected . value . add ( fn . getName ( ) || '' ) ;
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 ) ;
14480 }
14581 }
82+ }
14683
147- for ( const cls of file . getClasses ( ) ) {
148- if ( cls . isExported ( ) && ! blacklist . has ( cls . getName ( ) || '' ) ) {
149- collected . value . add ( cls . getName ( ) || '' ) ;
150- }
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 ;
15193 }
152-
153- for ( const vs of file . getVariableStatements ( ) ) {
154- if ( vs . isExported ( ) ) {
155- for ( const decl of vs . getDeclarations ( ) ) {
156- const name = decl . getName ( ) ;
157- if ( ! blacklist . has ( name ) ) {
158- collected . value . add ( name ) ;
159- }
160- }
161- }
94+ // Ambient namespaces in d.ts
95+ if (
96+ k === SyntaxKind . ModuleDeclaration &&
97+ n . getSourceFile ( ) . isDeclarationFile ( )
98+ ) {
99+ return true ;
162100 }
101+ return false ;
102+ }
163103
164- // Also collect type-only exports directly declared in this file
165- for ( const ta of file . getTypeAliases ( ) ) {
166- if ( ta . isExported ( ) && ! blacklist . has ( ta . getName ( ) ) ) {
167- collected . type . add ( ta . getName ( ) ) ;
168- }
169- }
170- for ( const i of file . getInterfaces ( ) ) {
171- if ( i . isExported ( ) && ! blacklist . has ( i . getName ( ) ) ) {
172- collected . type . add ( i . getName ( ) ) ;
173- }
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 ;
174125 }
175- for ( const e of file . getEnums ( ) ) {
176- if ( e . isExported ( ) && ! blacklist . has ( e . getName ( ) ) ) {
177- collected . value . add ( e . getName ( ) ) ;
126+
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 ;
140+ }
178141 }
179142 }
180143
181- logDebug (
182- `Finished ${ prettyPath ( currentPath ) } → ${ collected . value . size } value exports, ${ collected . type . size } type exports accumulated` ,
183- ) ;
144+ if ( hasValue ) valueExports . add ( name ) ;
145+ else if ( hasType ) typeExports . add ( name ) ;
184146}
185147
186- const cmaFile = project . addSourceFileAtPath ( CMA_CLIENT_PATH ) ;
187- logDebug ( `Loaded root CMA client declaration file ${ prettyPath ( cmaFile . getFilePath ( ) ) } ` ) ;
188- 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 ) ;
150+
151+ const values = Array . from ( valueExports ) . sort ( ) ;
152+ const types = Array . from ( typeExports ) . sort ( ) ;
189153
190154logDebug (
191- `Export collection completed across ${ visitedFiles . size } files → ${ collected . value . size } values, ${ collected . type . size } types ` ,
155+ `Collected ${ values . length } value exports and ${ types . length } type exports ` ,
192156) ;
193157
194- const valueExports = Array . from ( collected . value ) . sort ( ) ;
195- const typeExports = Array . from ( collected . type ) . sort ( ) ;
196-
197- const newExportBlock = `
198- ${ START_MARKER }
199- export {
200- ${ valueExports . join ( ',\n ' ) } ,
201- } from '@datocms/cma-client';
202-
203- export type {
204- ${ typeExports . join ( ',\n ' ) } ,
205- } from '@datocms/cma-client';
206- ${ END_MARKER }
207- ` . 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' ) ;
208172
209- // Read existing file
173+ // Read/patch target file
210174let content = '' ;
211175try {
212176 content = readFileSync ( SRC_FILE , 'utf-8' ) ;
177+ logDebug ( `Loaded target file: ${ pretty ( SRC_FILE ) } ` ) ;
213178} catch {
214- console . warn ( `File ${ SRC_FILE } not found. Creating a new one .`) ;
179+ logDebug ( `Target file ${ pretty ( SRC_FILE ) } not found; will create .`) ;
215180}
216181
217- // Replace old export block if present, otherwise prepend
218182if ( content . includes ( START_MARKER ) && content . includes ( END_MARKER ) ) {
219- const regex = new RegExp ( `${ START_MARKER } [\\s\\S]*?${ END_MARKER } ` , 'm' ) ;
220- content = content . replace ( regex , newExportBlock ) ;
221- logDebug ( 'Updated existing auto-generated export block' ) ;
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. ' ) ;
222186} else {
223- content = `${ newExportBlock } \n\n${ content } ` ;
224- logDebug ( 'Inserted new auto-generated export block at file top' ) ;
187+ content = `${ newBlock } \n\n${ content } ` ;
188+ logDebug ( 'Inserted new auto-generated export block at top. ' ) ;
225189}
226190
227- // Write updated file
228191writeFileSync ( SRC_FILE , content ) ;
229- logDebug ( `Wrote updated exports to ${ prettyPath ( SRC_FILE ) } ` ) ;
230- logDebug ( 'Re-export generation script completed' ) ;
192+ logDebug ( `Wrote exports to ${ pretty ( SRC_FILE ) } . Done.` ) ;
0 commit comments