@lakutata/cli 3.0.0 → 3.1.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.
- package/README.md +378 -309
- package/README.zh-CN.md +356 -0
- package/dist/esm/controllers/WorkspaceController.js +5 -3
- package/dist/esm/dtos/AddPackageDTO.js +1 -1
- package/dist/esm/dtos/MigrationDTO.js +1 -1
- package/dist/esm/lib/AddPackage.js +35 -22
- package/dist/esm/lib/Migration.js +2 -2
- package/dist/esm/lib/Output.js +1 -1
- package/dist/esm/lib/Processes.js +62 -0
- package/dist/esm/lib/ProjectCreator.js +25 -15
- package/dist/esm/lib/Registry.js +7 -0
- package/dist/esm/lib/TemplateFeatures.js +9 -1
- package/dist/esm/lib/native/NativeProject.js +16 -5
- package/dist/esm/lib/workspace/Run.js +9 -5
- package/dist/esm/lib/workspace/Version.js +24 -8
- package/dist/esm/lib/workspace/Workspace.js +33 -7
- package/dist/esm/locales/zh-CN.json +4 -4
- package/dist/esm/locales/zh-TW.json +4 -4
- package/package.json +28 -5
|
@@ -3,7 +3,7 @@ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { camelCase } from 'lakutata/utils';
|
|
5
5
|
import { LaunchEnvironment } from './Environment.js';
|
|
6
|
-
import { spawnCommand } from './Processes.js';
|
|
6
|
+
import { projectPackageManager, spawnCommand } from './Processes.js';
|
|
7
7
|
/**
|
|
8
8
|
* The files of a directory, none when it does not exist
|
|
9
9
|
* @param directory
|
|
@@ -26,7 +26,7 @@ async function startApplication(application, env) {
|
|
|
26
26
|
const base = Object.fromEntries(Object.entries(LaunchEnvironment).filter(([name]) => !name.toUpperCase().startsWith('MIGRATION_')));
|
|
27
27
|
return await new Promise((resolve, reject) => {
|
|
28
28
|
//The output of the application is the one of the command; the package managers are scripts on Windows
|
|
29
|
-
const child = spawnCommand(
|
|
29
|
+
const child = spawnCommand(projectPackageManager(application.directory), ['run', application.script], {
|
|
30
30
|
cwd: application.directory,
|
|
31
31
|
stdio: 'inherit',
|
|
32
32
|
env: { ...base, ...env, ...(application.database ? { MIGRATION_DATABASE: application.database } : {}) }
|
package/dist/esm/lib/Output.js
CHANGED
|
@@ -36,5 +36,5 @@ export function warn(message) {
|
|
|
36
36
|
export function table(rows, title) {
|
|
37
37
|
const width = Math.max(...rows.map(([name]) => name.length));
|
|
38
38
|
const lines = rows.map(([name, value]) => ` ${styled('blue', name.padEnd(width))} ${value}`);
|
|
39
|
-
return `${title ? `${styled('bold', title)}\n` : ''}${lines.
|
|
39
|
+
return `${title ? `${styled('bold', title)}\n` : ''}${lines.map((line) => `${line}\n`).join('')}`;
|
|
40
40
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { LaunchEnvironment } from './Environment.js';
|
|
4
5
|
/**
|
|
@@ -12,6 +13,67 @@ export function detectPackageManager() {
|
|
|
12
13
|
return 'yarn';
|
|
13
14
|
return 'npm';
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether a directory is the root of a workspace: its package.json declares workspaces, or a pnpm-workspace.yaml is
|
|
18
|
+
* next to it
|
|
19
|
+
* @param directory
|
|
20
|
+
*/
|
|
21
|
+
function workspaceRoot(directory) {
|
|
22
|
+
const manifest = path.join(directory, 'package.json');
|
|
23
|
+
if (!existsSync(manifest))
|
|
24
|
+
return false;
|
|
25
|
+
if (existsSync(path.join(directory, 'pnpm-workspace.yaml')))
|
|
26
|
+
return true;
|
|
27
|
+
try {
|
|
28
|
+
const workspaces = JSON.parse(readFileSync(manifest, 'utf8')).workspaces;
|
|
29
|
+
return Array.isArray(workspaces) || Array.isArray(workspaces?.packages);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The package manager of a project: the packageManager field of its package.json ("pnpm@9.15.0"), else its lock file
|
|
37
|
+
* (pnpm-lock.yaml or pnpm-workspace.yaml, yarn.lock, package-lock.json), looked for from the directory up to the root
|
|
38
|
+
* of its workspace (the directory itself when it is in none); npm by default
|
|
39
|
+
* @param directory The directory of the project, or of one of the packages of its workspace.
|
|
40
|
+
*/
|
|
41
|
+
export function projectPackageManager(directory) {
|
|
42
|
+
const start = path.resolve(directory);
|
|
43
|
+
//The directories of the project: up to the nearest root of a workspace, the directory itself without one
|
|
44
|
+
const directories = [];
|
|
45
|
+
for (let current = start;; current = path.dirname(current)) {
|
|
46
|
+
directories.push(current);
|
|
47
|
+
if (workspaceRoot(current))
|
|
48
|
+
break;
|
|
49
|
+
if (path.dirname(current) === current) {
|
|
50
|
+
directories.splice(1);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const current of directories) {
|
|
55
|
+
const manifest = path.join(current, 'package.json');
|
|
56
|
+
if (existsSync(manifest)) {
|
|
57
|
+
let declared;
|
|
58
|
+
try {
|
|
59
|
+
declared = JSON.parse(readFileSync(manifest, 'utf8')).packageManager;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
declared = undefined;
|
|
63
|
+
}
|
|
64
|
+
const name = typeof declared === 'string' ? declared.split('@')[0] : undefined;
|
|
65
|
+
if (name === 'npm' || name === 'pnpm' || name === 'yarn')
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(path.join(current, 'pnpm-lock.yaml')) || existsSync(path.join(current, 'pnpm-workspace.yaml')))
|
|
69
|
+
return 'pnpm';
|
|
70
|
+
if (existsSync(path.join(current, 'yarn.lock')))
|
|
71
|
+
return 'yarn';
|
|
72
|
+
if (existsSync(path.join(current, 'package-lock.json')))
|
|
73
|
+
return 'npm';
|
|
74
|
+
}
|
|
75
|
+
return 'npm';
|
|
76
|
+
}
|
|
15
77
|
/**
|
|
16
78
|
* An argument of a command run by cmd.exe (Windows): quoted as the programs read their arguments
|
|
17
79
|
* @param arg
|
|
@@ -8,7 +8,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
|
-
import { cp, readdir, readFile,
|
|
11
|
+
import { cp, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { Component } from 'lakutata';
|
|
14
14
|
import { Autoload, Inject, Singleton } from 'lakutata/decorator/di';
|
|
@@ -30,7 +30,7 @@ function isText(content) {
|
|
|
30
30
|
return !content.subarray(0, 8000).includes(0);
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* The files of a directory, relative to it
|
|
33
|
+
* The files of a directory, relative to it, without the directories which are not copied (SKIPPED)
|
|
34
34
|
* @param directory
|
|
35
35
|
* @param base
|
|
36
36
|
*/
|
|
@@ -38,6 +38,8 @@ async function listFiles(directory, base = directory) {
|
|
|
38
38
|
const files = [];
|
|
39
39
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
40
40
|
const target = path.join(directory, entry.name);
|
|
41
|
+
if (entry.isDirectory() && SKIPPED.includes(entry.name))
|
|
42
|
+
continue;
|
|
41
43
|
if (entry.isDirectory())
|
|
42
44
|
files.push(...await listFiles(target, base));
|
|
43
45
|
else if (entry.isFile())
|
|
@@ -74,22 +76,25 @@ let ProjectCreator = class ProjectCreator extends Component {
|
|
|
74
76
|
throw new Error(`The directory ${directory} is not empty (--init creates the project in an existing directory)`);
|
|
75
77
|
}
|
|
76
78
|
/**
|
|
77
|
-
* Copy the files of a template into the directory of the project
|
|
79
|
+
* Copy the files of a template into the directory of the project; the files the directory has already (--init)
|
|
80
|
+
* are kept as they are, the ones of the template not copied
|
|
78
81
|
* @param template
|
|
79
82
|
* @param directory
|
|
80
|
-
* @
|
|
83
|
+
* @param kept the files of the directory kept, relative to it
|
|
84
|
+
* @return the paths of the files copied, relative to the directory
|
|
81
85
|
*/
|
|
82
|
-
async copy(template, directory) {
|
|
83
|
-
await cp(template.root, directory, {
|
|
84
|
-
recursive: true,
|
|
85
|
-
filter: (source) => !SKIPPED.includes(path.basename(source))
|
|
86
|
-
});
|
|
86
|
+
async copy(template, directory, kept) {
|
|
87
87
|
const files = [];
|
|
88
|
-
for (const file of await listFiles(
|
|
88
|
+
for (const file of await listFiles(template.root)) {
|
|
89
89
|
//"_gitignore" becomes ".gitignore"
|
|
90
90
|
const renamed = projectPath(file);
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const target = path.join(directory, renamed);
|
|
92
|
+
if (existsSync(target)) {
|
|
93
|
+
kept.push(renamed);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
97
|
+
await cp(path.join(template.root, file), target);
|
|
93
98
|
files.push(renamed);
|
|
94
99
|
}
|
|
95
100
|
return files;
|
|
@@ -129,15 +134,20 @@ let ProjectCreator = class ProjectCreator extends Component {
|
|
|
129
134
|
*/
|
|
130
135
|
async create(plan, template) {
|
|
131
136
|
await this.checkDirectory(plan.directory, plan.init);
|
|
132
|
-
const
|
|
133
|
-
for (const feature of plan.features) {
|
|
137
|
+
for (const feature of plan.features)
|
|
134
138
|
if (!(feature in template.features))
|
|
135
139
|
throw new Error(`The template has no feature ${feature}`);
|
|
140
|
+
//Only the files written are filled: the other files of the directory (--init) are not the template's
|
|
141
|
+
const kept = [];
|
|
142
|
+
const files = await this.copy(template, plan.directory, kept);
|
|
143
|
+
for (const feature of plan.features) {
|
|
136
144
|
const directory = path.join(template.featuresDirectory, feature);
|
|
137
145
|
if (existsSync(directory))
|
|
138
|
-
files.push(...await applyFeatureFiles(directory, plan.directory));
|
|
146
|
+
files.push(...await applyFeatureFiles(directory, plan.directory, new Set(files), kept));
|
|
139
147
|
}
|
|
140
148
|
done(`Template ${template.label} copied (${files.length} files${plan.features.length ? `, with ${plan.features.join(', ')}` : ''})`);
|
|
149
|
+
if (kept.length)
|
|
150
|
+
warn(`The files of the directory are kept: ${[...new Set(kept)].sort().join(', ')}. Their versions in the template are not copied: merge what you need of them by hand (the scripts and the dependencies of package.json)`);
|
|
141
151
|
await this.fill(plan.directory, files, {
|
|
142
152
|
$APP_NAME: plan.name,
|
|
143
153
|
$APP_ID: plan.id,
|
package/dist/esm/lib/Registry.js
CHANGED
|
@@ -44,6 +44,13 @@ export function compareVersions(a, b) {
|
|
|
44
44
|
}
|
|
45
45
|
return 0;
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The highest of versions, the prereleases included (the strings which are not versions left out)
|
|
49
|
+
* @param versions
|
|
50
|
+
*/
|
|
51
|
+
export function highestVersion(versions) {
|
|
52
|
+
return versions.filter((version) => !!versionParts(version)).sort(compareVersions).at(-1);
|
|
53
|
+
}
|
|
47
54
|
/**
|
|
48
55
|
* The newest version of a major version (the prereleases only when the major version has no release)
|
|
49
56
|
* @param versions
|
|
@@ -72,13 +72,21 @@ async function listFiles(directory, base = directory) {
|
|
|
72
72
|
* (the scripts of package.json, the options of tsconfig.json)
|
|
73
73
|
* @param featureDirectory
|
|
74
74
|
* @param project
|
|
75
|
+
* @param written the files the creation of the project wrote: the other files of the directory (--init) are kept as
|
|
76
|
+
* they are, added to kept
|
|
77
|
+
* @param kept
|
|
75
78
|
* @return the files added to the project, relative to it
|
|
76
79
|
*/
|
|
77
|
-
export async function applyFeatureFiles(featureDirectory, project) {
|
|
80
|
+
export async function applyFeatureFiles(featureDirectory, project, written, kept = []) {
|
|
78
81
|
const added = [];
|
|
79
82
|
for (const file of await listFiles(featureDirectory)) {
|
|
80
83
|
const relative = projectPath(file);
|
|
81
84
|
const target = path.join(project, relative);
|
|
85
|
+
//A file the directory had before the project was created (--init) is kept as it is
|
|
86
|
+
if (written && existsSync(target) && !written.has(relative)) {
|
|
87
|
+
kept.push(relative);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
82
90
|
await mkdir(path.dirname(target), { recursive: true });
|
|
83
91
|
if (existsSync(target) && file.endsWith('.json')) {
|
|
84
92
|
const merged = mergeJson(JSON.parse(await readFile(target, 'utf8')), JSON.parse(await readFile(path.join(featureDirectory, file), 'utf8')));
|
|
@@ -73,6 +73,8 @@ function scopeOf(change, library) {
|
|
|
73
73
|
}
|
|
74
74
|
if (library && change.platform && library.platform !== 'unix' && change.platform !== library.platform)
|
|
75
75
|
throw new Error(`${library.file} is a library of ${library.platform}, not of ${change.platform}`);
|
|
76
|
+
if (change.pkgConfig && change.platform === 'win')
|
|
77
|
+
throw new Error('pkg-config is not on Windows: link the library by its file (deps/lib/foo.lib) or by its name');
|
|
76
78
|
if (change.platform)
|
|
77
79
|
return change.platform;
|
|
78
80
|
if (library)
|
|
@@ -268,7 +270,8 @@ async function changeCMake(directory, change, library, scope) {
|
|
|
268
270
|
else if (library?.kind === 'dynamic') {
|
|
269
271
|
commands([
|
|
270
272
|
link(source(library.platform === 'win' ? importLibrary(directory, library) : library.file)),
|
|
271
|
-
|
|
273
|
+
//The rpath of the addon is its directory only, not the directories of the libraries on the machine building it
|
|
274
|
+
...(library.platform === 'win' ? [] : [['set_target_properties', name, 'PROPERTIES', 'BUILD_WITH_INSTALL_RPATH', 'ON', 'INSTALL_RPATH', library.platform === 'mac' ? '@loader_path' : '$ORIGIN']]),
|
|
272
275
|
//Copied next to the addon, found there at run time
|
|
273
276
|
['add_custom_command', 'TARGET', name, 'POST_BUILD', 'COMMAND', '${CMAKE_COMMAND}', '-E', 'copy_if_different', source(library.file), `$<TARGET_FILE_DIR:${name}>`]
|
|
274
277
|
]);
|
|
@@ -315,7 +318,9 @@ async function changeRust(directory, change, library, scope) {
|
|
|
315
318
|
else if (library?.kind === 'dynamic') {
|
|
316
319
|
if (library.platform === 'win')
|
|
317
320
|
importLibrary(directory, library);
|
|
318
|
-
|
|
321
|
+
//A versioned library (libfoo.so.1) by its file name: -lfoo would find libfoo.so
|
|
322
|
+
const base = path.posix.basename(library.file);
|
|
323
|
+
directives.push(`rustc-link-search=native=${path.posix.dirname(library.file)}`, /\.so\.\d/.test(base) ? `rustc-link-lib=dylib:+verbatim=${base}` : `rustc-link-lib=dylib=${library.name}`);
|
|
319
324
|
if (library.platform === 'mac')
|
|
320
325
|
directives.push('rustc-cdylib-link-arg=-Wl,-rpath,@loader_path');
|
|
321
326
|
if (library.platform === 'linux')
|
|
@@ -333,13 +338,15 @@ async function changeRust(directory, change, library, scope) {
|
|
|
333
338
|
files.push('.cargo/config.toml');
|
|
334
339
|
}
|
|
335
340
|
if (library?.kind === 'dynamic') {
|
|
336
|
-
//Copied next to the addon
|
|
341
|
+
//Copied next to the addon by its build (when it is built on the platform of the library), whatever the package
|
|
342
|
+
//manager running it (pnpm runs no post scripts by default)
|
|
337
343
|
const manifestFile = path.join(directory, 'package.json');
|
|
338
344
|
const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
339
345
|
const copy = `node -e "const fs=require('node:fs');if(fs.existsSync('${library.file}'))fs.copyFileSync('${library.file}','build/Release/${path.posix.basename(library.file)}')"`;
|
|
340
346
|
const scripts = manifest.scripts ?? {};
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
const build = scripts['build:native'] ?? 'napi build --release --output-dir build/Release';
|
|
348
|
+
if (!build.includes(copy) && !(scripts['postbuild:native'] ?? '').includes(copy)) {
|
|
349
|
+
scripts['build:native'] = `${build} && ${copy}`;
|
|
343
350
|
manifest.scripts = scripts;
|
|
344
351
|
await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
345
352
|
files.push('package.json');
|
|
@@ -360,6 +367,10 @@ export async function changeNative(directory, change) {
|
|
|
360
367
|
throw new Error('The crates are the libraries of the Rust addons');
|
|
361
368
|
const library = change.action === 'link' && !change.crate && !change.pkgConfig && !change.framework ? libraryOfFile(directory, change.value) : undefined;
|
|
362
369
|
const scope = scopeOf(change, library);
|
|
370
|
+
//The headers of the package, or of the system (an absolute directory): the addon is built where it is installed
|
|
371
|
+
if (change.action === 'include' && !path.isAbsolute(change.value) && path.relative(directory, path.resolve(directory, change.value)).startsWith('..')) {
|
|
372
|
+
throw new Error(`${change.value} is not in the package: the addon is built where the package is installed (give a directory of the package, or an absolute directory of the system)`);
|
|
373
|
+
}
|
|
363
374
|
const files = build === 'gyp'
|
|
364
375
|
? await changeGyp(directory, change, library, scope)
|
|
365
376
|
: build === 'cmake' ? await changeCMake(directory, change, library, scope) : await changeRust(directory, change, library, scope);
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { LaunchEnvironment } from '../Environment.js';
|
|
3
|
-
import { spawnCommand } from '../Processes.js';
|
|
3
|
+
import { projectPackageManager, spawnCommand } from '../Processes.js';
|
|
4
4
|
import { styled, warn } from '../Output.js';
|
|
5
5
|
import { matchName } from './Workspace.js';
|
|
6
6
|
const COLORS = ['cyan', 'magenta', 'yellow', 'green', 'blue', 'red'];
|
|
7
7
|
/**
|
|
8
|
-
* Run a script of a package, its output prefixed with the name of the package
|
|
8
|
+
* Run a script of a package with the package manager of the workspace, its output prefixed with the name of the package
|
|
9
|
+
* @param packageManager
|
|
9
10
|
* @param pkg
|
|
10
11
|
* @param script
|
|
11
12
|
* @param args
|
|
12
13
|
* @param prefix
|
|
13
14
|
*/
|
|
14
|
-
function runPackageScript(pkg, script, args, prefix) {
|
|
15
|
+
function runPackageScript(packageManager, pkg, script, args, prefix) {
|
|
15
16
|
return new Promise((resolve) => {
|
|
16
|
-
const child = spawnCommand(
|
|
17
|
+
const child = spawnCommand(packageManager, ['run', script, ...(args.length ? ['--', ...args] : [])], { cwd: pkg.directory, env: LaunchEnvironment });
|
|
17
18
|
const forward = (stream, target) => {
|
|
18
19
|
let pending = '';
|
|
19
20
|
stream.on('data', (chunk) => {
|
|
@@ -45,7 +46,10 @@ function runPackageScript(pkg, script, args, prefix) {
|
|
|
45
46
|
* @param options
|
|
46
47
|
*/
|
|
47
48
|
export async function runScript(workspace, script, options = {}) {
|
|
49
|
+
const packageManager = projectPackageManager(workspace.root);
|
|
48
50
|
const inScope = (pkg) => !options.scope?.length || options.scope.some((pattern) => matchName(pkg.name, pattern));
|
|
51
|
+
if (options.scope?.length && !workspace.packages.some(inScope))
|
|
52
|
+
throw new Error(`The workspace has no package matching ${options.scope.join(', ')} (${workspace.packages.map((pkg) => pkg.name).join(', ')})`);
|
|
49
53
|
const selected = workspace.packages.filter((pkg) => inScope(pkg) && !!pkg.scripts[script]);
|
|
50
54
|
const skipped = workspace.packages.filter((pkg) => inScope(pkg) && !pkg.scripts[script]).map((pkg) => pkg.name);
|
|
51
55
|
const selectedNames = new Set(selected.map((pkg) => pkg.name));
|
|
@@ -81,7 +85,7 @@ export async function runScript(workspace, script, options = {}) {
|
|
|
81
85
|
if (![...waitsFor.get(pkg.name)].every((name) => done.has(name)))
|
|
82
86
|
continue;
|
|
83
87
|
const prefix = styled(COLORS[selected.indexOf(pkg) % COLORS.length], `${pkg.name.padEnd(width)} │`);
|
|
84
|
-
running.set(pkg.name, runPackageScript(pkg, script, options.args ?? [], prefix).then((code) => {
|
|
88
|
+
running.set(pkg.name, runPackageScript(packageManager, pkg, script, options.args ?? [], prefix).then((code) => {
|
|
85
89
|
running.delete(pkg.name);
|
|
86
90
|
if (code === 0) {
|
|
87
91
|
done.add(pkg.name);
|
|
@@ -4,6 +4,13 @@ import { runCommand } from '../Processes.js';
|
|
|
4
4
|
import { warn } from '../Output.js';
|
|
5
5
|
import { DEPENDENCY_FIELDS } from './Workspace.js';
|
|
6
6
|
const VERSION = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a string is a version (1.2.3, 1.3.0-beta.0)
|
|
9
|
+
* @param version
|
|
10
|
+
*/
|
|
11
|
+
export function isVersion(version) {
|
|
12
|
+
return VERSION.test(version);
|
|
13
|
+
}
|
|
7
14
|
/**
|
|
8
15
|
* The next version
|
|
9
16
|
* @param current
|
|
@@ -81,6 +88,19 @@ export function planVersion(workspace, bump, preid) {
|
|
|
81
88
|
* @param options
|
|
82
89
|
*/
|
|
83
90
|
export async function applyVersion(workspace, plan, options = {}) {
|
|
91
|
+
const git = async (args) => {
|
|
92
|
+
const result = await runCommand('git', args, workspace.root);
|
|
93
|
+
if (result.code !== 0)
|
|
94
|
+
throw new Error(`git ${args[0]} failed: ${result.output.trim()}`);
|
|
95
|
+
return result.output;
|
|
96
|
+
};
|
|
97
|
+
//Checked before a file is changed: a repository, and a tag which does not exist yet
|
|
98
|
+
if (options.git) {
|
|
99
|
+
if ((await runCommand('git', ['rev-parse', '--is-inside-work-tree'], workspace.root).catch((error) => ({ code: 1, output: error.message }))).code !== 0)
|
|
100
|
+
throw new Error(`${workspace.root} is not in a git repository: --git commits the version there`);
|
|
101
|
+
if ((await runCommand('git', ['rev-parse', '-q', '--verify', `refs/tags/v${plan.to}`], workspace.root)).code === 0)
|
|
102
|
+
throw new Error(`The tag v${plan.to} exists already`);
|
|
103
|
+
}
|
|
84
104
|
const names = new Set(workspace.packages.map((pkg) => pkg.name));
|
|
85
105
|
const changed = [];
|
|
86
106
|
for (const pkg of workspace.packages) {
|
|
@@ -94,8 +114,8 @@ export async function applyVersion(workspace, plan, options = {}) {
|
|
|
94
114
|
}
|
|
95
115
|
changed.push(pkg.manifest);
|
|
96
116
|
}
|
|
97
|
-
//The version of the workspace is kept by lerna.json when it
|
|
98
|
-
if (workspace.lerna) {
|
|
117
|
+
//The version of the workspace is kept by lerna.json when it gives one, by the root package.json otherwise
|
|
118
|
+
if (workspace.lerna?.data.version !== undefined) {
|
|
99
119
|
workspace.lerna.data.version = plan.to;
|
|
100
120
|
changed.push(workspace.lerna);
|
|
101
121
|
}
|
|
@@ -131,13 +151,9 @@ export async function applyVersion(workspace, plan, options = {}) {
|
|
|
131
151
|
warn('yarn.lock is not updated: run yarn install');
|
|
132
152
|
}
|
|
133
153
|
if (options.git) {
|
|
134
|
-
|
|
135
|
-
const result = await runCommand('git', args, workspace.root);
|
|
136
|
-
if (result.code !== 0)
|
|
137
|
-
throw new Error(`git ${args[0]} failed: ${result.output.trim()}`);
|
|
138
|
-
};
|
|
154
|
+
//Only the files of the version are committed, the changes staged before kept staged
|
|
139
155
|
await git(['add', ...files]);
|
|
140
|
-
await git(['commit', '-m', `v${plan.to}
|
|
156
|
+
await git(['commit', '-m', `v${plan.to}`, '--', ...files]);
|
|
141
157
|
await git(['tag', `v${plan.to}`]);
|
|
142
158
|
}
|
|
143
159
|
return files;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { highestVersion } from '../Registry.js';
|
|
5
5
|
/**
|
|
6
6
|
* The fields of the dependencies of a package
|
|
7
7
|
*/
|
|
@@ -79,14 +79,25 @@ async function expandPattern(root, pattern) {
|
|
|
79
79
|
return walk(root, 0);
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* The patterns of the packages of a pnpm workspace
|
|
82
|
+
* The patterns of the packages of a pnpm workspace: a block list (one "- 'packages/*'" per line) or a flow list
|
|
83
|
+
* (packages: ['packages/*', 'apps/*'], on one line or on several)
|
|
83
84
|
* @param file
|
|
84
85
|
*/
|
|
85
86
|
async function pnpmPatterns(file) {
|
|
86
87
|
const patterns = [];
|
|
88
|
+
const text = await readFile(file, 'utf8');
|
|
89
|
+
const flow = /^packages:\s*\[([^\]]*)\]/m.exec(text);
|
|
90
|
+
if (flow) {
|
|
91
|
+
for (const item of flow[1].replace(/#.*$/gm, '').split(',')) {
|
|
92
|
+
const pattern = item.trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
93
|
+
if (pattern)
|
|
94
|
+
patterns.push(pattern);
|
|
95
|
+
}
|
|
96
|
+
return patterns;
|
|
97
|
+
}
|
|
87
98
|
let inPackages = false;
|
|
88
|
-
for (const line of
|
|
89
|
-
if (/^packages:\s
|
|
99
|
+
for (const line of text.split('\n')) {
|
|
100
|
+
if (/^packages:\s*(#.*)?$/.test(line)) {
|
|
90
101
|
inPackages = true;
|
|
91
102
|
continue;
|
|
92
103
|
}
|
|
@@ -104,11 +115,25 @@ async function pnpmPatterns(file) {
|
|
|
104
115
|
* A workspace: its root, its packages and their fixed version
|
|
105
116
|
*/
|
|
106
117
|
export class Workspace {
|
|
107
|
-
constructor(root, manifest, packages, lerna) {
|
|
118
|
+
constructor(root, manifest, packages, lerna, patterns = []) {
|
|
108
119
|
this.root = root;
|
|
109
120
|
this.manifest = manifest;
|
|
110
121
|
this.packages = packages;
|
|
111
122
|
this.lerna = lerna;
|
|
123
|
+
this.patterns = patterns;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The directory where new packages go: the one of the first pattern of the workspace of the form <directory>/*
|
|
127
|
+
* (packages/* when it has it), undefined when its packages are all named one by one
|
|
128
|
+
*/
|
|
129
|
+
get packagesDirectory() {
|
|
130
|
+
const directories = this.patterns
|
|
131
|
+
.filter((pattern) => !pattern.startsWith('!'))
|
|
132
|
+
.map((pattern) => pattern.replace(/^\.\//, ''))
|
|
133
|
+
.filter((pattern) => /^[^*?{}[\]!]+\/\*$/.test(pattern))
|
|
134
|
+
.map((pattern) => pattern.slice(0, -2));
|
|
135
|
+
const directory = directories.includes('packages') ? 'packages' : directories[0];
|
|
136
|
+
return directory === undefined ? undefined : path.join(this.root, directory);
|
|
112
137
|
}
|
|
113
138
|
/**
|
|
114
139
|
* The workspace of a directory: the nearest directory declaring workspaces
|
|
@@ -171,7 +196,7 @@ export class Workspace {
|
|
|
171
196
|
pkg.dependencies = [...dependencies];
|
|
172
197
|
}
|
|
173
198
|
const lernaFile = path.join(root, 'lerna.json');
|
|
174
|
-
return new Workspace(root, manifest, packages, existsSync(lernaFile) ? await JSONFile.read(lernaFile) : undefined);
|
|
199
|
+
return new Workspace(root, manifest, packages, existsSync(lernaFile) ? await JSONFile.read(lernaFile) : undefined, patterns);
|
|
175
200
|
}
|
|
176
201
|
/**
|
|
177
202
|
* A package by name
|
|
@@ -224,8 +249,9 @@ export class Workspace {
|
|
|
224
249
|
}
|
|
225
250
|
/**
|
|
226
251
|
* The fixed version of the workspace: the one of lerna.json, of the root package.json, or the highest of the packages
|
|
252
|
+
* (a prerelease above the releases included)
|
|
227
253
|
*/
|
|
228
254
|
get version() {
|
|
229
|
-
return this.lerna?.data.version ?? this.manifest.data.version ??
|
|
255
|
+
return this.lerna?.data.version ?? this.manifest.data.version ?? highestVersion(this.packages.map((pkg) => pkg.version).filter((version) => !!version));
|
|
230
256
|
}
|
|
231
257
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "应用的作者",
|
|
15
15
|
"license": "应用的许可证",
|
|
16
16
|
"packageManager": "安装依赖使用的包管理器,默认是运行本工具的那个(none 表示不安装)",
|
|
17
|
-
"with": "启用模板的可选功能,多个用逗号分隔(lakutata templates 会列出):i18n",
|
|
17
|
+
"with": "启用模板的可选功能,多个用逗号分隔(lakutata templates 会列出):i18n、installer",
|
|
18
18
|
"git": "初始化 git 仓库",
|
|
19
19
|
"yes": "不经确认直接创建项目"
|
|
20
20
|
}
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"add": {
|
|
61
|
-
"description": "
|
|
61
|
+
"description": "向工作区添加一个包(放在工作区的包目录中:packages/<名称>):TypeScript 包,或用 --native [gyp|cmake|rust] 添加原生插件",
|
|
62
62
|
"arguments": {
|
|
63
|
-
"name": "
|
|
63
|
+
"name": "包的名称(它在工作区包目录中的目录:packages/<名称>)"
|
|
64
64
|
},
|
|
65
65
|
"options": {
|
|
66
66
|
"native": "原生插件,按构建系统:gyp(C++,node-gyp,默认)、cmake(C++,cmake-js)或 rust(napi-rs)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"script": "构建并启动应用的包脚本(generate、run、revert、show)",
|
|
115
115
|
"package": "应用所在的工作区包(默认是当前目录)",
|
|
116
116
|
"database": "数据库:其 Database 组件或提供者的名称(默认全部;应用有多个数据库时,回滚必须指定)",
|
|
117
|
-
"transaction": "执行或回滚迁移的事务方式:all
|
|
117
|
+
"transaction": "执行或回滚迁移的事务方式:all(所有迁移一个事务)、each(每个迁移一个)或 none;默认取数据库的 migrationsTransactionMode,没有时为 all",
|
|
118
118
|
"fake": "只标记迁移为已执行或已回滚,不实际执行"
|
|
119
119
|
}
|
|
120
120
|
},
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"author": "應用程式的作者",
|
|
15
15
|
"license": "應用程式的授權條款",
|
|
16
16
|
"packageManager": "安裝相依套件使用的套件管理器,預設是執行本工具的那個(none 表示不安裝)",
|
|
17
|
-
"with": "啟用範本的可選功能,多個用逗號分隔(lakutata templates 會列出):i18n",
|
|
17
|
+
"with": "啟用範本的可選功能,多個用逗號分隔(lakutata templates 會列出):i18n、installer",
|
|
18
18
|
"git": "初始化 git 儲存庫",
|
|
19
19
|
"yes": "不經確認直接建立專案"
|
|
20
20
|
}
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"add": {
|
|
61
|
-
"description": "
|
|
61
|
+
"description": "向工作區新增一個套件(放在工作區的套件目錄中:packages/<名稱>):TypeScript 套件,或用 --native [gyp|cmake|rust] 新增原生外掛",
|
|
62
62
|
"arguments": {
|
|
63
|
-
"name": "
|
|
63
|
+
"name": "套件的名稱(它在工作區套件目錄中的目錄:packages/<名稱>)"
|
|
64
64
|
},
|
|
65
65
|
"options": {
|
|
66
66
|
"native": "原生外掛,依建置系統:gyp(C++,node-gyp,預設)、cmake(C++,cmake-js)或 rust(napi-rs)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"script": "建置並啟動應用程式的套件指令碼(generate、run、revert、show)",
|
|
115
115
|
"package": "應用程式所在的工作區套件(預設是目前目錄)",
|
|
116
116
|
"database": "資料庫:其 Database 元件或提供者的名稱(預設全部;應用程式有多個資料庫時,復原必須指定)",
|
|
117
|
-
"transaction": "執行或復原遷移的交易方式:all
|
|
117
|
+
"transaction": "執行或復原遷移的交易方式:all(所有遷移一個交易)、each(每個遷移一個)或 none;預設取資料庫的 migrationsTransactionMode,沒有時為 all",
|
|
118
118
|
"fake": "只標記遷移為已執行或已復原,不實際執行"
|
|
119
119
|
}
|
|
120
120
|
},
|
package/package.json
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakutata/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "The command line tool of the lakutata framework: create the projects from the templates, run and version the packages of the workspaces, bundle the applications and make their executables (with @lakutata/builder).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lakutata",
|
|
7
|
+
"cli",
|
|
8
|
+
"scaffolding",
|
|
9
|
+
"templates",
|
|
10
|
+
"monorepo",
|
|
11
|
+
"workspaces",
|
|
12
|
+
"bundle",
|
|
13
|
+
"executable",
|
|
14
|
+
"installer",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://lakutata.com/packages/cli",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/lakutata/lakutata/issues",
|
|
20
|
+
"email": "admin@lakutata.com"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/lakutata/lakutata.git",
|
|
25
|
+
"directory": "packages/cli"
|
|
26
|
+
},
|
|
27
|
+
"author": "alex.ma",
|
|
5
28
|
"type": "module",
|
|
6
29
|
"bin": {
|
|
7
30
|
"lakutata": "./dist/esm/CLIApp.js"
|
|
@@ -16,10 +39,10 @@
|
|
|
16
39
|
"dependencies": {
|
|
17
40
|
"@inquirer/prompts": "^8.7.2",
|
|
18
41
|
"commander": "^15.0.0",
|
|
19
|
-
"lakutata": "3.
|
|
42
|
+
"lakutata": "3.1.0"
|
|
20
43
|
},
|
|
21
44
|
"peerDependencies": {
|
|
22
|
-
"@lakutata/builder": "3.
|
|
45
|
+
"@lakutata/builder": "3.1.0"
|
|
23
46
|
},
|
|
24
47
|
"peerDependenciesMeta": {
|
|
25
48
|
"@lakutata/builder": {
|
|
@@ -27,10 +50,10 @@
|
|
|
27
50
|
}
|
|
28
51
|
},
|
|
29
52
|
"devDependencies": {
|
|
30
|
-
"@lakutata/builder": "3.
|
|
53
|
+
"@lakutata/builder": "3.1.0"
|
|
31
54
|
},
|
|
32
55
|
"engines": {
|
|
33
|
-
"node": "
|
|
56
|
+
"node": "^22.13.0 || >=24.11.0"
|
|
34
57
|
},
|
|
35
58
|
"license": "MIT",
|
|
36
59
|
"files": [
|