Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,39 @@ jobs:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .nvmrc
# Until cap2ui5 0.2.0 is on npm, the samples run against cap2UI5's main:
# its plugin packed as `npm publish` packs it, and installed alone -
# which brings the other dependencies along, as the README says for a
# local checkout. Once 0.2.0 is published this goes, a package-lock.json
# pins it, and `cache: npm` and npm ci come back.
# Until cap2ui5 0.2.0 is on npm, the samples run against cap2UI5's plugin:
# packed as `npm publish` packs it, and installed alone - which brings the
# other dependencies along, as the README says for a local checkout. Once
# it is published this goes, a package-lock.json pins it, and `cache: npm`
# and npm ci come back.
#
# The ref is the branch that adds abap2js (and t.numc, which
# z2ui5_cl_smp_app_067 needs) until that is on cap2UI5's main - then this
# line goes too.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: cap2UI5/cap2UI5
ref: claude/absp2ui5-cap2ui5-transfer-7pg6dg
path: .cap2ui5
persist-credentials: false
- run: npm pack --pack-destination "$RUNNER_TEMP"
working-directory: .cap2ui5/plugin
- run: npm install --no-save --no-audit --no-fund "$RUNNER_TEMP"/cap2ui5-*.tgz
- run: npm test

# ---- the translation: srv/apps is what abap2js makes of the pinned samples
- id: pin
run: echo "sha=$(cat ABAP2UI5_SAMPLES_PIN)" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: abap2UI5/samples
ref: ${{ steps.pin.outputs.sha }}
path: .abap2UI5-samples
persist-credentials: false
- name: srv/apps is what abap2js makes of the pinned samples
run: node scripts/generate.mjs --check --samples .abap2UI5-samples
# every sample beside its transpiled ABAP original, roundtrip for roundtrip
- name: the samples behave as their ABAP originals
run: |
node scripts/originals.mjs --samples .abap2UI5-samples
node --test test/differential.mjs
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ node_modules/
*.sqlite-*
*.log
.DS_Store

