ivaldi/source/main.ts

753 lines
18 KiB
TypeScript
Raw Normal View History

2025-04-25 10:47:04 +02:00
/*
2025-04-25 12:50:44 +02:00
This file is part of »ivaldi«.
2025-04-25 10:47:04 +02:00
Copyright 2025 'Fenris Wolf' <fenris@folksprak.org>
2025-04-25 12:50:44 +02:00
»ivaldi« is free software: you can redistribute it and/or modify
2025-04-25 10:47:04 +02:00
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.
2025-04-25 12:50:44 +02:00
»ivaldi« is distributed in the hope that it will be useful,
2025-04-25 10:47:04 +02:00
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
2025-04-25 12:50:44 +02:00
along with »ivaldi«. If not, see <http://www.gnu.org/licenses/>.
2025-04-25 10:47:04 +02:00
*/
2025-04-25 12:50:44 +02:00
namespace _ivaldi
2025-04-25 10:47:04 +02:00
{
/**
* @todo outsource
*/
function execute(
command : string,
{
"log_only": log_only = false,
2025-04-25 10:54:26 +02:00
"working_directory": working_directory = null,
2025-04-25 10:47:04 +02:00
} : {
log_only ?: boolean;
2025-04-25 10:54:26 +02:00
working_directory ?: (null | string);
2025-04-25 10:47:04 +02:00
} = {
}
) : 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,
{
2025-04-25 10:54:26 +02:00
"cwd": ((working_directory === null) ? undefined : working_directory),
2025-04-25 10:47:04 +02:00
},
(error, stdout, stderr) => {
if (error) {
reject(error);
}
else {
resolve(
{
/**
* @todo assign correct value
*/
"code": 0,
"stdout": stdout,
"stderr": stderr,
}
);
}
}
);
}
}
)
);
}
2026-09-18 02:13:11 +02:00
/**
*/
function get_app_facade_name(
app : type_app
) : string
{
return lib_plankton.string.coin(
"app-{{name}}",
{
"name": app.name,
}
);
}
2025-04-25 10:47:04 +02:00
/**
*/
export async function main(
args_raw : Array<string>
) : Promise<void>
{
// 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<string, any> = 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
2026-09-18 02:13:11 +02:00
if (args.help)
{
2025-04-25 10:47:04 +02:00
process.stdout.write(
arg_handler.generate_help(
{
2025-07-08 15:06:16 +02:00
"programname": "ivaldi",
"description": "a collection of tools for building and deploying a software project",
"executable": "ivaldi",
2025-04-25 10:47:04 +02:00
}
)
);
}
2026-09-18 02:13:11 +02:00
else
{
2025-04-25 10:47:04 +02:00
const content : string = await lib_plankton.file.read(args.data_path);
const data_raw : any = lib_plankton.json.decode(content);
2025-04-25 12:50:44 +02:00
const data : _ivaldi.type_data = {
2025-04-25 10:47:04 +02:00
"version": data_raw["version"],
"name": data_raw["name"],
2025-07-08 15:06:16 +02:00
"target": (
data_raw["target"]
??
"node"
),
2026-09-18 02:31:28 +02:00
"settings": {
"typescript": {
"command": (((data_raw["settings"] ?? {})["typescript"] ?? {})["command"] ?? "tsc"),
"libs": (((data_raw["settings"] ?? {})["typescript"] ?? {})["libs"] ?? ["es2020"]),
},
},
2025-04-25 10:47:04 +02:00
"directories": {
2025-07-08 15:06:16 +02:00
"lib": (
(data_raw["misc"] ?? {})["lib"]
??
"lib"
),
"source": (
(data_raw["misc"] ?? {})["source"]
??
"source"
),
2026-09-18 02:13:11 +02:00
"misc": (
(data_raw["misc"] ?? {})["misc"]
??
"misc"
),
2025-07-08 15:06:16 +02:00
"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"]
??
2026-09-18 02:13:11 +02:00
lib_plankton.string.coin("/tmp/", {})
2025-07-08 15:06:16 +02:00
),
},
2026-09-18 02:13:11 +02:00
"libs": (data_raw["libs"] ?? {}),
"core": data_raw["core"],
"apps": (
data_raw["apps"].map(
(app_raw : Record<string, any>) => (
{
"name": app_raw["name"],
"sources": app_raw["sources"],
"additional_dependencies": (app_raw["additional_dependencies"] ?? []),
"destination": (app_raw["destination"] ?? "{{project}}-{{app}}"),
2026-09-18 02:31:28 +02:00
"head": (app_raw["head"] ?? null),
2026-09-18 02:13:11 +02:00
"executable": (app_raw["executable"] ?? false),
}
)
)
),
"custom_rules": (
(data_raw["custom_rules"] ?? []).map(
(custom_rule_raw : Record<string, any>) => (
{
"targets": custom_rule_raw["targets"],
"dependencies": custom_rule_raw["dependencies"],
"actions": custom_rule_raw["actions"],
"info": (custom_rule_raw["info"] ?? null),
}
)
)
),
2025-04-25 10:47:04 +02:00
};
2026-09-18 02:13:11 +02:00
switch (args.action)
{
default:
{
2025-04-25 10:47:04 +02:00
return Promise.reject<void>((new Error("unhandled action: " + args.action)));
break;
}
2026-09-18 02:13:11 +02:00
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,
}
);
2025-04-25 10:47:04 +02:00
}
}
return Promise.resolve<void>(undefined);
break;
}
2026-09-18 02:13:11 +02:00
case "clear":
{
2025-04-25 10:47:04 +02:00
const commands : Array<string> = [
2026-09-18 02:13:11 +02:00
_ivaldi.command_delete(data.directories.temp),
_ivaldi.command_delete(data.directories.build),
2025-04-25 10:47:04 +02:00
];
2025-04-25 10:54:26 +02:00
/**
* @todo do not use "execute" for that
*/
2026-09-18 02:13:11 +02:00
for (const command of commands)
{
2025-04-25 10:47:04 +02:00
await execute(command);
}
return Promise.resolve<void>(undefined);
break;
}
2026-09-18 02:13:11 +02:00
case "build":
{
const has_plankton : boolean = ((data.libs.plankton !== null) && (data.libs.plankton.length > 0));
2025-04-25 10:47:04 +02:00
const path_plankton_declaration : string = lib_plankton.string.coin(
"{{directory_libs}}/plankton/plankton.d.ts",
{
2025-07-08 15:06:16 +02:00
"directory_libs": data.directories.lib,
2025-04-25 10:47:04 +02:00
}
);
const path_plankton_implementation : string = lib_plankton.string.coin(
"{{directory_libs}}/plankton/plankton.js",
{
2025-07-08 15:06:16 +02:00
"directory_libs": data.directories.lib,
2025-04-25 10:47:04 +02:00
}
);
2026-09-18 02:13:11 +02:00
const path_core_declaration : string = lib_plankton.string.coin(
"{{directory_temp}}/core-unlinked.d.ts",
2025-04-25 10:47:04 +02:00
{
2025-07-08 15:06:16 +02:00
"directory_temp": data.directories.temp,
2025-04-25 10:47:04 +02:00
}
);
2026-09-18 02:13:11 +02:00
const path_core_implementation : string = lib_plankton.string.coin(
"{{directory_temp}}/core-unlinked.js",
2025-04-25 10:47:04 +02:00
{
2025-07-08 15:06:16 +02:00
"directory_temp": data.directories.temp,
2025-04-25 10:47:04 +02:00
}
);
const gnu_make_sheet : lib_plankton.gnu_make.type_sheet = {
2026-09-18 02:13:11 +02:00
"rules": (
(new Array<Array<lib_plankton.gnu_make.type_rule>>)
2025-04-25 10:47:04 +02:00
// root
2026-09-18 02:13:11 +02:00
.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<string>())
.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
)
),
2025-04-25 10:47:04 +02:00
}
2026-09-18 02:13:11 +02:00
})
)
]
)
// core
.concat(
[
[
{
"kind": "concrete",
"data": {
"targets": [
path_core_declaration,
path_core_implementation,
],
"dependencies": (
(new Array<string>())
.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,
2026-09-18 02:31:28 +02:00
"head": data.settings.typescript.command,
"libs": data.settings.typescript.libs,
2026-09-18 02:13:11 +02:00
}
),
]
2025-04-25 10:47:04 +02:00
}
2026-09-18 02:13:11 +02:00
},
]
]
)
// apps
.concat(
data.apps
.map(
app => {
const paths_sources : Array<string> = (
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",
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"directory_temp": data.directories.temp,
"name": app.name,
2025-04-25 10:47:04 +02:00
}
2026-09-18 02:13:11 +02:00
);
/*
const path_head : string = lib_plankton.string.coin(
"{{directory_temp}}/head.js",
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"directory_temp": data.directories.temp,
2025-04-25 10:47:04 +02:00
}
2026-09-18 02:13:11 +02:00
);
*/
const path_result : string = lib_plankton.string.coin(
"{{directory_build}}/{{destination}}",
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"directory_build": data.directories.build,
"destination": lib_plankton.string.coin(
app.destination,
{
"project": data.name,
"app": app.name,
}
),
2025-04-25 10:47:04 +02:00
}
2026-09-18 02:13:11 +02:00
);
return [
// compile
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"kind": "concrete",
"data": {
"targets": [
path_unlinked,
],
"dependencies": (
(new Array<string>())
.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(
["$^"],
"$@",
{
2026-09-18 02:31:28 +02:00
"head": data.settings.typescript.command,
"libs": data.settings.typescript.libs,
2026-09-18 02:13:11 +02:00
}
),
]
}
},
// link
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"kind": "concrete",
"data": {
"targets": [
path_result,
],
"dependencies": (
(new Array<string>())
// .concat([path_head])
.concat(has_plankton ? [path_plankton_implementation] : [])
.concat([path_unlinked])
),
"actions": (
(new Array<string>())
.concat(
[
_ivaldi.command_log(lib_plankton.string.coin("app:{{name}} | linking …", {"name": app.name})),
_ivaldi.command_mkdir("$(dir $@)"),
2026-09-18 02:31:28 +02:00
lib_plankton.string.coin("echo '{{head}}' > {{path}}", {"head": (app.head ?? ""), "path": path_result}),
2026-09-18 02:13:11 +02:00
_ivaldi.command_cat(["$^"], "$@", {"append": true}),
]
)
// make executable
.concat(
(
app.executable
?
[
/**
* @todo _ivaldi.command_
*/
"chmod +x $@",
]
:
[
]
)
)
)
},
},
// facade
2025-04-25 10:47:04 +02:00
{
2026-09-18 02:13:11 +02:00
"kind": "meta",
"data": {
"phony": true,
"name": get_app_facade_name(app),
"dependencies": (
(new Array<string>())
.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),
[]
)
),
2025-04-25 10:47:04 +02:00
};
const makefile : string = lib_plankton.gnu_make.render_sheet(
gnu_make_sheet,
{
"break_dependencies": true,
"silent_actions": true,
}
);
2025-04-25 12:50:44 +02:00
const makefile_path : string = lib_plankton.string.coin(
"{{directory}}/ivaldi-makefile-{{name}}",
{
2025-07-08 15:06:16 +02:00
"directory": data.directories.makefile,
2025-04-25 12:50:44 +02:00
"name": data.name,
}
);
2025-04-25 10:47:04 +02:00
await lib_plankton.file.write(
2025-04-25 12:50:44 +02:00
makefile_path,
2025-04-25 10:47:04 +02:00
makefile
);
const command : string = lib_plankton.string.coin(
"make -f {{path}}",
{
2025-04-25 12:50:44 +02:00
"path": makefile_path,
2025-04-25 10:47:04 +02:00
}
);
2025-04-25 12:50:44 +02:00
const result = await execute(command);
process.stdout.write(result.stdout + "\n");
process.stderr.write(result.stderr + "\n");
2025-04-25 10:47:04 +02:00
return Promise.resolve<void>(undefined);
break;
}
2026-09-18 02:13:11 +02:00
case "deploy":
{
2025-04-25 10:47:04 +02:00
return Promise.reject<void>(new Error("not yet implemented"));
break;
}
}
}
}
}
2025-04-25 12:50:44 +02:00
_ivaldi.main(process.argv.slice(2))
2025-04-25 10:47:04 +02:00
.then(() => {})
.catch((reason) => {process.stderr.write(String(reason) + "\n");})
;