@lakutata/cli 2.17.0 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +455 -28
  2. package/dist/esm/CLIApp.js +6 -0
  3. package/dist/esm/Config.js +53 -0
  4. package/dist/esm/controllers/CommandLineController.js +180 -0
  5. package/dist/esm/controllers/I18nController.js +67 -0
  6. package/dist/esm/controllers/MigrationController.js +66 -0
  7. package/dist/esm/controllers/NativeController.js +41 -0
  8. package/dist/esm/controllers/PackController.js +88 -0
  9. package/dist/esm/controllers/WorkspaceController.js +91 -0
  10. package/dist/esm/dtos/AddPackageDTO.js +25 -0
  11. package/dist/esm/dtos/BundleDTO.js +29 -0
  12. package/dist/esm/dtos/CleanDTO.js +17 -0
  13. package/dist/esm/dtos/CreateProjectDTO.js +61 -0
  14. package/dist/esm/dtos/ExecutableDTO.js +37 -0
  15. package/dist/esm/dtos/I18nDTO.js +41 -0
  16. package/dist/esm/dtos/InstallerDTO.js +33 -0
  17. package/dist/{options/ListTemplatesOptions.js → esm/dtos/ListTemplatesDTO.js} +5 -14
  18. package/dist/esm/dtos/MigrationDTO.js +45 -0
  19. package/dist/esm/dtos/NativeDTO.js +41 -0
  20. package/dist/esm/dtos/RunScriptDTO.js +33 -0
  21. package/dist/esm/dtos/VersionDTO.js +29 -0
  22. package/dist/esm/lib/AddPackage.js +487 -0
  23. package/dist/esm/lib/Builder.js +21 -0
  24. package/dist/esm/lib/Clean.js +146 -0
  25. package/dist/esm/lib/Environment.js +11 -0
  26. package/dist/esm/lib/I18n.js +67 -0
  27. package/dist/esm/lib/Language.js +24 -0
  28. package/dist/esm/lib/Migration.js +94 -0
  29. package/dist/esm/lib/Output.js +40 -0
  30. package/dist/esm/lib/Processes.js +54 -0
  31. package/dist/esm/lib/ProjectCreator.js +191 -0
  32. package/dist/esm/lib/Registry.js +152 -0
  33. package/dist/esm/lib/Settings.js +25 -0
  34. package/dist/esm/lib/Tar.js +145 -0
  35. package/dist/esm/lib/TemplateCatalog.js +195 -0
  36. package/dist/esm/lib/TemplateFeatures.js +141 -0
  37. package/dist/esm/lib/Versions.js +58 -0
  38. package/dist/esm/lib/native/CMake.js +357 -0
  39. package/dist/esm/lib/native/Gyp.js +398 -0
  40. package/dist/esm/lib/native/NativeProject.js +370 -0
  41. package/dist/esm/lib/native/TextEdits.js +68 -0
  42. package/dist/esm/lib/native/Toml.js +325 -0
  43. package/dist/esm/lib/workspace/Run.js +111 -0
  44. package/dist/esm/lib/workspace/Version.js +144 -0
  45. package/dist/esm/lib/workspace/Workspace.js +231 -0
  46. package/dist/esm/locales/zh-CN.json +194 -0
  47. package/dist/esm/locales/zh-TW.json +194 -0
  48. package/dist/esm/types/ProcessResult.js +1 -0
  49. package/package.json +32 -43
  50. package/CHANGELOG.md +0 -452
  51. package/dist/CLIApp.d.ts +0 -3
  52. package/dist/CLIApp.d.ts.map +0 -1
  53. package/dist/CLIApp.js +0 -45
  54. package/dist/config/Config.d.ts +0 -3
  55. package/dist/config/Config.d.ts.map +0 -1
  56. package/dist/config/Config.js +0 -63
  57. package/dist/config/SetupCLIEntrypoint.d.ts +0 -3
  58. package/dist/config/SetupCLIEntrypoint.d.ts.map +0 -1
  59. package/dist/config/SetupCLIEntrypoint.js +0 -31
  60. package/dist/controllers/CommandLineController.d.ts +0 -25
  61. package/dist/controllers/CommandLineController.d.ts.map +0 -1
  62. package/dist/controllers/CommandLineController.js +0 -78
  63. package/dist/lib/ConvertDTO2Inquirer.d.ts +0 -3
  64. package/dist/lib/ConvertDTO2Inquirer.d.ts.map +0 -1
  65. package/dist/lib/ConvertDTO2Inquirer.js +0 -61
  66. package/dist/lib/GetDataDirectory.d.ts +0 -2
  67. package/dist/lib/GetDataDirectory.d.ts.map +0 -1
  68. package/dist/lib/GetDataDirectory.js +0 -11
  69. package/dist/lib/GetLocalDataFilename.d.ts +0 -2
  70. package/dist/lib/GetLocalDataFilename.d.ts.map +0 -1
  71. package/dist/lib/GetLocalDataFilename.js +0 -12
  72. package/dist/lib/ListTemplateNames.d.ts +0 -2
  73. package/dist/lib/ListTemplateNames.d.ts.map +0 -1
  74. package/dist/lib/ListTemplateNames.js +0 -44
  75. package/dist/lib/SpecialChar.d.ts +0 -9
  76. package/dist/lib/SpecialChar.d.ts.map +0 -1
  77. package/dist/lib/SpecialChar.js +0 -11
  78. package/dist/lib/components/DeGitPuller.d.ts +0 -19
  79. package/dist/lib/components/DeGitPuller.d.ts.map +0 -1
  80. package/dist/lib/components/DeGitPuller.js +0 -65
  81. package/dist/lib/components/Spinner.d.ts +0 -23
  82. package/dist/lib/components/Spinner.d.ts.map +0 -1
  83. package/dist/lib/components/Spinner.js +0 -61
  84. package/dist/lib/providers/Creator.d.ts +0 -46
  85. package/dist/lib/providers/Creator.d.ts.map +0 -1
  86. package/dist/lib/providers/Creator.js +0 -179
  87. package/dist/lib/providers/Information.d.ts +0 -91
  88. package/dist/lib/providers/Information.d.ts.map +0 -1
  89. package/dist/lib/providers/Information.js +0 -225
  90. package/dist/lib/providers/OnlineLatestVersion.d.ts +0 -14
  91. package/dist/lib/providers/OnlineLatestVersion.d.ts.map +0 -1
  92. package/dist/lib/providers/OnlineLatestVersion.js +0 -47
  93. package/dist/lib/providers/TemplateManager.d.ts +0 -36
  94. package/dist/lib/providers/TemplateManager.d.ts.map +0 -1
  95. package/dist/lib/providers/TemplateManager.js +0 -164
  96. package/dist/options/CreateProjectOptions.d.ts +0 -15
  97. package/dist/options/CreateProjectOptions.d.ts.map +0 -1
  98. package/dist/options/CreateProjectOptions.js +0 -94
  99. package/dist/options/ListTemplatesOptions.d.ts +0 -5
  100. package/dist/options/ListTemplatesOptions.d.ts.map +0 -1
  101. package/tsconfig.json +0 -7
