/* This file is part of »ivaldi«. Copyright 2025 'Fenris Wolf' »ivaldi« is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. »ivaldi« is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with »ivaldi«. If not, see . */ namespace _ivaldi { /** * @todo outsource */ function execute( command : string, { "log_only": log_only = false, "working_directory": working_directory = null, } : { log_only ?: boolean; working_directory ?: (null | string); } = { } ) : Promise< { code : int; stdout : string; stderr : string; } > { const nm_child_process = require("child_process"); return ( new Promise( (resolve, reject) => { lib_plankton.log._info( "execute", { "details": { "command": command, } } ); if (log_only) { resolve( { "code": 0, "stdout": "", "stderr": "", } ); } else { nm_child_process.exec( command, { "cwd": ((working_directory === null) ? undefined : working_directory), }, (error, stdout, stderr) => { if (error) { reject(error); } else { resolve( { /** * @todo assign correct value */ "code": 0, "stdout": stdout, "stderr": stderr, } ); } } ); } } ) ); } /** */ function get_app_facade_name( app : type_app ) : string { return lib_plankton.string.coin( "app-{{name}}", { "name": app.name, } ); } /** */ export async function main( args_raw : Array ) : Promise { // args const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler( { "action": lib_plankton.args.class_argument.positional({ "index": 0, "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "help", "info": "what to do : fetch | clear | build | deploy", "name": "action", }), "data_path": lib_plankton.args.class_argument.volatile({ "indicators_long": ["data-path"], "indicators_short": ["d"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "ivaldi.json", "info": "path to data file", "name": "data-path", }), "verbosity": lib_plankton.args.class_argument.volatile({ "indicators_long": ["verbosity"], "indicators_short": ["v"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "notice", "info": "error | warning | notice | info | debug", "name": "verbosity", }), "help": lib_plankton.args.class_argument.volatile({ "indicators_long": ["help"], "indicators_short": ["h"], "type": lib_plankton.args.enum_type.boolean, "mode": lib_plankton.args.enum_mode.replace, "default": false, "info": "alias for action 'help'", "name": "help", }), } ); const args : Record = arg_handler.read( lib_plankton.args.enum_environment.cli, args_raw.join(" ") ); // init lib_plankton.log.set_main_logger( [ { "kind": "filtered", "data": { "core": { "kind": "std", "data": { "target": "stdout", "format": { "kind": "human_readable", "data": { } } } }, "predicate": [ [ { "item": { "kind": "level", "data": { "threshold": args.verbosity, } }, } ] ], } }, ] ); // exec if (args.help) { process.stdout.write( arg_handler.generate_help( { "programname": "ivaldi", "description": "a collection of tools for building and deploying a software project", "executable": "ivaldi", } ) ); } else { const content : string = await lib_plankton.file.read(args.data_path); const data_raw : any = lib_plankton.json.decode(content); const data : _ivaldi.type_data = { "version": data_raw["version"], "name": data_raw["name"], "target": ( data_raw["target"] ?? "node" ), "settings": { "typescript": { "command": (((data_raw["settings"] ?? {})["typescript"] ?? {})["command"] ?? "tsc"), "libs": (((data_raw["settings"] ?? {})["typescript"] ?? {})["libs"] ?? ["es2020"]), }, }, "directories": { "lib": ( (data_raw["misc"] ?? {})["lib"] ?? "lib" ), "source": ( (data_raw["misc"] ?? {})["source"] ?? "source" ), "misc": ( (data_raw["misc"] ?? {})["misc"] ?? "misc" ), "temp": ( (data_raw["misc"] ?? {})["temp"] ?? lib_plankton.string.coin("/tmp/_{{name}}", {"name": data_raw["name"]}) ), "build": ( (data_raw["misc"] ?? {})["build"] ?? lib_plankton.string.coin("/tmp/{{name}}", {"name": data_raw["name"]}) ), "makefile": ( (data_raw["misc"] ?? {})["makefile"] ?? lib_plankton.string.coin("/tmp/", {}) ), }, "libs": (data_raw["libs"] ?? {}), "core": data_raw["core"], "apps": ( data_raw["apps"].map( (app_raw : Record) => ( { "name": app_raw["name"], "sources": app_raw["sources"], "additional_dependencies": (app_raw["additional_dependencies"] ?? []), "destination": (app_raw["destination"] ?? "{{project}}-{{app}}"), "head": (app_raw["head"] ?? null), "executable": (app_raw["executable"] ?? false), } ) ) ), "custom_rules": ( (data_raw["custom_rules"] ?? []).map( (custom_rule_raw : Record) => ( { "targets": custom_rule_raw["targets"], "dependencies": custom_rule_raw["dependencies"], "actions": custom_rule_raw["actions"], "info": (custom_rule_raw["info"] ?? null), } ) ) ), }; switch (args.action) { default: { return Promise.reject((new Error("unhandled action: " + args.action))); break; } case "fetch": { // plankton { if (data.libs.plankton === null) { // do nothing } else { const directory : string = lib_plankton.string.coin( "{{directory}}/plankton", { "directory": data.directories.lib, } ); /** * @todo do not use "execute" for that */ await execute( _ivaldi.command_mkdir(directory), ); await execute( _ivaldi.command_ptk_bundle(data.target, data.libs.plankton), { "working_directory": directory, } ); } } // node { if (data.libs.node === null) { // do nothing } else { const directory : string = lib_plankton.string.coin( "{{directory}}/node", { "directory": data.directories.lib, } ); /** * @todo do not use "execute" for that */ await execute( _ivaldi.command_mkdir(directory) ); await execute( _ivaldi.command_npm_install(data.libs.node), { "working_directory": directory, } ); } } return Promise.resolve(undefined); break; } case "clear": { const commands : Array = [ _ivaldi.command_delete(data.directories.temp), _ivaldi.command_delete(data.directories.build), ]; /** * @todo do not use "execute" for that */ for (const command of commands) { await execute(command); } return Promise.resolve(undefined); break; } case "build": { const has_plankton : boolean = ((data.libs.plankton !== null) && (data.libs.plankton.length > 0)); const path_plankton_declaration : string = lib_plankton.string.coin( "{{directory_libs}}/plankton/plankton.d.ts", { "directory_libs": data.directories.lib, } ); const path_plankton_implementation : string = lib_plankton.string.coin( "{{directory_libs}}/plankton/plankton.js", { "directory_libs": data.directories.lib, } ); const path_core_declaration : string = lib_plankton.string.coin( "{{directory_temp}}/core-unlinked.d.ts", { "directory_temp": data.directories.temp, } ); const path_core_implementation : string = lib_plankton.string.coin( "{{directory_temp}}/core-unlinked.js", { "directory_temp": data.directories.temp, } ); const gnu_make_sheet : lib_plankton.gnu_make.type_sheet = { "rules": ( (new Array>) // root .concat( [ { "kind": "meta", "data": { "phony": true, "name": "_root", "dependencies": data.apps.map(app => get_app_facade_name(app)), } }, ] ) // node modules .concat( [ { "kind": "concrete", "data": { "targets": [ "node_modules" ], "dependencies": [], "actions": [ _ivaldi.command_log("placing node modules …"), _ivaldi.command_mkdir( lib_plankton.string.coin("{{directory}}/node_modules", {"directory": data.directories.build}) ), _ivaldi.command_copy( lib_plankton.string.coin("{{directory}}/node/node_modules/*", {"directory": data.directories.lib}), lib_plankton.string.coin("{{directory}}/node_modules/", {"directory": data.directories.build}) ) ] } }, ] ) // custom rules .concat( [ data.custom_rules .map( data_custom_rule => ({ "kind": "concrete", "data": { "targets": ( data_custom_rule.targets .map( target => lib_plankton.string.coin( target, { "dir_build": data.directories.build, } ) ) ), "dependencies": ( data_custom_rule.dependencies.map( dependency => lib_plankton.string.coin( dependency, { "dir_source": data.directories.source, "dir_misc": data.directories.misc, } ), ) ), "actions": ( (new Array()) .concat( (data_custom_rule.info === null) ? [] : [ _ivaldi.command_log( lib_plankton.string.coin( "{{info}} …", { "info": data_custom_rule.info, } ), ) ] ) .concat( /** * @todo coin? */ data_custom_rule.actions ) ), } }) ) ] ) // core .concat( [ [ { "kind": "concrete", "data": { "targets": [ path_core_declaration, path_core_implementation, ], "dependencies": ( (new Array()) .concat(has_plankton ? [path_plankton_declaration] : []) .concat( data.core.map( path => lib_plankton.string.coin( "{{directory}}/{{path}}", { "directory": data.directories.source, "path": path, } ) ) ) ), "actions": [ // log _ivaldi.command_log("core | compiling …"), // mkdir _ivaldi.command_mkdir("$(dir $@)"), // command _ivaldi.command_tsc( ["$^"], path_core_implementation, { "declaration": true, "head": data.settings.typescript.command, "libs": data.settings.typescript.libs, } ), ] } }, ] ] ) // apps .concat( data.apps .map( app => { const paths_sources : Array = ( app.sources .map( source => lib_plankton.string.coin( "{{directory}}/{{name}}", { "directory": data.directories.source, "name": source, } ) ) ); const path_unlinked : string = lib_plankton.string.coin( "{{directory_temp}}/app-{{name}}-unlinked.js", { "directory_temp": data.directories.temp, "name": app.name, } ); /* const path_head : string = lib_plankton.string.coin( "{{directory_temp}}/head.js", { "directory_temp": data.directories.temp, } ); */ const path_result : string = lib_plankton.string.coin( "{{directory_build}}/{{destination}}", { "directory_build": data.directories.build, "destination": lib_plankton.string.coin( app.destination, { "project": data.name, "app": app.name, } ), } ); return [ // compile { "kind": "concrete", "data": { "targets": [ path_unlinked, ], "dependencies": ( (new Array()) .concat(has_plankton ? [path_plankton_declaration] : []) .concat([path_core_declaration]) .concat(paths_sources) ), "actions": [ _ivaldi.command_log(lib_plankton.string.coin("app:{{name}} | compiling …", {"name": app.name})), _ivaldi.command_mkdir("$(dir $@)"), _ivaldi.command_tsc( ["$^"], "$@", { "head": data.settings.typescript.command, "libs": data.settings.typescript.libs, } ), ] } }, // link { "kind": "concrete", "data": { "targets": [ path_result, ], "dependencies": ( (new Array()) // .concat([path_head]) .concat(has_plankton ? [path_plankton_implementation] : []) .concat([path_unlinked]) ), "actions": ( (new Array()) .concat( [ _ivaldi.command_log(lib_plankton.string.coin("app:{{name}} | linking …", {"name": app.name})), _ivaldi.command_mkdir("$(dir $@)"), lib_plankton.string.coin("echo '{{head}}' > {{path}}", {"head": (app.head ?? ""), "path": path_result}), _ivaldi.command_cat(["$^"], "$@", {"append": true}), ] ) // make executable .concat( ( app.executable ? [ /** * @todo _ivaldi.command_ */ "chmod +x $@", ] : [ ] ) ) ) }, }, // facade { "kind": "meta", "data": { "phony": true, "name": get_app_facade_name(app), "dependencies": ( (new Array()) .concat( [ /** * @todo variable */ "node_modules", path_result, ] ) // additional dependencies .concat( app.additional_dependencies .map( dependency => lib_plankton.string.coin( dependency, { "dir_source": data.directories.source, "dir_misc": data.directories.misc, "dir_temp": data.directories.temp, "dir_build": data.directories.build, } ) ) ) ), }, }, ]; } ) ) .reduce( (x, y) => x.concat(y), [] ) ), }; const makefile : string = lib_plankton.gnu_make.render_sheet( gnu_make_sheet, { "break_dependencies": true, "silent_actions": true, } ); const makefile_path : string = lib_plankton.string.coin( "{{directory}}/ivaldi-makefile-{{name}}", { "directory": data.directories.makefile, "name": data.name, } ); await lib_plankton.file.write( makefile_path, makefile ); const command : string = lib_plankton.string.coin( "make -f {{path}}", { "path": makefile_path, } ); const result = await execute(command); process.stdout.write(result.stdout + "\n"); process.stderr.write(result.stderr + "\n"); return Promise.resolve(undefined); break; } case "deploy": { return Promise.reject(new Error("not yet implemented")); break; } } } } } _ivaldi.main(process.argv.slice(2)) .then(() => {}) .catch((reason) => {process.stderr.write(String(reason) + "\n");}) ;