@@ -200,36 +200,33 @@ function resolveConfigurePanel(serviceDir, folder) {
200200 return null
201201}
202202
203- const serviceFolders = readdirSync ( servicesDir )
203+ // Scan for subdirectories that contain data.ts
204+ const services = readdirSync ( servicesDir )
204205 . filter ( ( name ) => {
205206 if ( SKIP . has ( name ) ) return false
206207 const dir = join ( servicesDir , name )
207208 return statSync ( dir ) . isDirectory ( ) && existsSync ( join ( dir , 'data.ts' ) )
208209 } )
209- . sort ( )
210-
211- const services = serviceFolders . map ( ( folder ) => {
212- const dataPath = join ( servicesDir , folder , 'data.ts' )
213- const serviceDir = join ( servicesDir , folder )
214- const demoPath = resolveDemoPath ( serviceDir )
215- const configurePanel = resolveConfigurePanel ( serviceDir , folder )
210+ . map ( ( folder ) => {
211+ const dataPath = join ( servicesDir , folder , 'data.ts' )
212+ const serviceDir = join ( servicesDir , folder )
213+ const demoPath = resolveDemoPath ( serviceDir )
214+ const configurePanel = resolveConfigurePanel ( serviceDir , folder )
216215
217- return {
218- folder,
219- dataAlias : toAlias ( folder ) ,
220- hasDemo : demoPath !== null ,
221- demoPath,
222- demoExport : demoPath ? parseDemoExport ( demoPath ) : null ,
223- configurePanelExport : configurePanel
224- ? parseDemoExport ( configurePanel . filePath )
225- : null ,
226- configurePanelImportPath : configurePanel ?. importPath ?? null ,
227- hasWorker : hasExport ( dataPath , 'worker' ) ,
228- hasDocs : existsSync ( join ( servicesDir , folder , 'docs.ts' ) ) ,
229- }
230- } )
231-
232- const servicesWithDemo = services . filter ( ( s ) => s . hasDemo )
216+ return {
217+ folder,
218+ dataAlias : toAlias ( folder ) ,
219+ demoExport : demoPath ? parseDemoExport ( demoPath ) : null ,
220+ configurePanelExport : configurePanel
221+ ? parseDemoExport ( configurePanel . filePath )
222+ : null ,
223+ configurePanelImportPath : configurePanel ?. importPath ?? null ,
224+ hasWorker : hasExport ( dataPath , 'worker' ) ,
225+ hasDocs : existsSync ( join ( servicesDir , folder , 'docs.ts' ) ) ,
226+ }
227+ } )
228+ // Sort for deterministic output
229+ . sort ( ( a , b ) => a . folder . localeCompare ( b . folder ) )
233230
234231// ─── Service code generation ──────────────────────────────────────
235232
@@ -255,27 +252,29 @@ const lines = [
255252 ...HEADER ,
256253 'import type { Service as ServiceType } from "@/payload-types";' ,
257254 '' ,
258- '// Data imports (only for services that provide demos) ' ,
259- ...servicesWithDemo . map (
255+ '// Data imports' ,
256+ ...services . map (
260257 ( { folder, dataAlias } ) =>
261258 `import { service as ${ dataAlias } } from "../${ folder } /data";` ,
262259 ) ,
263260 '' ,
264261 '// Demo imports' ,
265- ...servicesWithDemo . map (
266- ( { folder, demoExport } ) =>
267- `import { ${ demoExport } } from "../${ folder } /demo";` ,
268- ) ,
262+ ...services
263+ . filter ( ( s ) => s . demoExport )
264+ . map (
265+ ( { folder, demoExport } ) =>
266+ `import { ${ demoExport } } from "../${ folder } /demo";` ,
267+ ) ,
269268 '' ,
270269 'import type { Service } from "../types";' ,
271270 '' ,
272271 '/** Full service map including React demo components. */' ,
273272 'export const serviceMap: Record<ServiceType["type"], Service> = {' ,
274- ...servicesWithDemo . flatMap ( ( { folder, dataAlias, demoExport } ) => [
273+ ...services . flatMap ( ( { folder, dataAlias, demoExport } ) => [
275274 ` ${ key ( folder ) } : {` ,
276275 ` ...${ dataAlias } ,` ,
277276 ' status: "offline",' ,
278- ` demo: ${ demoExport } ,` ,
277+ ... ( demoExport ? [ ` demo: ${ demoExport } ,` ] : [ ] ) ,
279278 ' },' ,
280279 ] ) ,
281280 '};' ,
@@ -361,7 +360,6 @@ const workerLines = [
361360writeFileSync ( workerRegistryPath , workerLines . join ( '\n' ) , 'utf8' )
362361
363362// --- _generated/docs.ts (docs factory registry) ---
364- // Docs registry should include any service that has docs.ts (metadata-only)
365363const docsServices = services . filter ( ( s ) => s . hasDocs )
366364
367365/** Convert a folder name to a camelCase docs alias (e.g. "speech-to-text" → "speechToTextDocs") */
@@ -525,10 +523,84 @@ const engineLines = [
525523
526524writeFileSync ( enginesOutputPath , engineLines . join ( '\n' ) , 'utf8' )
527525
526+ // ═══════════════════════════════════════════════════════════════════
527+ // ─── Port allocation summary ──────────────────────────────────────
528+ // ═══════════════════════════════════════════════════════════════════
529+
530+ /** Extract `port: <number>` from a data.ts file */
531+ function extractPort ( filePath ) {
532+ const src = readFileSync ( filePath , 'utf8' )
533+ const m = src . match ( / \b p o r t : \s * ( \d + ) / )
534+ return m ? parseInt ( m [ 1 ] , 10 ) : null
535+ }
536+
537+ /** Extract `reservedPorts: [<numbers>]` from a data.ts file */
538+ function extractReservedPorts ( filePath ) {
539+ const src = readFileSync ( filePath , 'utf8' )
540+ const m = src . match ( / \b r e s e r v e d P o r t s : \s * \[ ( [ ^ \] ] * ?) \] / )
541+ if ( ! m ) return [ ]
542+ return m [ 1 ]
543+ . split ( ',' )
544+ . map ( ( s ) => s . trim ( ) )
545+ . filter ( Boolean )
546+ . map ( Number )
547+ . filter ( ( n ) => ! isNaN ( n ) )
548+ }
549+
550+ const portEntries = [ ]
551+ const allPorts = new Set ( )
552+
553+ for ( const { folder } of services ) {
554+ const dataPath = join ( servicesDir , folder , 'data.ts' )
555+ const port = extractPort ( dataPath )
556+ if ( port !== null ) {
557+ portEntries . push ( { port, owner : folder } )
558+ allPorts . add ( port )
559+ }
560+ for ( const rp of extractReservedPorts ( dataPath ) ) {
561+ portEntries . push ( { port : rp , owner : `${ folder } (reserved)` } )
562+ allPorts . add ( rp )
563+ }
564+ }
565+
566+ for ( const folder of sampleFolders ) {
567+ const dataPath = join ( samplesDir , folder , 'data.ts' )
568+ for ( const rp of extractReservedPorts ( dataPath ) ) {
569+ portEntries . push ( { port : rp , owner : `sample:${ folder } (reserved)` } )
570+ allPorts . add ( rp )
571+ }
572+ }
573+
574+ portEntries . sort ( ( a , b ) => a . port - b . port )
575+
576+ // Find first duplicate
577+ const seen = new Map ( )
578+ let duplicateWarning = ''
579+ for ( const { port, owner } of portEntries ) {
580+ if ( seen . has ( port ) ) {
581+ duplicateWarning += ` ⚠️ Port ${ port } used by both "${ seen . get ( port ) } " and "${ owner } "\n`
582+ }
583+ seen . set ( port , owner )
584+ }
585+
586+ // Compute next available port
587+ let nextPort = 8001
588+ while ( allPorts . has ( nextPort ) ) nextPort ++
589+
528590// ─── Summary ──────────────────────────────────────────────────────
529591console . log (
530592 `✓ Generated registries:\n` +
531593 ` services: ${ services . length } services, ${ workerServices . length } workers, ${ docsServices . length } docs → src/services/_generated/\n` +
532594 ` samples: ${ sampleFolders . length } samples → src/samples/_generated/\n` +
533595 ` engines: ${ engineFolders . length } engines → src/engines/_generated/` ,
534596)
597+
598+ console . log ( '\n📡 Port allocation (ascending):' )
599+ for ( const { port, owner } of portEntries ) {
600+ console . log ( ` ${ port } ${ owner } ` )
601+ }
602+ if ( duplicateWarning ) {
603+ console . log ( '\n⚠️ Duplicate port warnings:' )
604+ process . stdout . write ( duplicateWarning )
605+ }
606+ console . log ( `\n✅ Next available port: ${ nextPort } ` )
0 commit comments