@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 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. Imports and registered
59
- MIME filenames reference the new paths.
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. The CLI does not remove old generated files or unrelated
66
- files automatically.
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 outputFolder = config.output_folder;
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
- // breaking change? rename this output file,
362
- fs.writeFileSync(outputFolder + path.sep + "index.mjs", output.unitTestScript);
363
- fs.writeFileSync(outputFolder + path.sep + "_unit_open.mjs", output.unitTestScriptOpen);
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 files = await file_operations_1.FileOperations.readAllFiles([...filesToRead], config.output_folder);
551
- result.push({ name: names[index], files });
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.writeObjects = writeObjects;
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
- const file_operations_1 = __webpack_require__(/*! ./file_operations */ "./build/file_operations.js");
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
- const target = path.resolve(root, filename);
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.relative !== undefined) {
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
- // Include sources for local and cloned libraries; clones are removed before emission.
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
- await file_operations_1.FileOperations.writeFiles(filesToWrite);
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.0",
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.0",
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
  },