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
3 changes: 3 additions & 0 deletions Apps/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Apps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
107 changes: 107 additions & 0 deletions Apps/scripts/downlevelNativeScripts.mjs
Original file line number Diff line number Diff line change
@@ -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 <file-or-directory> [...]\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`);
79 changes: 66 additions & 13 deletions Apps/scripts/getNightly.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});