# the differential test's transpiled originals and open-abap-core (scripts/originals.mjs)
test/.originals/
.deps/
1 change: 1 addition & 0 deletions ABAP2UI5_SAMPLES_PIN
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cdd5c8b09a4ecdc51943b827ff6711d6c3af13a5
200 changes: 163 additions & 37 deletions README.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
"cap2ui5": "^0.2.0"
},
"devDependencies": {
"@abaplint/transpiler-cli": "2.13.91",
"@cap-js/cds-test": "^1.0.2",
"fast-xml-parser": "^5.11.1"
},
"scripts": {
"start": "cds-serve",
"watch": "cds watch",
"test": "node --test test/*.test.mjs"
"test": "node --test test/*.test.mjs",
"generate": "node scripts/generate.mjs",
"check:generated": "node scripts/generate.mjs --check",
"originals": "node scripts/originals.mjs",
"differential": "node scripts/originals.mjs && node --test test/differential.mjs"
},
"cds": {
"requires": {
Expand Down
79 changes: 79 additions & 0 deletions scripts/generate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// generate - the abap2UI5 samples as cap2UI5 apps, translated, not ported.
//
// node scripts/generate.mjs [--check] [--samples <abap2UI5/samples checkout>]
//
// Every class scripts/samples.json lists under "generated" is read from an
// abap2UI5/samples checkout AT the commit ABAP2UI5_SAMPLES_PIN names and
// translated by cap2ui5's abap2js into srv/apps/<class>.js - the same class
// name, so ?app_start= is the same on both sides, and the original's lines,
// comments and texts. Nothing in those files is edited by hand: a change goes
// into the ABAP sample, the pin moves, and this runs again.
//
// The checkout is --samples, else $ABAP2UI5_SAMPLES, else ../abap2UI5-samples.
// It has to be at the pin, so that `@origin` in a module names the source the
// module really came from.
//
// --check writes nothing and exits 1 when a generated module is missing or
// differs from what the translation writes - the CI gate that keeps srv/apps
// and the pin in step. The HAND-WRITTEN ports (samples.json "handwritten") are
// never touched.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { abap2js, Abap2jsError } from "cap2ui5";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const args = process.argv.slice(2);
const check = args.includes("--check");
const at = args.indexOf("--samples");
const checkout = path.resolve(at >= 0 ? args[at + 1] : process.env.ABAP2UI5_SAMPLES ?? path.join(ROOT, "..", "abap2UI5-samples"));

const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").trim();
const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8"));
const src = path.join(checkout, "src");

if (!fs.existsSync(src)) {
console.error(`generate: no abap2UI5/samples checkout at ${checkout} - clone it and check out ${pin}:\n` +
` git clone https://github.com/abap2UI5/samples ${checkout} && git -C ${checkout} checkout ${pin}`);
process.exit(1);
}
let head = "";
try { head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(); } catch { /* no git */ }
if (head !== pin) {
console.error(`generate: ${checkout} is at ${head || "an unknown commit"}, ABAP2UI5_SAMPLES_PIN says ${pin}`);
process.exit(1);
}
const overlap = generated.filter((n) => n in handwritten);
if (overlap.length) {
console.error(`generate: ${overlap.join(", ")} listed as generated AND handwritten in scripts/samples.json`);
process.exit(1);
}

let failed = 0;
let changed = 0;
for (const name of generated) {
const file = path.join(src, `${name}.clas.abap`);
let code;
try {
({ code } = abap2js(fs.readFileSync(file, "utf8"), {
file, lib: [src], origin: `abap2UI5/samples src/${name}.clas.abap`, format: "esm",
}));
} catch (e) {
if (!(e instanceof Abap2jsError)) throw e;
console.error(`refused: ${e.message}`);
failed++;
continue;
}
const target = path.join(ROOT, "srv", "apps", `${name}.js`);
const before = fs.existsSync(target) ? fs.readFileSync(target, "utf8") : null;
if (before === code) continue;
changed++;
if (check) console.error(`${before === null ? "missing" : "differs"}: srv/apps/${name}.js`);
else fs.writeFileSync(target, code);
}

console.log(`generate: ${generated.length} samples from abap2UI5/samples@${pin.slice(0, 7)}, ` +
`${changed} ${check ? "to regenerate" : "written"}, ${failed} refused`);
process.exit(failed || (check && changed) ? 1 : 0);
88 changes: 88 additions & 0 deletions scripts/originals.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env node
// originals - the ABAP originals of this repository's samples, transpiled, for
// the differential test (test/differential.mjs).
//
// node scripts/originals.mjs [--samples <abap2UI5/samples checkout>]
//
// Every class scripts/samples.json names is copied from the checkout at the
// pin, renamed Z2UI5_CL_SMP_APP_nnn -> ZABAP_SMP_APP_nnn so that original and
// translation can be served side by side, and transpiled by
// @abaplint/transpiler - the version @abap2ui5/node-runtime was built with,
// against the ABAP that package ships in downport/ - into test/.originals/.
// The runtime's own transpiler output is what cap2UI5 hosts, so the original
// runs on exactly the framework the translation runs on.
//
// Needs the network once: open-abap-core is cloned into .deps/.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const require = createRequire(import.meta.url);
const args = process.argv.slice(2);
const at = args.indexOf("--samples");
const checkout = path.resolve(at >= 0 ? args[at + 1] : process.env.ABAP2UI5_SAMPLES ?? path.join(ROOT, "..", "abap2UI5-samples"));
const pin = fs.readFileSync(path.join(ROOT, "ABAP2UI5_SAMPLES_PIN"), "utf8").trim();
const { generated, handwritten } = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "samples.json"), "utf8"));
const names = [...generated, ...Object.keys(handwritten)];

const head = execFileSync("git", ["-C", checkout, "rev-parse", "HEAD"], { encoding: "utf8" }).trim();
if (head !== pin) throw new Error(`${checkout} is at ${head}, ABAP2UI5_SAMPLES_PIN says ${pin}`);

