1414 ********************************************************************************/
1515
1616// default implementation of W3C WoT Servient (http(s) and file bindings)
17- import DefaultServient from "./cli-default-servient" ;
18- import ErrnoException = NodeJS . ErrnoException ;
17+ import DefaultServient , { ScriptOptions } from "./cli-default-servient" ;
1918
2019// tools
21- import fs = require( "fs" ) ;
22- import * as dotenv from "dotenv" ;
2320import * as path from "path" ;
24- import { Command , InvalidArgumentError , Argument } from "commander" ;
25- import Ajv , { ValidateFunction , ErrorObject } from "ajv" ;
21+ import { Command , Argument } from "commander" ;
22+ import Ajv , { ValidateFunction } from "ajv" ;
2623import ConfigSchema from "./wot-servient-schema.conf.json" ;
27- import _ from "lodash" ;
2824import { version } from "@node-wot/core/package.json" ;
2925import { createLoggers } from "@node-wot/core" ;
30- import inspector from "inspector" ;
26+ import { buildConfig } from "./config-builder" ;
27+ import { loadCompiler , loadEnvVariables } from "./utils" ;
28+ import { runScripts } from "./script-runner" ;
29+ import { readdir } from "fs/promises" ;
30+ import * as logger from "debug" ;
31+ import { parseConfigFile , parseConfigParams , parseIp } from "./parsers" ;
3132
32- const { error, info, warn } = createLoggers ( "cli" , "cli" ) ;
33+ const { error, info, warn, debug } = createLoggers ( "cli" , "cli" ) ;
3334
3435const program = new Command ( ) ;
3536const ajv = new Ajv ( { strict : true } ) ;
3637const schemaValidator = ajv . compile ( ConfigSchema ) as ValidateFunction ;
3738const defaultFile = "wot-servient.conf.json" ;
3839const baseDir = "." ;
3940
40- const dotEnvConfigParameters : DotEnvConfigParameter = { } ;
41-
4241// General commands
4342program
4443 . name ( "wot-servient" )
@@ -115,87 +114,19 @@ VAR1=Value1
115114VAR2=Value2`
116115) ;
117116
118- // Typings
119- type DotEnvConfigParameter = {
120- [ key : string ] : unknown ;
121- } ;
122- interface DebugParams {
123- shouldBreak : boolean ;
124- host : string ;
125- port : number ;
126- }
127-
128- // Parsers & validators
129- function parseIp ( value : string , previous : string ) {
130- if ( ! / ^ ( [ a - z ] * | [ \d . ] * ) ( : [ 0 - 9 ] { 2 , 5 } ) ? $ / . test ( value ) ) {
131- throw new InvalidArgumentError ( "Invalid host:port combo" ) ;
132- }
133-
134- return value ;
135- }
136- function parseConfigFile ( filename : string , previous : string ) {
137- try {
138- const open = filename || path . join ( baseDir , defaultFile ) ;
139- const data = fs . readFileSync ( open , "utf-8" ) ;
140- if ( ! schemaValidator ( JSON . parse ( data ) ) ) {
141- throw new InvalidArgumentError (
142- `Config file contains invalid an JSON: ${ ( schemaValidator . errors ?? [ ] )
143- . map ( ( o : ErrorObject ) => o . message )
144- . join ( "\n" ) } `
145- ) ;
146- }
147- return filename ;
148- } catch ( err ) {
149- throw new InvalidArgumentError ( `Error reading config file: ${ err } ` ) ;
150- }
151- }
152- function parseConfigParams ( param : string , previous : unknown ) {
153- // Validate key-value pair
154- if ( ! / ^ ( [ a - z A - Z 0 - 9 _ . ] + ) : = ( [ a - z A - Z 0 - 9 _ ] + ) $ / . test ( param ) ) {
155- throw new InvalidArgumentError ( "Invalid key-value pair" ) ;
156- }
157- const fieldNamePath = param . split ( ":=" ) [ 0 ] ;
158- const fieldNameValue = param . split ( ":=" ) [ 1 ] ;
159- let fieldNameValueCast ;
160- if ( Number ( fieldNameValue ) ) {
161- fieldNameValueCast = + fieldNameValue ;
162- } else if ( fieldNameValue === "true" || fieldNameValue === "false" ) {
163- fieldNameValueCast = Boolean ( fieldNameValue ) ;
164- } else {
165- fieldNameValueCast = fieldNamePath ;
166- }
167-
168- // Build object using dot-notation JSON path
169- const obj = _ . set ( { } , fieldNamePath , fieldNameValueCast ) ;
170- if ( ! schemaValidator ( obj ) ) {
171- throw new InvalidArgumentError (
172- `Config parameter '${ param } ' is not valid: ${ ( schemaValidator . errors ?? [ ] )
173- . map ( ( o : ErrorObject ) => o . message )
174- . join ( "\n" ) } `
175- ) ;
176- }
177- // Concatenate validated parameters
178- let result = previous ?? { } ;
179- result = _ . merge ( result , obj ) ;
180- return result ;
181- }
182-
183117// CLI options declaration
184118program
185119 . option ( "-i, --inspect [host]:[port]" , "activate inspector on host:port (default: 127.0.0.1:9229)" , parseIp )
186120 . option ( "-ib, --inspect-brk [host]:[port]" , "activate inspector on host:port (default: 127.0.0.1:9229)" , parseIp )
187121 . option ( "-c, --client-only" , "do not start any servers (enables multiple instances without port conflicts)" )
188122 . option ( "-cp, --compiler <module>" , "load module as a compiler" )
189- . option (
190- "-f, --config-file <file>" ,
191- "load configuration from specified file" ,
192- parseConfigFile ,
193- "wot-servient.conf.json"
123+ . option ( "-f, --config-file <file>" , "load configuration from specified file" , ( value , previous ) =>
124+ parseConfigFile ( value , previous , schemaValidator )
194125 )
195126 . option (
196127 "-p, --config-params <param...>" ,
197128 "override configuration parameters [key1:=value1 key2:=value2 ...] (e.g. http.port:=8080)" ,
198- parseConfigParams
129+ ( value , previous ) => parseConfigParams ( value , previous , schemaValidator )
199130 ) ;
200131
201132// CLI arguments
@@ -206,189 +137,54 @@ program.addArgument(
206137 )
207138) ;
208139
209- program . parse ( process . argv ) ;
210- const options = program . opts ( ) ;
211- const args = program . args ;
212-
213- // .env parsing
214- const env : dotenv . DotenvConfigOutput = dotenv . config ( ) ;
215- const errorNoException : ErrnoException | undefined = env . error ;
216- if ( errorNoException ?. code !== "ENOENT" ) {
217- throw env . error ;
218- } else if ( env . parsed ) {
219- for ( const [ key , value ] of Object . entries ( env . parsed ) ) {
220- // Parse and validate on configfile-related entries
221- if ( key . startsWith ( "config." ) ) {
222- dotEnvConfigParameters [ key . replace ( "config." , "" ) ] = value ;
223- }
140+ program . action ( async function ( _ , options , cmd ) {
141+ if ( process . env . DEBUG == null ) {
142+ // by default enable error logs and warnings
143+ // user can override it using DEBUG env
144+ logger . enable ( "node-wot:**:error" ) ;
145+ logger . enable ( "node-wot:**:warn" ) ;
224146 }
225- }
226147
227- // Functions
228- async function buildConfig ( ) : Promise < unknown > {
229- const fileToOpen = options ?. configFile ?? path . join ( baseDir , defaultFile ) ;
230- let configFileData = { } ;
148+ const args = cmd . args ;
149+ const env = loadEnvVariables ( ) ;
150+ const defaultFilePath = path . join ( baseDir , defaultFile ) ;
151+ let servient : DefaultServient ;
152+
153+ debug ( "command line options %O" , options ) ;
154+ debug ( "command line arguments %O" , args ) ;
155+ debug ( "command line environment variables" , args ) ;
231156
232- // JSON config file
233157 try {
234- configFileData = JSON . parse ( await fs . promises . readFile ( fileToOpen , "utf-8" ) ) ;
158+ const config = await buildConfig ( options , defaultFilePath , env ) ;
159+ servient = new DefaultServient ( options . clientOnly , config ) ;
235160 } catch ( err ) {
236- error ( `WoT-Servient config file error: ${ err } ` ) ;
237- }
238-
239- // .env file
240- for ( const [ key , value ] of Object . entries ( dotEnvConfigParameters ) ) {
241- const obj = _ . set ( { } , key , value ) ;
242- configFileData = _ . merge ( configFileData , obj ) ;
243- }
244-
245- // CLI arguments
246- if ( options ?. configParams != null ) {
247- configFileData = _ . merge ( configFileData , options . configParams ) ;
248- }
249-
250- return configFileData ;
251- }
252- const loadCompilerFunction = function ( compilerModule : string | undefined ) {
253- if ( compilerModule != null ) {
254- const compilerMod = require ( compilerModule ) ;
255-
256- if ( compilerMod . create == null ) {
257- throw new Error ( "No create function defined for " + compilerModule ) ;
161+ if ( ( err as NodeJS . ErrnoException ) ?. code !== "ENOENT" || options . configFile != null ) {
162+ error ( "WoT-Servient config file error. %O" , err ) ;
163+ process . exit ( ( err as NodeJS . ErrnoException ) . errno ?? 1 ) ;
258164 }
259165
260- const compilerObject = compilerMod . create ( ) ;
261-
262- if ( compilerObject . compile == null ) {
263- throw new Error ( "No compile function defined for create return object" ) ;
264- }
265- return compilerObject . compile ;
266- }
267- return undefined ;
268- } ;
269- const loadEnvVariables = function ( ) {
270- const env : dotenv . DotenvConfigOutput = dotenv . config ( ) ;
271-
272- const errorNoException : ErrnoException | undefined = env . error ;
273- // ignore file not found but throw otherwise
274- if ( errorNoException ?. code !== "ENOENT" ) {
275- throw env . error ;
166+ warn ( `WoT-Servient using defaults as %s does not exist` , defaultFile ) ;
167+ servient = new DefaultServient ( options . clientOnly ) ;
276168 }
277- return env ;
278- } ;
279-
280- const runScripts = async function ( servient : DefaultServient , scripts : Array < string > , debug ?: DebugParams ) {
281- const env = loadEnvVariables ( ) ;
282169
283- const launchScripts = ( scripts : Array < string > ) => {
284- const compile = loadCompilerFunction ( options . compiler ) ;
285- scripts . forEach ( ( fname : string ) => {
286- info ( `WoT-Servient reading script ${ fname } ` ) ;
287- fs . readFile ( fname , "utf8" , ( err , data ) => {
288- if ( err ) {
289- error ( `WoT-Servient experienced error while reading script. ${ err } ` ) ;
290- } else {
291- // limit printout to first line
292- info (
293- `WoT-Servient running script '${ data . substr ( 0 , data . indexOf ( "\n" ) ) . replace ( "\r" , "" ) } '... (${
294- data . split ( / \r \n | \r | \n / ) . length
295- } lines)`
296- ) ;
170+ await servient . start ( ) ;
297171
298- fname = path . resolve ( fname ) ;
299- servient . runScript ( data , fname , {
300- argv : args ,
301- env : env . parsed ,
302- compiler : compile ,
303- } ) ;
304- }
305- } ) ;
306- } ) ;
172+ const scriptOptions : ScriptOptions = {
173+ env,
174+ argv : args ,
175+ compiler : loadCompiler ( options . compiler ) ,
307176 } ;
308177
309- if ( debug && debug . shouldBreak ) {
310- // Activate inspector only if is not already opened and wait for the debugger to attach
311- inspector . url ( ) == null && inspector . open ( debug . port , debug . host , true ) ;
312-
313- // Set a breakpoint at the first line of of first script
314- // the breakpoint gives time to inspector clients to set their breakpoints
315- const session = new inspector . Session ( ) ;
316- session . connect ( ) ;
317- session . post ( "Debugger.enable" , ( error : Error ) => {
318- if ( error != null ) {
319- warn ( "Cannot set breakpoint; reason: cannot enable debugger" ) ;
320- warn ( error . toString ( ) ) ;
321- }
322-
323- session . post (
324- "Debugger.setBreakpointByUrl" ,
325- {
326- lineNumber : 0 ,
327- url : "file:///" + path . resolve ( scripts [ 0 ] ) . replace ( / \\ / g, "/" ) ,
328- } ,
329- ( err : Error | null ) => {
330- if ( err != null ) {
331- warn ( "Cannot set breakpoint" ) ;
332- warn ( error . toString ( ) ) ;
333- }
334- launchScripts ( scripts ) ;
335- }
336- ) ;
337- } ) ;
338- } else {
339- // Activate inspector only if is not already opened and don't wait
340- debug != null && inspector . url ( ) == null && inspector . open ( debug . port , debug . host , false ) ;
341- launchScripts ( scripts ) ;
178+ if ( args . length > 0 ) {
179+ return runScripts ( servient , args , scriptOptions , options . inspect ?? options . inspectBrk ) ;
342180 }
343- } ;
344181
345- const runAllScripts = function ( servient : DefaultServient , debug ?: DebugParams ) {
346- fs . readdir ( baseDir , ( err , files ) => {
347- if ( err ) {
348- warn ( `WoT-Servient experienced error while loading directory. ${ err } ` ) ;
349- return ;
350- }
182+ const files = await readdir ( baseDir ) ;
183+ const scripts = files . filter ( ( file ) => ! file . startsWith ( "." ) && file . slice ( - 3 ) === ".js" ) ;
351184
352- // unhidden .js files
353- const scripts = files . filter ( ( file ) => {
354- return file . substr ( 0 , 1 ) !== "." && file . slice ( - 3 ) === ".js" ;
355- } ) ;
356- info ( `WoT-Servient using current directory with ${ scripts . length } script${ scripts . length > 1 ? "s" : "" } ` ) ;
185+ info ( `WoT-Servient using current directory with %d script${ scripts . length > 1 ? "s" : "" } ` , scripts . length ) ;
357186
358- runScripts (
359- servient ,
360- scripts . map ( ( filename ) => path . resolve ( path . join ( baseDir , filename ) ) ) ,
361- debug
362- ) ;
363- } ) ;
364- } ;
187+ return runScripts ( servient , args , scriptOptions , options . inspect ?? options . inspectBrk ) ;
188+ } ) ;
365189
366- buildConfig ( )
367- . then ( ( conf ) => {
368- return new DefaultServient ( options . clientOnly , conf ) ;
369- } )
370- . catch ( ( err ) => {
371- if ( err . code === "ENOENT" && options . configFile == null ) {
372- warn ( `WoT-Servient using defaults as '${ defaultFile } ' does not exist` ) ;
373- return new DefaultServient ( options . clientOnly ) ;
374- } else {
375- error ( `"WoT-Servient config file error. ${ err } ` ) ;
376- process . exit ( err . errno ) ;
377- }
378- } )
379- . then ( ( servient ) => {
380- servient
381- . start ( )
382- . then ( ( ) => {
383- if ( args . length > 0 ) {
384- info ( `WoT-Servient loading ${ args . length } command line script${ args . length > 1 ? "s" : "" } ` ) ;
385- return runScripts ( servient , args , options . inspect ?? options . inspectBrk ) ;
386- } else {
387- return runAllScripts ( servient , options . inspect ?? options . inspectBrk ) ;
388- }
389- } )
390- . catch ( ( err ) => {
391- error ( `WoT-Servient cannot start. ${ err } ` ) ;
392- } ) ;
393- } )
394- . catch ( ( err ) => error ( `WoT-Servient main error. ${ err } ` ) ) ;
190+ program . parse ( process . argv ) ;
0 commit comments