@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.
@@ -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('npm', ['run', application.script], {
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 } : {}) }
@@ -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.join('\n')}\n`;
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, rename, stat, writeFile } from 'node:fs/promises';
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
- * @return the paths of the files of the project, relative to its directory
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(directory)) {
88
+ for (const file of await listFiles(template.root)) {
89
89
  //"_gitignore" becomes ".gitignore"
90
90
  const renamed = projectPath(file);
91
- if (renamed !== file)
92
- await rename(path.join(directory, file), path.join(directory, renamed));
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 files = await this.copy(template, plan.directory);
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,
@@ -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
- ...(library.platform === 'win' ? [] : [['set_target_properties', name, 'PROPERTIES', 'BUILD_RPATH', library.platform === 'mac' ? '@loader_path' : '$ORIGIN']]),
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
- directives.push(`rustc-link-search=native=${path.posix.dirname(library.file)}`, `rustc-link-lib=dylib=${library.name}`);
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 after its build (when it is built on the platform of the library)
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
- if (!(scripts['postbuild:native'] ?? '').includes(copy)) {
342
- scripts['postbuild:native'] = scripts['postbuild:native'] ? `${scripts['postbuild:native']} && ${copy}` : copy;
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('npm', ['run', script, ...(args.length ? ['--', ...args] : [])], { cwd: pkg.directory, env: LaunchEnvironment });
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 has one, by the root package.json otherwise
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
- const git = async (args) => {
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 { newestVersion } from '../Registry.js';
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 (await readFile(file, 'utf8')).split('\n')) {
89
- if (/^packages:\s*$/.test(line)) {
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 ?? newestVersion(this.packages.map((pkg) => pkg.version).filter((version) => !!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": "向工作区添加一个包(packages/<名称>):TypeScript 包,或用 --native [gyp|cmake|rust] 添加原生插件",
61
+ "description": "向工作区添加一个包(放在工作区的包目录中:packages/<名称>):TypeScript 包,或用 --native [gyp|cmake|rust] 添加原生插件",
62
62
  "arguments": {
63
- "name": "包的名称(工作区的 packages/<名称>)"
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(一个事务,默认)、each(每个迁移一个)或 none",
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": "向工作區新增一個套件(packages/<名稱>):TypeScript 套件,或用 --native [gyp|cmake|rust] 新增原生外掛",
61
+ "description": "向工作區新增一個套件(放在工作區的套件目錄中:packages/<名稱>):TypeScript 套件,或用 --native [gyp|cmake|rust] 新增原生外掛",
62
62
  "arguments": {
63
- "name": "套件的名稱(工作區的 packages/<名稱>)"
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(一個交易,預設)、each(每個遷移一個)或 none",
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.0.0",
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.0.0"
42
+ "lakutata": "3.1.0"
20
43
  },
21
44
  "peerDependencies": {
22
- "@lakutata/builder": "3.0.0"
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.0.0"
53
+ "@lakutata/builder": "3.1.0"
31
54
  },
32
55
  "engines": {
33
- "node": ">=22.12"
56
+ "node": "^22.13.0 || >=24.11.0"
34
57
  },
35
58
  "license": "MIT",
36
59
  "files": [