diff --git a/Apps/package-lock.json b/Apps/package-lock.json index d8adf7009..bada42b2f 100644 --- a/Apps/package-lock.json +++ b/Apps/package-lock.json @@ -20,6 +20,9 @@ "babylonjs-serializers": "^9.15.0", "jsc-android": "^241213.1.0", "v8-android": "^7.8.2" + }, + "devDependencies": { + "typescript": "^5.9.3" } }, "node_modules/@babel/cli": { diff --git a/Apps/package.json b/Apps/package.json index 632285ede..fad341b3f 100644 --- a/Apps/package.json +++ b/Apps/package.json @@ -6,7 +6,11 @@ "UnitTests/JavaScript" ], "scripts": { - "getNightly": "node scripts/getNightly.js" + "getNightly": "node scripts/getNightly.js && npm run downlevel:native-scripts -- node_modules/babylonjs/babylon.max.js", + "downlevel:native-scripts": "node scripts/downlevelNativeScripts.mjs" + }, + "devDependencies": { + "typescript": "^5.9.3" }, "dependencies": { "babylonjs": "^9.15.0", diff --git a/Apps/scripts/downlevelNativeScripts.mjs b/Apps/scripts/downlevelNativeScripts.mjs new file mode 100644 index 000000000..68e9a3351 --- /dev/null +++ b/Apps/scripts/downlevelNativeScripts.mjs @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +// Kept byte-identical to scripts/downlevelNativeScripts.mjs in the Babylon.js repo, which applies the +// same transform to the artifacts this repo publishes. Diff the two before changing either. + +import { readdir, readFile, stat, writeFile } from "fs/promises"; +import { basename, join, resolve } from "path"; +import ts from "typescript"; + +const targets = process.argv.slice(2); + +if (targets.length === 0) { + process.stderr.write("Usage: node scripts/downlevelNativeScripts.mjs [...]\n"); + process.exit(1); +} + +// The TC39 decorator migration forces the Native UMD bundle to be emitted at an ES2015 target (the +// `accessor` keyword requires ES2015+, and esbuild cannot emit ES5 classes). Babylon Native's Chakra +// engine consumes ES5-level script, so the bundle must be down-leveled before it runs. +// +// We use the TypeScript transpiler (not Babel) for this. Babel's ES5 class transform emits +// `Reflect.construct`/`_wrapNativeSuper` machinery for classes that extend native built-ins (e.g. +// `Error`, `Array`); that machinery executes at class-definition time and hard-crashes Chakra when +// the bundle loads. TypeScript instead lowers classes with its `__extends` helper (plain prototype +// assignment, no `Reflect.construct`) - the exact emit Babylon Native ran successfully for years when +// the UMD bundles were built directly at an ES5 target. `ts.transpileModule` performs a purely +// syntactic, single-file transform (no type checking), so it does not choke on the multi-megabyte +// bundle, and it inlines self-contained helpers (no external `tslib`/`regeneratorRuntime` required). +const compilerOptions = { + target: ts.ScriptTarget.ES5, + // The bundles are UMD/IIFE scripts with no top-level module syntax; leave module output untouched. + module: ts.ModuleKind.None, + // Lower `for..of`, spread and other iterator protocols correctly for ES5. + downlevelIteration: true, + // Inline the emit helpers into each file so the bundle stays self-contained on Chakra. + importHelpers: false, + newLine: ts.NewLineKind.LineFeed, + sourceMap: false, + // The ES5/`module: none`/`downlevelIteration` combination emits TS 7.0 deprecation notices; they + // are informational and do not affect the emitted output. + ignoreDeprecations: "6.0", +}; + +function isNativeScriptFile(filePath) { + return /^babylon.*\.js$/i.test(basename(filePath)); +} + +async function collectFiles(target) { + const resolvedTarget = resolve(target); + const targetStat = await stat(resolvedTarget); + + if (targetStat.isFile()) { + return isNativeScriptFile(resolvedTarget) ? [resolvedTarget] : []; + } + + if (!targetStat.isDirectory()) { + return []; + } + + const entries = await readdir(resolvedTarget, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + const entryPath = join(resolvedTarget, entry.name); + if (entry.isDirectory()) { + files.push(...(await collectFiles(entryPath))); + } else if (entry.isFile() && isNativeScriptFile(entryPath)) { + files.push(entryPath); + } + } + + return files; +} + +const files = [...new Set((await Promise.all(targets.map(collectFiles))).flat())]; + +if (files.length === 0) { + process.stdout.write("No Babylon Native scripts found to downlevel.\n"); + process.exit(0); +} + +for (const file of files) { + const code = await readFile(file, "utf8"); + const result = ts.transpileModule(code, { compilerOptions, fileName: file, reportDiagnostics: true }); + + // `transpileModule` only surfaces syntactic and command-line/config diagnostics (it has no type + // information). Command-line/config diagnostics (code >= 5000) are informational for our + // transpile-only use; a real problem shows up as a syntactic error (code < 5000). + const fatalDiagnostics = (result.diagnostics || []).filter((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error && diagnostic.code < 5000); + if (fatalDiagnostics.length > 0) { + const formatted = ts.formatDiagnostics(fatalDiagnostics, { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: () => process.cwd(), + getNewLine: () => "\n", + }); + throw new Error(`TypeScript reported errors while down-leveling ${file}:\n${formatted}`); + } + + if (!result.outputText) { + throw new Error(`TypeScript did not produce output for ${file}`); + } + + await writeFile(file, result.outputText, "utf8"); + process.stdout.write(`Downleveled ${file}\n`); +} + +process.stdout.write(`Downleveled ${files.length} Babylon Native script file(s).\n`); diff --git a/Apps/scripts/getNightly.js b/Apps/scripts/getNightly.js index 52d13cb03..b3c8dc70b 100644 --- a/Apps/scripts/getNightly.js +++ b/Apps/scripts/getNightly.js @@ -2,20 +2,73 @@ var https = require('https'); var fs = require('fs'); function download(filename, url) { - var file = fs.createWriteStream(filename); - var request = https.get(url, function(response) { - response.pipe(file); + return new Promise(function (resolve, reject) { + var file = fs.createWriteStream(filename); + var settled = false; + + function fail(err) { + if (settled) { + return; + } + settled = true; + file.destroy(); + reject(err); + } + + file.on('error', fail); + // 'close', not 'finish': 'finish' fires once the data is flushed but before the descriptor is + // released, and the down-level step runs against these files immediately afterwards. + file.on('close', function () { + if (!settled) { + settled = true; + resolve(); + } + }); + + var request = https.get(url, function (response) { + if (response.statusCode !== 200) { + response.resume(); + fail(new Error('GET ' + url + ' failed with status ' + response.statusCode)); + return; + } + response.on('error', fail); + // A server-side abort emits neither 'finish' nor 'error' on the file, so without this the + // promise would never settle. + response.on('aborted', function () { + fail(new Error('GET ' + url + ' aborted by the server')); + }); + response.pipe(file); + }); + + request.on('error', fail); + request.setTimeout(120000, function () { + request.destroy(new Error('GET ' + url + ' timed out')); + }); }); } +var files = [ + ['node_modules/babylonjs/babylon.max.js', 'https://preview.babylonjs.com/babylon.max.js'], + ['node_modules/babylonjs/babylon.max.js.map', 'https://preview.babylonjs.com/babylon.max.js.map'], + ['node_modules/babylonjs-addons/babylonjs.addons.js', 'https://preview.babylonjs.com/addons/babylonjs.addons.js'], + ['node_modules/babylonjs-addons/babylonjs.addons.js.map', 'https://preview.babylonjs.com/addons/babylonjs.addons.js.map'], + ['node_modules/babylonjs-materials/babylonjs.materials.js', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js'], + ['node_modules/babylonjs-materials/babylonjs.materials.js.map', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js.map'], + ['node_modules/babylonjs-loaders/babylonjs.loaders.js', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js'], + ['node_modules/babylonjs-loaders/babylonjs.loaders.js.map', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js.map'], + ['node_modules/babylonjs-gui/babylon.gui.js', 'https://preview.babylonjs.com/gui/babylon.gui.js'], + ['node_modules/babylonjs-gui/babylon.gui.js.map', 'https://preview.babylonjs.com/gui/babylon.gui.js.map'], + ['node_modules/babylonjs-serializers/babylonjs.serializers.js', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js'], + ['node_modules/babylonjs-serializers/babylonjs.serializers.js.map', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js.map'], +]; + console.log('Downloading babylon.js nightly'); -download('node_modules/babylonjs/babylon.max.js', 'https://preview.babylonjs.com/babylon.max.js'); -download('node_modules/babylonjs/babylon.max.js.map', 'https://preview.babylonjs.com/babylon.max.js.map'); -download('node_modules/babylonjs-materials/babylonjs.materials.js', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js'); -download('node_modules/babylonjs-materials/babylonjs.materials.js.map', 'https://preview.babylonjs.com/materialsLibrary/babylonjs.materials.js.map'); -download('node_modules/babylonjs-loaders/babylonjs.loaders.js', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js'); -download('node_modules/babylonjs-loaders/babylonjs.loaders.js.map', 'https://preview.babylonjs.com/loaders/babylonjs.loaders.js.map'); -download('node_modules/babylonjs-gui/babylon.gui.js', 'https://preview.babylonjs.com/gui/babylon.gui.js'); -download('node_modules/babylonjs-gui/babylon.gui.js.map', 'https://preview.babylonjs.com/gui/babylon.gui.js.map'); -download('node_modules/babylonjs-serializers/babylonjs.serializers.js', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js'); -download('node_modules/babylonjs-serializers/babylonjs.serializers.js.map', 'https://preview.babylonjs.com/serializers/babylonjs.serializers.js.map'); + +// Awaited so a failed or partial download fails the script instead of silently leaving the previous +// file in place, and so the down-level step that runs next cannot race an incomplete write. +Promise.all(files.map(function (f) { return download(f[0], f[1]); })).then(function () { + console.log('Downloaded ' + files.length + ' file(s)'); +}, function (err) { + console.error(err.message); + process.exit(1); +});