// the transpiler the runtime was built with: its output is tied to the runtime
const runtimeDir = path.dirname(require.resolve("@abap2ui5/node-runtime/package.json", { paths: [require.resolve("cap2ui5")] }));
const wanted = JSON.parse(fs.readFileSync(path.join(runtimeDir, "package.json"), "utf8")).abap2ui5?.transpiler;
const have = JSON.parse(fs.readFileSync(require.resolve("@abaplint/transpiler-cli/package.json"), "utf8")).version;
if (wanted && wanted !== have) {
throw new Error(`@abaplint/transpiler-cli ${have} is installed, @abap2ui5/node-runtime was built with ${wanted} - ` +
`npm i -D --save-exact @abaplint/transpiler-cli@${wanted}`);
}

const deps = path.join(ROOT, ".deps", "open-abap-core");
if (!fs.existsSync(deps)) {
execFileSync("git", ["clone", "--depth", "1", "https://github.com/open-abap/open-abap-core", deps], { stdio: "inherit" });
}

const work = fs.mkdtempSync(path.join(os.tmpdir(), "originals-"));
try {
const input = path.join(work, "abap");
fs.mkdirSync(input);
const rename = (s) => s.replace(/z2ui5_cl_smp_app_(\d{3})/g, "zabap_smp_app_$1").replace(/Z2UI5_CL_SMP_APP_(\d{3})/g, "ZABAP_SMP_APP_$1");
for (const name of names) {
for (const ext of ["clas.abap", "clas.xml"]) {
const file = path.join(checkout, "src", `${name}.${ext}`);
if (fs.existsSync(file)) fs.writeFileSync(path.join(input, `${rename(name)}.${ext}`), rename(fs.readFileSync(file, "utf8")));
}
}
// the transpiler reads a lib folder relative to where it runs
fs.symlinkSync(path.join(runtimeDir, "downport"), path.join(work, "downport"));
fs.symlinkSync(deps, path.join(work, "open-abap-core"));
fs.writeFileSync(path.join(work, "abap_transpile.json"), JSON.stringify({
input_folder: "abap",
output_folder: "output",
libs: [
{ folder: "/downport", files: "/**/*.*" },
{ folder: "/open-abap-core" },
],
write_unit_tests: false,
options: { ignoreSyntaxCheck: false, addFilenames: true, unknownTypes: "runtimeError" },
}, null, 2));
execFileSync(process.execPath, [require.resolve("@abaplint/transpiler-cli/abap_transpile"), "abap_transpile.json"],
{ cwd: work, stdio: ["ignore", "ignore", "inherit"] });
const out = path.join(ROOT, "test", ".originals");
fs.rmSync(out, { recursive: true, force: true });
fs.mkdirSync(out, { recursive: true });
let n = 0;
for (const name of names) {
const file = path.join(work, "output", `${rename(name)}.clas.mjs`);
if (!fs.existsSync(file)) throw new Error(`the transpiler wrote no ${rename(name)}.clas.mjs`);
fs.copyFileSync(file, path.join(out, path.basename(file)));
n++;
}
console.log(`originals: ${n} classes from abap2UI5/samples@${pin.slice(0, 7)} transpiled into test/.originals/`);
} finally {
fs.rmSync(work, { recursive: true, force: true });
}
78 changes: 78 additions & 0 deletions scripts/samples.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$comment": "Which abap2UI5 samples are cap2UI5 apps, and how. generated: translated by cap2ui5 abap2js from the abap2UI5/samples commit in ABAP2UI5_SAMPLES_PIN (scripts/generate.mjs) - never edited by hand. handwritten: ported by hand, because abap2js refuses a construct the class needs; the reason is what it refuses. A sample moves from handwritten to generated when abap2js learns the construct and the differential test holds it to its original.",
"generated": [
"z2ui5_cl_smp_app_026",
"z2ui5_cl_smp_app_027",
"z2ui5_cl_smp_app_050",
"z2ui5_cl_smp_app_065",
"z2ui5_cl_smp_app_067",
"z2ui5_cl_smp_app_088",
"z2ui5_cl_smp_app_109",
"z2ui5_cl_smp_app_120",
"z2ui5_cl_smp_app_122",
"z2ui5_cl_smp_app_125",
"z2ui5_cl_smp_app_133",
"z2ui5_cl_smp_app_143",
"z2ui5_cl_smp_app_161",
"z2ui5_cl_smp_app_163",
"z2ui5_cl_smp_app_166",
"z2ui5_cl_smp_app_167",
"z2ui5_cl_smp_app_170",
"z2ui5_cl_smp_app_173",
"z2ui5_cl_smp_app_176",
"z2ui5_cl_smp_app_186",
"z2ui5_cl_smp_app_189",
"z2ui5_cl_smp_app_202",
"z2ui5_cl_smp_app_255",
"z2ui5_cl_smp_app_279",
"z2ui5_cl_smp_app_305",
"z2ui5_cl_smp_app_316",
"z2ui5_cl_smp_app_325",
"z2ui5_cl_smp_app_327",
"z2ui5_cl_smp_app_361",
"z2ui5_cl_smp_app_382",
"z2ui5_cl_smp_app_445",
"z2ui5_cl_smp_app_448",
"z2ui5_cl_smp_app_449",
"z2ui5_cl_smp_app_450",
"z2ui5_cl_smp_app_456",
"z2ui5_cl_smp_app_457",
"z2ui5_cl_smp_app_460",
"z2ui5_cl_smp_app_462",
"z2ui5_cl_smp_app_465",
"z2ui5_cl_smp_app_466",
"z2ui5_cl_smp_app_467",
"z2ui5_cl_smp_app_469",
"z2ui5_cl_smp_app_470",
"z2ui5_cl_smp_app_472",
"z2ui5_cl_smp_app_473",
"z2ui5_cl_smp_app_474",
"z2ui5_cl_smp_app_489",
"z2ui5_cl_smp_app_490",
"z2ui5_cl_smp_app_491",
"z2ui5_cl_smp_app_492",
"z2ui5_cl_smp_app_493",
"z2ui5_cl_smp_app_494",
"z2ui5_cl_smp_app_495",
"z2ui5_cl_smp_app_496",
"z2ui5_cl_smp_app_498",
"z2ui5_cl_smp_app_499",
"z2ui5_cl_smp_app_503",
"z2ui5_cl_smp_app_506",
"z2ui5_cl_smp_app_507",
"z2ui5_cl_smp_app_508",
"z2ui5_cl_smp_app_509",
"z2ui5_cl_smp_app_510",
"z2ui5_cl_smp_app_511",
"z2ui5_cl_smp_app_513",
"z2ui5_cl_smp_app_514",
"z2ui5_cl_smp_app_515",
"z2ui5_cl_smp_app_516",
"z2ui5_cl_smp_app_518",
"z2ui5_cl_smp_app_519"
],
"handwritten": {
"z2ui5_cl_smp_app_011": "LOOP AT t_tab REFERENCE INTO writes through the row, and DELETE t_tab WHERE (abap2js: neither supported yet)",
"z2ui5_cl_smp_app_488": "ASSIGN ls_get-r_event_data->* TO FIELD-SYMBOL( ) (abap2js: not supported yet)"
}
}
2 changes: 1 addition & 1 deletion srv/apps/z2ui5_cl_smp_app_011.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ defineApp("Z2UI5_CL_SMP_APP_011", class {
.a({ n: "xmlns:mvc", v: "sap.ui.core.mvc" });
const page = view.ele("Shell")
.ele("Page")
.a({ n: "title", v: "cap2UI5 - Table - Editable Cells, Add and Delete Rows" })
.a({ n: "title", v: "abap2UI5 - Table - Editable Cells, Add and Delete Rows" })
.a({ n: "showNavButton", b: this.client.check_app_prev_stack() })
.a({ n: "navButtonPress", v: this.client._event_nav_app_leave() })
.a({ n: "id", v: "test2" });
Expand Down
Loading
Loading