Skip to content
File

Blob: src/workerd/api/tests/worker-loader-test.js

javascript1065 lines
1// Copyright (c) 2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4import { WorkerEntrypoint, DurableObject } from 'cloudflare:workers';
5import assert from 'node:assert';
6 
7export class GreeterLoopback extends WorkerEntrypoint {
8 async greet(name) {
9 return `${this.ctx.props.greeting}, ${name}!`;
10 }
11}
12 
13export let basics = {
14 async test(ctrl, env, ctx) {
15 let worker = env.loader.get('basics', () => {
16 return {
17 compatibilityDate: '2025-01-01',
18 mainModule: 'foo.js',
19 modules: {
20 'foo.js': `
21 import {WorkerEntrypoint} from "cloudflare:workers";
22 export default {
23 greet(name) { return "Hello, " + name; }
24 }
25 export let alternate = {
26 greet(name, env, ctx) { return \`\${ctx.props.greeting}, \${name}\`; }
27 }
28 export class FancyPropsEntrypoint extends WorkerEntrypoint {
29 async run() {
30 let greet1 = await this.ctx.props.greeter.greet("Dave");
31 let greet2 = await this.ctx.props.greeter2.greet("Eve");
32 return [greet1, greet2].join("\\n");
33 }
34 }
35 `,
36 },
37 };
38 });
39 
40 {
41 let result = await worker.getEntrypoint().greet('Alice');
42 assert.strictEqual(result, 'Hello, Alice');
43 }
44 
45 let greeter = worker.getEntrypoint('alternate', {
46 props: { greeting: 'Welcome' },
47 });
48 let greeter2 = worker.getEntrypoint('alternate', {
49 props: { greeting: 'Howdy' },
50 });
51 
52 {
53 let result = await greeter.greet('Bob');
54 assert.strictEqual(result, 'Welcome, Bob');
55 }
56 
57 {
58 let result = await greeter2.greet('Carol');
59 assert.strictEqual(result, 'Howdy, Carol');
60 }
61 
62 let fancyProps = await worker.getEntrypoint('FancyPropsEntrypoint', {
63 props: {
64 greeter: ctx.exports.GreeterLoopback({ props: { greeting: "G'day" } }),
65 greeter2: ctx.exports.GreeterLoopback({ props: { greeting: 'Sup' } }),
66 },
67 });
68 
69 {
70 let result = await fancyProps.run();
71 assert.strictEqual(result, "G'day, Dave!\nSup, Eve!");
72 }
73 
74 // Note that we CANNOT transfer a dynamic worker entrypoint.
75 assert.rejects(
76 () =>
77 worker.getEntrypoint('FancyPropsEntrypoint', {
78 props: { greeter, greeter2 },
79 }),
80 {
81 name: 'DataCloneError',
82 message:
83 'Entrypoints to dynamically-loaded workers cannot be transferred to other Workers, ' +
84 'because the system does not know how to reload this Worker from scratch. Instead, ' +
85 'have the parent Worker expose an entrypoint which constructs the dynamic worker ' +
86 'and forwards to it.',
87 }
88 );
89 
90 // Let's quickly verify that if we use ctx.exports.GreeterLoopback *without* invoking it, then
91 // we get an error that it isn't serializable. (This isn't really testing worker-loader, it's
92 // more testing LoopbackServiceStub and that serializability is not inherited.)
93 assert.rejects(
94 () =>
95 worker.getEntrypoint('FancyPropsEntrypoint', {
96 props: {
97 greeter: ctx.exports.GreeterLoopback,
98 greeter2: ctx.exports.GreeterLoopback,
99 },
100 }),
101 {
102 name: 'DataCloneError',
103 message:
104 'Could not serialize object of type "LoopbackServiceStub". This type does not support ' +
105 'serialization.',
106 }
107 );
108 },
109};
110 
111export let pythonBasics = {
112 async test(ctrl, env, ctx) {
113 let worker = env.loader.get('pythonBasics', () => {
114 return {
115 compatibilityDate: '2025-01-01',
116 mainModule: 'foo.py',
117 compatibilityFlags: ['python_workers', 'python_no_global_handlers'],
118 modules: {
119 'foo.py': `
120from workers import WorkerEntrypoint
121class Default(WorkerEntrypoint):
122 async def greet(self, name):
123 return "Hello, " + name
124class alternate(WorkerEntrypoint):
125 async def greet(self, name):
126 return "Welcome, " + name
127 `,
128 },
129 };
130 });
131 
132 {
133 let result = await worker.getEntrypoint().greet('Stacey');
134 assert.strictEqual(result, 'Hello, Stacey');
135 }
136 
137 {
138 let result = await worker.getEntrypoint('alternate').greet('Anthony');
139 assert.strictEqual(result, 'Welcome, Anthony');
140 }
141 },
142};
143 
144// Test supplying a basic `env` object.
145export let passEnv = {
146 async test(ctrl, env, ctx) {
147 let worker = env.loader.get('passEnv', () => {
148 return {
149 compatibilityDate: '2025-01-01',
150 mainModule: 'foo.js',
151 modules: {
152 'foo.js': `
153 export default {
154 fetch(req, env, ctx) {
155 return new Response("env.hello = " + env.hello);
156 },
157 }
158 `,
159 },
160 env: {
161 hello: 123,
162 },
163 };
164 });
165 
166 let resp = await worker.getEntrypoint().fetch('https://example.com');
167 assert.strictEqual(await resp.text(), 'env.hello = 123');
168 },
169};
170 
171export let passEnvCaps = {
172 async test(ctrl, env, ctx) {
173 let worker = env.loader.get('passEnvCaps', () => {
174 return {
175 compatibilityDate: '2025-01-01',
176 mainModule: 'foo.js',
177 modules: {
178 'foo.js': `
179 export default {
180 async fetch(req, env, ctx) {
181 let greet1 = await env.greeter.greet("Alice");
182 let greet2 = await env.greeter2.greet("Bob");
183 return new Response([greet1, greet2].join("\\n"));
184 },
185 }
186 `,
187 },
188 env: {
189 greeter: ctx.exports.GreeterLoopback({
190 props: { greeting: 'Hello' },
191 }),
192 greeter2: ctx.exports.GreeterLoopback({
193 props: { greeting: 'Welcome' },
194 }),
195 },
196 };
197 });
198 
199 let resp = await worker.getEntrypoint().fetch('https://example.com');
200 assert.strictEqual(await resp.text(), 'Hello, Alice!\nWelcome, Bob!');
201 },
202};
203 
204export let testOutbound = {
205 async fetch(req, env, ctx) {
206 return new Response('hello from testOutbound');
207 },
208};
209 
210// Test overriding globalOutbound
211export let overrideGlobalOutbound = {
212 async test(ctrl, env, ctx) {
213 let worker = env.loader.get('overrideGlobalOutbound', () => {
214 return {
215 compatibilityDate: '2025-01-01',
216 mainModule: 'foo.js',
217 modules: {
218 'foo.js': `
219 export default {
220 fetch(req, env, ctx) {
221 return fetch(req);
222 },
223 }
224 `,
225 },
226 
227 // Set globalOutbound to redirect to our own `testOutbound` entrypoint.
228 globalOutbound: ctx.exports.testOutbound,
229 };
230 });
231 
232 let resp = await worker.getEntrypoint().fetch('https://example.com');
233 assert.strictEqual(await resp.text(), 'hello from testOutbound');
234 },
235};
236 
237// This test's own `globalOutbound` is configured to loop back to this entrypoint.
238export let defaultOutbound = {
239 async fetch(req, env, ctx) {
240 return new Response('hello from defaultOutbound');
241 },
242};
243 
244// Test that globalOutbound is inherited if not specified.
245export let inheritGlobalOutbound = {
246 async test(ctrl, env, ctx) {
247 let worker = env.loader.get('inheritGlobalOutbound', () => {
248 return {
249 compatibilityDate: '2025-01-01',
250 mainModule: 'foo.js',
251 modules: {
252 'foo.js': `
253 export default {
254 fetch(req, env, ctx) {
255 return fetch(req);
256 },
257 }
258 `,
259 },
260 
261 // Don't set `globalOutbound` at all.
262 };
263 });
264 
265 let resp = await worker.getEntrypoint().fetch('https://example.com');
266 assert.strictEqual(await resp.text(), 'hello from defaultOutbound');
267 },
268};
269 
270// Test setting `globalOutbound` to null.
271export let nullGlobalOutbound = {
272 async test(ctrl, env, ctx) {
273 let worker = env.loader.get('nullGlobalOutbound', () => {
274 return {
275 compatibilityDate: '2025-01-01',
276 mainModule: 'foo.js',
277 modules: {
278 'foo.js': `
279 export default {
280 fetch(req, env, ctx) {
281 return fetch(req);
282 },
283 }
284 `,
285 },
286 
287 globalOutbound: null,
288 };
289 });
290 
291 await assert.rejects(worker.getEntrypoint().fetch('https://example.com'), {
292 name: 'Error',
293 message:
294 'This worker is not permitted to access the internet via global functions like fetch(). ' +
295 "It must use capabilities (such as bindings in 'env') to talk to the outside world.",
296 });
297 },
298};
299 
300// We need to bounce the tail event through an actor to receive it back in the execution context
301// that set up the dynamic tail. `RendezvousActor` helps with that.
302export class RendezvousActor extends DurableObject {
303 promiseAndResolvers = Promise.withResolvers();
304 
305 resolve(value) {
306 this.promiseAndResolvers.resolve(value);
307 }
308 
309 wait() {
310 return this.promiseAndResolvers.promise;
311 }
312}
313 
314export class TestTail extends WorkerEntrypoint {
315 async tail(event) {
316 // HACK: Currently, tail events are not serializable over RPC. :(
317 event = JSON.parse(JSON.stringify(event));
318 
319 await this.ctx.exports.RendezvousActor.getByName('tails').resolve({
320 props: this.ctx.props,
321 event,
322 });
323 }
324}
325 
326// Test tail worker on dynamically-loaded worker.
327export let tails = {
328 async test(ctrl, env, ctx) {
329 let worker = env.loader.get('tails', () => {
330 return {
331 compatibilityDate: '2025-01-01',
332 mainModule: 'foo.js',
333 modules: {
334 'foo.js': `
335 export default {
336 fetch(req, env, ctx) {
337 console.log("hello, tail");
338 return new Response("OK");
339 },
340 }
341 `,
342 },
343 
344 tails: [ctx.exports.TestTail({ props: { foo: 123 } })],
345 };
346 });
347 
348 let resp = await worker.getEntrypoint().fetch('https://example.com');
349 assert.strictEqual(await resp.text(), 'OK');
350 
351 let { props, event } =
352 await ctx.exports.RendezvousActor.getByName('tails').wait();
353 assert.deepEqual(props, { foo: 123 });
354 assert.strictEqual(event[0].logs[0].message[0], 'hello, tail');
355 },
356};
357 
358export class GreeterFacet extends DurableObject {
359 async greet(name) {
360 return `${this.ctx.props.greeting}, ${name}?`;
361 }
362}
363 
364export class FacetTestActor extends DurableObject {
365 async doTest() {
366 let worker = this.env.loader.get('facets', () => {
367 return {
368 compatibilityDate: '2025-01-01',
369 mainModule: 'foo.js',
370 modules: {
371 'foo.js': `
372 import {DurableObject} from "cloudflare:workers";
373 export class MyActor extends DurableObject {
374 i = 0;
375
376 increment(j = 1) {
377 this.i += j;
378 return this.i;
379 }
380 myProps() {
381 let result = {...this.ctx.props};
382 delete result.propsGreeter;
383 return result;
384 }
385 async greets() {
386 let greeter1 = this.ctx.facets.get("greeter1", () => {
387 return { class: this.env.envGreeter };
388 });
389 let greeter2 = this.ctx.facets.get("greeter2", () => {
390 return { class: this.ctx.props.propsGreeter };
391 });
392 let greeter3 = this.ctx.facets.get("greeter3", () => {
393 return { class: this.env.envGreeter2 };
394 });
395 let greet1 = await greeter1.greet("Alice");
396 let greet2 = await greeter2.greet("Bob");
397 let greet3 = await greeter3.greet("Carol");
398 return [greet1, greet2, greet3].join("\\n");
399 }
400 }
401 `,
402 },
403 
404 // Set globalOutbound to redirect to our own `testOutbound` entrypoint.
405 globalOutbound: this.ctx.exports.testOutbound,
406 
407 env: {
408 envGreeter: this.ctx.exports.GreeterFacet({
409 props: { greeting: 'Hello' },
410 }),
411 envGreeter2: this.ctx.exports.GreeterFacet({
412 props: { greeting: 'Howdy' },
413 }),
414 
415 // Catch a bug where actor class channel numbers were assigned incorrectly.
416 someSubrequestChannel: this.ctx.exports.testOutbound({}),
417 },
418 };
419 });
420 
421 let cls = worker.getDurableObjectClass('MyActor', {
422 props: {
423 foo: 123,
424 bar: 456,
425 propsGreeter: this.ctx.exports.GreeterFacet({
426 props: { greeting: 'Welcome' },
427 }),
428 },
429 });
430 
431 let facet = this.ctx.facets.get('bar', () => {
432 return { class: cls };
433 });
434 
435 assert.strictEqual(await facet.increment(), 1);
436 assert.strictEqual(await facet.increment(4), 5);
437 
438 assert.deepEqual(await facet.myProps(), { foo: 123, bar: 456 });
439 
440 assert.strictEqual(
441 await facet.greets(),
442 'Hello, Alice?\nWelcome, Bob?\nHowdy, Carol?'
443 );
444 }
445}
446 
447export let facets = {
448 async test(ctrl, env, ctx) {
449 let id = ctx.exports.FacetTestActor.idFromName('foo');
450 let stub = ctx.exports.FacetTestActor.get(id);
451 await stub.doTest();
452 },
453};
454 
455// Test isolate uniqueness when loading the same name multiple times.
456export let isolateUniqueness = {
457 async test(ctrl, env, ctx) {
458 let loadCount = 0;
459 let loadCodeCallback = () => {
460 ++loadCount;
461 return {
462 compatibilityDate: '2025-01-01',
463 mainModule: 'foo.js',
464 modules: {
465 'foo.js': `
466 import {WorkerEntrypoint} from "cloudflare:workers";
467 let i = 0;
468 export default class extends WorkerEntrypoint {
469 increment() {
470 return i++;
471 }
472 }
473 `,
474 },
475 };
476 };
477 
478 let assertNotCalled = () => {
479 throw new Error(
480 'Code loader callback called when it should have reused an existing isolate.'
481 );
482 };
483 
484 let name = 'isolateUniqueness';
485 let name2 = 'isolateUniquenessOtherName';
486 
487 // Load the same name from all four of our loaders:
488 // - Two loaders with the same ID.
489 // - One with a unique ID.
490 // - One with no ID (anonymous).
491 let shared1 = env.sharedLoader1.get(name, loadCodeCallback).getEntrypoint();
492 let shared2 = env.sharedLoader2.get(name, assertNotCalled).getEntrypoint();
493 let unique = env.uniqueLoader.get(name, loadCodeCallback).getEntrypoint();
494 let anonymous = env.loader.get(name, loadCodeCallback).getEntrypoint();
495 
496 // Also try loading a different name from various loaders.
497 let anonymousOtherName = env.loader
498 .get(name2, loadCodeCallback)
499 .getEntrypoint();
500 let sharedOtherName = env.sharedLoader1
501 .get(name2, loadCodeCallback)
502 .getEntrypoint();
503 
504 // Same name from loaders with the same ID should go to the same isolate.
505 assert.strictEqual(await shared1.increment(), 0);
506 assert.strictEqual(await shared1.increment(), 1);
507 assert.strictEqual(await shared2.increment(), 2);
508 assert.strictEqual(await shared2.increment(), 3);
509 
510 // Different IDs -> different isolates.
511 assert.strictEqual(await anonymous.increment(), 0);
512 assert.strictEqual(await anonymous.increment(), 1);
513 assert.strictEqual(await unique.increment(), 0);
514 assert.strictEqual(await unique.increment(), 1);
515 
516 // Different names -> different isolates.
517 assert.strictEqual(await anonymousOtherName.increment(), 0);
518 assert.strictEqual(await anonymousOtherName.increment(), 1);
519 assert.strictEqual(await sharedOtherName.increment(), 0);
520 assert.strictEqual(await sharedOtherName.increment(), 1);
521 
522 // Load the same name from the same loader -> same isolate.
523 let anonymousAgain = env.loader.get(name, assertNotCalled).getEntrypoint();
524 assert.strictEqual(await anonymousAgain.increment(), 2);
525 assert.strictEqual(await anonymousAgain.increment(), 3);
526 
527 // Test that null/undefined name parameter creates unique isolates each time
528 // Each call should generate a unique name internally.
529 let noName1 = env.loader.get(null, loadCodeCallback).getEntrypoint();
530 let noName2 = env.loader.get(undefined, loadCodeCallback).getEntrypoint();
531 let noName3 = env.loader.get(undefined, loadCodeCallback).getEntrypoint();
532 
533 // Each should be a different isolate
534 assert.strictEqual(await noName1.increment(), 0);
535 assert.strictEqual(await noName1.increment(), 1);
536 assert.strictEqual(await noName2.increment(), 0);
537 assert.strictEqual(await noName2.increment(), 1);
538 assert.strictEqual(await noName3.increment(), 0);
539 assert.strictEqual(await noName3.increment(), 1);
540 
541 assert.strictEqual(loadCount, 8);
542 },
543};
544 
545// Test non-string module types
546export let moduleTypes = {
547 async test(ctrl, env, ctx) {
548 let worker = env.loader.get('moduleTypes', () => {
549 return {
550 compatibilityDate: '2025-01-01',
551 mainModule: 'main.js',
552 modules: {
553 'main.js': {
554 js: `
555 import {WorkerEntrypoint} from "cloudflare:workers";
556 import textModule from './text.txt';
557 import jsonModule from './data.json';
558 import cjsModule from './cjs.cjs';
559 import dataModule from './binary.dat';
560
561 export default class extends WorkerEntrypoint {
562 getTextModule() { return textModule; }
563 getJsonModule() { return jsonModule; }
564 getCjsModule() { return cjsModule.greet('Alice'); }
565 getDataModule() { return dataModule; }
566 }
567 `,
568 },
569 'text.txt': {
570 text: 'Hello from text module!',
571 },
572 'data.json': {
573 json: { message: 'Hello from JSON module!', value: 42 },
574 },
575 'cjs.cjs': {
576 cjs: `
577 module.exports = {
578 greet: function(name) {
579 return 'Hello from CommonJS, ' + name;
580 }
581 };
582 `,
583 },
584 'binary.dat': {
585 data: new TextEncoder().encode('Hello from binary data!'),
586 },
587 },
588 };
589 });
590 
591 let entrypoint = worker.getEntrypoint();
592 
593 assert.strictEqual(
594 await entrypoint.getTextModule(),
595 'Hello from text module!'
596 );
597 
598 let jsonData = await entrypoint.getJsonModule();
599 assert.strictEqual(jsonData.message, 'Hello from JSON module!');
600 assert.strictEqual(jsonData.value, 42);
601 
602 assert.strictEqual(
603 await entrypoint.getCjsModule(),
604 'Hello from CommonJS, Alice'
605 );
606 
607 let dataModule = await entrypoint.getDataModule();
608 let decoder = new TextDecoder();
609 assert.strictEqual(decoder.decode(dataModule), 'Hello from binary data!');
610 },
611};
612 
613// Test WASM module support
614export let wasmModules = {
615 async test(ctrl, env, ctx) {
616 // Create a simple WASM module that exports an add function
617 // (module (func (export "add") (param i32 i32) (result i32) (local.get 0) (local.get 1) (i32.add)))
618 const wasmBytes = new Uint8Array([
619 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
620 0x02, 0x7f, 0x7f, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
621 0x03, 0x61, 0x64, 0x64, 0x00, 0x00, 0x0a, 0x09, 0x01, 0x07, 0x00, 0x20,
622 0x00, 0x20, 0x01, 0x6a, 0x0b,
623 ]);
624 
625 let worker = env.loader.get('wasmModules', () => {
626 return {
627 compatibilityDate: '2025-01-01',
628 mainModule: 'main.js',
629 modules: {
630 'main.js': {
631 js: `
632 import {WorkerEntrypoint} from "cloudflare:workers";
633 import wasmModule from './math.wasm';
634
635 export default class extends WorkerEntrypoint {
636 async getWasmAdd(a, b) {
637 const instance = await WebAssembly.instantiate(wasmModule);
638 return instance.exports.add(a, b);
639 }
640 }
641 `,
642 },
643 'math.wasm': {
644 wasm: wasmBytes,
645 },
646 },
647 };
648 });
649 
650 let entrypoint = worker.getEntrypoint();
651 
652 let result = await entrypoint.getWasmAdd(5, 7);
653 assert.strictEqual(result, 12);
654 
655 result = await entrypoint.getWasmAdd(100, 42);
656 assert.strictEqual(result, 142);
657 },
658};
659 
660// Test setting compat date / flags works
661export let compatDateFlags = {
662 async test(ctrl, env, ctx) {
663 let getCode = (compatibilityDate, compatibilityFlags) => {
664 return {
665 compatibilityDate,
666 compatibilityFlags,
667 mainModule: 'main.js',
668 modules: {
669 'main.js': `
670 import {WorkerEntrypoint} from "cloudflare:workers";
671 export default class extends WorkerEntrypoint {
672 hasNodeCompat() {
673 return typeof process !== 'undefined';
674 }
675 canUseRequestCache() {
676 try {
677 new Request("https://foo", {cache: "no-store"});
678 return true;
679 } catch {
680 return false;
681 }
682 }
683 }
684 `,
685 },
686 };
687 };
688 
689 // Test old date (2023), with node compat turned on.
690 {
691 let worker = env.loader.get('compatDateFlags1', () =>
692 getCode('2023-01-01', ['nodejs_compat', 'nodejs_compat_v2'])
693 );
694 let entrypoint = await worker.getEntrypoint();
695 assert.strictEqual(await entrypoint.hasNodeCompat(), true);
696 assert.strictEqual(await entrypoint.canUseRequestCache(), false);
697 }
698 
699 // Test newer date (2025), with node compat not enabled.
700 {
701 let worker = env.loader.get('compatDateFlags2', () =>
702 getCode('2025-01-01', [])
703 );
704 let entrypoint = await worker.getEntrypoint();
705 assert.strictEqual(await entrypoint.hasNodeCompat(), false);
706 assert.strictEqual(await entrypoint.canUseRequestCache(), true);
707 }
708 },
709};
710 
711// Test code loader callback that does something asynchronous
712export let asyncCodeLoader = {
713 async test(ctrl, env, ctx) {
714 let worker = env.loader.get('asyncCodeLoader', async () => {
715 // Simulate async work
716 await new Promise((resolve) => setTimeout(resolve, 1));
717 
718 return {
719 compatibilityDate: '2025-01-01',
720 mainModule: 'main.js',
721 modules: {
722 'main.js': `
723 import {WorkerEntrypoint} from "cloudflare:workers";
724 export default class extends WorkerEntrypoint {
725 getMessage() {
726 return 'Hello from async loaded worker!';
727 }
728 }
729 `,
730 },
731 };
732 });
733 
734 let result = await worker.getEntrypoint().getMessage();
735 assert.strictEqual(result, 'Hello from async loaded worker!');
736 },
737};
738 
739// Test what happens if the code getter callback throws an exception
740export let codeLoaderException = {
741 async test(ctrl, env, ctx) {
742 let worker = env.loader.get('codeLoaderException', () => {
743 throw new Error('Code loader failed!');
744 });
745 
746 let ep = worker.getEntrypoint();
747 try {
748 await ep.someMethod();
749 assert.fail('Expected exception to be thrown');
750 } catch (error) {
751 assert.strictEqual(error.message, 'Code loader failed!');
752 }
753 },
754};
755 
756// Test that ctx.exports works in dynamically-loaded workers.
757export let ctxExports = {
758 async test(ctrl, env, ctx) {
759 let worker = env.loader.get('ctxExports', () => {
760 return {
761 compatibilityDate: '2025-01-01',
762 compatibilityFlags: ['enable_ctx_exports'],
763 mainModule: 'foo.js',
764 modules: {
765 'foo.js': `
766 import {WorkerEntrypoint} from "cloudflare:workers";
767 export default class extends WorkerEntrypoint {
768 async greet(name) {
769 let greeting = await this.ctx.exports.AltEntrypoint.greet(name);
770 return greeting + "!";
771 }
772 }
773 export class AltEntrypoint extends WorkerEntrypoint {
774 greet(name) { return "Hello, " + name; }
775 }
776 `,
777 },
778 };
779 });
780 
781 let result = await worker.getEntrypoint().greet('Alice');
782 assert.strictEqual(result, 'Hello, Alice!');
783 },
784};
785 
786const mixedModules = {
787 compatibilityDate: '2025-01-01',
788 modules: {
789 'foo.py': `
790from workers import WorkerEntrypoint
791class Default(WorkerEntrypoint):
792async def greet(self, name):
793return "Hello, " + name
794 `,
795 'foo.js': `
796 export default {
797 greet(name) { return "Hello, " + name; }
798 }
799 `,
800 },
801};
802 
803export let noMixedJsPythonModules = {
804 async test(ctrl, env, ctx) {
805 let worker = env.loader.get('noMixedJsPythonModules', () => {
806 return {
807 ...mixedModules,
808 mainModule: 'foo.py',
809 };
810 });
811 
812 await assert.rejects(worker.getEntrypoint().greet('Alice'), {
813 name: 'TypeError',
814 message:
815 'Module "foo.js" is a JS module, but the main module is a Python module.',
816 });
817 },
818};
819 
820export let noMixedJsPythonModules2 = {
821 async test(ctrl, env, ctx) {
822 let worker = env.loader.get('noMixedJsPythonModules2', () => {
823 return {
824 ...mixedModules,
825 mainModule: 'foo.js',
826 };
827 });
828 
829 await assert.rejects(worker.getEntrypoint().greet('Alice'), {
830 name: 'TypeError',
831 message:
832 'Module "foo.py" is a Python module, but the main module isn\'t a Python module.',
833 });
834 },
835};
836 
837export let suggestWorkerBundlerForTypeScriptModules = {
838 async test(ctrl, env, ctx) {
839 for (let mainModule of ['main.ts', 'main.tsx', 'main.jsx']) {
840 let worker = env.loader.get(mainModule, () => {
841 return {
842 compatibilityDate: '2026-01-01',
843 mainModule,
844 modules: {
845 [mainModule]: `
846 export default {
847 fetch() {
848 return new Response('ok');
849 }
850 }
851 `,
852 },
853 };
854 });
855 
856 await assert.rejects(
857 worker.getEntrypoint().fetch('https://example.com'),
858 {
859 name: 'TypeError',
860 message:
861 `Module name must end with '.js' or '.py' (or the content must be an object indicating the type explicitly). Got: ${mainModule}. ` +
862 "If you're trying to load TypeScript, bundle it first with '@cloudflare/worker-bundler' and pass the generated JavaScript modules.",
863 }
864 );
865 }
866 },
867};
868 
869// Test that startup exceptions show proper error messages, not internal errors.
870export let startupException = {
871 async test(ctrl, env, ctx) {
872 let worker = env.loader.get('startupException', () => {
873 return {
874 compatibilityDate: '2025-01-01',
875 mainModule: 'main.js',
876 modules: {
877 'main.js': `
878 import {WorkerEntrypoint} from "cloudflare:workers";
879
880 // This will throw during startup (global scope)
881 throw new Error('intentional startup error');
882
883 export default class extends WorkerEntrypoint {
884 greet(name) {
885 return "Hello, " + name;
886 }
887 }
888 `,
889 },
890 };
891 });
892 
893 let ep = worker.getEntrypoint();
894 try {
895 await ep.greet('Alice');
896 assert.fail('Expected exception to be thrown');
897 } catch (error) {
898 assert(
899 error.message.includes('intentional startup error'),
900 `Expected error message to contain 'intentional startup error', but got: ${error.message}`
901 );
902 
903 // The error should NOT be a generic internal error
904 assert(
905 !error.message.includes('internal error; reference'),
906 `Error should not be a generic internal error, got: ${error.message}`
907 );
908 }
909 },
910};
911 
912export let justLoad = {
913 async test(ctrl, env, ctx) {
914 let worker = env.loader.load({
915 compatibilityDate: '2025-01-01',
916 mainModule: 'foo.js',
917 modules: {
918 'foo.js': `
919 import {WorkerEntrypoint} from "cloudflare:workers";
920 export default {
921 greet(name) { return "Hello, " + name; }
922 }
923 export let alternate = {
924 greet(name, env, ctx) { return \`\${ctx.props.greeting}, \${name}\`; }
925 }
926 export class FancyPropsEntrypoint extends WorkerEntrypoint {
927 async run() {
928 let greet1 = await this.ctx.props.greeter.greet("Dave");
929 let greet2 = await this.ctx.props.greeter2.greet("Eve");
930 return [greet1, greet2].join("\\n");
931 }
932 }
933 `,
934 },
935 });
936 
937 {
938 let result = await worker.getEntrypoint().greet('Alice');
939 assert.strictEqual(result, 'Hello, Alice');
940 }
941 
942 let greeter = worker.getEntrypoint('alternate', {
943 props: { greeting: 'Welcome' },
944 });
945 let greeter2 = worker.getEntrypoint('alternate', {
946 props: { greeting: 'Howdy' },
947 });
948 
949 {
950 let result = await greeter.greet('Bob');
951 assert.strictEqual(result, 'Welcome, Bob');
952 }
953 
954 {
955 let result = await greeter2.greet('Carol');
956 assert.strictEqual(result, 'Howdy, Carol');
957 }
958 },
959};
960 
961// Test that abortIsolate() works correctly for dynamic workers.
962// When abortIsolate() is called on a dynamic worker, it should:
963// 1. Terminate the isolate (not the entire process).
964// 2. Cause subsequent lookups by name to reload the isolate from scratch.
965export let abortIsolateDynamic = {
966 async test(ctrl, env, ctx) {
967 // Create a dynamic worker that calls abortIsolate. ping() increments and returns a
968 // module-scoped counter so we can distinguish fresh isolates from reused ones.
969 let loadCount = 0;
970 const getWorkerEntrypoint = () =>
971 env.loader
972 .get('abort-test', () => {
973 loadCount++;
974 return {
975 compatibilityDate: '2025-01-01',
976 compatibilityFlags: ['experimental'],
977 allowExperimental: true,
978 mainModule: 'worker.js',
979 modules: {
980 'worker.js': `
981 import {WorkerEntrypoint, abortIsolate} from "cloudflare:workers";
982 let counter = 0;
983 export class TestEntrypoint extends WorkerEntrypoint {
984 async crash() {
985 abortIsolate('Dynamic worker abort test');
986 }
987 async ping() {
988 return ++counter;
989 }
990 }
991 `,
992 },
993 };
994 })
995 .getEntrypoint('TestEntrypoint');
996 
997 let entrypoint = getWorkerEntrypoint();
998 // First, verify the worker works normally. Each ping increments the counter.
999 assert.strictEqual(await entrypoint.ping(), 1);
1000 assert.strictEqual(await entrypoint.ping(), 2);
1001 assert.strictEqual(loadCount, 1);
1002 
1003 entrypoint = getWorkerEntrypoint();
1004 assert.strictEqual(await entrypoint.ping(), 3);
1005 assert.strictEqual(loadCount, 1);
1006 
1007 // Now trigger abortIsolate - this should terminate the isolate but not crash the process.
1008 await assert.rejects(
1009 () => entrypoint.crash(),
1010 (err) => {
1011 return err.message.startsWith('internal error; reference =');
1012 }
1013 );
1014 
1015 entrypoint = getWorkerEntrypoint();
1016 assert.strictEqual(await entrypoint.ping(), 1);
1017 assert.strictEqual(await entrypoint.ping(), 2);
1018 assert.strictEqual(loadCount, 2);
1019 },
1020};
1021 
1022// Test that abortIsolate() works correctly for anonymous dynamic workers.
1023// Anonymous workers don't have a name and therefore aren't stored in the loader's map.
1024export let abortIsolateDynamicAnonymous = {
1025 async test(ctrl, env, ctx) {
1026 // Create an anonymous dynamic worker (no name provided to get())
1027 let worker = env.loader.get(null, () => {
1028 return {
1029 compatibilityDate: '2025-01-01',
1030 compatibilityFlags: ['experimental'],
1031 allowExperimental: true,
1032 mainModule: 'worker.js',
1033 modules: {
1034 'worker.js': `
1035 import {WorkerEntrypoint, abortIsolate} from "cloudflare:workers";
1036 let counter = 0;
1037 export class TestEntrypoint extends WorkerEntrypoint {
1038 async crash() {
1039 abortIsolate('Anonymous dynamic worker abort test');
1040 }
1041 async ping() {
1042 return ++counter;
1043 }
1044 }
1045 `,
1046 },
1047 };
1048 });
1049 
1050 // First, verify the worker works normally.
1051 let entrypoint = worker.getEntrypoint('TestEntrypoint');
1052 assert.strictEqual(await entrypoint.ping(), 1);
1053 assert.strictEqual(await entrypoint.ping(), 2);
1054 
1055 // Now trigger abortIsolate - this should terminate the isolate
1056 // but NOT crash the entire process.
1057 await assert.rejects(
1058 () => entrypoint.crash(),
1059 (err) => {
1060 return err.message.startsWith('internal error; reference =');
1061 }
1062 );
1063 },
1064};