Skip to content

Commit 25960e2

Browse files
authored
fix(TypedMessenger): Make sure TypedMessenger still works in minified builds (#933)
1 parent 8abbbb1 commit 25960e2

File tree

14 files changed

+645
-114
lines changed

14 files changed

+645
-114
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ deno_dir
1010
studio/src/styles/projectSelectorStyles.js
1111
studio/src/styles/studioStyles.js
1212
studio/src/styles/shadowStyles.js
13+
test/minified/out

.eslintrc.cjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ module.exports = {
189189
"no-useless-constructor": "error",
190190
"no-useless-rename": "error",
191191
"no-var": "error",
192-
"object-shorthand": "error",
192+
"object-shorthand": ["error", "always", {avoidQuotes: true}],
193193
"prefer-arrow-callback": "error",
194194
"prefer-const": ["error", {destructuring: "all"}],
195195
"prefer-numeric-literals": "error",

.github/workflows/ci.yml

+25
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,31 @@ jobs:
9595
flags: unittests
9696
token: ${{ secrets.CODECOV_TOKEN }}
9797

98+
minified-tests:
99+
runs-on: ubuntu-latest
100+
steps:
101+
- name: Setup repo
102+
uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8
103+
104+
- name: Cache
105+
uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7
106+
with:
107+
path: |
108+
deno_dir
109+
.denoTypes
110+
npm_packages
111+
studio/deps
112+
key: ci-${{ github.run_id }}_${{ github.run_attempt }}
113+
restore-keys: ci-
114+
115+
- name: Setup Deno
116+
uses: denoland/setup-deno@041b854f97b325bd60e53e9dc2de9cb9f9ac0cba
117+
with:
118+
deno-version: ${{ env.DENO_VERSION }}
119+
120+
- name: Run minified tests
121+
run: deno task test test/minified
122+
98123
e2e-tests:
99124
runs-on: ubuntu-latest
100125
steps:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ npm_packages
66
deno.lock
77
npmPackage
88
jsrPackage
9+
test/minified/out/

jsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"test/**/*.ts",
2828
],
2929
"exclude": [
30-
"test/e2e/studio/projects/"
30+
"test/e2e/studio/projects/",
31+
"test/minified/out"
3132
],
3233
"files": [
3334
"src/mod.js",

scripts/shared/rollupTerserPlugin.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { minify } from "terser";
2+
3+
const nameCache = {};
4+
5+
/**
6+
* A rollup plugin for minifying builds.
7+
* @param {import("terser").MinifyOptions} minifyOptions
8+
* @returns {import("rollup").Plugin}
9+
*/
10+
export function rollupTerserPlugin(minifyOptions = {}) {
11+
return {
12+
name: "terser",
13+
async renderChunk(code, chunk, outputOptions) {
14+
const output = await minify(code, {
15+
...minifyOptions,
16+
nameCache,
17+
});
18+
if (!output.code) return null;
19+
return {
20+
code: output.code,
21+
map: output.map,
22+
};
23+
},
24+
};
25+
}

scripts/test.js

+124-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#!/usr/bin/env -S deno run --unstable --no-check --allow-run --allow-read --allow-write --allow-env --allow-net
22

3-
import { join, resolve } from "std/path/mod.ts";
3+
import * as path from "std/path/mod.ts";
4+
import * as fs from "std/fs/mod.ts";
45
import { setCwd } from "chdir-anywhere";
56
import { dev } from "./dev.js";
67
import { parseArgs } from "../test/shared/testArgs.js";
8+
import { buildEngine } from "./buildEngine.js";
9+
import { rollup } from "rollup";
10+
import { rollupTerserPlugin } from "./shared/rollupTerserPlugin.js";
711

812
setCwd();
913
Deno.chdir("..");
@@ -35,6 +39,7 @@ if (Deno.args.length > 0 && !Deno.args[0].startsWith("-")) {
3539
}
3640

3741
const needsUnitTests = !filteredTests || filteredTests.startsWith("test/unit");
42+
const needsMinifiedTests = !filteredTests || filteredTests.startsWith("test/minified");
3843
const needsE2eTests = !filteredTests || filteredTests.startsWith("test/e2e");
3944

4045
await dev({
@@ -60,7 +65,7 @@ if (needsUnitTests) {
6065
await removeMaybeDirectory(DENO_COVERAGE_DIR);
6166
await removeMaybeDirectory(FAKE_IMPORTS_COVERAGE_DIR);
6267
denoTestArgs.push(`--coverage=${DENO_COVERAGE_DIR}`);
63-
const coverageMapPath = join(Deno.cwd(), FAKE_IMPORTS_COVERAGE_DIR);
68+
const coverageMapPath = path.join(Deno.cwd(), FAKE_IMPORTS_COVERAGE_DIR);
6469
applicationCmdArgs.add(`--fi-coverage-map=${coverageMapPath}`);
6570
}
6671
denoTestArgs.push(filteredTests || "test/unit/");
@@ -69,6 +74,122 @@ if (needsUnitTests) {
6974
testCommands.push(cmd);
7075
}
7176

77+
// Minified tests
78+
if (needsMinifiedTests) {
79+
const testMinifiedDir = path.resolve("test/minified");
80+
const testsDir = path.resolve(testMinifiedDir, "tests");
81+
const outDir = path.resolve(testMinifiedDir, "out");
82+
const engineDir = path.resolve(outDir, "engine");
83+
const testOutDir = path.resolve(outDir, "tests");
84+
const minifiedRendaPath = path.resolve(testMinifiedDir, "shared/minifiedRenda.js");
85+
const unminifiedRendaPath = path.resolve(testMinifiedDir, "shared/unminifiedRenda.js");
86+
87+
const noBuildFlag = Deno.args.includes("--no-build");
88+
89+
const denoTestArgs = [Deno.execPath(), "test", "--no-check", "--allow-env", "--allow-read", "--allow-net", "--parallel"];
90+
91+
if (noBuildFlag) {
92+
denoTestArgs.push(filteredTests || "test/minified/tests/");
93+
} else {
94+
denoTestArgs.push("test/minified/out/tests/");
95+
96+
try {
97+
await Deno.remove(outDir, { recursive: true });
98+
} catch (e) {
99+
if (e instanceof Deno.errors.NotFound) {
100+
// Already removed
101+
} else {
102+
throw e;
103+
}
104+
}
105+
106+
await buildEngine(engineDir);
107+
108+
/**
109+
* @returns {import("rollup").Plugin}
110+
*/
111+
function rollupRedirectBuildPlugin() {
112+
return {
113+
name: "redirectBuild",
114+
async resolveId(id, importer) {
115+
if (importer) {
116+
const dirname = path.dirname(importer);
117+
const resolved = path.resolve(dirname, id);
118+
if (resolved == minifiedRendaPath) {
119+
return path.resolve(engineDir, "renda.js");
120+
} else if (resolved == unminifiedRendaPath) {
121+
// We want to allow tests to export from both the built and non-built library.
122+
// This allows us to simulate situations such as minified client code that wants to
123+
// communicate with non-minified server code.
124+
// By marking ./src/unminifiedRenda.js as external, we make sure that the build of our tests
125+
// keep referencing this file directly, as opposed to including its contents in the bundle.
126+
// But we do have to rewrite the imported path, since bundled tests will live at another location.
127+
const newResolved = path.relative(testOutDir, unminifiedRendaPath);
128+
return {
129+
id: newResolved,
130+
external: true,
131+
};
132+
}
133+
}
134+
// Treat std as external
135+
if (id.startsWith("std/")) {
136+
return false;
137+
}
138+
return null;
139+
},
140+
};
141+
}
142+
143+
const testFiles = [];
144+
for await (const entry of fs.walk(testsDir)) {
145+
if (entry.name.endsWith(".test.js")) {
146+
const relative = path.relative(testsDir, entry.path);
147+
if (relative.startsWith("out/")) continue;
148+
testFiles.push(entry.path);
149+
}
150+
}
151+
const bundle = await rollup({
152+
input: testFiles,
153+
plugins: [rollupRedirectBuildPlugin()],
154+
onwarn: (message) => {
155+
if (message.code == "CIRCULAR_DEPENDENCY") return;
156+
console.error(message.message);
157+
},
158+
});
159+
const debug = Deno.args.includes("--debug") || Deno.args.includes("-d") || inspect;
160+
await bundle.write({
161+
dir: testOutDir,
162+
plugins: [
163+
rollupTerserPlugin({
164+
/* eslint-disable camelcase */
165+
module: true,
166+
keep_classnames: debug,
167+
keep_fnames: debug,
168+
compress: {
169+
drop_debugger: false,
170+
},
171+
sourceMap: debug,
172+
mangle: {
173+
module: true,
174+
properties: {
175+
debug,
176+
keep_quoted: "strict",
177+
reserved: ["Deno", "test", "fn"],
178+
},
179+
},
180+
output: {
181+
beautify: debug,
182+
},
183+
/* eslint-enable camelcase */
184+
}),
185+
],
186+
});
187+
}
188+
189+
if (inspect) denoTestArgs.push("--inspect-brk");
190+
testCommands.push(denoTestArgs);
191+
}
192+
72193
// E2e tests
73194
if (needsE2eTests) {
74195
const cmd = [Deno.execPath(), "run", "--allow-env", "--allow-read", "--allow-write", "--allow-run", "--allow-net"];
@@ -144,5 +265,5 @@ if (needsCoverage) {
144265
throw e;
145266
}
146267
}
147-
await Deno.rename(resolve(DENO_COVERAGE_DIR, "html"), DENO_HTML_COVERAGE_DIR);
268+
await Deno.rename(path.resolve(DENO_COVERAGE_DIR, "html"), DENO_HTML_COVERAGE_DIR);
148269
}

src/util/TypedMessenger/TypedMessenger.js

+26-27
Original file line numberDiff line numberDiff line change
@@ -406,10 +406,9 @@ export class TypedMessenger {
406406
* ## Example
407407
*
408408
* ```js
409-
* const result = await messenger.proxy.myFunction(1, 2, 3);
409+
* const result = await messenger.send.myFunction(1, 2, 3);
410410
* ```
411-
* where `myFunction` is the name of one of the functions provided in {@linkcode initialize} or {@linkcode setResponseHandlers}.
412-
*
411+
* where `myFunction` is the name of one of the functions provided in {@linkcode setResponseHandlers} or one of the `initialize` methods.
413412
*/
414413
this.send = /** @type {TypedMessengerProxy<TReq>} */ (proxy);
415414

@@ -453,7 +452,7 @@ export class TypedMessenger {
453452
*/
454453
initializeWorker(worker, responseHandlers) {
455454
this.setSendHandler((data) => {
456-
worker.postMessage(data.sendData, data.transfer);
455+
worker.postMessage(data["sendData"], data["transfer"]);
457456
});
458457
worker.addEventListener("message", (event) => {
459458
this.handleReceivedMessage(event.data);
@@ -480,8 +479,8 @@ export class TypedMessenger {
480479
*/
481480
initializeWorkerContext(responseHandlers) {
482481
this.setSendHandler((data) => {
483-
globalThis.postMessage(data.sendData, {
484-
transfer: data.transfer,
482+
globalThis.postMessage(data["sendData"], {
483+
transfer: data["transfer"],
485484
});
486485
});
487486
globalThis.addEventListener("message", (event) => {
@@ -528,7 +527,7 @@ export class TypedMessenger {
528527
});
529528
await promise;
530529
}
531-
webSocket.send(JSON.stringify(data.sendData));
530+
webSocket.send(JSON.stringify(data["sendData"]));
532531
});
533532
webSocket.addEventListener("message", async (message) => {
534533
try {
@@ -580,21 +579,21 @@ export class TypedMessenger {
580579
* @param {TypedMessengerMessageSendData<TRes, TReq>} data
581580
*/
582581
async handleReceivedMessage(data) {
583-
if (data.direction == "request") {
582+
if (data["direction"] == "request") {
584583
if (!this.responseHandlers) {
585584
throw new Error("Failed to handle message, no request handlers set. Make sure to call `setResponseHandlers` before handling messages.");
586585
}
587586
if (!this.sendHandler) {
588587
throw new Error("Failed to handle message, no send handler set. Make sure to call `setSendHandler` before handling messages.");
589588
}
590-
const handler = this.responseHandlers[data.type];
589+
const handler = this.responseHandlers[data["type"]];
591590
let returnValue;
592591
/** @type {Transferable[]} */
593592
let transfer = [];
594593
let didThrow = false;
595594
if (handler) {
596595
try {
597-
returnValue = await handler(...data.args);
596+
returnValue = await handler(...data["args"]);
598597
} catch (e) {
599598
returnValue = e;
600599
if (this.serializeErrorHook) {
@@ -618,27 +617,27 @@ export class TypedMessenger {
618617
}
619618

620619
await this.sendHandler(/** @type {TypedMessengerResponseMessageHelper<TRes, typeof data.type>} */ ({
621-
sendData: {
622-
direction: "response",
623-
id: data.id,
624-
didThrow,
625-
type: data.type,
626-
returnValue,
620+
"sendData": {
621+
"direction": "response",
622+
"id": data["id"],
623+
"didThrow": didThrow,
624+
"type": data["type"],
625+
"returnValue": returnValue,
627626
},
628627
transfer,
629628
}));
630629

631630
if (respondOptions && respondOptions.afterSendHook) {
632631
respondOptions.afterSendHook();
633632
}
634-
} else if (data.direction == "response") {
635-
const cbs = this.onRequestIdMessageCbs.get(data.id);
633+
} else if (data["direction"] == "response") {
634+
const cbs = this.onRequestIdMessageCbs.get(data["id"]);
636635
if (cbs) {
637636
for (const cb of cbs) {
638637
cb(data);
639638
}
640639
}
641-
this.onRequestIdMessageCbs.delete(data.id);
640+
this.onRequestIdMessageCbs.delete(data["id"]);
642641
}
643642
}
644643

@@ -726,9 +725,9 @@ export class TypedMessenger {
726725
} else {
727726
promise = new Promise((resolve, reject) => {
728727
this.onResponseMessage(requestId, (message) => {
729-
if (message.didThrow) {
728+
if (message["didThrow"]) {
730729
/** @type {unknown} */
731-
let rejectValue = message.returnValue;
730+
let rejectValue = message["returnValue"];
732731
if (this.deserializeErrorHook) {
733732
rejectValue = this.deserializeErrorHook(rejectValue);
734733
}
@@ -737,18 +736,18 @@ export class TypedMessenger {
737736
}
738737
reject(rejectValue);
739738
} else {
740-
resolve(message.returnValue);
739+
resolve(message["returnValue"]);
741740
}
742741
});
743742
});
744743
}
745744

746745
await this.sendHandler(/** @type {TypedMessengerRequestMessageHelper<TReq, T>} */ ({
747-
sendData: {
748-
direction: "request",
749-
id: requestId,
750-
type,
751-
args,
746+
"sendData": {
747+
"direction": "request",
748+
"id": requestId,
749+
"type": type,
750+
"args": args,
752751
},
753752
transfer: sendOptions.transfer || [],
754753
}));

test/minified/shared/minifiedRenda.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// This re-exports all engine files.
2+
// When building the tests, this file will export from 'dist/renda.min.js' instead.
3+
// This allows us to run the tests without building as well, and makes it easer
4+
// to check types when the tests haven't been built yet.
5+
6+
export * from "../../../src/mod.js";

0 commit comments

Comments
 (0)