/* 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 { /** */ function refine_definition( definition_raw : any ) : _ivaldi.type_definition { return { "version": definition_raw["version"], "name": definition_raw["name"], "target": ( definition_raw["target"] ?? "node" ), "settings": { "typescript": { "command": (((definition_raw["settings"] ?? {})["typescript"] ?? {})["command"] ?? "tsc"), "libs": (((definition_raw["settings"] ?? {})["typescript"] ?? {})["libs"] ?? ["es2020"]), }, }, "directories": { "lib": ( (definition_raw["misc"] ?? {})["lib"] ?? "lib" ), "source": ( (definition_raw["misc"] ?? {})["source"] ?? "source" ), "misc": ( (definition_raw["misc"] ?? {})["misc"] ?? "misc" ), }, "libs": ((libs_raw) => ({ "plankton": (libs_raw["plankton"] ?? null), "node": (libs_raw["node"] ?? null), })) (definition_raw["libs"] ?? {}), "core": definition_raw["core"], "apps": ( definition_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": ( (definition_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), } ) ) ), }; } /** */ function get_target_name( app_name : string ) : string { return lib_plankton.string.coin( "app-{{name}}", { "name": app_name, } ); } /** */ function get_temp_directory( definition : _ivaldi.type_definition, pattern : string ) : string { return lib_plankton.string.coin( pattern, { "project": definition.name, } ); } /** */ function get_build_directory( definition : _ivaldi.type_definition, pattern : string ) : string { return lib_plankton.string.coin( pattern, { "project": definition.name, } ); } /** */ async function action_fetch( definition : _ivaldi.type_definition ) : Promise { // plankton { if (definition.libs.plankton === null) { // do nothing } else { const directory : string = lib_plankton.string.coin( "{{directory}}/plankton", { "directory": definition.directories.lib, } ); /** * @todo do not use "_ivaldi.execute" for that */ await _ivaldi.execute( _ivaldi.command_mkdir(directory), ); await _ivaldi.execute( _ivaldi.command_ptk_bundle(definition.target, definition.libs.plankton), { "working_directory": directory, } ); } } // node { if (definition.libs.node === null) { // do nothing } else { const directory : string = lib_plankton.string.coin( "{{directory}}/node", { "directory": definition.directories.lib, } ); /** * @todo do not use "_ivaldi.execute" for that */ await _ivaldi.execute( _ivaldi.command_mkdir(directory) ); await _ivaldi.execute( _ivaldi.command_npm_install(definition.libs.node), { "working_directory": directory, } ); } } } /** */ async function action_clear( definition : _ivaldi.type_definition, temp_directory_pattern : string, build_directory_pattern : string ) : Promise { const commands : Array = [ _ivaldi.command_delete(get_temp_directory(definition, temp_directory_pattern)), _ivaldi.command_delete(get_build_directory(definition, build_directory_pattern)), ]; /** * @todo do not use "_ivaldi.execute" for that */ for (const command of commands) { await _ivaldi.execute(command); } } /** */ async function action_build( definition : _ivaldi.type_definition, assets : Array<{from : string; to : string;}>, temp_directory_pattern : string, build_directory_pattern : string, makefile_path_pattern : string, targets : (null | Array), { "show_actions": show_actions = true, } : { show_actions ?: boolean } = { } ) : Promise { const temp_directory : string = get_build_directory(definition, temp_directory_pattern); const build_directory : string = get_build_directory(definition, build_directory_pattern); // makefile stuff { const has_plankton : boolean = ((definition.libs.plankton !== null) && (definition.libs.plankton.length > 0)); const has_node : boolean = ((definition.libs.node !== null) && (definition.libs.node.length > 0)); const path_plankton_declaration : string = lib_plankton.string.coin( "{{directory_libs}}/plankton/plankton.d.ts", { "directory_libs": definition.directories.lib, } ); const path_plankton_implementation : string = lib_plankton.string.coin( "{{directory_libs}}/plankton/plankton.js", { "directory_libs": definition.directories.lib, } ); const path_core_declaration : string = lib_plankton.string.coin( "{{directory_temp}}/core-unlinked.d.ts", { "directory_temp": temp_directory, } ); const path_core_implementation : string = lib_plankton.string.coin( "{{directory_temp}}/core-unlinked.js", { "directory_temp": temp_directory, } ); const gnu_make_sheet : lib_plankton.gnu_make.type_sheet = { "rules": ( (new Array>) // root .concat( [ { "kind": "meta", "data": { "phony": true, "name": "_root", "dependencies": definition.apps.map((app : _ivaldi.type_app) => get_target_name(app.name)), } }, ] ) // node modules .concat( has_node ? [ { "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": build_directory}) ), _ivaldi.command_copy( lib_plankton.string.coin("{{directory}}/node/node_modules/*", {"directory": definition.directories.lib}), lib_plankton.string.coin("{{directory}}/node_modules/", {"directory": build_directory}) ) ] } }, ] : [] ) // custom rules .concat( [ definition.custom_rules .map( custom_rule => ({ "kind": "concrete", "data": { "targets": ( custom_rule.targets .map( target => lib_plankton.string.coin( target, { "dir_build": build_directory, } ) ) ), "dependencies": ( custom_rule.dependencies.map( dependency => lib_plankton.string.coin( dependency, { "dir_source": definition.directories.source, "dir_misc": definition.directories.misc, } ), ) ), "actions": ( (new Array()) .concat( (custom_rule.info === null) ? [] : [ _ivaldi.command_log( lib_plankton.string.coin( "{{info}} …", { "info": custom_rule.info, } ), ) ] ) .concat( /** * @todo coin? */ 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( definition.core.map( path => lib_plankton.string.coin( "{{directory}}/{{path}}", { "directory": definition.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": definition.settings.typescript.command, "libs": definition.settings.typescript.libs, } ), ] } }, ] ] ) // apps .concat( definition.apps .map( app => { const paths_sources : Array = ( app.sources .map( source => lib_plankton.string.coin( "{{directory}}/{{name}}", { "directory": definition.directories.source, "name": source, } ) ) ); const path_app_unlinked : string = lib_plankton.string.coin( "{{directory_temp}}/app-{{name}}-unlinked.js", { "directory_temp": temp_directory, "name": app.name, } ); const path_app_complete : string = lib_plankton.string.coin( "{{directory_build}}/{{destination}}", { "directory_build": build_directory, "destination": lib_plankton.string.coin( app.destination, { "project": definition.name, "app": app.name, } ), } ); return [ // compile { "kind": "concrete", "data": { "targets": [ path_app_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": definition.settings.typescript.command, "libs": definition.settings.typescript.libs, } ), ] } }, // link { "kind": "concrete", "data": { "targets": [ path_app_complete, ], "dependencies": ( (new Array()) .concat(has_plankton ? [path_plankton_implementation] : []) .concat([path_core_implementation]) .concat([path_app_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_app_complete}), _ivaldi.command_cat(["$^"], "$@", {"append": true}), ] ) // make executable .concat( ( app.executable ? [ /** * @todo _ivaldi.command_ */ "chmod +x $@", ] : [ ] ) ) ) }, }, // facade { "kind": "meta", "data": { "phony": true, "name": get_target_name(app.name), "dependencies": ( (new Array()) .concat(has_node ? ["node_modules"] : []) .concat([path_app_complete]) // additional dependencies .concat( app.additional_dependencies .map( dependency => lib_plankton.string.coin( dependency, { "dir_source": definition.directories.source, "dir_misc": definition.directories.misc, "dir_temp": temp_directory, "dir_build": build_directory, } ) ) ) ), }, }, ]; } ) ) .reduce( (x, y) => x.concat(y), [] ) ), }; const makefile_content : string = lib_plankton.gnu_make.render_sheet( gnu_make_sheet, { "break_dependencies": true, "silent_actions": (! show_actions), } ); const makefile_path : string = lib_plankton.string.coin( makefile_path_pattern, { "project": definition.name, } ); await lib_plankton.file.write( makefile_path, makefile_content ); const command : string = lib_plankton.string.coin( "make --file={{path}} {{targets}}", { "path": makefile_path, "targets": ( (targets === null) ? "" : targets.map(target => get_target_name(target)).join(" ") ), } ); const result = await _ivaldi.execute(command); process.stdout.write(result.stdout + "\n"); process.stderr.write(result.stderr + "\n"); } // assets { const commands : Array = assets.map( asset => _ivaldi.command_copy( asset.from, lib_plankton.string.coin("{{directory}}/{{path}}", {"directory": build_directory, "path": asset.to}) ) ); for (const command of commands) { await _ivaldi.execute(command); } } } /** */ /* async function action_deploy( definition : _ivaldi.type_definition, build_directory_pattern : string, deploy_system : string, deploy_directory : string ) : Promise { const command : string = lib_plankton.string.coin( "rsync --recursive --update{{excludes}} {{from}}/ {{to_system}}:{{to_directory}}", { "from": get_build_directory(definition, build_directory_pattern), "to_system": deploy_system, "to_directory": deploy_directory, } ); await _ivaldi.execute(command); } */ /** */ 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", }), "definition_path": lib_plankton.args.class_argument.volatile({ "indicators_long": ["definition-path"], "indicators_short": ["d"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "ivaldi.json", "info": "path to definition file", "name": "definition-path", }), "assets": lib_plankton.args.class_argument.volatile({ "indicators_long": ["asset"], "indicators_short": ["a"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.accumulate, "default": [], "info": ":", "name": "assets", }), "targets": lib_plankton.args.class_argument.volatile({ "indicators_long": ["target"], "indicators_short": ["t"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.accumulate, "default": [], "info": "list of targets to build", "name": "targets", }), "temp_directory_pattern": lib_plankton.args.class_argument.volatile({ "indicators_long": ["temp-directory"], "indicators_short": ["T"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "/tmp/{{project}}-temp", "info": "pattern for temp directory", "name": "temp-directory-pattern", }), "build_directory_pattern": lib_plankton.args.class_argument.volatile({ "indicators_long": ["build-directory"], "indicators_short": ["B"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "/tmp/{{project}}", "info": "pattern for build directory", "name": "build-directory-pattern", }), "makefile_path_pattern": lib_plankton.args.class_argument.volatile({ "indicators_long": ["makefile-path"], "indicators_short": ["M"], "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "/tmp/{{project}}-makefile", "info": "pattern for makefile path", "name": "makefile-path-pattern", }), "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 on basis of typescript and node", "executable": "ivaldi", } ) ); } else { const content : string = await lib_plankton.file.read(args.definition_path as string); const data_raw : any = lib_plankton.json.decode(content); const definition : _ivaldi.type_definition = refine_definition(data_raw); switch (args.action) { default: { return Promise.reject((new Error("unhandled action: " + args.action))); break; } case "fetch": { await action_fetch( definition ); break; } case "clear": { await action_clear( definition, args.temp_directory_pattern, args.build_directory_pattern ); break; } case "build": { await action_build( definition, args.assets.map( (asset_raw : string) => { const parts : Array = asset_raw.split(":", 2); return {"from": parts[0], "to": parts[1]}; } ), args.temp_directory_pattern, args.build_directory_pattern, args.makefile_path_pattern, ((args.targets.length <= 0) ? null : args.targets), { "show_actions": ["debug"].includes(args.verbosity), } ); break; } /* case "deploy": { await action_deploy( definition, args.build_directory_pattern, args.deploy_system, args.deploy_directory ); break; } */ } } } } _ivaldi.main(process.argv.slice(2)) .then(() => {}) .catch((reason) => {process.stderr.write(String(reason) + "\n");}) ;