1- import chokidar from "chokidar" ;
1+ import { exec } from "node:child_process" ;
2+ import fs from "node:fs/promises" ;
23import path from "node:path" ;
3- import { HookBuildTask } from "./devServerTasks/hooksBuildTask.js" ;
4- import { EndpointBuildTask } from "./devServerTasks/endpointBuildTask.js" ;
4+ import chokidar from "chokidar" ;
5+ import { safeTryPromise } from "../safeTry/safeTry.js" ;
6+ import { getSettings } from "../settingsUtils/settingsUtils.js" ;
57import {
8+ BuildMode ,
69 DevServerStatusChangeCallback ,
10+ DevServerTaskStatus ,
711 StatusChangePayload ,
812} from "./interfaces.js" ;
9- import { BaseBuildTask } from "./devServerTasks/baseBuildTask.js" ;
10- import { BuildMode } from "./devServerTasks/interface.js" ;
11- import { InterfaceBuildTask } from "./devServerTasks/interfaceBuildTask.js" ;
12- import { OperaionsBuildTask } from "./devServerTasks/operationsBuildTask.js" ;
13- import { getSettings } from "../settingsUtils/settingsUtils.js" ;
1413
1514export class DevServer {
16- private readonly tasks : BaseBuildTask [ ] = [
17- new HookBuildTask ( ) ,
18- new EndpointBuildTask ( ) ,
19- new InterfaceBuildTask ( ) ,
20- new OperaionsBuildTask ( ) ,
21- ] ;
2215 private statuses : Record < string , StatusChangePayload > = { } ;
2316 private onStatusChangeCallback : DevServerStatusChangeCallback | undefined ;
2417
2518 async start ( ) {
19+ for ( const folder of await this . getExtensionsFolders ( ) ) {
20+ chokidar
21+ . watch ( folder , {
22+ ignored : [ "**/node_modules/**/*" , "**/.git/**/*" , "**/dist/**/*" ] ,
23+ ignoreInitial : true ,
24+ } )
25+ . on ( "all" , ( ) => {
26+ this . buildExtension ( folder , BuildMode . Dev ) ;
27+ } ) ;
28+ }
29+ }
30+
31+ private async getExtensionsFolders ( ) {
2632 const settings = await getSettings ( ) ;
2733 const projectSettings = settings . project ;
2834
2935 if ( ! projectSettings ) {
30- return ;
36+ return [ ] ;
3137 }
3238
33- for ( const task of this . tasks ) {
34- task . setStatusChangeCallback ( ( status ) => {
35- this . statuses [ task . name ] = status ;
36- this . onStatusChangeCallback ?.( this . statuses ) ;
37- } ) ;
39+ const srcDir = path . resolve ( process . cwd ( ) , projectSettings . src_dir ) ;
40+ const extensions = await fs . readdir ( srcDir ) ;
3841
39- chokidar
40- . watch (
41- path . resolve ( process . cwd ( ) , projectSettings . src_dir , task . trigger ) ,
42- {
43- ignored : [ "**/node_modules/**/*" , "**/.git/**/*" , "**/dist/**/*" ] ,
44- ignoreInitial : true ,
45- } ,
46- )
47- . on ( "all" , ( _ , path ) => {
48- task . perform ( path , BuildMode . Dev ) ;
49- } ) ;
50- }
42+ return extensions . map ( ( folderBaseName ) =>
43+ path . resolve ( srcDir , folderBaseName ) ,
44+ ) ;
5145 }
5246
5347 async buildProd ( ) {
@@ -58,25 +52,166 @@ export class DevServer {
5852 return ;
5953 }
6054
61- for ( const task of this . tasks ) {
62- const taskRoot = path . resolve (
63- process . cwd ( ) ,
64- projectSettings . src_dir ,
65- task . trigger ,
66- ) ;
67- this . appendStatusListener ( task ) ;
68- task . perform ( taskRoot , BuildMode . Prod ) ;
55+ for ( const folder of await this . getExtensionsFolders ( ) ) {
56+ this . buildExtension ( folder , BuildMode . Prod ) ;
6957 }
7058 }
7159
7260 onStatusChange ( callback : DevServerStatusChangeCallback ) {
7361 this . onStatusChangeCallback = callback ;
7462 }
7563
76- private appendStatusListener ( task : BaseBuildTask ) {
77- task . setStatusChangeCallback ( ( status ) => {
78- this . statuses [ task . name ] = status ;
79- this . onStatusChangeCallback ?.( this . statuses ) ;
64+ async buildExtension ( extensionPath : string , mode : BuildMode ) {
65+ const extensionName = path . basename ( extensionPath ) ;
66+ const status = this . statuses [ extensionName ] ;
67+ const settings = await getSettings ( ) ;
68+ const targets =
69+ mode === BuildMode . Dev
70+ ? settings . project ?. dev_targets
71+ : settings . project ?. targets ;
72+
73+ if ( ! targets ?. length ) {
74+ return ;
75+ }
76+
77+ const finishedBuildStatuses = [
78+ DevServerTaskStatus . Done ,
79+ DevServerTaskStatus . Error ,
80+ ] ;
81+ const isPreviousBuildFinished =
82+ ! status || finishedBuildStatuses . includes ( status . status ) ;
83+
84+ if ( ! isPreviousBuildFinished ) {
85+ return ;
86+ }
87+
88+ const hasNodeModules = await this . hasNodeModules ( extensionPath ) ;
89+
90+ if ( ! hasNodeModules ) {
91+ await this . installPackages ( extensionPath ) ;
92+ }
93+
94+ this . setStatus ( extensionName , {
95+ status : DevServerTaskStatus . Building ,
96+ } ) ;
97+
98+ const [ _ , err ] = await safeTryPromise ( ( ) =>
99+ this . run ( this . getBuildCommand ( mode ) , extensionPath , ( data ) => {
100+ this . setStatus ( extensionName , {
101+ status : DevServerTaskStatus . Building ,
102+ message : data ,
103+ } ) ;
104+ } ) ,
105+ ) ;
106+
107+ if ( err ) {
108+ this . setStatus ( extensionName , {
109+ status : DevServerTaskStatus . Error ,
110+ message : ( err as Error ) ?. message ,
111+ } ) ;
112+
113+ return ;
114+ }
115+
116+ const [ , buildCopyError ] = await safeTryPromise ( ( ) =>
117+ this . copyBuild ( extensionPath , targets ) ,
118+ ) ;
119+
120+ if ( buildCopyError ) {
121+ this . setStatus ( extensionName , {
122+ status : DevServerTaskStatus . Error ,
123+ message : `Build copy error: ${ ( buildCopyError as Error ) . message } ` ,
124+ } ) ;
125+
126+ return ;
127+ }
128+
129+ this . setStatus ( extensionName , {
130+ status : DevServerTaskStatus . Done ,
131+ } ) ;
132+ }
133+
134+ private async installPackages ( root : string ) {
135+ this . setStatus ( root , { status : DevServerTaskStatus . InstallingPackages } ) ;
136+
137+ await this . run ( "npm ci" , root , ( data ) => {
138+ this . setStatus ( root , {
139+ status : DevServerTaskStatus . InstallingPackages ,
140+ message : data ,
141+ } ) ;
142+ } ) ;
143+ }
144+
145+ private async hasNodeModules ( root : string ) {
146+ const nodeModulesPath = path . resolve ( root , "node_modules" ) ;
147+
148+ try {
149+ await fs . access ( nodeModulesPath ) ;
150+ return true ;
151+ } catch {
152+ return false ;
153+ }
154+ }
155+
156+ private setStatus ( extension : string , payload : StatusChangePayload ) {
157+ const name = path . basename ( extension ) ;
158+ this . statuses [ name ] = payload ;
159+ this . onStatusChangeCallback ?.( this . statuses ) ;
160+ }
161+
162+ protected getBuildCommand ( mode : BuildMode ) {
163+ if ( mode === BuildMode . Prod ) {
164+ return "npm run build" ;
165+ }
166+
167+ return "npm run build:q" ;
168+ }
169+
170+ protected async copyBuild ( extensionPath : string , targets : string [ ] ) {
171+ const buildPath = path . resolve ( extensionPath , "dist" ) ;
172+ const packageJsonPath = path . resolve ( extensionPath , "package.json" ) ;
173+
174+ for ( const target of targets ) {
175+ const targetPath = path . resolve (
176+ process . cwd ( ) ,
177+ target ,
178+ `directus-extension-${ path . basename ( extensionPath ) } ` ,
179+ ) ;
180+
181+ await fs . mkdir ( path . resolve ( targetPath , "dist" ) , { recursive : true } ) ;
182+
183+ await fs . cp ( buildPath , path . resolve ( targetPath , "dist" ) , {
184+ recursive : true ,
185+ } ) ;
186+ await fs . copyFile (
187+ packageJsonPath ,
188+ path . resolve ( targetPath , "package.json" ) ,
189+ ) ;
190+ }
191+ }
192+
193+ private run ( command : string , cwd : string , onData ?: ( data : string ) => void ) {
194+ return new Promise < void > ( ( res , rej ) => {
195+ const childProcess = exec (
196+ command ,
197+ {
198+ cwd,
199+ } ,
200+ async ( err : unknown ) => {
201+ if ( ! err ) {
202+ res ( ) ;
203+ return ;
204+ }
205+
206+ rej ( err ) ;
207+ } ,
208+ ) ;
209+
210+ if ( onData ) {
211+ childProcess . stdout ?. on ( "data" , ( data ) => {
212+ onData ( data . toString ( "utf-8" ) ) ;
213+ } ) ;
214+ }
80215 } ) ;
81216 }
82217}
0 commit comments