ivaldi/source/main.ts

834 lines
20 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
{
/**
*/
2026-09-18 09:53:28 +02:00
function refine_definition(
definition_raw : any
) : _ivaldi.type_definition
2025-04-25 10:47:04 +02:00
{
2026-09-18 09:53:28 +02:00
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": (definition_raw["libs"] ?? {}),
"core": definition_raw["core"],
"apps": (
definition_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}}"),
"head": (app_raw["head"] ?? null),
"executable": (app_raw["executable"] ?? false),
2025-04-25 10:47:04 +02:00
}
2026-09-18 09:53:28 +02:00
)
)
),
"custom_rules": (
(definition_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
/**
*/
2026-09-18 09:53:28 +02:00
function get_target_name(
app_name : string
2026-09-18 02:13:11 +02:00
) : string
{
return lib_plankton.string.coin(
"app-{{name}}",
{
2026-09-18 09:53:28 +02:00
"name": app_name,
2026-09-18 02:13:11 +02:00
}
);
}
2026-09-18 09:53:28 +02:00
/**
*/
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<void>
{
// 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<void>
{
const commands : Array<string> = [
_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<string>),
{
"show_actions": show_actions = true,
}
:
{
show_actions ?: boolean
}
=
{
}
) : Promise<void>
{
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<Array<lib_plankton.gnu_make.type_rule>>)
// 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<string>())
.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<string>())
.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<string> = (
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<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(
["$^"],
"$@",
{
"head": definition.settings.typescript.command,
"libs": definition.settings.typescript.libs,
}
),
]
}
},
// link
{
"kind": "concrete",
"data": {
"targets": [
path_app_complete,
],
"dependencies": (
(new Array<string>())
.concat(has_plankton ? [path_plankton_implementation] : [])
.concat([path_core_implementation])
.concat([path_app_unlinked])
),
"actions": (
(new Array<string>())
.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<string>())
.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<string> = 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);
}
}
}
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",
}),
2026-09-18 09:53:28 +02:00
"definition_path": lib_plankton.args.class_argument.volatile({
"indicators_long": ["definition-path"],
2025-04-25 10:47:04 +02:00
"indicators_short": ["d"],
"type": lib_plankton.args.enum_type.string,
"mode": lib_plankton.args.enum_mode.replace,
"default": "ivaldi.json",
2026-09-18 09:53:28 +02:00
"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": "<from>:<to>",
"name": "assets",
}),
"targets": lib_plankton.args.class_argument.volatile({
"indicators_long": ["targets"],
"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-pattern"],
"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-pattern"],
"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-pattern"],
"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",
2025-04-25 10:47:04 +02:00
}),
"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",
2026-09-18 09:53:28 +02:00
"description": "a collection of tools for building and deploying a software project on basis of typescript and node",
2025-07-08 15:06:16 +02:00
"executable": "ivaldi",
2025-04-25 10:47:04 +02:00
}
)
);
}
2026-09-18 02:13:11 +02:00
else
2026-09-18 09:53:28 +02:00
{
const content : string = await lib_plankton.file.read(args.definition_path as string);
2025-04-25 10:47:04 +02:00
const data_raw : any = lib_plankton.json.decode(content);
2026-09-18 09:53:28 +02:00
const definition : _ivaldi.type_definition = refine_definition(data_raw);
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":
{
2026-09-18 09:53:28 +02:00
await action_fetch(
definition
);
2025-04-25 10:47:04 +02:00
break;
}
2026-09-18 02:13:11 +02:00
case "clear":
{
2026-09-18 09:53:28 +02:00
await action_clear(
definition,
args.temp_directory_pattern,
args.build_directory_pattern
);
2025-04-25 10:47:04 +02:00
break;
}
2026-09-18 02:13:11 +02:00
case "build":
{
2026-09-18 09:53:28 +02:00
await action_build(
definition,
args.assets.map(
(asset_raw : string) => {
const parts : Array<string> = asset_raw.split(":", 2);
return {"from": parts[0], "to": parts[1]};
}
2026-09-18 02:13:11 +02:00
),
2026-09-18 09:53:28 +02:00
args.temp_directory_pattern,
args.build_directory_pattern,
args.makefile_path_pattern,
((args.targets.length <= 0) ? null : args.targets),
2025-04-25 10:47:04 +02:00
{
2026-09-18 09:53:28 +02:00
"show_actions": ["debug"].includes(args.verbosity),
2025-04-25 10:47:04 +02:00
}
);
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");})
;