@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,370 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { findGypTarget, GypDocument, normalizeCondition } from './Gyp.js';
7
+ import { cMakeArgumentText, CMakeDocument } from './CMake.js';
8
+ import { TomlDocument } from './Toml.js';
9
+ import { applyEdits } from './TextEdits.js';
10
+ const run = promisify(execFile);
11
+ /**
12
+ * The build system of a native addon, by its build file
13
+ * @param directory
14
+ */
15
+ export function detectBuildSystem(directory) {
16
+ const builds = ['gyp', 'cmake', 'rust'].filter((build) => existsSync(path.join(directory, { gyp: 'binding.gyp', cmake: 'CMakeLists.txt', rust: 'Cargo.toml' }[build])));
17
+ if (!builds.length)
18
+ throw new Error(`${directory} is not a native addon (no binding.gyp, CMakeLists.txt nor Cargo.toml)`);
19
+ if (builds.length > 1)
20
+ throw new Error(`${directory} has the build files of ${builds.join(', ')}: only one build system is changed`);
21
+ return builds[0];
22
+ }
23
+ /**
24
+ * The library of a file of the package, by its extension: static (.a everywhere but Windows, .lib on Windows) or
25
+ * dynamic (.dylib on macOS, .so on Linux, .dll on Windows)
26
+ * @param directory
27
+ * @param value
28
+ */
29
+ function libraryOfFile(directory, value) {
30
+ if (!/[\\/]/.test(value) && !/\.(a|lib|so(\.\d+)*|dylib|dll)$/.test(value))
31
+ return undefined;
32
+ const absolute = path.resolve(directory, value);
33
+ if (!existsSync(absolute))
34
+ throw new Error(`${value} does not exist in ${directory}`);
35
+ const file = path.relative(directory, absolute).split(path.sep).join('/');
36
+ if (file.startsWith('..'))
37
+ throw new Error(`${value} is not in the package: its libraries are shipped with it`);
38
+ const base = path.basename(file);
39
+ const name = base.replace(/^lib/, '').replace(/\.(a|lib|so(\.\d+)*|dylib|dll)$/, '');
40
+ if (/\.a$/.test(base))
41
+ return { file: file, name: name, kind: 'static', platform: 'unix' };
42
+ if (/\.lib$/.test(base))
43
+ return { file: file, name: name, kind: 'static', platform: 'win' };
44
+ if (/\.dylib$/.test(base))
45
+ return { file: file, name: name, kind: 'dynamic', platform: 'mac' };
46
+ if (/\.so(\.\d+)*$/.test(base))
47
+ return { file: file, name: name, kind: 'dynamic', platform: 'linux' };
48
+ if (/\.dll$/.test(base))
49
+ return { file: file, name: name, kind: 'dynamic', platform: 'win' };
50
+ throw new Error(`${value} is not a library (.a, .lib, .so, .dylib, .dll)`);
51
+ }
52
+ /**
53
+ * The import library of a DLL (foo.dll → foo.lib, next to it), which the linker of Windows links
54
+ * @param directory
55
+ * @param library
56
+ */
57
+ function importLibrary(directory, library) {
58
+ const file = library.file.replace(/\.dll$/, '.lib');
59
+ if (!existsSync(path.join(directory, file)))
60
+ throw new Error(`The import library of ${library.file} (${file}) does not exist: Windows links a DLL by its import library`);
61
+ return file;
62
+ }
63
+ /**
64
+ * The scope of a change: its platform, the one of its library, or every platform
65
+ * @param change
66
+ * @param library
67
+ */
68
+ function scopeOf(change, library) {
69
+ if (change.framework) {
70
+ if (change.platform && change.platform !== 'mac')
71
+ throw new Error('The frameworks are the libraries of macOS');
72
+ return 'mac';
73
+ }
74
+ if (library && change.platform && library.platform !== 'unix' && change.platform !== library.platform)
75
+ throw new Error(`${library.file} is a library of ${library.platform}, not of ${change.platform}`);
76
+ if (change.platform)
77
+ return change.platform;
78
+ if (library)
79
+ return library.platform;
80
+ //pkg-config is not on Windows
81
+ if (change.pkgConfig)
82
+ return 'unix';
83
+ return undefined;
84
+ }
85
+ /**
86
+ * The notes about a dynamic library of macOS: found through the rpath when its install name is @rpath/…
87
+ * @param directory
88
+ * @param library
89
+ */
90
+ async function installNameNotes(directory, library) {
91
+ if (library.platform !== 'mac' || library.kind !== 'dynamic' || process.platform !== 'darwin')
92
+ return [];
93
+ try {
94
+ //otool -D prints the file, then its install name (none for a file which is not a dynamic library)
95
+ const { stdout } = await run('otool', ['-D', path.join(directory, library.file)]);
96
+ const installName = stdout.trim().split('\n')[1]?.trim();
97
+ if (!installName || installName.startsWith('@rpath/'))
98
+ return [];
99
+ return [`${library.file} is named ${installName}: the addon finds it next to it once its name is @rpath/${path.basename(library.file)} (install_name_tool -id @rpath/${path.basename(library.file)} ${library.file})`];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }
105
+ /**
106
+ * Change a gyp addon
107
+ * @param directory
108
+ * @param change
109
+ * @param library
110
+ * @param scope
111
+ */
112
+ async function changeGyp(directory, change, library, scope) {
113
+ const located = await findGypTarget(path.join(directory, 'binding.gyp'), change.target);
114
+ const name = GypDocument.get(located.target, 'target_name');
115
+ const targetName = name?.kind === 'string' ? name.value : undefined;
116
+ let document = located.document;
117
+ let changed = false;
118
+ const conditions = { mac: 'OS=="mac"', linux: 'OS=="linux"', win: 'OS=="win"', unix: 'OS!="win"' };
119
+ //The target of the document (found again once the document is changed)
120
+ const target = () => {
121
+ const targets = GypDocument.get(document.root, 'targets');
122
+ const found = targets?.kind === 'list' ? targets.items.find((item) => item.kind === 'dict' && (targetName === undefined || GypDocument.get(item, 'target_name')?.value === targetName)) : undefined;
123
+ if (found?.kind !== 'dict')
124
+ throw new Error(`The target of ${document.file} is not found`);
125
+ return found;
126
+ };
127
+ const commit = (edited) => {
128
+ if (!edited)
129
+ return;
130
+ document = document.applied();
131
+ changed = true;
132
+ };
133
+ //The dictionary of the scope: the target, or the dictionary of its condition (created when missing)
134
+ const scoped = () => {
135
+ if (scope === undefined)
136
+ return target();
137
+ const condition = conditions[scope];
138
+ const find = () => {
139
+ const list = GypDocument.get(target(), 'conditions');
140
+ if (list?.kind !== 'list')
141
+ return undefined;
142
+ for (const item of list.items) {
143
+ if (item.kind === 'list' && item.items[0]?.kind === 'string' && normalizeCondition(item.items[0].value) === normalizeCondition(condition) && item.items[1]?.kind === 'dict')
144
+ return item.items[1];
145
+ }
146
+ return undefined;
147
+ };
148
+ if (!find()) {
149
+ const list = GypDocument.get(target(), 'conditions');
150
+ if (list === undefined)
151
+ commit(document.addEntry(target(), 'conditions', [[condition, {}]]));
152
+ else if (list.kind === 'list') {
153
+ document.addItem(list, [condition, {}]);
154
+ commit(true);
155
+ }
156
+ else {
157
+ throw new Error(`The conditions of ${document.file} are not a list`);
158
+ }
159
+ }
160
+ return find();
161
+ };
162
+ //The scope first: creating its condition changes the document
163
+ const add = (key, values) => {
164
+ const dict = scoped();
165
+ commit(document.addToList(dict, key, values));
166
+ };
167
+ const addToDict = (key, list, values) => {
168
+ const scope = scoped();
169
+ commit(document.addEntry(scope, key, {}));
170
+ const dict = GypDocument.get(scoped(), key);
171
+ if (dict?.kind !== 'dict')
172
+ throw new Error(`"${key}" of ${document.file} is not a dictionary`);
173
+ commit(document.addToList(dict, list, values));
174
+ };
175
+ const module = (file) => `<(module_root_dir)/${file}`;
176
+ if (change.action === 'define') {
177
+ add('defines', [change.value]);
178
+ }
179
+ else if (change.action === 'include') {
180
+ add('include_dirs', [existsSync(path.join(directory, change.value)) ? module(path.relative(directory, path.resolve(directory, change.value)).split(path.sep).join('/')) : change.value]);
181
+ }
182
+ else if (change.framework) {
183
+ addToDict('xcode_settings', 'OTHER_LDFLAGS', [`-framework ${change.value}`]);
184
+ }
185
+ else if (change.pkgConfig) {
186
+ add('include_dirs', [`<!@(pkg-config --cflags-only-I ${change.value} | sed s/-I//g)`]);
187
+ add('libraries', [`<!@(pkg-config --libs ${change.value})`]);
188
+ }
189
+ else if (library?.kind === 'static') {
190
+ add('libraries', [module(library.file)]);
191
+ }
192
+ else if (library?.kind === 'dynamic') {
193
+ //Linux: by its name (-l:libfoo.so), not its path, which the addon would need (a library without its soname)
194
+ add('libraries', library.platform === 'linux'
195
+ ? [`-L${module(path.posix.dirname(library.file))}`, `-l:${path.posix.basename(library.file)}`]
196
+ : [module(library.platform === 'win' ? importLibrary(directory, library) : library.file)]);
197
+ //Copied next to the addon, found there at run time
198
+ const scope = scoped();
199
+ const copies = GypDocument.get(scope, 'copies');
200
+ const destination = copies?.kind === 'list' ? copies.items.find((item) => item.kind === 'dict' && GypDocument.get(item, 'destination')?.value === '<(PRODUCT_DIR)') : undefined;
201
+ if (destination?.kind === 'dict')
202
+ commit(document.addToList(destination, 'files', [module(library.file)]));
203
+ else if (copies?.kind === 'list') {
204
+ document.addItem(copies, { destination: '<(PRODUCT_DIR)', files: [module(library.file)] });
205
+ commit(true);
206
+ }
207
+ else {
208
+ commit(document.addEntry(scope, 'copies', [{ destination: '<(PRODUCT_DIR)', files: [module(library.file)] }]));
209
+ }
210
+ if (library.platform === 'mac')
211
+ addToDict('xcode_settings', 'OTHER_LDFLAGS', ['-Wl,-rpath,@loader_path']);
212
+ if (library.platform === 'linux')
213
+ add('ldflags', ['-Wl,-rpath,\'$$ORIGIN\'']);
214
+ }
215
+ else {
216
+ add('libraries', [scope === 'win' ? `${change.value}.lib` : `-l${change.value}`]);
217
+ }
218
+ if (!changed)
219
+ return [];
220
+ await writeFile(document.file, document.text);
221
+ return [path.relative(directory, document.file).split(path.sep).join('/')];
222
+ }
223
+ /**
224
+ * Change a CMake addon
225
+ * @param directory
226
+ * @param change
227
+ * @param library
228
+ * @param scope
229
+ */
230
+ async function changeCMake(directory, change, library, scope) {
231
+ let document = await CMakeDocument.read(path.join(directory, 'CMakeLists.txt'));
232
+ const target = document.target(change.target);
233
+ const conditions = { mac: 'APPLE', linux: 'CMAKE_SYSTEM_NAME STREQUAL "Linux"', win: 'WIN32', unix: 'NOT WIN32' };
234
+ let changed = false;
235
+ const source = (file) => `\${CMAKE_SOURCE_DIR}/${file}`;
236
+ //The commands not written yet, in the block of the scope, after the commands of the target
237
+ const commands = (lines) => {
238
+ const missing = lines.filter((line) => !document.has(line[0], line.slice(1)));
239
+ if (!missing.length)
240
+ return;
241
+ const current = document.target(change.target);
242
+ document.insertAfterTarget(current, missing.map((line) => `${line[0]}(${line.slice(1).map(cMakeArgumentText).join(' ')})`), scope === undefined ? undefined : conditions[scope]);
243
+ document = document.applied();
244
+ changed = true;
245
+ };
246
+ const name = target.text;
247
+ //The signature of target_link_libraries the target uses: its uses are all plain or all with keywords (CMP0023)
248
+ const plain = document.commands.some((command) => command.name === 'target_link_libraries' && command.args.length > 1 &&
249
+ document.resolve(command.args[0].value) === target.name && !['PRIVATE', 'PUBLIC', 'INTERFACE', 'LINK_PRIVATE', 'LINK_PUBLIC', 'LINK_INTERFACE_LIBRARIES'].includes(command.args[1].value));
250
+ const link = (library) => ['target_link_libraries', name, ...(plain ? [] : ['PRIVATE']), library];
251
+ if (change.action === 'define') {
252
+ commands([['target_compile_definitions', name, 'PRIVATE', change.value]]);
253
+ }
254
+ else if (change.action === 'include') {
255
+ commands([['target_include_directories', name, 'PRIVATE', existsSync(path.join(directory, change.value)) ? source(path.relative(directory, path.resolve(directory, change.value)).split(path.sep).join('/')) : change.value]]);
256
+ }
257
+ else if (change.framework) {
258
+ commands([link(`-framework ${change.value}`)]);
259
+ }
260
+ else if (change.pkgConfig) {
261
+ const variable = change.value.toUpperCase().replace(/[^A-Z0-9_]/g, '_');
262
+ //Found in the block of its platform (pkg-config is not on Windows)
263
+ commands([['find_package', 'PkgConfig', 'REQUIRED'], ['pkg_check_modules', variable, 'REQUIRED', 'IMPORTED_TARGET', change.value], link(`PkgConfig::${variable}`)]);
264
+ }
265
+ else if (library?.kind === 'static') {
266
+ commands([link(source(library.file))]);
267
+ }
268
+ else if (library?.kind === 'dynamic') {
269
+ commands([
270
+ 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']]),
272
+ //Copied next to the addon, found there at run time
273
+ ['add_custom_command', 'TARGET', name, 'POST_BUILD', 'COMMAND', '${CMAKE_COMMAND}', '-E', 'copy_if_different', source(library.file), `$<TARGET_FILE_DIR:${name}>`]
274
+ ]);
275
+ }
276
+ else {
277
+ commands([link(change.value)]);
278
+ }
279
+ if (!changed)
280
+ return [];
281
+ await writeFile(document.file, document.text);
282
+ return ['CMakeLists.txt'];
283
+ }
284
+ /**
285
+ * Change a Rust addon: its crates by cargo add, its native libraries by the directives of cargo its build.rs gives,
286
+ * written in the environment of .cargo/config.toml (the flags of rustc would be given to its every crate)
287
+ * @param directory
288
+ * @param change
289
+ * @param library
290
+ * @param scope
291
+ */
292
+ async function changeRust(directory, change, library, scope) {
293
+ if (change.action !== 'link')
294
+ throw new Error(`The Rust addons have no ${change.action === 'include' ? 'directory of headers' : 'definition'} of C: their libraries are crates (--crate), the -sys crates building and linking the native libraries`);
295
+ if (change.pkgConfig)
296
+ throw new Error('The Rust addons find the libraries of pkg-config by their -sys crates (--crate), whose build scripts use the pkg-config crate');
297
+ if (change.crate) {
298
+ const cfg = { mac: 'cfg(target_os = "macos")', linux: 'cfg(target_os = "linux")', win: 'cfg(windows)', unix: 'cfg(unix)' };
299
+ const before = await readFile(path.join(directory, 'Cargo.toml'), 'utf8');
300
+ await run('cargo', ['add', change.value, ...(scope === undefined ? [] : ['--target', cfg[scope]])], { cwd: directory });
301
+ return before === await readFile(path.join(directory, 'Cargo.toml'), 'utf8') ? [] : ['Cargo.toml'];
302
+ }
303
+ const script = path.join(directory, 'build.rs');
304
+ if (!existsSync(script) || !(await readFile(script, 'utf8')).includes('LAKUTATA_NATIVE'))
305
+ throw new Error('The build.rs of the addon does not give the native libraries of .cargo/config.toml (LAKUTATA_NATIVE): the build.rs of lakutata add --native rust does');
306
+ const file = path.join(directory, '.cargo', 'config.toml');
307
+ const document = await TomlDocument.read(file);
308
+ const directives = [];
309
+ if (change.framework) {
310
+ directives.push(`rustc-link-lib=framework=${change.value}`);
311
+ }
312
+ else if (library?.kind === 'static') {
313
+ directives.push(`rustc-link-search=native=${path.posix.dirname(library.file)}`, `rustc-link-lib=static=${library.name}`);
314
+ }
315
+ else if (library?.kind === 'dynamic') {
316
+ if (library.platform === 'win')
317
+ importLibrary(directory, library);
318
+ directives.push(`rustc-link-search=native=${path.posix.dirname(library.file)}`, `rustc-link-lib=dylib=${library.name}`);
319
+ if (library.platform === 'mac')
320
+ directives.push('rustc-cdylib-link-arg=-Wl,-rpath,@loader_path');
321
+ if (library.platform === 'linux')
322
+ directives.push('rustc-cdylib-link-arg=-Wl,-rpath,$ORIGIN');
323
+ }
324
+ else {
325
+ directives.push(`rustc-link-lib=${change.value}`);
326
+ }
327
+ //The variable of the platform (build.rs reads the ones of its target)
328
+ const variable = scope === undefined ? 'LAKUTATA_NATIVE' : `LAKUTATA_NATIVE_${{ mac: 'MACOS', linux: 'LINUX', win: 'WINDOWS', unix: 'UNIX' }[scope]}`;
329
+ const files = [];
330
+ if (document.addToStringList(['env'], variable, directives, ';')) {
331
+ await mkdir(path.dirname(file), { recursive: true });
332
+ await writeFile(file, applyEdits(document.text, document.changes));
333
+ files.push('.cargo/config.toml');
334
+ }
335
+ if (library?.kind === 'dynamic') {
336
+ //Copied next to the addon after its build (when it is built on the platform of the library)
337
+ const manifestFile = path.join(directory, 'package.json');
338
+ const manifest = JSON.parse(await readFile(manifestFile, 'utf8'));
339
+ 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
+ const scripts = manifest.scripts ?? {};
341
+ if (!(scripts['postbuild:native'] ?? '').includes(copy)) {
342
+ scripts['postbuild:native'] = scripts['postbuild:native'] ? `${scripts['postbuild:native']} && ${copy}` : copy;
343
+ manifest.scripts = scripts;
344
+ await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
345
+ files.push('package.json');
346
+ }
347
+ }
348
+ return files;
349
+ }
350
+ /**
351
+ * Change the build of the native addon of a directory: link a library, add a directory of headers, define a macro,
352
+ * in its build file (binding.gyp and the files it includes, CMakeLists.txt, Cargo.toml and .cargo/config.toml), its
353
+ * comments and its layout kept
354
+ * @param directory
355
+ * @param change
356
+ */
357
+ export async function changeNative(directory, change) {
358
+ const build = detectBuildSystem(directory);
359
+ if (change.crate && build !== 'rust')
360
+ throw new Error('The crates are the libraries of the Rust addons');
361
+ const library = change.action === 'link' && !change.crate && !change.pkgConfig && !change.framework ? libraryOfFile(directory, change.value) : undefined;
362
+ const scope = scopeOf(change, library);
363
+ const files = build === 'gyp'
364
+ ? await changeGyp(directory, change, library, scope)
365
+ : build === 'cmake' ? await changeCMake(directory, change, library, scope) : await changeRust(directory, change, library, scope);
366
+ const notes = library ? await installNameNotes(directory, library) : [];
367
+ if (scope !== undefined && scope !== 'unix' && scope !== (process.platform === 'darwin' ? 'mac' : process.platform === 'win32' ? 'win' : 'linux'))
368
+ notes.push(`The change is for ${scope}: it is built and tried there`);
369
+ return { build: build, files: files, notes: notes };
370
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The text with its edits, applied from its end (the offsets of the edits are the ones of the original text)
3
+ * @param text
4
+ * @param edits
5
+ */
6
+ export function applyEdits(text, edits) {
7
+ //From the end of the text; the edits at the same offset in their order (the last one applied first)
8
+ const sorted = edits.map((edit, index) => [edit, index])
9
+ .sort((a, b) => b[0].offset - a[0].offset || b[1] - a[1])
10
+ .map(([edit]) => edit);
11
+ let result = text;
12
+ for (const edit of sorted)
13
+ result = result.slice(0, edit.offset) + edit.insert + result.slice(edit.offset + edit.remove);
14
+ return result;
15
+ }
16
+ /**
17
+ * The indentation of the line of an offset
18
+ * @param text
19
+ * @param offset
20
+ */
21
+ export function lineIndent(text, offset) {
22
+ const start = text.lastIndexOf('\n', offset - 1) + 1;
23
+ return /^[ \t]*/.exec(text.slice(start))[0];
24
+ }
25
+ /**
26
+ * The unit of indentation of a text: the smallest indentation of its indented lines (two spaces by default)
27
+ * @param text
28
+ */
29
+ export function indentUnit(text) {
30
+ let unit;
31
+ for (const line of text.split('\n')) {
32
+ const indent = /^[ \t]*/.exec(line)[0];
33
+ if (!indent || indent.length === line.length)
34
+ continue;
35
+ if (indent.startsWith('\t'))
36
+ return '\t';
37
+ if (unit === undefined || indent.length < unit.length)
38
+ unit = indent;
39
+ }
40
+ return unit ?? ' ';
41
+ }
42
+ /**
43
+ * The offset where a line starts
44
+ * @param text
45
+ * @param offset
46
+ */
47
+ export function lineStart(text, offset) {
48
+ return text.lastIndexOf('\n', offset - 1) + 1;
49
+ }
50
+ /**
51
+ * The offset of the end of a line (its newline, or the end of the text)
52
+ * @param text
53
+ * @param offset
54
+ */
55
+ export function lineEndOf(text, offset) {
56
+ const end = text.indexOf('\n', offset);
57
+ return end < 0 ? text.length : end;
58
+ }
59
+ /**
60
+ * An error of syntax at an offset of a file, telling its line and its column
61
+ */
62
+ export class NativeSyntaxError extends Error {
63
+ constructor(file, text, offset, message) {
64
+ const line = text.slice(0, offset).split('\n').length;
65
+ const column = offset - lineStart(text, offset) + 1;
66
+ super(`${file}:${line}:${column}: ${message}`);
67
+ }
68
+ }