Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(web): lm-worker bundling + wrapper script rework 📜 #10264

Merged
merged 12 commits into from
Jan 8, 2024
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
6 changes: 3 additions & 3 deletions common/web/lm-worker/build-bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import esbuild from 'esbuild';
import { bundleObjEntryPoints, esmConfiguration, prepareTslibTreeshaking } from '../es-bundling/build/index.mjs';
import { bundleObjEntryPoints, iifeConfiguration, prepareTslibTreeshaking } from '../es-bundling/build/index.mjs';

import fs from 'fs';

Expand All @@ -31,8 +31,8 @@ if(process.argv.length > 2) {
}

const embeddedWorkerBuildOptions = await prepareTslibTreeshaking({
...esmConfiguration,
...bundleObjEntryPoints('lib', 'build/obj/index.js', 'build/obj/worker-main.js'),
...iifeConfiguration,
...bundleObjEntryPoints('intermediate', 'build/obj/worker-main.js'),
// To be explicit, in comparison to the other build below. Even if our other
// configs change their default, we should not minify for THIS build; we'll
// have a separate minify pass later, after concatenating our polyfills.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,72 @@ import SourceMapCombiner from 'combine-source-map';
import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats.
// Base64, stringified-JSON, end-of-file comment...

import esbuild from 'esbuild';

let sourceFromArgs;
let destFromArgs;

function doHelp(errMessage) {
if(errMessage) {
console.error(errMessage + '\n');
}
console.log(`
Summary:
Creates a polyfilled version of the lm-worker to ensure compatibility with the needs of
Keyman for Android when run on Android 5.0 / API 21 without an updated Chrome webview.

Usage:
node build-polyfiller.js <input-file> [options...]

Parameters:
<input-file>: Fully-bundled and compiled JS file to be polyfilled.

Options:
--help Shows this script's documentation
--out <out-file> Specifies the destination path for the polyfilled output.

If missing, the output will be placed next to the source and given
the same path, but with '.polyfilled.js' replacing the original '.js'
extension.

Either way, a minified version will also be output, using the same
path but with the final '.js' replaced by '.min.js'.
` );
process.exit(errMessage ? 1 : 0);
}

if(process.argv.length > 2) {
for(let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];

switch(arg) {
case '--help':
doHelp();
break;
case '--out':
destFromArgs = process.argv[++i];
break;
default:
if(!sourceFromArgs) {
sourceFromArgs = arg;
} else {
doHelp("Input file can only be specified once; aborting");
}
}
}
} else {
// Display help + abort.
doHelp("Required parameters missing");
}

if(!sourceFromArgs || sourceFromArgs.substring(sourceFromArgs.length - 3) != '.js') {
doHelp("No input file has been specified; aborting.");
}

const sourceFile = sourceFromArgs;
const destFile = destFromArgs || sourceFromArgs.substring(0, sourceFromArgs.length - 3) + '.polyfilled.js';
const minDestFile = destFile.substring(0, destFile.length - 3) + '.min.js';

let loadPolyfill = function(scriptFile, sourceMapFile) {
// May want to retool the pathing somewhat!
return {
Expand Down Expand Up @@ -89,10 +155,10 @@ let sourceFileSet = [
// Needed to support Symbol.iterator, as used by the correction algorithm.
// Needed for Android / Chromium browser pre-43.
loadPolyfill('src/polyfills/symbol-es6.min.js', 'src/polyfills/symbol-es6.min.js'),
loadCompiledModuleFilePair('build/lib/worker-main.mjs', 'build/lib/worker-main.mjs')
loadCompiledModuleFilePair(sourceFile, sourceFile)
];

let fullWorkerConcatenation = concatScriptsAndSourcemaps(sourceFileSet, "worker-main.polyfilled.js", separatorFile);
let fullWorkerConcatenation = concatScriptsAndSourcemaps(sourceFileSet, destFile, separatorFile);

// New stage: cleaning the sourcemaps

Expand All @@ -109,7 +175,19 @@ console.log("Pass 2: Output intermediate state and perform minification");
fullWorkerConcatenation.script = fullWorkerConcatenation.script.substring(0, fullWorkerConcatenation.script.lastIndexOf('//# sourceMappingURL'));
fullWorkerConcatenation.script += `//# sourceMappingURL=${fullWorkerConcatenation.scriptFilename}.map`;

fs.writeFileSync(`build/lib/${fullWorkerConcatenation.scriptFilename}`, fullWorkerConcatenation.script);
fs.writeFileSync(fullWorkerConcatenation.scriptFilename, fullWorkerConcatenation.script);
if(fullWorkerConcatenation.sourcemapJSON) {
fs.writeFileSync(`build/lib/${fullWorkerConcatenation.scriptFilename}.map`, convertSourceMap.fromObject(fullWorkerConcatenation.sourcemapJSON).toJSON());
}
fs.writeFileSync(`${fullWorkerConcatenation.scriptFilename}.map`, convertSourceMap.fromObject(fullWorkerConcatenation.sourcemapJSON).toJSON());
}

await esbuild.build({
entryPoints: [destFile],
sourcemap: 'external',
sourcesContent: true,
minify: true,
// Do NOT enable - will break under Android 5.0 / Chrome 35 environments, likely through Chrome 42.
// https://caniuse.com/mdn-javascript_builtins_function_name_configurable_true
Comment on lines +188 to +189
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it might be flipped in 18.0? Do we need an issue to track this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming I'm properly understanding you here, we will not need an issue to track this. From #10257:

export function forES6(config: esbuild.BuildOptions): esbuild.BuildOptions {
return {
...config,
target: "es6",
conditions: ['es6-bundling'],
// Even if they'd be tree-shaken out, esbuild will fall over from their presence
// within an imported `@keymanapp/common-types`.
external: ['timers', 'buffer', 'events'],
// Available with ES6, but not necessarily with ES5.
keepNames: true
};
}

The ES6 target's configuration will re-enable this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we ever want keepNames: false?

Copy link
Contributor Author

@jahorton jahorton Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not viable on Android 5.0 devices, so yes.

https://caniuse.com/mdn-javascript_builtins_function_name_configurable_true

keepNames: false,
target: 'es5',
outfile: minDestFile
});
88 changes: 0 additions & 88 deletions common/web/lm-worker/build-wrap-and-minify.js

This file was deleted.

112 changes: 112 additions & 0 deletions common/web/lm-worker/build-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import fs from 'fs';

import convertSourcemap from 'convert-source-map'; // Transforms sourcemaps among various common formats.
// Base64, stringified-JSON, end-of-file comment...

let INCLUDE_SRCMAPS = false;

let sourceFromArgs;
let destFromArgs;

function doHelp(errMessage) {
if(errMessage) {
console.error(errMessage + '\n');
}

console.log(`
Summary:
Creates a "wrapped" version of the lm-worker for compilation into and inclusion as part
of a different JS bundle for later use as the core of a predictive-text WebWorker.

Usage:
node build-wrapper.js <input-file> [options...]

Parameters:
<input-file>: Fully-bundled and compiled JS file to be wrapped.

Options:
--help Shows this script's documentation
--out <out-file> Specifies the destination path for the wrapped output.

If missing, the output will be placed next to the source and given
the same path, but with '.wrapped.js' replacing the original '.js'
extension.
--sourceMap Includes the script's original sourcemaps within the wrapped output
` );
process.exit(errMessage ? 1 : 0);
}

if(process.argv.length > 2) {
for(let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];

switch(arg) {
case '--help':
doHelp();
break;
case '--sourceMap': // bc TS uses this exact flag. esbuild... uses sourcemap (in the JS config)
jahorton marked this conversation as resolved.
Show resolved Hide resolved
case '--sourcemap':
case '--source-map':
INCLUDE_SRCMAPS = true;
break;
case '--out':
destFromArgs = process.argv[++i];
break;
default:
if(!sourceFromArgs) {
sourceFromArgs = arg;
} else {
doHelp("Input file can only be specified once; aborting");
}
}
}
} else {
// Display help + abort.
doHelp("Required parameters missing");
}

if(!sourceFromArgs || sourceFromArgs.substring(sourceFromArgs.length - 3) != '.js') {
doHelp("No input file has been specified; aborting.");
}

const sourceFile = sourceFromArgs;
const destFile = destFromArgs || sourceFromArgs.substring(0, sourceFromArgs.length - 3) + '.wrapped.js';

// Now, to build the wrapper...

const script = fs.readFileSync(sourceFile);

// Wrapped in a function so we can leverage `const` with the result.
function buildSrcMapString() {
const sourcemapJSON = convertSourcemap.fromJSON(fs.readFileSync(`${sourceFile}.map`)).toObject();
const encodedSrcMap = convertSourcemap.fromObject(sourcemapJSON).toBase64();
return `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${encodedSrcMap}`;
}

// While it IS possible to do partial sourcemaps (without the sources, but with everything else) within the worker...
// the resulting sourcemaps are -surprisingly- large - larger than the code itself!
const srcMapString = INCLUDE_SRCMAPS ? buildSrcMapString() : "";

/*
* It'd be nice to do a 'partial' encodeURIComponent that only gets the important bits...
* but my attempts to do so end up triggering errors when loading.
*/

let rawScript = script.toString();
// Two layers of encoding: one for the raw source (parsed by the JS engine),
// one to 'unwrap' it from a string _within_ that source.
let jsonEncoded = JSON.stringify(rawScript);

let wrapper = `
// Autogenerated code. Do not modify!
// --START:LMLayerWorkerCode--

export var LMLayerWorkerCode = ${jsonEncoded};

${!INCLUDE_SRCMAPS && "// Sourcemaps have been omitted for this release build." || ''}
export var LMLayerWorkerSourcemapComment = "${srcMapString}";

// --END:LMLayerWorkerCode
`;

fs.writeFileSync(destFile, wrapper);
18 changes: 12 additions & 6 deletions common/web/lm-worker/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ cd "$THIS_SCRIPT_PATH"
WORKER_OUTPUT=build/obj
WORKER_OUTPUT_FILENAME=build/lib/worker-main.js

INTERMEDIATE=./build/intermediate
LIB=./build/lib

################################ Main script ################################

builder_describe \
Expand Down Expand Up @@ -50,14 +53,17 @@ function do_build() {
fi

# Declaration bundling.
tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts
tsc --emitDeclarationOnly --outFile ./build/lib/worker-main.d.ts
tsc --emitDeclarationOnly --outFile $INTERMEDIATE/worker-main.d.ts

echo "Preparing the polyfills + worker for script-embedding"
node build-polyfill-concatenator.js

node build-wrap-and-minify.js --debug
node build-wrap-and-minify.js --minify
node build-polyfiller.js $INTERMEDIATE/worker-main.js \
--out $INTERMEDIATE/worker-main.polyfilled.js

mkdir -p $LIB
node build-wrapper.js $INTERMEDIATE/worker-main.polyfilled.js \
--out $LIB/worker-main.wrapped.js --sourceMap
node build-wrapper.js $INTERMEDIATE/worker-main.polyfilled.min.js \
--out $LIB/worker-main.wrapped.min.js
}

function do_test() {
Expand Down