@abaplint/transpiler-cli 2.14.0 → 2.14.2
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 +48 -5
- package/build/bundle.js +398 -39
- package/build/types.d.ts +4 -0
- package/package.json +2 -2
- package/schema.json +46 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ basename of `folder` for a local-only library. Set `libs[].name` to override it:
|
|
|
22
22
|
{
|
|
23
23
|
"input_folder": ["src", "extra"],
|
|
24
24
|
"output_folder": "output",
|
|
25
|
+
"incremental_output": true,
|
|
25
26
|
"libs": [
|
|
26
27
|
{"url": "https://github.com/open-abap/open-abap-core"},
|
|
27
28
|
{"folder": "./deps/utilities", "name": "utilities"}
|
|
@@ -51,20 +52,62 @@ output/
|
|
|
51
52
|
Names must be valid single directory names on Windows and POSIX. Names are
|
|
52
53
|
unique without regard to case, and `project` is reserved. A conflicting derived
|
|
53
54
|
name needs an explicit `libs[].name`. Duplicate ABAP objects across libraries
|
|
54
|
-
are rejected; a project object replaces a library object of the same type/name.
|
|
55
|
+
are rejected by default; a project object replaces a library object of the same type/name.
|
|
55
56
|
|
|
56
57
|
Class locals, test classes, function groups, MIME data, and
|
|
57
58
|
source maps follow their owning object. Source maps include ABAP source content,
|
|
58
|
-
including libraries loaded from temporary Git checkouts
|
|
59
|
-
|
|
59
|
+
including libraries loaded from temporary Git checkouts; they use stable
|
|
60
|
+
library-qualified paths and do not expose the temporary checkout location.
|
|
61
|
+
Imports and registered MIME filenames reference the new paths.
|
|
60
62
|
|
|
61
63
|
Shared initialization scripts and test runners remain at the output root.
|
|
62
64
|
Run programs with `node output/project/zapp.prog.mjs`; run project unit tests
|
|
63
65
|
with `node output/index.mjs`. When migrating from flat output, rebuild a clean
|
|
64
66
|
generated output directory and update scripts that reference individual
|
|
65
|
-
modules or assets.
|
|
66
|
-
|
|
67
|
+
modules or assets.
|
|
68
|
+
|
|
69
|
+
Set `incremental_output` to `true` to skip writing unchanged files, replace
|
|
70
|
+
changed generated files, and remove obsolete files recorded in
|
|
71
|
+
`output/.abap-transpile-manifest.json`. The CLI preserves files it did not
|
|
72
|
+
generate, including custom setup scripts and other user data. Existing output
|
|
73
|
+
from runs before this option was enabled has no ownership inventory; do one
|
|
74
|
+
clean build when adopting the option to remove legacy stale files. Set the
|
|
75
|
+
option to `false` (the default) to restore unconditional writes. Files
|
|
76
|
+
generated while it is disabled are not added to the inventory.
|
|
77
|
+
|
|
78
|
+
The manifest is updated atomically and retained as a recovery aid if a run
|
|
79
|
+
fails. This does not make the entire output tree transactional. Use one
|
|
80
|
+
transpiler process per output directory.
|
|
67
81
|
|
|
68
82
|
The transpiler library retains flat output unless its caller supplies an output
|
|
69
83
|
folder map as the third argument to `Transpiler.run()`. This layout is enabled
|
|
70
84
|
automatically by the CLI.
|
|
85
|
+
|
|
86
|
+
## Duplicate dependencies
|
|
87
|
+
|
|
88
|
+
By default, two libraries defining the same ABAP object (type and name) cause
|
|
89
|
+
an `Ambiguous dependency object` error. To keep existing dependency repositories
|
|
90
|
+
and skip later copies, set this top-level option in `abap_transpile.json`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"skip_duplicate_dependencies": true
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The first library in `libs` containing an object wins. Every file of that
|
|
99
|
+
object in later libraries is skipped, including metadata, class locals, and
|
|
100
|
+
binary assets. The CLI warns once per skipped object per library, naming the
|
|
101
|
+
selected library. Imports, output folders, and source maps use the selected
|
|
102
|
+
copy. Project objects still take precedence over dependencies, and shared
|
|
103
|
+
`package.devc.xml` metadata keeps its existing behavior.
|
|
104
|
+
|
|
105
|
+
Library order now affects generated output. Different definitions can change
|
|
106
|
+
DDIC types, available methods, and runtime behavior, or cause syntax errors in
|
|
107
|
+
code expecting the skipped version. This option does not check that copies are
|
|
108
|
+
equivalent or suppress syntax checking. Put the intended provider first and
|
|
109
|
+
run your application's tests. For selective control, use `libs[].exclude_filter`
|
|
110
|
+
to exclude only the unwanted object's files instead. With `incremental_output`
|
|
111
|
+
enabled, changing providers removes obsolete files tracked in the manifest.
|
|
112
|
+
Use a clean generated output directory when changing providers in legacy mode
|
|
113
|
+
or when untracked stale output remains from earlier builds.
|
package/build/bundle.js
CHANGED
|
@@ -71,6 +71,7 @@ class TranspilerConfig {
|
|
|
71
71
|
input_folder: "src",
|
|
72
72
|
input_filter: [],
|
|
73
73
|
output_folder: "output",
|
|
74
|
+
incremental_output: false,
|
|
74
75
|
libs: [
|
|
75
76
|
{ "url": "https://github.com/open-abap/open-abap-core" },
|
|
76
77
|
],
|
|
@@ -314,14 +315,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
314
315
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
315
316
|
};
|
|
316
317
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
317
|
-
const fs = __importStar(__webpack_require__(/*! fs */ "fs"));
|
|
318
|
-
const path = __importStar(__webpack_require__(/*! path */ "path"));
|
|
319
318
|
const progress_1 = __importDefault(__webpack_require__(/*! progress */ "./node_modules/progress/index.js"));
|
|
320
319
|
const Transpiler = __importStar(__webpack_require__(/*! @abaplint/transpiler */ "./node_modules/@abaplint/transpiler/build/src/index.js"));
|
|
321
320
|
const config_1 = __webpack_require__(/*! ./config */ "./build/config.js");
|
|
322
321
|
const file_operations_1 = __webpack_require__(/*! ./file_operations */ "./build/file_operations.js");
|
|
323
322
|
const libraries_1 = __webpack_require__(/*! ./libraries */ "./build/libraries.js");
|
|
324
323
|
const write_objects_1 = __webpack_require__(/*! ./write_objects */ "./build/write_objects.js");
|
|
324
|
+
const output_writer_1 = __webpack_require__(/*! ./output_writer */ "./build/output_writer.js");
|
|
325
325
|
class Progress {
|
|
326
326
|
bar;
|
|
327
327
|
set(total, _text) {
|
|
@@ -340,7 +340,7 @@ async function build(config, files) {
|
|
|
340
340
|
options.ignoreSourceMap = true;
|
|
341
341
|
}
|
|
342
342
|
const t = new Transpiler.Transpiler(options);
|
|
343
|
-
const { reg, folders, sources } = (0, libraries_1.libraryRegistry)(files, libraries);
|
|
343
|
+
const { reg, folders, sources } = (0, libraries_1.libraryRegistry)(files, libraries, config.skip_duplicate_dependencies);
|
|
344
344
|
const output = await t.run(reg, new Progress(), folders);
|
|
345
345
|
return { output, sources };
|
|
346
346
|
}
|
|
@@ -351,23 +351,23 @@ async function run() {
|
|
|
351
351
|
console.log("\nBuilding");
|
|
352
352
|
const { output, sources } = await build(config, files);
|
|
353
353
|
console.log("\nOutput");
|
|
354
|
-
const
|
|
355
|
-
if (!fs.existsSync(outputFolder)) {
|
|
356
|
-
fs.mkdirSync(outputFolder, { recursive: true });
|
|
357
|
-
}
|
|
358
|
-
await (0, write_objects_1.writeObjects)(output.objects, config, sources);
|
|
359
|
-
console.log(output.objects.length + " objects written to disk");
|
|
354
|
+
const artifacts = (0, write_objects_1.collectObjectFiles)(output.objects, config, sources);
|
|
360
355
|
if (config.write_unit_tests === true) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
356
|
+
artifacts.push({ path: "index.mjs", contents: output.unitTestScript });
|
|
357
|
+
artifacts.push({ path: "_unit_open.mjs", contents: output.unitTestScriptOpen });
|
|
358
|
+
}
|
|
359
|
+
artifacts.push({ path: "init.mjs", contents: output.initializationScript });
|
|
360
|
+
artifacts.push({ path: "_init.mjs", contents: output.initializationScript2 });
|
|
361
|
+
artifacts.push({ path: "_top.mjs", contents: `import runtime from "@abaplint/runtime";
|
|
362
|
+
globalThis.abap = new runtime.ABAP();` });
|
|
363
|
+
const written = await (0, output_writer_1.writeOutput)(config.output_folder, artifacts, config.incremental_output === true);
|
|
364
|
+
if (config.incremental_output === true) {
|
|
365
|
+
console.log("Output files: " + written.created + " created, " +
|
|
366
|
+
written.updated + " updated, " + written.unchanged + " unchanged, " + written.deleted + " deleted");
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.log(`Output files: ${written.updated} written`);
|
|
364
370
|
}
|
|
365
|
-
// breaking change? rename this output file,
|
|
366
|
-
fs.writeFileSync(outputFolder + path.sep + "init.mjs", output.initializationScript);
|
|
367
|
-
// new static referenced imports,
|
|
368
|
-
fs.writeFileSync(outputFolder + path.sep + "_init.mjs", output.initializationScript2);
|
|
369
|
-
fs.writeFileSync(outputFolder + path.sep + "_top.mjs", `import runtime from "@abaplint/runtime";
|
|
370
|
-
globalThis.abap = new runtime.ABAP();`);
|
|
371
371
|
}
|
|
372
372
|
run().then(() => {
|
|
373
373
|
process.exit();
|
|
@@ -491,6 +491,11 @@ const abaplint = __importStar(__webpack_require__(/*! @abaplint/core */ "./node_
|
|
|
491
491
|
const file_operations_1 = __webpack_require__(/*! ./file_operations */ "./build/file_operations.js");
|
|
492
492
|
const git_clone_1 = __webpack_require__(/*! ./git_clone */ "./build/git_clone.js");
|
|
493
493
|
const lib_folder_1 = __webpack_require__(/*! ./lib_folder */ "./build/lib_folder.js");
|
|
494
|
+
function logicalLibrarySource(library, directory, filename) {
|
|
495
|
+
const relative = path.relative(directory, filename).split(path.sep)
|
|
496
|
+
.map(segment => encodeURIComponent(segment)).join("/");
|
|
497
|
+
return "abaplint://" + encodeURIComponent(library) + "/" + relative;
|
|
498
|
+
}
|
|
494
499
|
function libraryNames(libs) {
|
|
495
500
|
const used = new Set(["project"]);
|
|
496
501
|
return libs.map(lib => {
|
|
@@ -514,6 +519,7 @@ async function loadLibraries(config) {
|
|
|
514
519
|
const names = libraryNames(libs);
|
|
515
520
|
const result = [];
|
|
516
521
|
for (const [index, lib] of libs.entries()) {
|
|
522
|
+
const libraryName = names[index];
|
|
517
523
|
let dir;
|
|
518
524
|
let cleanupFolder = false;
|
|
519
525
|
const folder = (0, lib_folder_1.resolveLibFolder)(lib.folder, process.cwd());
|
|
@@ -547,8 +553,15 @@ async function loadLibraries(config) {
|
|
|
547
553
|
filesToRead.add(filename);
|
|
548
554
|
}
|
|
549
555
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
556
|
+
const filenames = [...filesToRead];
|
|
557
|
+
const files = await file_operations_1.FileOperations.readAllFiles(filenames, config.output_folder);
|
|
558
|
+
if (cleanupFolder) {
|
|
559
|
+
for (const [index, file] of files.entries()) {
|
|
560
|
+
file.sourceMapPath = logicalLibrarySource(libraryName, dir, filenames[index]);
|
|
561
|
+
delete file.relative;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
result.push({ name: libraryName, files });
|
|
552
565
|
console.log("\t" + files.length + " files added from lib");
|
|
553
566
|
}
|
|
554
567
|
finally {
|
|
@@ -559,13 +572,14 @@ async function loadLibraries(config) {
|
|
|
559
572
|
}
|
|
560
573
|
return result;
|
|
561
574
|
}
|
|
562
|
-
function libraryRegistry(files, libraries) {
|
|
575
|
+
function libraryRegistry(files, libraries, skipDuplicateDependencies = false) {
|
|
563
576
|
const reg = new abaplint.Registry();
|
|
564
577
|
const folders = new Map();
|
|
565
578
|
const objects = new Map();
|
|
566
579
|
const sources = new Map();
|
|
567
580
|
// Libraries first: addFile() then replaces a dependency object with the project's object.
|
|
568
581
|
for (const lib of libraries) {
|
|
582
|
+
const skipped = new Set();
|
|
569
583
|
for (const file of lib.files) {
|
|
570
584
|
const memory = new abaplint.MemoryFile(file.filename, file.contents);
|
|
571
585
|
const type = memory.getObjectType()?.toUpperCase();
|
|
@@ -576,6 +590,14 @@ function libraryRegistry(files, libraries) {
|
|
|
576
590
|
const owner = objects.get(key);
|
|
577
591
|
// abapGit repeats package.devc.xml across repositories; the registry keeps the last copy.
|
|
578
592
|
if (key !== "DEVC:PACKAGE" && owner !== undefined && owner !== lib.name) {
|
|
593
|
+
if (skipDuplicateDependencies === true) {
|
|
594
|
+
if (!skipped.has(key)) {
|
|
595
|
+
console.warn("Skipping duplicate dependency object " + key + " in " + lib.name + "; using " + owner);
|
|
596
|
+
skipped.add(key);
|
|
597
|
+
}
|
|
598
|
+
// Skip every file of the losing object, including locals, metadata, and binary assets.
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
579
601
|
throw new Error("Ambiguous dependency object " + key + " in " + owner + " and " + lib.name);
|
|
580
602
|
}
|
|
581
603
|
objects.set(key, lib.name);
|
|
@@ -595,6 +617,347 @@ function libraryRegistry(files, libraries) {
|
|
|
595
617
|
|
|
596
618
|
/***/ },
|
|
597
619
|
|
|
620
|
+
/***/ "./build/output_writer.js"
|
|
621
|
+
/*!********************************!*\
|
|
622
|
+
!*** ./build/output_writer.js ***!
|
|
623
|
+
\********************************/
|
|
624
|
+
(__unused_webpack_module, exports, __webpack_require__) {
|
|
625
|
+
|
|
626
|
+
"use strict";
|
|
627
|
+
|
|
628
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
629
|
+
if (k2 === undefined) k2 = k;
|
|
630
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
631
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
632
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
633
|
+
}
|
|
634
|
+
Object.defineProperty(o, k2, desc);
|
|
635
|
+
}) : (function(o, m, k, k2) {
|
|
636
|
+
if (k2 === undefined) k2 = k;
|
|
637
|
+
o[k2] = m[k];
|
|
638
|
+
}));
|
|
639
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
640
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
641
|
+
}) : function(o, v) {
|
|
642
|
+
o["default"] = v;
|
|
643
|
+
});
|
|
644
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
645
|
+
var ownKeys = function(o) {
|
|
646
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
647
|
+
var ar = [];
|
|
648
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
649
|
+
return ar;
|
|
650
|
+
};
|
|
651
|
+
return ownKeys(o);
|
|
652
|
+
};
|
|
653
|
+
return function (mod) {
|
|
654
|
+
if (mod && mod.__esModule) return mod;
|
|
655
|
+
var result = {};
|
|
656
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
657
|
+
__setModuleDefault(result, mod);
|
|
658
|
+
return result;
|
|
659
|
+
};
|
|
660
|
+
})();
|
|
661
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
662
|
+
exports.writeOutput = writeOutput;
|
|
663
|
+
const fsPromises = __importStar(__webpack_require__(/*! node:fs/promises */ "node:fs/promises"));
|
|
664
|
+
const os = __importStar(__webpack_require__(/*! node:os */ "node:os"));
|
|
665
|
+
const path = __importStar(__webpack_require__(/*! node:path */ "node:path"));
|
|
666
|
+
const file_operations_1 = __webpack_require__(/*! ./file_operations */ "./build/file_operations.js");
|
|
667
|
+
const MANIFEST = ".abap-transpile-manifest.json";
|
|
668
|
+
function concurrency() {
|
|
669
|
+
return Math.max(1, Math.min(8, os.cpus().length));
|
|
670
|
+
}
|
|
671
|
+
async function forEachLimit(items, task, limit = concurrency()) {
|
|
672
|
+
let next = 0;
|
|
673
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
674
|
+
while (next < items.length) {
|
|
675
|
+
const index = next++;
|
|
676
|
+
await task(items[index]);
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
const results = await Promise.allSettled(workers);
|
|
680
|
+
const failed = results.find(result => result.status === "rejected");
|
|
681
|
+
if (failed?.status === "rejected") {
|
|
682
|
+
throw failed.reason;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function normalizeRelative(filename, label) {
|
|
686
|
+
if (filename.length === 0 || filename.startsWith("/") || filename.includes("\\")
|
|
687
|
+
|| /^[a-zA-Z]:/.test(filename)) {
|
|
688
|
+
throw new Error("Invalid " + label + " path: " + filename);
|
|
689
|
+
}
|
|
690
|
+
const segments = filename.split("/");
|
|
691
|
+
if (segments.some(segment => segment.length === 0 || segment === "." || segment === "..")) {
|
|
692
|
+
throw new Error("Invalid " + label + " path: " + filename);
|
|
693
|
+
}
|
|
694
|
+
return segments.join("/");
|
|
695
|
+
}
|
|
696
|
+
function validatePaths(filenames, label) {
|
|
697
|
+
const paths = new Map();
|
|
698
|
+
for (const filename of filenames) {
|
|
699
|
+
if (typeof filename !== "string") {
|
|
700
|
+
throw new Error("Invalid " + label + " path");
|
|
701
|
+
}
|
|
702
|
+
const relative = normalizeRelative(filename, label);
|
|
703
|
+
const key = relative.toLowerCase();
|
|
704
|
+
if (key === MANIFEST || key.startsWith(MANIFEST + "/")) {
|
|
705
|
+
throw new Error("Reserved " + label + " path: " + relative);
|
|
706
|
+
}
|
|
707
|
+
const existing = paths.get(key);
|
|
708
|
+
if (existing !== undefined) {
|
|
709
|
+
throw new Error("Conflicting " + label + " paths: " + existing + " and " + relative);
|
|
710
|
+
}
|
|
711
|
+
paths.set(key, relative);
|
|
712
|
+
}
|
|
713
|
+
for (const filename of paths.values()) {
|
|
714
|
+
const segments = filename.split("/");
|
|
715
|
+
for (let index = 1; index < segments.length; index++) {
|
|
716
|
+
if (paths.has(segments.slice(0, index).join("/").toLowerCase())) {
|
|
717
|
+
throw new Error("Conflicting " + label + " file and directory paths: " + filename);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return [...paths.values()];
|
|
722
|
+
}
|
|
723
|
+
function validateArtifactPaths(artifacts) {
|
|
724
|
+
const paths = validatePaths(artifacts.map(artifact => artifact.path.replace(/\\/g, "/")), "output");
|
|
725
|
+
return artifacts.map((artifact, index) => ({
|
|
726
|
+
...artifact,
|
|
727
|
+
relative: paths[index],
|
|
728
|
+
byteLength: Buffer.byteLength(artifact.contents, file_operations_1.FileOperations.isBinaryFilename(paths[index]) ? "latin1" : "utf8"),
|
|
729
|
+
binary: file_operations_1.FileOperations.isBinaryFilename(paths[index]),
|
|
730
|
+
}));
|
|
731
|
+
}
|
|
732
|
+
function parseManifest(contents) {
|
|
733
|
+
let parsed;
|
|
734
|
+
try {
|
|
735
|
+
parsed = JSON.parse(contents);
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
throw new Error("Invalid incremental output manifest JSON: " + String(error));
|
|
739
|
+
}
|
|
740
|
+
if (typeof parsed !== "object" || parsed === null || !("version" in parsed) || !("files" in parsed)
|
|
741
|
+
|| parsed.version !== 1 || !Array.isArray(parsed.files)) {
|
|
742
|
+
throw new Error("Unsupported incremental output manifest");
|
|
743
|
+
}
|
|
744
|
+
return { version: 1, files: validatePaths(parsed.files, "incremental output manifest") };
|
|
745
|
+
}
|
|
746
|
+
function serializeManifest(files) {
|
|
747
|
+
return JSON.stringify({ version: 1, files: [...files].sort() }, null, 2) + "\n";
|
|
748
|
+
}
|
|
749
|
+
async function lstatOrMissing(filename) {
|
|
750
|
+
try {
|
|
751
|
+
return await fsPromises.lstat(filename);
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
if (error.code === "ENOENT") {
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function isBelowRoot(root, filename) {
|
|
761
|
+
const relative = path.relative(root, filename);
|
|
762
|
+
return relative !== "" && relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
763
|
+
}
|
|
764
|
+
async function inspectPaths(root, relativePaths, rootStats, ioConcurrency) {
|
|
765
|
+
const metadata = new Map();
|
|
766
|
+
const nodes = new Map();
|
|
767
|
+
for (const filename of relativePaths) {
|
|
768
|
+
const segments = filename.split("/");
|
|
769
|
+
for (let index = 1; index <= segments.length; index++) {
|
|
770
|
+
const relative = segments.slice(0, index).join("/");
|
|
771
|
+
const isDirectory = index < segments.length;
|
|
772
|
+
nodes.set(relative, (nodes.get(relative) ?? false) || isDirectory);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (rootStats !== undefined) {
|
|
776
|
+
const byDepth = new Map();
|
|
777
|
+
for (const relative of nodes.keys()) {
|
|
778
|
+
const depth = relative.split("/").length;
|
|
779
|
+
const group = byDepth.get(depth) ?? [];
|
|
780
|
+
group.push(relative);
|
|
781
|
+
byDepth.set(depth, group);
|
|
782
|
+
}
|
|
783
|
+
for (const depth of [...byDepth.keys()].sort((a, b) => a - b)) {
|
|
784
|
+
const candidates = byDepth.get(depth).filter(relative => {
|
|
785
|
+
const parent = path.posix.dirname(relative);
|
|
786
|
+
return depth === 1 || metadata.get(parent)?.isDirectory() === true;
|
|
787
|
+
});
|
|
788
|
+
await forEachLimit(candidates, async (relative) => {
|
|
789
|
+
const absolute = path.resolve(root, ...relative.split("/"));
|
|
790
|
+
const stat = await lstatOrMissing(absolute);
|
|
791
|
+
if (stat?.isSymbolicLink()) {
|
|
792
|
+
throw new Error("Output path must not traverse a symlink: " + relative);
|
|
793
|
+
}
|
|
794
|
+
if (stat !== undefined && nodes.get(relative) === true && !stat.isDirectory()) {
|
|
795
|
+
throw new Error("Output path parent is not a directory: " + relative);
|
|
796
|
+
}
|
|
797
|
+
if (stat !== undefined && nodes.get(relative) === false && !stat.isFile()) {
|
|
798
|
+
throw new Error("Generated output path is not a regular file: " + relative);
|
|
799
|
+
}
|
|
800
|
+
metadata.set(relative, stat);
|
|
801
|
+
}, ioConcurrency);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
for (const relative of nodes.keys()) {
|
|
806
|
+
metadata.set(relative, undefined);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return metadata;
|
|
810
|
+
}
|
|
811
|
+
async function atomicWriteManifest(root, contents, current) {
|
|
812
|
+
if (current === contents) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
await fsPromises.mkdir(root, { recursive: true });
|
|
816
|
+
const temporary = path.join(root, MANIFEST + ".tmp-" + process.pid + "-" + Math.random().toString(36).slice(2));
|
|
817
|
+
try {
|
|
818
|
+
await fsPromises.writeFile(temporary, contents, { flag: "wx", encoding: "utf8" });
|
|
819
|
+
await fsPromises.rename(temporary, path.join(root, MANIFEST));
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
try {
|
|
823
|
+
await fsPromises.unlink(temporary);
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// The temporary file can be absent when creation failed.
|
|
827
|
+
}
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function writeOutput(rootFolder, artifacts, incremental, ioConcurrency = concurrency()) {
|
|
832
|
+
const root = path.resolve(rootFolder);
|
|
833
|
+
const prepared = validateArtifactPaths(artifacts);
|
|
834
|
+
const result = {
|
|
835
|
+
created: 0, updated: 0, unchanged: 0, deleted: 0,
|
|
836
|
+
};
|
|
837
|
+
if (!incremental) {
|
|
838
|
+
const files = prepared.map(file => ({ path: path.resolve(root, file.relative), contents: file.contents }));
|
|
839
|
+
await file_operations_1.FileOperations.writeFiles(files);
|
|
840
|
+
result.updated = files.length;
|
|
841
|
+
return result;
|
|
842
|
+
}
|
|
843
|
+
const manifestPath = path.join(root, MANIFEST);
|
|
844
|
+
const rootStats = await lstatOrMissing(root);
|
|
845
|
+
if (rootStats?.isSymbolicLink()) {
|
|
846
|
+
throw new Error("Output folder must not be a symlink: " + root);
|
|
847
|
+
}
|
|
848
|
+
if (rootStats !== undefined && !rootStats.isDirectory()) {
|
|
849
|
+
throw new Error("Output folder is not a directory: " + root);
|
|
850
|
+
}
|
|
851
|
+
let manifestContents;
|
|
852
|
+
let previous = [];
|
|
853
|
+
if (rootStats !== undefined) {
|
|
854
|
+
const manifestStat = await lstatOrMissing(manifestPath);
|
|
855
|
+
if (manifestStat?.isSymbolicLink() || (manifestStat !== undefined && !manifestStat.isFile())) {
|
|
856
|
+
throw new Error("Incremental output manifest must be a regular file");
|
|
857
|
+
}
|
|
858
|
+
if (manifestStat !== undefined) {
|
|
859
|
+
const contents = await fsPromises.readFile(manifestPath, "utf8");
|
|
860
|
+
manifestContents = contents;
|
|
861
|
+
previous = parseManifest(contents).files;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// A path can belong to both inventories; each inventory rejects its own duplicates.
|
|
865
|
+
validatePaths([...new Set([...prepared.map(file => file.relative), ...previous])], "incremental output");
|
|
866
|
+
const previousPaths = new Set(previous);
|
|
867
|
+
const current = new Set(prepared.map(file => file.relative));
|
|
868
|
+
const stale = previous.filter(filename => !current.has(filename));
|
|
869
|
+
const additions = prepared.some(file => !previousPaths.has(file.relative));
|
|
870
|
+
const pathsToInspect = [...new Set([...prepared.map(file => file.relative), ...stale])];
|
|
871
|
+
const metadata = await inspectPaths(root, pathsToInspect, rootStats, ioConcurrency);
|
|
872
|
+
await forEachLimit(prepared, async (file) => {
|
|
873
|
+
const stat = metadata.get(file.relative);
|
|
874
|
+
file.existed = stat !== undefined;
|
|
875
|
+
if (stat === undefined || stat.size !== file.byteLength) {
|
|
876
|
+
file.changed = true;
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
let existing;
|
|
880
|
+
try {
|
|
881
|
+
existing = await fsPromises.readFile(path.resolve(root, ...file.relative.split("/")));
|
|
882
|
+
}
|
|
883
|
+
catch (error) {
|
|
884
|
+
if (error.code === "ENOENT") {
|
|
885
|
+
file.existed = false;
|
|
886
|
+
file.changed = true;
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
const generated = Buffer.from(file.contents, file.binary ? "latin1" : "utf8");
|
|
892
|
+
file.changed = !existing.equals(generated);
|
|
893
|
+
}, ioConcurrency);
|
|
894
|
+
if (additions) {
|
|
895
|
+
const union = serializeManifest(new Set([...previous, ...current]));
|
|
896
|
+
await atomicWriteManifest(root, union, manifestContents);
|
|
897
|
+
manifestContents = union;
|
|
898
|
+
}
|
|
899
|
+
const toWrite = prepared.filter(file => file.changed);
|
|
900
|
+
const directories = new Map();
|
|
901
|
+
for (const file of toWrite) {
|
|
902
|
+
const segments = file.relative.split("/");
|
|
903
|
+
for (let index = 1; index < segments.length; index++) {
|
|
904
|
+
const relative = segments.slice(0, index).join("/");
|
|
905
|
+
if (metadata.get(relative) === undefined) {
|
|
906
|
+
directories.set(relative, index);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const directoryDepths = [...new Set(directories.values())].sort((a, b) => a - b);
|
|
911
|
+
for (const depth of directoryDepths) {
|
|
912
|
+
const atDepth = [...directories.entries()].filter(([, candidateDepth]) => candidateDepth === depth)
|
|
913
|
+
.map(([relative]) => path.resolve(root, ...relative.split("/")));
|
|
914
|
+
await forEachLimit(atDepth, async (directory) => {
|
|
915
|
+
await fsPromises.mkdir(directory);
|
|
916
|
+
}, ioConcurrency);
|
|
917
|
+
}
|
|
918
|
+
await forEachLimit(toWrite, async (file) => {
|
|
919
|
+
await fsPromises.writeFile(path.resolve(root, ...file.relative.split("/")), file.contents, file.binary ? { encoding: "latin1" } : undefined);
|
|
920
|
+
}, ioConcurrency);
|
|
921
|
+
const staleFiles = stale.map(filename => path.resolve(root, ...filename.split("/")));
|
|
922
|
+
const removedDirectories = new Set();
|
|
923
|
+
for (const filename of staleFiles) {
|
|
924
|
+
try {
|
|
925
|
+
await fsPromises.unlink(filename);
|
|
926
|
+
result.deleted++;
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
if (error.code !== "ENOENT") {
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
let directory = path.dirname(filename);
|
|
934
|
+
while (isBelowRoot(root, directory)) {
|
|
935
|
+
removedDirectories.add(directory);
|
|
936
|
+
directory = path.dirname(directory);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
await forEachLimit([...removedDirectories].sort((a, b) => b.length - a.length), async (directory) => {
|
|
940
|
+
try {
|
|
941
|
+
await fsPromises.rmdir(directory);
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
const code = error.code;
|
|
945
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST") {
|
|
946
|
+
throw error;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
const finalManifest = serializeManifest(current);
|
|
951
|
+
await atomicWriteManifest(root, finalManifest, manifestContents);
|
|
952
|
+
result.created = prepared.filter(file => file.changed && !file.existed).length;
|
|
953
|
+
result.updated = prepared.filter(file => file.changed && file.existed).length;
|
|
954
|
+
result.unchanged = prepared.filter(file => !file.changed).length;
|
|
955
|
+
return result;
|
|
956
|
+
}
|
|
957
|
+
//# sourceMappingURL=output_writer.js.map
|
|
958
|
+
|
|
959
|
+
/***/ },
|
|
960
|
+
|
|
598
961
|
/***/ "./build/write_objects.js"
|
|
599
962
|
/*!********************************!*\
|
|
600
963
|
!*** ./build/write_objects.js ***!
|
|
@@ -637,28 +1000,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
637
1000
|
};
|
|
638
1001
|
})();
|
|
639
1002
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
640
|
-
exports.
|
|
1003
|
+
exports.collectObjectFiles = collectObjectFiles;
|
|
641
1004
|
const path = __importStar(__webpack_require__(/*! path */ "path"));
|
|
642
1005
|
const url_1 = __webpack_require__(/*! url */ "url");
|
|
643
1006
|
const transpiler_1 = __webpack_require__(/*! @abaplint/transpiler */ "./node_modules/@abaplint/transpiler/build/src/index.js");
|
|
644
|
-
|
|
645
|
-
async function writeObjects(outputFiles, config, files) {
|
|
1007
|
+
function collectObjectFiles(outputFiles, config, files) {
|
|
646
1008
|
const root = path.resolve(config.output_folder);
|
|
647
1009
|
const filesToWrite = [];
|
|
648
|
-
const destinations = new Set();
|
|
649
1010
|
const sources = new Map(files.map(file => [file.filename.toLowerCase(), file]));
|
|
650
1011
|
const add = (filename, contents) => {
|
|
651
|
-
|
|
652
|
-
const relative = path.relative(root, target);
|
|
653
|
-
if (relative === "" || relative.startsWith(".." + path.sep) || relative === ".." || path.isAbsolute(relative)) {
|
|
654
|
-
throw new Error("Output file escapes output_folder: " + filename);
|
|
655
|
-
}
|
|
656
|
-
const key = target.toLowerCase();
|
|
657
|
-
if (destinations.has(key)) {
|
|
658
|
-
throw new Error("Conflicting output file: " + filename);
|
|
659
|
-
}
|
|
660
|
-
destinations.add(key);
|
|
661
|
-
filesToWrite.push({ path: target, contents });
|
|
1012
|
+
filesToWrite.push({ path: filename.replace(/\\/g, "/"), contents });
|
|
662
1013
|
};
|
|
663
1014
|
for (const output of outputFiles) {
|
|
664
1015
|
const type = output.object.type.toUpperCase();
|
|
@@ -679,21 +1030,24 @@ async function writeObjects(outputFiles, config, files) {
|
|
|
679
1030
|
if (file === undefined) {
|
|
680
1031
|
continue;
|
|
681
1032
|
}
|
|
682
|
-
if (file.
|
|
1033
|
+
if (file.sourceMapPath !== undefined) {
|
|
1034
|
+
sourcePaths[filename] = file.sourceMapPath;
|
|
1035
|
+
}
|
|
1036
|
+
else if (file.relative !== undefined) {
|
|
683
1037
|
const source = path.resolve(root, file.relative, file.filename);
|
|
684
1038
|
const relative = path.relative(mapDirectory, source);
|
|
685
1039
|
// Sources on another Windows drive cannot be expressed as relative paths.
|
|
686
1040
|
sourcePaths[filename] = path.isAbsolute(relative) ? (0, url_1.pathToFileURL)(source).href
|
|
687
1041
|
: relative.split(path.sep).map(segment => encodeURIComponent(segment)).join("/");
|
|
688
1042
|
}
|
|
689
|
-
//
|
|
1043
|
+
// Embedding source text keeps maps useful after temporary checkouts are removed.
|
|
690
1044
|
sourceContents[filename] = file.contents;
|
|
691
1045
|
}
|
|
692
1046
|
add(name, output.chunk.getMap(path.basename(output.filename), { generatedLineOffset, sourcePaths, sourceContents }));
|
|
693
1047
|
}
|
|
694
1048
|
add(output.filename, contents);
|
|
695
1049
|
}
|
|
696
|
-
|
|
1050
|
+
return filesToWrite;
|
|
697
1051
|
}
|
|
698
1052
|
//# sourceMappingURL=write_objects.js.map
|
|
699
1053
|
|
|
@@ -127810,6 +128164,11 @@ run().then(() => {
|
|
|
127810
128164
|
}
|
|
127811
128165
|
methods.push(m.name);
|
|
127812
128166
|
}
|
|
128167
|
+
if (methods.length === 0) {
|
|
128168
|
+
// FOR TESTING helper class without test methods, nothing to run; it might also
|
|
128169
|
+
// live in locals_imp, which is not exported from the .testclasses module
|
|
128170
|
+
continue;
|
|
128171
|
+
}
|
|
127813
128172
|
tests.push({
|
|
127814
128173
|
obj,
|
|
127815
128174
|
filename: `${(0, output_layout_1.importPath)("index.mjs", this.layout.objectModule(obj, ".testclasses"))}`,
|
package/build/types.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface ITranspilerConfig {
|
|
|
12
12
|
exclude_filter?: string[];
|
|
13
13
|
/** @minLength 1 */
|
|
14
14
|
output_folder: string;
|
|
15
|
+
/** Skip unchanged generated files and remove tracked obsolete output */
|
|
16
|
+
incremental_output?: boolean;
|
|
15
17
|
/** experimental */
|
|
16
18
|
converter?: {
|
|
17
19
|
/** @uniqueItems true */
|
|
@@ -33,6 +35,8 @@ export interface ITranspilerConfig {
|
|
|
33
35
|
*/
|
|
34
36
|
exclude_filter?: string[];
|
|
35
37
|
}[];
|
|
38
|
+
/** skip duplicate dependency objects after the first library in libs, with a warning; defaults to false */
|
|
39
|
+
skip_duplicate_dependencies?: boolean;
|
|
36
40
|
write_unit_tests?: boolean;
|
|
37
41
|
write_source_map?: boolean;
|
|
38
42
|
options: ITranspilerOptions;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abaplint/transpiler-cli",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.2",
|
|
4
4
|
"description": "Transpiler - Command Line Interface",
|
|
5
5
|
"funding": "https://github.com/sponsors/larshp",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@abaplint/core": "^2.120.71",
|
|
31
|
-
"@abaplint/transpiler": "^2.14.
|
|
31
|
+
"@abaplint/transpiler": "^2.14.2",
|
|
32
32
|
"@types/glob": "^9.0.0",
|
|
33
33
|
"@types/node": "^26.6.2",
|
|
34
34
|
"@types/progress": "^2.0.7",
|
package/schema.json
CHANGED
|
@@ -5,6 +5,35 @@
|
|
|
5
5
|
"ITranspilerConfig": {
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"properties": {
|
|
8
|
+
"converter": {
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"description": "experimental",
|
|
11
|
+
"properties": {
|
|
12
|
+
"input_folder": {
|
|
13
|
+
"anyOf": [
|
|
14
|
+
{
|
|
15
|
+
"type": "string"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "string"
|
|
20
|
+
},
|
|
21
|
+
"type": "array"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"uniqueItems": true
|
|
25
|
+
},
|
|
26
|
+
"output_folder": {
|
|
27
|
+
"minLength": 1,
|
|
28
|
+
"type": "string"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": [
|
|
32
|
+
"input_folder",
|
|
33
|
+
"output_folder"
|
|
34
|
+
],
|
|
35
|
+
"type": "object"
|
|
36
|
+
},
|
|
8
37
|
"exclude_filter": {
|
|
9
38
|
"description": "list of regex, case insensitive",
|
|
10
39
|
"items": {
|
|
@@ -13,6 +42,10 @@
|
|
|
13
42
|
"type": "array",
|
|
14
43
|
"uniqueItems": true
|
|
15
44
|
},
|
|
45
|
+
"incremental_output": {
|
|
46
|
+
"description": "Skip unchanged generated files and remove tracked obsolete output",
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
16
49
|
"input_filter": {
|
|
17
50
|
"description": "list of regex, case insensitive, empty gives all files, positive list",
|
|
18
51
|
"items": {
|
|
@@ -32,7 +65,8 @@
|
|
|
32
65
|
},
|
|
33
66
|
"type": "array"
|
|
34
67
|
}
|
|
35
|
-
]
|
|
68
|
+
],
|
|
69
|
+
"uniqueItems": true
|
|
36
70
|
},
|
|
37
71
|
"libs": {
|
|
38
72
|
"items": {
|
|
@@ -60,6 +94,12 @@
|
|
|
60
94
|
]
|
|
61
95
|
},
|
|
62
96
|
"folder": {
|
|
97
|
+
"description": "relative to the current working directory",
|
|
98
|
+
"type": "string"
|
|
99
|
+
},
|
|
100
|
+
"name": {
|
|
101
|
+
"description": "output directory name; defaults to the repository or local folder name",
|
|
102
|
+
"minLength": 1,
|
|
63
103
|
"type": "string"
|
|
64
104
|
},
|
|
65
105
|
"url": {
|
|
@@ -74,8 +114,13 @@
|
|
|
74
114
|
"$ref": "#/definitions/ITranspilerOptions"
|
|
75
115
|
},
|
|
76
116
|
"output_folder": {
|
|
117
|
+
"minLength": 1,
|
|
77
118
|
"type": "string"
|
|
78
119
|
},
|
|
120
|
+
"skip_duplicate_dependencies": {
|
|
121
|
+
"description": "skip duplicate dependency objects after the first library in libs, with a warning; defaults to false",
|
|
122
|
+
"type": "boolean"
|
|
123
|
+
},
|
|
79
124
|
"write_source_map": {
|
|
80
125
|
"type": "boolean"
|
|
81
126
|
},
|