@@ -0,0 +1,67 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { normalizeLocale } from 'lakutata/com/i18n';
5
+ /**
6
+ * The messages of the validation (validation.json, the validation key of a file): not copied to a new language, which
7
+ * has its built-in ones
8
+ */
9
+ const VALIDATION = 'validation';
10
+ /**
11
+ * Write a file of the messages of a new language, made of a file of the default language (its validation messages left)
12
+ * @param source
13
+ * @param target
14
+ */
15
+ async function copyMessages(source, target) {
16
+ const messages = JSON.parse(await readFile(source, 'utf8'));
17
+ delete messages[VALIDATION];
18
+ await writeFile(target, `${JSON.stringify(messages, null, 2)}\n`);
19
+ }
20
+ /**
21
+ * Copy the files of the messages of a directory of the default language, its validation messages left
22
+ * @param source
23
+ * @param target
24
+ * @param root whether it is the directory of the language (its validation.json and validation directory are left)
25
+ */
26
+ async function copyDirectory(source, target, root) {
27
+ const files = [];
28
+ await mkdir(target, { recursive: true });
29
+ for (const entry of (await readdir(source, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
30
+ if (root && (entry.name === `${VALIDATION}.json` || entry.name === VALIDATION))
31
+ continue;
32
+ if (entry.isDirectory()) {
33
+ files.push(...await copyDirectory(path.join(source, entry.name), path.join(target, entry.name), false));
34
+ }
35
+ else if (entry.isFile() && entry.name.endsWith('.json')) {
36
+ await copyMessages(path.join(source, entry.name), path.join(target, entry.name));
37
+ files.push(path.join(target, entry.name));
38
+ }
39
+ }
40
+ return files;
41
+ }
42
+ /**
43
+ * Add the messages of a language to a directory of the messages, made of the ones of the default language (a file or a
44
+ * directory, as the default language has): they are to be translated, lakutata i18n check telling the ones which are not
45
+ * @param directory
46
+ * @param locale
47
+ * @param defaultLocale
48
+ * @return the files written
49
+ */
50
+ export async function addLocale(directory, locale, defaultLocale) {
51
+ const tag = normalizeLocale(locale);
52
+ if (!tag)
53
+ throw new Error(`"${locale}" is not the tag of a language (en, zh-CN, zh-Hant-TW)`);
54
+ const base = normalizeLocale(defaultLocale) ?? defaultLocale;
55
+ if (tag === base)
56
+ throw new Error(`${tag} is the default language`);
57
+ if (existsSync(path.join(directory, `${tag}.json`)) || existsSync(path.join(directory, tag)))
58
+ throw new Error(`The messages of ${tag} exist already in ${directory}`);
59
+ if (existsSync(path.join(directory, `${base}.json`))) {
60
+ const target = path.join(directory, `${tag}.json`);
61
+ await copyMessages(path.join(directory, `${base}.json`), target);
62
+ return [target];
63
+ }
64
+ if (existsSync(path.join(directory, base)))
65
+ return await copyDirectory(path.join(directory, base), path.join(directory, tag), true);
66
+ throw new Error(`There are no messages of the default language ${base} in ${directory} (${base}.json or ${base}/)`);
67
+ }
@@ -0,0 +1,24 @@
1
+ import { I18nMiddleware } from 'lakutata/com/i18n';
2
+ /**
3
+ * The environment variable giving the language of the tool before the ones of the environment (LC_ALL, LANG)
4
+ */
5
+ export const LANGUAGE_VARIABLE = 'LAKUTATA_LANG';
6
+ /**
7
+ * The language of the commands of the tool: LAKUTATA_LANG, then the environment
8
+ */
9
+ export class LanguageMiddleware extends I18nMiddleware {
10
+ constructor() {
11
+ super(...arguments);
12
+ this.environmentVariable = LANGUAGE_VARIABLE;
13
+ }
14
+ }
15
+ /**
16
+ * A text of the tool in the language of the user: its message (cli.<key>), its English text without it
17
+ * @param i18n
18
+ * @param key
19
+ * @param text
20
+ * @param params
21
+ */
22
+ export function localize(i18n, key, text, params = {}) {
23
+ return i18n.has(`cli.${key}`) ? i18n.t(`cli.${key}`, params) : text.replace(/\{(\w+)\}/g, (all, name) => params[name] ?? all);
24
+ }
@@ -0,0 +1,94 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { camelCase } from 'lakutata/utils';
5
+ import { LaunchEnvironment } from './Environment.js';
6
+ import { spawnCommand } from './Processes.js';
7
+ /**
8
+ * The files of a directory, none when it does not exist
9
+ * @param directory
10
+ */
11
+ async function listFiles(directory) {
12
+ return existsSync(directory) ? await readdir(directory) : [];
13
+ }
14
+ /**
15
+ * Start the application of a package by its script, with the environment of a command of the migrations: the
16
+ * application does the command and exits
17
+ * @param application
18
+ * @param env
19
+ * @return the exit code of the application
20
+ */
21
+ async function startApplication(application, env) {
22
+ const manifest = JSON.parse(await readFile(path.join(application.directory, 'package.json'), 'utf8'));
23
+ if (!manifest.scripts?.[application.script])
24
+ throw new Error(`The package ${application.directory} has no script "${application.script}" building and starting the application (--script)`);
25
+ //The variables of a previous command are not inherited
26
+ const base = Object.fromEntries(Object.entries(LaunchEnvironment).filter(([name]) => !name.toUpperCase().startsWith('MIGRATION_')));
27
+ return await new Promise((resolve, reject) => {
28
+ //The output of the application is the one of the command; the package managers are scripts on Windows
29
+ const child = spawnCommand('npm', ['run', application.script], {
30
+ cwd: application.directory,
31
+ stdio: 'inherit',
32
+ env: { ...base, ...env, ...(application.database ? { MIGRATION_DATABASE: application.database } : {}) }
33
+ });
34
+ child.once('error', reject);
35
+ child.once('close', (exitCode) => resolve(exitCode ?? 1));
36
+ });
37
+ }
38
+ /**
39
+ * Generate the migrations of an application: its script builds and starts it with MIGRATION_GENERATE, the application
40
+ * compares the entities of its Database components and providers with their databases, writes the migrations of the
41
+ * changes into the directory, and exits (as `cross-env MIGRATION_GENERATE=<dir> npm run <script>` in lakutata 2.x)
42
+ * @param options
43
+ */
44
+ export async function generateMigrations(options) {
45
+ const directory = path.resolve(options.directory, options.dir);
46
+ const before = new Set(await listFiles(directory));
47
+ const code = await startApplication(options, { MIGRATION_GENERATE: options.dir });
48
+ const files = (await listFiles(directory)).filter((file) => !before.has(file)).sort();
49
+ return { code: code, files: files.map((file) => path.join(options.dir, file)) };
50
+ }
51
+ /**
52
+ * Run, revert or show the migrations of the databases of an application: its script builds and starts it with
53
+ * MIGRATION_RUN, MIGRATION_REVERT or MIGRATION_SHOW, the application connects its databases with their options (without
54
+ * synchronizing their schemas nor running their migrations), does the command, tells what it did, and exits
55
+ * @param options
56
+ * @return the exit code of the application
57
+ */
58
+ export async function executeMigrations(options) {
59
+ return await startApplication(options, {
60
+ [`MIGRATION_${options.action.toUpperCase()}`]: '1',
61
+ ...(options.transaction ? { MIGRATION_TRANSACTION: options.transaction } : {}),
62
+ ...(options.fake ? { MIGRATION_FAKE: '1' } : {})
63
+ });
64
+ }
65
+ /**
66
+ * Create an empty migration, written by hand (the data of the tables, the changes the entities do not describe):
67
+ * `<timestamp>-<name>.ts`, its class `<Name><timestamp>` importing its types from @lakutata/orm when the package depends
68
+ * on it, otherwise from lakutata/orm (as the migrations generated)
69
+ * @param options
70
+ * @return the migration, relative to the package
71
+ */
72
+ export async function createMigration(options) {
73
+ const timestamp = options.timestamp ?? Date.now();
74
+ const manifest = JSON.parse(await readFile(path.join(options.directory, 'package.json'), 'utf8'));
75
+ const module = manifest.dependencies?.['@lakutata/orm'] || manifest.devDependencies?.['@lakutata/orm'] ? '@lakutata/orm' : 'lakutata/orm';
76
+ const camel = camelCase(options.name);
77
+ const className = `${camel.charAt(0).toUpperCase()}${camel.slice(1)}${timestamp}`;
78
+ const file = path.join(options.dir, `${timestamp}-${options.name}.ts`);
79
+ await mkdir(path.resolve(options.directory, options.dir), { recursive: true });
80
+ await writeFile(path.resolve(options.directory, file), `import { MigrationInterface, QueryRunner } from "${module}";
81
+
82
+ export class ${className} implements MigrationInterface {
83
+ name = '${className}'
84
+
85
+ public async up(queryRunner: QueryRunner): Promise<void> {
86
+ }
87
+
88
+ public async down(queryRunner: QueryRunner): Promise<void> {
89
+ }
90
+
91
+ }
92
+ `, { flag: 'wx' });
93
+ return file;
94
+ }
@@ -0,0 +1,40 @@
1
+ import { styleText } from 'node:util';
2
+ /**
3
+ * A text in a style, when the output is a terminal
4
+ * @param style
5
+ * @param text
6
+ */
7
+ export function styled(style, text) {
8
+ return process.stdout.isTTY && !process.env.NO_COLOR ? styleText(style, text) : text;
9
+ }
10
+ /**
11
+ * Print a step done
12
+ * @param message
13
+ */
14
+ export function done(message) {
15
+ process.stdout.write(`${styled('green', '✔')} ${message}\n`);
16
+ }
17
+ /**
18
+ * Print a step in progress
19
+ * @param message
20
+ */
21
+ export function step(message) {
22
+ process.stdout.write(`${styled('cyan', '›')} ${message}\n`);
23
+ }
24
+ /**
25
+ * Print a warning
26
+ * @param message
27
+ */
28
+ export function warn(message) {
29
+ process.stderr.write(`${styled('yellow', '!')} ${message}\n`);
30
+ }
31
+ /**
32
+ * A table of names and values
33
+ * @param rows
34
+ * @param title
35
+ */
36
+ export function table(rows, title) {
37
+ const width = Math.max(...rows.map(([name]) => name.length));
38
+ const lines = rows.map(([name, value]) => ` ${styled('blue', name.padEnd(width))} ${value}`);
39
+ return `${title ? `${styled('bold', title)}\n` : ''}${lines.join('\n')}\n`;
40
+ }
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { LaunchEnvironment } from './Environment.js';
4
+ /**
5
+ * The package manager running the tool (npx, pnpm dlx, yarn dlx), npm by default
6
+ */
7
+ export function detectPackageManager() {
8
+ const agent = process.env.npm_config_user_agent ?? '';
9
+ if (agent.startsWith('pnpm'))
10
+ return 'pnpm';
11
+ if (agent.startsWith('yarn'))
12
+ return 'yarn';
13
+ return 'npm';
14
+ }
15
+ /**
16
+ * An argument of a command run by cmd.exe (Windows): quoted as the programs read their arguments
17
+ * @param arg
18
+ */
19
+ function quoted(arg) {
20
+ if (/^[\w\-.:/\\=@,+]+$/.test(arg))
21
+ return arg;
22
+ return `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
23
+ }
24
+ /**
25
+ * Spawn a command: on Windows, the commands found in the PATH by their names (the package managers, the commands of npm:
26
+ * npm.cmd, bun.cmd) are scripts, run by cmd.exe with their arguments quoted; the executables given by their paths (the
27
+ * ones of spaces: C:\Program Files\nodejs\node.exe) are run as they are
28
+ * @param command
29
+ * @param args
30
+ * @param options
31
+ */
32
+ export function spawnCommand(command, args, options) {
33
+ if (process.platform === 'win32' && !path.isAbsolute(command))
34
+ return spawn([command, ...args].map(quoted).join(' '), { ...options, shell: true });
35
+ return spawn(command, args, options);
36
+ }
37
+ /**
38
+ * Run a command, its output is given back
39
+ * @param command
40
+ * @param args
41
+ * @param cwd
42
+ * @return the exit code and the output
43
+ */
44
+ export function runCommand(command, args, cwd) {
45
+ return new Promise((resolve, reject) => {
46
+ const child = spawnCommand(command, args, { cwd: cwd, env: LaunchEnvironment });
47
+ let output = '';
48
+ //Piped (the default stdio)
49
+ child.stdout.on('data', (chunk) => output += chunk.toString());
50
+ child.stderr.on('data', (chunk) => output += chunk.toString());
51
+ child.once('error', reject);
52
+ child.once('close', (code) => resolve({ code: code ?? 1, output: output }));
53
+ });
54
+ }
@@ -0,0 +1,191 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { existsSync } from 'node:fs';
11
+ import { cp, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { Component } from 'lakutata';
14
+ import { Autoload, Inject, Singleton } from 'lakutata/decorator/di';
15
+ import { templating } from 'lakutata/utils';
16
+ import { TemplateCatalog } from './TemplateCatalog.js';
17
+ import { runCommand } from './Processes.js';
18
+ import { done, step, warn } from './Output.js';
19
+ import { applyFeatureBlocks, applyFeatureFiles, projectPath } from './TemplateFeatures.js';
20
+ import { frameworkVersion } from './Versions.js';
21
+ /**
22
+ * The directories of a template which are not copied
23
+ */
24
+ const SKIPPED = ['node_modules', '.git'];
25
+ /**
26
+ * Whether a file is text: no NUL byte in its beginning
27
+ * @param content
28
+ */
29
+ function isText(content) {
30
+ return !content.subarray(0, 8000).includes(0);
31
+ }
32
+ /**
33
+ * The files of a directory, relative to it
34
+ * @param directory
35
+ * @param base
36
+ */
37
+ async function listFiles(directory, base = directory) {
38
+ const files = [];
39
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
40
+ const target = path.join(directory, entry.name);
41
+ if (entry.isDirectory())
42
+ files.push(...await listFiles(target, base));
43
+ else if (entry.isFile())
44
+ files.push(path.relative(base, target));
45
+ }
46
+ return files;
47
+ }
48
+ /**
49
+ * Create the projects from the templates
50
+ */
51
+ let ProjectCreator = class ProjectCreator extends Component {
52
+ /**
53
+ * Check the directory of a project
54
+ * @param directory
55
+ * @param init the project is created in an existing directory
56
+ */
57
+ async checkDirectory(directory, init) {
58
+ let exists = true;
59
+ try {
60
+ if (!(await stat(directory)).isDirectory())
61
+ throw new Error(`${directory} is not a directory`);
62
+ }
63
+ catch (error) {
64
+ if (error.code !== 'ENOENT')
65
+ throw error;
66
+ exists = false;
67
+ }
68
+ if (!exists) {
69
+ if (init)
70
+ throw new Error(`The directory ${directory} does not exist`);
71
+ return;
72
+ }
73
+ if (!init && (await readdir(directory)).length)
74
+ throw new Error(`The directory ${directory} is not empty (--init creates the project in an existing directory)`);
75
+ }
76
+ /**
77
+ * Copy the files of a template into the directory of the project
78
+ * @param template
79
+ * @param directory
80
+ * @return the paths of the files of the project, relative to its directory
81
+ */
82
+ async copy(template, directory) {
83
+ await cp(template.root, directory, {
84
+ recursive: true,
85
+ filter: (source) => !SKIPPED.includes(path.basename(source))
86
+ });
87
+ const files = [];
88
+ for (const file of await listFiles(directory)) {
89
+ //"_gitignore" becomes ".gitignore"
90
+ const renamed = projectPath(file);
91
+ if (renamed !== file)
92
+ await rename(path.join(directory, file), path.join(directory, renamed));
93
+ files.push(renamed);
94
+ }
95
+ return files;
96
+ }
97
+ /**
98
+ * Fill the information of the project into the files of the template ({$APP_NAME}…), the blocks of the features
99
+ * kept or removed
100
+ * @param directory
101
+ * @param files
102
+ * @param fields
103
+ * @param features the features enabled
104
+ */
105
+ async fill(directory, files, fields, features) {
106
+ for (const file of files) {
107
+ const target = path.join(directory, file);
108
+ const content = await readFile(target);
109
+ if (!isText(content))
110
+ continue;
111
+ const raw = content.toString('utf8');
112
+ const filled = templating(applyFeatureBlocks(raw, features, file), fields, { ignoreMissing: true });
113
+ if (filled !== raw)
114
+ await writeFile(target, filled);
115
+ }
116
+ }
117
+ /**
118
+ * Get the files of a template
119
+ * @param source
120
+ */
121
+ async prepare(source) {
122
+ step('Getting the template');
123
+ return await this.catalog.prepare(source);
124
+ }
125
+ /**
126
+ * Create a project from its template (prepared, its cleanup done by the caller)
127
+ * @param plan
128
+ * @param template
129
+ */
130
+ async create(plan, template) {
131
+ await this.checkDirectory(plan.directory, plan.init);
132
+ const files = await this.copy(template, plan.directory);
133
+ for (const feature of plan.features) {
134
+ if (!(feature in template.features))
135
+ throw new Error(`The template has no feature ${feature}`);
136
+ const directory = path.join(template.featuresDirectory, feature);
137
+ if (existsSync(directory))
138
+ files.push(...await applyFeatureFiles(directory, plan.directory));
139
+ }
140
+ done(`Template ${template.label} copied (${files.length} files${plan.features.length ? `, with ${plan.features.join(', ')}` : ''})`);
141
+ await this.fill(plan.directory, files, {
142
+ $APP_NAME: plan.name,
143
+ $APP_ID: plan.id,
144
+ $APP_DESC: plan.description,
145
+ $APP_AUTHOR: plan.author,
146
+ $APP_LICENSE: plan.license,
147
+ $LAKUTATA_VERSION: frameworkVersion()
148
+ }, new Set(plan.features));
149
+ done('Project information filled');
150
+ if (plan.packageManager !== 'none') {
151
+ step(`Installing the dependencies with ${plan.packageManager}`);
152
+ let result;
153
+ try {
154
+ result = await runCommand(plan.packageManager, ['install'], plan.directory);
155
+ }
156
+ catch (error) {
157
+ throw new Error(`${plan.packageManager} cannot be run: ${error.message}`, { cause: error });
158
+ }
159
+ if (result.code !== 0)
160
+ throw new Error(`${plan.packageManager} install failed:\n${result.output.trim().split('\n').slice(-20).join('\n')}`);
161
+ done('Dependencies installed');
162
+ }
163
+ if (plan.git) {
164
+ try {
165
+ const result = await runCommand('git', ['init'], plan.directory);
166
+ if (result.code === 0)
167
+ done('Git repository initialized');
168
+ else
169
+ warn(`git init failed: ${result.output.trim()}`);
170
+ }
171
+ catch {
172
+ warn('git is not installed, the git repository is not initialized');
173
+ }
174
+ }
175
+ }
176
+ };
177
+ __decorate([
178
+ Inject(TemplateCatalog),
179
+ __metadata("design:type", TemplateCatalog
180
+ /**
181
+ * Check the directory of a project
182
+ * @param directory
183
+ * @param init the project is created in an existing directory
184
+ */
185
+ )
186
+ ], ProjectCreator.prototype, "catalog", void 0);
187
+ ProjectCreator = __decorate([
188
+ Autoload(),
189
+ Singleton()
190
+ ], ProjectCreator);
191
+ export { ProjectCreator };
@@ -0,0 +1,152 @@
1
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { Settings } from './Settings.js';
4
+ /**
5
+ * The URL of a package on the registry (the scope separator is encoded)
6
+ * @param name
7
+ */
8
+ function packageURL(name) {
9
+ return `${Settings.registry}/${name.replace('/', '%2f')}`;
10
+ }
11
+ /**
12
+ * The parts of a version
13
+ * @param version
14
+ */
15
+ function versionParts(version) {
16
+ const matched = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/.exec(version);
17
+ return matched ? { numbers: [Number(matched[1]), Number(matched[2]), Number(matched[3])], prerelease: matched[4] ? matched[4].split('.') : [] } : undefined;
18
+ }
19
+ /**
20
+ * Compare two versions as semver does: a prerelease is before its release, its identifiers are compared one by one
21
+ * (the numbers as numbers, before the words)
22
+ * @param a
23
+ * @param b
24
+ */
25
+ export function compareVersions(a, b) {
26
+ const [x, y] = [versionParts(a), versionParts(b)];
27
+ const numbers = x.numbers[0] - y.numbers[0] || x.numbers[1] - y.numbers[1] || x.numbers[2] - y.numbers[2];
28
+ if (numbers)
29
+ return numbers;
30
+ if (!x.prerelease.length || !y.prerelease.length)
31
+ return y.prerelease.length - x.prerelease.length;
32
+ for (let index = 0; index < Math.max(x.prerelease.length, y.prerelease.length); index++) {
33
+ const [left, right] = [x.prerelease[index], y.prerelease[index]];
34
+ if (left === undefined || right === undefined)
35
+ return left === undefined ? -1 : 1;
36
+ if (left === right)
37
+ continue;
38
+ const [leftNumber, rightNumber] = [/^\d+$/.test(left), /^\d+$/.test(right)];
39
+ if (leftNumber && rightNumber)
40
+ return Number(left) - Number(right);
41
+ if (leftNumber !== rightNumber)
42
+ return leftNumber ? -1 : 1;
43
+ return left < right ? -1 : 1;
44
+ }
45
+ return 0;
46
+ }
47
+ /**
48
+ * The newest version of a major version (the prereleases only when the major version has no release)
49
+ * @param versions
50
+ * @param major any major version when undefined
51
+ */
52
+ export function newestVersion(versions, major) {
53
+ const candidates = versions.filter((version) => {
54
+ const parts = versionParts(version);
55
+ return !!parts && (major === undefined || parts.numbers[0] === major);
56
+ });
57
+ const releases = candidates.filter((version) => !versionParts(version).prerelease.length);
58
+ return (releases.length ? releases : candidates).sort(compareVersions).at(-1);
59
+ }
60
+ /**
61
+ * The manifest of the newest version of a major version of a package (the prereleases when the major version has no
62
+ * release: the tags do not tell the major versions), undefined when it cannot be fetched
63
+ * @param name
64
+ * @param major
65
+ */
66
+ export async function majorManifest(name, major) {
67
+ try {
68
+ const pkg = await resolvePackage(name, major);
69
+ if (!pkg)
70
+ return undefined;
71
+ const response = await fetch(`${packageURL(name)}/${encodeURIComponent(pkg.version)}`, { signal: AbortSignal.timeout(15000) });
72
+ return response.ok ? await response.json() : undefined;
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
77
+ }
78
+ /**
79
+ * The directory of the cached archives of a package
80
+ * @param name
81
+ */
82
+ function cacheDirectory(name) {
83
+ return path.join(Settings.cacheDirectory, 'packages', name);
84
+ }
85
+ /**
86
+ * The version of a package to download: the newest of the major version, or the given version
87
+ * @param name
88
+ * @param wanted a version ("1.2.3"), a major version (3) or the newest version (undefined)
89
+ * @return undefined when the package does not exist
90
+ */
91
+ export async function resolvePackage(name, wanted) {
92
+ const response = await fetch(packageURL(name), { headers: { Accept: 'application/vnd.npm.install-v1+json' }, signal: AbortSignal.timeout(15000) });
93
+ if (response.status === 404)
94
+ return undefined;
95
+ if (!response.ok)
96
+ throw new Error(`The package ${name} cannot be fetched from ${Settings.registry}: ${response.status} ${response.statusText}`);
97
+ const metadata = await response.json();
98
+ const versions = Object.keys(metadata.versions ?? {});
99
+ const version = typeof wanted === 'string' ? (versions.includes(wanted) ? wanted : undefined) : newestVersion(versions, wanted);
100
+ if (!version)
101
+ throw new Error(`The package ${name} has no version ${typeof wanted === 'number' ? `${wanted}.x` : wanted} (${versions.join(', ') || 'no version'})`);
102
+ return { name: name, version: version, tarball: metadata.versions[version].dist.tarball };
103
+ }
104
+ /**
105
+ * The archive of a version of a package, downloaded once and cached
106
+ * @param pkg
107
+ */
108
+ export async function downloadPackage(pkg) {
109
+ const file = path.join(cacheDirectory(pkg.name), `${pkg.version}.tgz`);
110
+ try {
111
+ return await readFile(file);
112
+ }
113
+ catch {
114
+ //Not cached
115
+ }
116
+ const response = await fetch(pkg.tarball, { signal: AbortSignal.timeout(60000) });
117
+ if (!response.ok)
118
+ throw new Error(`The package ${pkg.name}@${pkg.version} cannot be downloaded: ${response.status} ${response.statusText}`);
119
+ const archive = Buffer.from(await response.arrayBuffer());
120
+ await mkdir(path.dirname(file), { recursive: true });
121
+ await writeFile(file, archive);
122
+ return archive;
123
+ }
124
+ /**
125
+ * The newest cached archive of a package, when the registry cannot be reached
126
+ * @param name
127
+ * @param major
128
+ */
129
+ export async function cachedPackage(name, major) {
130
+ let files;
131
+ try {
132
+ files = await readdir(cacheDirectory(name));
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ const version = newestVersion(files.filter((file) => file.endsWith('.tgz')).map((file) => file.slice(0, -4)), major);
138
+ return version ? { version: version, archive: await readFile(path.join(cacheDirectory(name), `${version}.tgz`)) } : undefined;
139
+ }
140
+ /**
141
+ * The package.json of the latest version of a package, undefined when it cannot be fetched
142
+ * @param name
143
+ */
144
+ export async function latestManifest(name) {
145
+ try {
146
+ const response = await fetch(`${packageURL(name)}/latest`, { signal: AbortSignal.timeout(5000) });
147
+ return response.ok ? await response.json() : undefined;
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
@@ -0,0 +1,25 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ /**
4
+ * The settings of the tool, from the environment variables (the npm registry and the GitHub mirror, the cache)
5
+ */
6
+ export const Settings = {
7
+ /**
8
+ * The npm registry of the templates and of the versions: the one of npm (npm_config_registry) by default
9
+ */
10
+ get registry() {
11
+ return (process.env.LAKUTATA_NPM_REGISTRY ?? process.env.npm_config_registry ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
12
+ },
13
+ /**
14
+ * The host of the archives of the GitHub repositories (the templates "github:owner/repo")
15
+ */
16
+ get githubDownload() {
17
+ return (process.env.LAKUTATA_GITHUB_DOWNLOAD ?? 'https://github.com').replace(/\/+$/, '');
18
+ },
19
+ /**
20
+ * The directory of the cache of the tool
21
+ */
22
+ get cacheDirectory() {
23
+ return process.env.LAKUTATA_CACHE_DIR ?? path.join(process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache'), 'lakutata');
24
+ }
25
+ };