@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,487 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { Workspace } from './workspace/Workspace.js';
5
+ /**
6
+ * The versions of the dependencies of the native addons
7
+ */
8
+ const NATIVE_VERSIONS = {
9
+ 'node-addon-api': '^8.9.2',
10
+ 'node-gyp-build': '^4.8.4',
11
+ 'node-gyp': '^12.4.0',
12
+ 'cmake-js': '^8.0.0',
13
+ '@napi-rs/cli': '^3.10.6'
14
+ };
15
+ /**
16
+ * The compiler options of the packages (as the ones of the templates), their declarations emitted for the packages
17
+ * depending on them
18
+ * @param outDir
19
+ */
20
+ function tsConfig(outDir) {
21
+ return `${JSON.stringify({
22
+ compilerOptions: {
23
+ target: 'ES2022',
24
+ lib: ['ES2022'],
25
+ module: 'NodeNext',
26
+ moduleResolution: 'NodeNext',
27
+ rootDir: 'src',
28
+ outDir: outDir,
29
+ declaration: true,
30
+ types: ['node'],
31
+ strict: true,
32
+ esModuleInterop: true,
33
+ skipLibCheck: true
34
+ },
35
+ include: ['src']
36
+ }, null, 2)}\n`;
37
+ }
38
+ /**
39
+ * The files of a TypeScript package
40
+ * @param name
41
+ * @param version
42
+ */
43
+ function libraryFiles(name, version) {
44
+ return {
45
+ 'package.json': `${JSON.stringify({
46
+ name: name,
47
+ version: version,
48
+ private: true,
49
+ type: 'module',
50
+ main: 'build/index.js',
51
+ types: 'build/index.d.ts',
52
+ exports: { '.': { types: './build/index.d.ts', default: './build/index.js' } },
53
+ files: ['build', '!build/tests'],
54
+ scripts: {
55
+ clean: 'lakutata clean',
56
+ build: 'tsc',
57
+ rebuild: 'npm run clean && npm run build',
58
+ test: 'node --test "build/tests/**/*.spec.js"'
59
+ }
60
+ }, null, 2)}\n`,
61
+ 'tsconfig.json': tsConfig('build'),
62
+ 'src/index.ts': `/**
63
+ * Greet someone
64
+ * @param name
65
+ */
66
+ export function Greet(name: string): string {
67
+ return \`Hello, \${name}\`
68
+ }
69
+ `,
70
+ 'src/tests/index.spec.ts': `import {describe, it} from 'node:test'
71
+ import assert from 'node:assert'
72
+ import {Greet} from '../index.js'
73
+
74
+ describe('${name}', (): void => {
75
+ it('greets', (): void => {
76
+ assert.strictEqual(Greet('lakutata'), 'Hello, lakutata')
77
+ })
78
+ })
79
+ `,
80
+ 'README.md': `# ${name}\n\n\`\`\`bash\nnpm run rebuild # compile\nnpm test # run the tests\n\`\`\`\n`
81
+ };
82
+ }
83
+ /**
84
+ * The build files of an addon built by node-gyp: binding.gyp, its options commented
85
+ * @param target
86
+ */
87
+ function gypFiles(target) {
88
+ return {
89
+ 'binding.gyp': `# The build of the addon by node-gyp (gyp: a Python literal, its comments start with #)
90
+ {
91
+ 'targets': [
92
+ {
93
+ 'target_name': '${target}',
94
+ 'sources': ['cpp/binding.cc'],
95
+ # The C++ exceptions of node-addon-api (thrown in JavaScript as errors) on every platform
96
+ 'dependencies': ["<!(node -p \\"require('node-addon-api').targets\\"):node_addon_api_except"],
97
+ 'defines': ['NAPI_VERSION=8'],
98
+ 'include_dirs': [
99
+ # The headers of the addon and of its libraries: 'deps/include'
100
+ ],
101
+ 'libraries': [
102
+ # The libraries of the addon (lakutata native link writes them, or by hand):
103
+ # A library of the system: '-lpcap' (Linux, macOS), 'ws2_32.lib' (Windows, in its condition below)
104
+ # A static library of the package: '<(module_root_dir)/deps/lib/libfoo.a'
105
+ # A library found by pkg-config: '<!@(pkg-config --libs libpcap)' (its headers in include_dirs:
106
+ # '<!@(pkg-config --cflags-only-I libpcap | sed s/-I//g)')
107
+ # A dynamic library shipped with the addon (deps/lib/libfoo.so, .dylib): '-L<(module_root_dir)/deps/lib', '-lfoo',
108
+ # copied next to the addon (copies below) and found there at run time (the rpath of the conditions below)
109
+ ],
110
+ # 'copies': [{'destination': '<(PRODUCT_DIR)', 'files': ['<(module_root_dir)/deps/lib/libfoo.so']}],
111
+ 'conditions': [
112
+ ['OS=="mac"', {
113
+ 'xcode_settings': {
114
+ 'MACOSX_DEPLOYMENT_TARGET': '11.0',
115
+ # One addon for the Macs with Intel and with Apple silicon
116
+ 'OTHER_CFLAGS': ['-arch x86_64', '-arch arm64'],
117
+ 'OTHER_LDFLAGS': [
118
+ '-arch x86_64', '-arch arm64',
119
+ # The dynamic libraries shipped with the addon, found next to it: '-Wl,-rpath,@loader_path'
120
+ # A framework of the system: '-framework CoreFoundation'
121
+ ],
122
+ # The standard of C++: 'CLANG_CXX_LANGUAGE_STANDARD': 'c++20'
123
+ }
124
+ }],
125
+ ['OS=="linux"', {
126
+ 'ldflags': [
127
+ # The dynamic libraries shipped with the addon, found next to it: "-Wl,-rpath,'$$ORIGIN'"
128
+ ],
129
+ # The standard of C++: 'cflags_cc': ['-std=c++20']
130
+ }],
131
+ ['OS=="win"', {
132
+ 'msvs_settings': {
133
+ 'VCCLCompilerTool': {
134
+ 'AdditionalOptions': ['/utf-8']
135
+ # The standard of C++: '/std:c++20'
136
+ }
137
+ },
138
+ 'libraries': [
139
+ # A library of the system: 'ws2_32.lib', 'iphlpapi.lib'
140
+ # A static library of the package: '<(module_root_dir)/deps/lib/foo.lib'
141
+ # A DLL shipped with the addon (deps/lib/foo.dll): its import library '<(module_root_dir)/deps/lib/foo.lib',
142
+ # the DLL copied next to the addon (copies below), where Windows finds it at run time
143
+ ],
144
+ # 'copies': [{'destination': '<(PRODUCT_DIR)', 'files': ['<(module_root_dir)/deps/lib/foo.dll']}]
145
+ }]
146
+ ]
147
+ }
148
+ ]
149
+ }
150
+ `,
151
+ 'cpp/binding.cc': cppBinding(target)
152
+ };
153
+ }
154
+ /**
155
+ * The build files of an addon built by cmake-js: CMakeLists.txt, its options commented
156
+ * @param target
157
+ */
158
+ function cMakeFiles(target) {
159
+ return {
160
+ 'CMakeLists.txt': `# The build of the addon by cmake-js (npm run build:native)
161
+ cmake_minimum_required(VERSION 3.15)
162
+ cmake_policy(SET CMP0091 NEW)
163
+ cmake_policy(SET CMP0042 NEW)
164
+
165
+ # One addon for the Macs with Intel and with Apple silicon (forced: cmake-js gives the architecture of the machine)
166
+ set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "The architectures of macOS" FORCE)
167
+ set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0")
168
+
169
+ project(${target})
170
+
171
+ # The standard of C++: the headers of Node.js (and node-addon-api) need C++20, as node-gyp gives it
172
+ set(CMAKE_CXX_STANDARD 20)
173
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
174
+
175
+ add_library(\${PROJECT_NAME} SHARED cpp/binding.cc \${CMAKE_JS_SRC})
176
+ set_target_properties(\${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node")
177
+
178
+ # The headers of Node.js and of node-addon-api; the C++ exceptions thrown in JavaScript as errors
179
+ execute_process(COMMAND node -p "require('node-addon-api').include" WORKING_DIRECTORY \${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE NODE_ADDON_API_DIR)
180
+ string(REGEX REPLACE "[\\r\\n\\"]" "" NODE_ADDON_API_DIR \${NODE_ADDON_API_DIR})
181
+ target_include_directories(\${PROJECT_NAME} PRIVATE \${CMAKE_JS_INC} \${NODE_ADDON_API_DIR})
182
+ target_compile_definitions(\${PROJECT_NAME} PRIVATE NAPI_VERSION=8 NAPI_CPP_EXCEPTIONS)
183
+ if(MSVC)
184
+ target_compile_options(\${PROJECT_NAME} PRIVATE /utf-8 /EHsc)
185
+ endif()
186
+ target_link_libraries(\${PROJECT_NAME} \${CMAKE_JS_LIB})
187
+
188
+ # The libraries of the addon (lakutata native link writes them, or by hand):
189
+ # - a library of the system: target_link_libraries(\${PROJECT_NAME} pcap) (Windows: ws2_32)
190
+ # - a library found by pkg-config:
191
+ # find_package(PkgConfig REQUIRED)
192
+ # pkg_check_modules(PCAP REQUIRED libpcap)
193
+ # target_include_directories(\${PROJECT_NAME} PRIVATE \${PCAP_INCLUDE_DIRS})
194
+ # target_link_libraries(\${PROJECT_NAME} \${PCAP_LIBRARIES})
195
+ # - a package of CMake: find_package(ZLIB REQUIRED) then target_link_libraries(\${PROJECT_NAME} ZLIB::ZLIB)
196
+ # - a static library of the package: target_link_libraries(\${PROJECT_NAME} \${CMAKE_SOURCE_DIR}/deps/lib/libfoo.a)
197
+ # (Windows: deps/lib/foo.lib)
198
+ # - a library built from its sources (its own CMakeLists.txt): add_subdirectory(deps/foo) then
199
+ # target_link_libraries(\${PROJECT_NAME} foo)
200
+ # - a dynamic library shipped with the addon (deps/lib), copied next to it and found there at run time:
201
+ # target_link_libraries(\${PROJECT_NAME} \${CMAKE_SOURCE_DIR}/deps/lib/libfoo.so)
202
+ # set_target_properties(\${PROJECT_NAME} PROPERTIES BUILD_RPATH "$<IF:$<PLATFORM_ID:Darwin>,@loader_path,$ORIGIN>")
203
+ # add_custom_command(TARGET \${PROJECT_NAME} POST_BUILD COMMAND \${CMAKE_COMMAND} -E copy \${CMAKE_SOURCE_DIR}/deps/lib/libfoo.so $<TARGET_FILE_DIR:\${PROJECT_NAME}>)
204
+ # (Windows: the DLL linked by its import library, deps/lib/foo.lib, and copied next to the addon, where Windows finds
205
+ # it; no rpath)
206
+
207
+ # The import library of Node.js on Windows
208
+ if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET)
209
+ execute_process(COMMAND \${CMAKE_AR} /def:\${CMAKE_JS_NODELIB_DEF} /out:\${CMAKE_JS_NODELIB_TARGET} \${CMAKE_STATIC_LINKER_FLAGS})
210
+ endif()
211
+ `,
212
+ 'cpp/binding.cc': cppBinding(target)
213
+ };
214
+ }
215
+ /**
216
+ * The build files of an addon of Rust built by napi-rs: Cargo.toml, build.rs and rust/lib.rs
217
+ * @param target
218
+ * @param version
219
+ */
220
+ function rustFiles(target, version) {
221
+ return {
222
+ 'Cargo.toml': `[package]
223
+ name = "${target}"
224
+ version = "${version.replace(/[-+].*$/, '')}"
225
+ edition = "2021"
226
+
227
+ [lib]
228
+ crate-type = ["cdylib"]
229
+ path = "rust/lib.rs"
230
+
231
+ [dependencies]
232
+ # Node-API: the same addon runs in Node.js and Electron
233
+ napi = { version = "3", default-features = false, features = ["napi8"] }
234
+ napi-derive = "3"
235
+ # The crates of the addon: their native libraries are built and linked by cargo (their build.rs)
236
+
237
+ [build-dependencies]
238
+ napi-build = "2"
239
+
240
+ [profile.release]
241
+ lto = true
242
+ strip = "symbols"
243
+ `,
244
+ 'build.rs': `use std::env;
245
+ use std::path::Path;
246
+
247
+ fn main() {
248
+ napi_build::setup();
249
+ // The native libraries of the addon (beside its crates), given by lakutata native link in .cargo/config.toml: the
250
+ // directives of cargo of every platform (LAKUTATA_NATIVE) and of one (LAKUTATA_NATIVE_MACOS, LAKUTATA_NATIVE_LINUX,
251
+ // LAKUTATA_NATIVE_WINDOWS, LAKUTATA_NATIVE_UNIX), separated by ";", their directories relative to the package
252
+ let root = env::var("CARGO_MANIFEST_DIR").unwrap();
253
+ let mut keys = vec![String::from("LAKUTATA_NATIVE")];
254
+ for cfg in ["CARGO_CFG_TARGET_OS", "CARGO_CFG_TARGET_FAMILY"] {
255
+ for value in env::var(cfg).unwrap_or_default().split(',').filter(|value| !value.is_empty()) {
256
+ let key = format!("LAKUTATA_NATIVE_{}", value.to_uppercase());
257
+ if !keys.contains(&key) {
258
+ keys.push(key);
259
+ }
260
+ }
261
+ }
262
+ for key in keys {
263
+ println!("cargo:rerun-if-env-changed={}", key);
264
+ for directive in env::var(&key).unwrap_or_default().split(';').map(str::trim).filter(|directive| !directive.is_empty()) {
265
+ match directive.strip_prefix("rustc-link-search=") {
266
+ Some(search) => {
267
+ let (kind, directory) = search.split_once('=').unwrap_or(("all", search));
268
+ println!("cargo:rustc-link-search={}={}", kind, Path::new(&root).join(directory).display());
269
+ }
270
+ None => println!("cargo:{}", directive),
271
+ }
272
+ }
273
+ }
274
+ }
275
+ `,
276
+ 'rust/lib.rs': `use napi_derive::napi;
277
+
278
+ /// Greet someone: greet(name)
279
+ #[napi]
280
+ pub fn greet(name: String) -> String {
281
+ format!("Hello, {} (native)", name)
282
+ }
283
+ `
284
+ };
285
+ }
286
+ /**
287
+ * The C++ of an addon (node-addon-api)
288
+ * @param target
289
+ */
290
+ function cppBinding(target) {
291
+ return `#include <napi.h>
292
+
293
+ /**
294
+ * Greet someone: greet(name)
295
+ */
296
+ static Napi::Value Greet(const Napi::CallbackInfo &info) {
297
+ Napi::Env env = info.Env();
298
+ if (info.Length() < 1 || !info[0].IsString()) {
299
+ throw Napi::TypeError::New(env, "The name must be a string");
300
+ }
301
+ return Napi::String::New(env, "Hello, " + info[0].As<Napi::String>().Utf8Value() + " (native)");
302
+ }
303
+
304
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
305
+ exports.Set("greet", Napi::Function::New(env, Greet));
306
+ return exports;
307
+ }
308
+
309
+ NODE_API_MODULE(${target}, Init)
310
+ `;
311
+ }
312
+ /**
313
+ * How the libraries and the options of the build are given, by build system
314
+ */
315
+ const NATIVE_GUIDES = {
316
+ gyp: `C++ with [node-addon-api](https://github.com/nodejs/node-addon-api), built by node-gyp from \`binding.gyp\`.
317
+
318
+ The libraries and the options of the build are given in \`binding.gyp\`, whose comments show them: the libraries of
319
+ the system (\`-lpcap\`, \`ws2_32.lib\`, the frameworks of macOS), the ones found by pkg-config, the static libraries
320
+ of the package, the dynamic libraries shipped with the addon (copied next to it, found there at run time by its rpath:
321
+ \`$ORIGIN\` on Linux, \`@loader_path\` on macOS), the standard of C++, the options of each platform (\`conditions\`).`,
322
+ cmake: `C++ with [node-addon-api](https://github.com/nodejs/node-addon-api), built by [cmake-js](https://github.com/cmake-js/cmake-js)
323
+ from \`CMakeLists.txt\`.
324
+
325
+ The libraries and the options of the build are given in \`CMakeLists.txt\`, whose comments show them: the libraries of
326
+ the system, the ones found by pkg-config or by \`find_package\`, the static libraries of the package, the libraries built
327
+ from their sources (\`add_subdirectory\`), the dynamic libraries shipped with the addon (copied next to it, found there
328
+ at run time by its rpath). The headers of Node.js need C++20 (\`CMAKE_CXX_STANDARD\`).`,
329
+ rust: `Rust with [napi-rs](https://napi.rs), built by cargo (\`Cargo.toml\`, \`build.rs\`, \`rust/lib.rs\`).
330
+
331
+ The libraries of the addon are crates (\`Cargo.toml\`), their native libraries built and linked by cargo; the other
332
+ native libraries are linked by \`build.rs\`, given by \`lakutata native link\`. The addon is built for the platform and the
333
+ architecture of the machine (\`rustup target add\` builds the other ones).`
334
+ };
335
+ /**
336
+ * The files of a native addon: built into build/Release (gyp, cmake-js, napi-rs), loaded by node-gyp-build from the
337
+ * TypeScript compiled into dist (the build systems own build)
338
+ * @param name
339
+ * @param version
340
+ * @param build
341
+ */
342
+ function nativeFiles(name, version, build) {
343
+ const target = name.replace(/^@[^/]+\//, '').replace(/[^A-Za-z0-9_]/g, '_');
344
+ const builds = {
345
+ gyp: {
346
+ files: gypFiles(target),
347
+ buildNative: 'node-gyp rebuild',
348
+ //Built when installed (node-gyp-build builds it without prebuilt binaries)
349
+ install: 'node-gyp-build',
350
+ sources: ['cpp', 'binding.gyp'],
351
+ dependencies: { 'node-addon-api': NATIVE_VERSIONS['node-addon-api'], 'node-gyp-build': NATIVE_VERSIONS['node-gyp-build'] },
352
+ devDependencies: { 'node-gyp': NATIVE_VERSIONS['node-gyp'] },
353
+ toolchain: 'Python and a C++ toolchain (the Xcode command line tools on macOS, build-essential on Linux, the Visual Studio Build Tools on Windows)'
354
+ },
355
+ cmake: {
356
+ files: cMakeFiles(target),
357
+ buildNative: 'cmake-js rebuild',
358
+ install: 'cmake-js compile',
359
+ sources: ['cpp', 'CMakeLists.txt'],
360
+ dependencies: { 'node-addon-api': NATIVE_VERSIONS['node-addon-api'], 'node-gyp-build': NATIVE_VERSIONS['node-gyp-build'], 'cmake-js': NATIVE_VERSIONS['cmake-js'] },
361
+ devDependencies: {},
362
+ toolchain: 'CMake and a C++ toolchain (the Xcode command line tools on macOS, build-essential on Linux, the Visual Studio Build Tools on Windows)'
363
+ },
364
+ rust: {
365
+ files: rustFiles(target, version),
366
+ buildNative: 'napi build --release --output-dir build/Release',
367
+ install: 'napi build --release --output-dir build/Release',
368
+ sources: ['rust', 'Cargo.toml', 'build.rs'],
369
+ dependencies: { 'node-gyp-build': NATIVE_VERSIONS['node-gyp-build'], '@napi-rs/cli': NATIVE_VERSIONS['@napi-rs/cli'] },
370
+ devDependencies: {},
371
+ toolchain: 'Rust (rustup, or the package manager of the system)'
372
+ }
373
+ };
374
+ const chosen = builds[build];
375
+ return {
376
+ 'package.json': `${JSON.stringify({
377
+ name: name,
378
+ version: version,
379
+ private: true,
380
+ type: 'module',
381
+ main: 'dist/index.js',
382
+ types: 'dist/index.d.ts',
383
+ exports: { '.': { types: './dist/index.d.ts', default: './dist/index.js' } },
384
+ ...(build === 'gyp' ? { gypfile: true } : {}),
385
+ ...(build === 'rust' ? { napi: { binaryName: target } } : {}),
386
+ files: ['dist', '!dist/tests', ...chosen.sources],
387
+ scripts: {
388
+ //The addon is built when the package is installed (from its sources, without prebuilt binaries)
389
+ install: chosen.install,
390
+ clean: 'lakutata clean',
391
+ 'clean:native': build === 'rust' ? 'lakutata clean build target' : 'lakutata clean build',
392
+ 'build:native': chosen.buildNative,
393
+ build: 'tsc',
394
+ rebuild: 'npm run clean && npm run build:native && npm run build',
395
+ test: 'node --test "dist/tests/**/*.spec.js"'
396
+ },
397
+ dependencies: chosen.dependencies,
398
+ ...(Object.keys(chosen.devDependencies).length ? { devDependencies: chosen.devDependencies } : {})
399
+ }, null, 2)}\n`,
400
+ ...chosen.files,
401
+ 'src/index.ts': `import {createRequire} from 'node:module'
402
+ import path from 'node:path'
403
+ import {fileURLToPath} from 'node:url'
404
+
405
+ /**
406
+ * The functions of the addon
407
+ */
408
+ type Binding = {
409
+ greet(name: string): string
410
+ }
411
+
412
+ /**
413
+ * The addon built into build/Release, loaded from the directory of the package
414
+ */
415
+ const binding: Binding = createRequire(import.meta.url)('node-gyp-build')(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'))
416
+
417
+ /**
418
+ * Greet someone, in ${build === 'rust' ? 'Rust' : 'C++'}
419
+ * @param name
420
+ */
421
+ export function Greet(name: string): string {
422
+ return binding.greet(name)
423
+ }
424
+ `,
425
+ 'src/tests/index.spec.ts': `import {describe, it} from 'node:test'
426
+ import assert from 'node:assert'
427
+ import {Greet} from '../index.js'
428
+
429
+ describe('${name}', (): void => {
430
+ it('greets in ${build === 'rust' ? 'Rust' : 'C++'}', (): void => {
431
+ assert.strictEqual(Greet('lakutata'), 'Hello, lakutata (native)')
432
+ })
433
+
434
+ it('throws the errors of the addon as JavaScript errors', (): void => {
435
+ assert.throws((): unknown => Greet(42 as unknown as string), ${build === 'rust' ? '/String/' : "{name: 'TypeError', message: 'The name must be a string'}"})
436
+ })
437
+ })
438
+ `,
439
+ 'tsconfig.json': tsConfig('dist'),
440
+ 'README.md': `# ${name}
441
+
442
+ A native addon: ${NATIVE_GUIDES[build]}
443
+
444
+ Node-API is stable across the versions of Node.js and Electron: the same addon runs in both without being rebuilt.
445
+ \`lakutata package bundle\` and \`lakutata package exe\` ship it with the application.
446
+
447
+ \`\`\`bash
448
+ npm run rebuild # build the addon (build/Release) and compile the TypeScript (dist)
449
+ npm test # run the tests
450
+ \`\`\`
451
+
452
+ A library built by its own Makefile (or its own build) is built before the addon by a script, then linked by it:
453
+ \`"build:deps": "make -C deps/foo"\`, \`"build:native": "npm run build:deps && ${chosen.buildNative}"\`.
454
+
455
+ Building the addon needs ${chosen.toolchain}.
456
+ `
457
+ };
458
+ }
459
+ /**
460
+ * Add a package to the workspace of the current directory: packages/<name> (without its scope), of the version of the
461
+ * workspace; a package of the workspace may depend on it
462
+ * @param options
463
+ */
464
+ export async function addPackage(options) {
465
+ if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(options.name))
466
+ throw new Error(`"${options.name}" is not a name of package (lower case, digits, - . _ ~, an optional @scope/)`);
467
+ const workspace = await Workspace.find();
468
+ if (workspace.packages.some((pkg) => pkg.name === options.name))
469
+ throw new Error(`The workspace has already a package ${options.name}`);
470
+ const dependent = options.to === undefined ? undefined : workspace.packages.find((pkg) => pkg.name === options.to);
471
+ if (options.to !== undefined && !dependent)
472
+ throw new Error(`The workspace has no package ${options.to} (${workspace.packages.map((pkg) => pkg.name).join(', ')})`);
473
+ const directory = path.join(workspace.root, 'packages', options.name.replace(/^@[^/]+\//, ''));
474
+ if (existsSync(directory))
475
+ throw new Error(`${path.relative(process.cwd(), directory) || directory} exists already`);
476
+ const version = workspace.lerna?.data.version ?? workspace.manifest.data.version ?? '0.0.1';
477
+ const files = options.native ? nativeFiles(options.name, version, options.native) : libraryFiles(options.name, version);
478
+ for (const [file, content] of Object.entries(files)) {
479
+ await mkdir(path.dirname(path.join(directory, file)), { recursive: true });
480
+ await writeFile(path.join(directory, file), content);
481
+ }
482
+ if (dependent) {
483
+ dependent.manifest.data.dependencies = { ...(dependent.manifest.data.dependencies ?? {}), [options.name]: `^${version}` };
484
+ await dependent.manifest.write();
485
+ }
486
+ return { directory: directory, files: Object.keys(files), version: version, to: dependent?.name };
487
+ }
@@ -0,0 +1,21 @@
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ /**
5
+ * The builder of a project: the one installed in the project (its node_modules, the ones of its workspace), else the one
6
+ * installed with the tool
7
+ * @param project
8
+ */
9
+ export async function loadBuilder(project) {
10
+ for (const base of [path.join(project, 'package.json'), import.meta.url]) {
11
+ let entry;
12
+ try {
13
+ entry = createRequire(base).resolve('@lakutata/builder');
14
+ }
15
+ catch {
16
+ continue;
17
+ }
18
+ return await import(pathToFileURL(entry).href);
19
+ }
20
+ throw new Error('The builder of the applications is not installed: install it in the project (npm install --save-dev @lakutata/builder)');
21
+ }
@@ -0,0 +1,146 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile, rm } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
+ /**
6
+ * The paths a clean never removes: the package itself, its sources, its dependencies and its manifest
7
+ */
8
+ const PROTECTED = ['node_modules', 'src', 'package.json', '.git'];
9
+ /**
10
+ * The text of a JSON with comments and trailing commas (a tsconfig.json) as JSON: the comments and the commas before a
11
+ * closing bracket are removed outside of the strings
12
+ * @param text
13
+ */
14
+ function stripJSONC(text) {
15
+ let output = '';
16
+ for (let index = 0; index < text.length; index++) {
17
+ const char = text[index];
18
+ if (char === '"') {
19
+ //A string, its escaped quotes included
20
+ let end = index + 1;
21
+ while (end < text.length && text[end] !== '"')
22
+ end += text[end] === '\\' ? 2 : 1;
23
+ output += text.slice(index, end + 1);
24
+ index = end;
25
+ }
26
+ else if (char === '/' && text[index + 1] === '/') {
27
+ while (index < text.length && text[index] !== '\n')
28
+ index++;
29
+ output += '\n';
30
+ }
31
+ else if (char === '/' && text[index + 1] === '*') {
32
+ index = text.indexOf('*/', index + 2);
33
+ if (index < 0)
34
+ break;
35
+ index++;
36
+ }
37
+ else if (char === ',') {
38
+ //A trailing comma: the next character, whitespaces and comments skipped, closes an object or an array
39
+ let next = index + 1;
40
+ for (;;) {
41
+ while (next < text.length && /\s/.test(text[next]))
42
+ next++;
43
+ if (text[next] === '/' && text[next + 1] === '/')
44
+ next = text.indexOf('\n', next) < 0 ? text.length : text.indexOf('\n', next);
45
+ else if (text[next] === '/' && text[next + 1] === '*')
46
+ next = text.indexOf('*/', next + 2) < 0 ? text.length : text.indexOf('*/', next + 2) + 2;
47
+ else
48
+ break;
49
+ }
50
+ if (text[next] !== '}' && text[next] !== ']')
51
+ output += char;
52
+ }
53
+ else {
54
+ output += char;
55
+ }
56
+ }
57
+ return output;
58
+ }
59
+ /**
60
+ * The file of a configuration extended: a path relative to the configuration, or a module (lakutata/tsconfig.json)
61
+ * @param from the configuration extending it
62
+ * @param specifier
63
+ */
64
+ function extendedConfiguration(from, specifier) {
65
+ if (specifier.startsWith('.') || path.isAbsolute(specifier)) {
66
+ const file = path.resolve(path.dirname(from), specifier);
67
+ return [file, `${file}.json`].find((candidate) => existsSync(candidate) && !candidate.endsWith(path.sep));
68
+ }
69
+ const require = createRequire(from);
70
+ for (const candidate of [specifier, `${specifier}.json`, `${specifier}/tsconfig.json`]) {
71
+ try {
72
+ return require.resolve(candidate);
73
+ }
74
+ catch {
75
+ //The next form of the specifier
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ /**
81
+ * The output directories of a TypeScript configuration and of the ones it extends (outDir, declarationDir), absolute:
82
+ * each one relative to the configuration giving it, the closest configuration winning
83
+ * @param file
84
+ * @param depth
85
+ */
86
+ async function configurationOutputs(file, depth = 0) {
87
+ const configuration = JSON.parse(stripJSONC(await readFile(file, 'utf8')));
88
+ let outputs = {};
89
+ const extended = configuration.extends === undefined ? [] : Array.isArray(configuration.extends) ? configuration.extends : [configuration.extends];
90
+ if (depth < 8) {
91
+ for (const specifier of extended) {
92
+ const parent = extendedConfiguration(file, specifier);
93
+ if (parent)
94
+ outputs = { ...outputs, ...await configurationOutputs(parent, depth + 1) };
95
+ }
96
+ }
97
+ for (const option of ['outDir', 'declarationDir']) {
98
+ const value = configuration.compilerOptions?.[option];
99
+ if (typeof value === 'string')
100
+ outputs[option] = path.resolve(path.dirname(file), value);
101
+ }
102
+ return outputs;
103
+ }
104
+ /**
105
+ * The output directories of the TypeScript configuration of a package (its tsconfig.json): outDir and declarationDir
106
+ * @param directory
107
+ * @return the directories, relative to the package
108
+ */
109
+ export async function outputDirectories(directory) {
110
+ const file = path.join(directory, 'tsconfig.json');
111
+ if (!existsSync(file))
112
+ throw new Error(`${directory} has no tsconfig.json: give the paths removed (lakutata clean <paths...>)`);
113
+ const outputs = await configurationOutputs(file);
114
+ const directories = [...new Set([outputs.outDir, outputs.declarationDir].filter((output) => !!output))];
115
+ if (!directories.length)
116
+ throw new Error(`The tsconfig.json of ${directory} gives no outDir: give the paths removed (lakutata clean <paths...>)`);
117
+ return directories.map((output) => path.relative(directory, output));
118
+ }
119
+ /**
120
+ * Remove paths of a package (as rm -rf, on every platform): only the paths inside the package, never the package itself,
121
+ * its sources, its dependencies nor its manifest
122
+ * @param directory the directory of the package
123
+ * @param paths relative to the package
124
+ */
125
+ export async function clean(directory, paths) {
126
+ const root = path.resolve(directory);
127
+ const targets = paths.map((target) => {
128
+ const absolute = path.resolve(root, target);
129
+ const relative = path.relative(root, absolute);
130
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
131
+ throw new Error(`${target} is not inside the package ${root}: it is not removed`);
132
+ if (PROTECTED.includes(relative.split(path.sep)[0]))
133
+ throw new Error(`${target} is protected (${PROTECTED.join(', ')}): it is not removed`);
134
+ return { relative: relative, absolute: absolute };
135
+ });
136
+ const result = { removed: [], missing: [] };
137
+ for (const target of targets) {
138
+ if (!existsSync(target.absolute)) {
139
+ result.missing.push(target.relative);
140
+ continue;
141
+ }
142
+ await rm(target.absolute, { recursive: true, force: true });
143
+ result.removed.push(target.relative);
144
+ }
145
+ return result;
146
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The environment the tool was started with: the application of the tool sets some variables (NODE_ENV from its mode…),
3
+ * the commands it runs (the scripts of the packages, the installations) are given the environment of the user
4
+ */
5
+ export const LaunchEnvironment = { ...process.env };
6
+ //The commands of the migrations (MIGRATION_GENERATE, MIGRATION_RUN…) are given to the applications of the projects, not
7
+ //to the tool, an application of lakutata too: the tool run by a script of an application started by a migration command
8
+ //(lakutata clean in its build) does its own command; the commands it runs are still given them (LaunchEnvironment)
9
+ for (const name of Object.keys(process.env))
10
+ if (name.toUpperCase().startsWith('MIGRATION_'))
11
+ delete process.env[name];