@abaplint/transpiler-cli 2.14.1 → 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"}
@@ -55,15 +56,28 @@ are rejected by default; a project object replaces a library object of the same
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
@@ -93,5 +107,7 @@ DDIC types, available methods, and runtime behavior, or cause syntax errors in
93
107
  code expecting the skipped version. This option does not check that copies are
94
108
  equivalent or suppress syntax checking. Put the intended provider first and
95
109
  run your application's tests. For selective control, use `libs[].exclude_filter`
96
- to exclude only the unwanted object's files instead. Use a clean generated
97
- output directory when changing providers, since old output is not removed.
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) {
@@ -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 {
@@ -604,6 +617,347 @@ function libraryRegistry(files, libraries, skipDuplicateDependencies = false) {
604
617
 
605
618
  /***/ },
606
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
+
607
961
  /***/ "./build/write_objects.js"
608
962
  /*!********************************!*\
609
963
  !*** ./build/write_objects.js ***!
@@ -646,28 +1000,16 @@ var __importStar = (this && this.__importStar) || (function () {
646
1000
  };
647
1001
  })();
648
1002
  Object.defineProperty(exports, "__esModule", ({ value: true }));
649
- exports.writeObjects = writeObjects;
1003
+ exports.collectObjectFiles = collectObjectFiles;
650
1004
  const path = __importStar(__webpack_require__(/*! path */ "path"));
651
1005
  const url_1 = __webpack_require__(/*! url */ "url");
652
1006
  const transpiler_1 = __webpack_require__(/*! @abaplint/transpiler */ "./node_modules/@abaplint/transpiler/build/src/index.js");
653
- const file_operations_1 = __webpack_require__(/*! ./file_operations */ "./build/file_operations.js");
654
- async function writeObjects(outputFiles, config, files) {
1007
+ function collectObjectFiles(outputFiles, config, files) {
655
1008
  const root = path.resolve(config.output_folder);
656
1009
  const filesToWrite = [];
657
- const destinations = new Set();
658
1010
  const sources = new Map(files.map(file => [file.filename.toLowerCase(), file]));
659
1011
  const add = (filename, contents) => {
660
- const target = path.resolve(root, filename);
661
- const relative = path.relative(root, target);
662
- if (relative === "" || relative.startsWith(".." + path.sep) || relative === ".." || path.isAbsolute(relative)) {
663
- throw new Error("Output file escapes output_folder: " + filename);
664
- }
665
- const key = target.toLowerCase();
666
- if (destinations.has(key)) {
667
- throw new Error("Conflicting output file: " + filename);
668
- }
669
- destinations.add(key);
670
- filesToWrite.push({ path: target, contents });
1012
+ filesToWrite.push({ path: filename.replace(/\\/g, "/"), contents });
671
1013
  };
672
1014
  for (const output of outputFiles) {
673
1015
  const type = output.object.type.toUpperCase();
@@ -688,21 +1030,24 @@ async function writeObjects(outputFiles, config, files) {
688
1030
  if (file === undefined) {
689
1031
  continue;
690
1032
  }
691
- if (file.relative !== undefined) {
1033
+ if (file.sourceMapPath !== undefined) {
1034
+ sourcePaths[filename] = file.sourceMapPath;
1035
+ }
1036
+ else if (file.relative !== undefined) {
692
1037
  const source = path.resolve(root, file.relative, file.filename);
693
1038
  const relative = path.relative(mapDirectory, source);
694
1039
  // Sources on another Windows drive cannot be expressed as relative paths.
695
1040
  sourcePaths[filename] = path.isAbsolute(relative) ? (0, url_1.pathToFileURL)(source).href
696
1041
  : relative.split(path.sep).map(segment => encodeURIComponent(segment)).join("/");
697
1042
  }
698
- // Include sources for local and cloned libraries; clones are removed before emission.
1043
+ // Embedding source text keeps maps useful after temporary checkouts are removed.
699
1044
  sourceContents[filename] = file.contents;
700
1045
  }
701
1046
  add(name, output.chunk.getMap(path.basename(output.filename), { generatedLineOffset, sourcePaths, sourceContents }));
702
1047
  }
703
1048
  add(output.filename, contents);
704
1049
  }
705
- await file_operations_1.FileOperations.writeFiles(filesToWrite);
1050
+ return filesToWrite;
706
1051
  }
707
1052
  //# sourceMappingURL=write_objects.js.map
708
1053
 
@@ -127819,6 +128164,11 @@ run().then(() => {
127819
128164
  }
127820
128165
  methods.push(m.name);
127821
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
+ }
127822
128172
  tests.push({
127823
128173
  obj,
127824
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 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abaplint/transpiler-cli",
3
- "version": "2.14.1",
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.1",
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",