Skip to content

Commit

Permalink
Merge pull request #1423 from cloudflare/jsnell/workerd-module-fallba…
Browse files Browse the repository at this point in the history
…ck-service
  • Loading branch information
jasnell authored Dec 8, 2023
2 parents 216838e + d14f17f commit 292c015
Show file tree
Hide file tree
Showing 15 changed files with 569 additions and 136 deletions.
28 changes: 28 additions & 0 deletions samples/module_fallback/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Module Fallback Example

To run the example on http://localhost:8080

```sh
$ ./workerd serve config.capnp
```

To run using bazel

```sh
$ bazel run //src/workerd/server:workerd -- serve ~/cloudflare/workerd/samples/module_fallback/config.capnp
```

To create a standalone binary that can be run:

```sh
$ ./workerd compile config.capnp > helloworld

$ ./helloworld
```

To test:

```sh
% curl http://localhost:8080
Hello World
```
5 changes: 5 additions & 0 deletions samples/module_fallback/cjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const assert = require('assert');
const vm = require('vm');
assert(vm !== undefined);

module.exports = {};
19 changes: 19 additions & 0 deletions samples/module_fallback/config.capnp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Workerd = import "/workerd/workerd.capnp";

const helloWorldExample :Workerd.Config = (
services = [
(name = "main", worker = .helloWorld),
],

sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ]
);

const helloWorld :Workerd.Worker = (
modules = [
(name = "worker", esModule = embed "worker.js"),
(name = "cjs", nodeJsCompatModule = embed "cjs.js"),
],
compatibilityDate = "2023-02-28",
compatibilityFlags = ["nodejs_compat"],
moduleFallback = "localhost:8888",
);
45 changes: 45 additions & 0 deletions samples/module_fallback/fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env node

const { createServer } = require('http');

const server = createServer((req, res) => {
// The response from the fallback service must be a vaid JSON
// serialization of a Worker::Module config.

// The x-resolve-method tells us if the module was imported or required.
console.log(req.headers['x-resolve-method']);

// The req.url query params tell us what we are importing
const url = new URL(req.url, "http://example.org");
const specifier = url.searchParams.get('specifier');
const referrer = url.searchParams.get('referrer');
console.log(specifier, referrer);

// The fallback service can tell the client to map the request
// specifier to another specifier using a 301 redirect, using
// the location header to specify the alternative specifier.
if (specifier == "/foo") {
console.log('Redirecting /foo to /baz');
res.writeHead(301, { location: '/baz' });
res.end();
return;
}

if (specifier == "/bar") {
res.writeHead(404);
res.end();
return;
}

console.log(`Returning module spec for ${specifier}`);
// Returning the name is optional. If it is included, then it MUST match the
// request specifier!
res.end(`{
"name": "${specifier}",
"esModule":"export default 1;"
}`);
});

server.listen(8888, () => {
console.log('ready...');
});
25 changes: 25 additions & 0 deletions samples/module_fallback/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) 2017-2023 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

import * as foo from 'foo';
import * as baz from 'baz';
import * as vm from 'node:vm';
import { strictEqual } from 'node:assert';
import * as cjs from 'cjs';

try {
await import('bar');
throw new Error('bar should not have been imported');
} catch {
console.log('tried to import bar which does not exist');
};

console.log(foo, baz, vm);

export default {
async fetch(req, env) {
strictEqual(1 + 1, 2);
return new Response("Hello World\n");
}
};
2 changes: 1 addition & 1 deletion src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,7 @@ Worker::Worker(kj::Own<const Script> scriptParam,
}
KJ_CASE_ONEOF(mainModule, kj::Path) {
auto& registry = (*jsContext)->getModuleRegistry();
KJ_IF_SOME(entry, registry.resolve(lock, mainModule)) {
KJ_IF_SOME(entry, registry.resolve(lock, mainModule, kj::none)) {
JSG_REQUIRE(entry.maybeSynthetic == kj::none, TypeError,
"Main module must be an ES module.");
auto module = entry.module.getHandle(lock);
Expand Down
13 changes: 13 additions & 0 deletions src/workerd/io/worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,19 @@ class Worker::Api {
virtual kj::Maybe<const api::CryptoAlgorithm&> getCryptoAlgorithm(kj::StringPtr name) const {
return kj::none;
}

// Set the module fallback service callback, if any.
using ModuleFallbackCallback =
kj::Maybe<kj::OneOf<kj::String, jsg::ModuleRegistry::ModuleInfo>>(
jsg::Lock& js,
kj::StringPtr,
kj::Maybe<kj::String>,
jsg::CompilationObserver&,
jsg::ModuleRegistry::ResolveMethod);
virtual void setModuleFallbackCallback(
kj::Function<ModuleFallbackCallback>&& callback) const {
// By default does nothing.
}
};

enum class UncaughtExceptionSource {
Expand Down
29 changes: 25 additions & 4 deletions src/workerd/jsg/modules.c++
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include "jsg.h"
#include "promise.h"
#include "setup.h"
#include <kj/mutex.h>
#include <set>

Expand Down Expand Up @@ -68,7 +69,7 @@ v8::MaybeLocal<v8::Module> resolveCallback(v8::Local<v8::Context> context,
ref.specifier.parent().eval(spec) :
kj::Path::parse(spec);

KJ_IF_SOME(resolved, registry->resolve(js, targetPath,
KJ_IF_SOME(resolved, registry->resolve(js, targetPath, ref.specifier,
internalOnly ?
ModuleRegistry::ResolveOption::INTERNAL_ONLY :
ModuleRegistry::ResolveOption::DEFAULT)) {
Expand All @@ -81,7 +82,7 @@ v8::MaybeLocal<v8::Module> resolveCallback(v8::Local<v8::Context> context,
// is using the prefix itself. (which isn't likely but is possible).
// We only need to do this if internalOnly is false.
if (!internalOnly && (spec.startsWith("node:") || spec.startsWith("cloudflare:"))) {
KJ_IF_SOME(resolve, registry->resolve(js, kj::Path::parse(spec),
KJ_IF_SOME(resolve, registry->resolve(js, kj::Path::parse(spec), ref.specifier,
ModuleRegistry::ResolveOption::DEFAULT)) {
result = resolve.module.getHandle(js);
return;
Expand Down Expand Up @@ -249,7 +250,10 @@ v8::Local<v8::Value> CommonJsModuleContext::require(jsg::Lock& js, kj::String sp
// require() is only exposed to worker bundle modules so the resolve here is only
// permitted to require worker bundle or built-in modules. Internal modules are
// excluded.
auto& info = JSG_REQUIRE_NONNULL(modulesForResolveCallback->resolve(js, targetPath),
auto& info = JSG_REQUIRE_NONNULL(
modulesForResolveCallback->resolve(js, targetPath, path,
ModuleRegistry::ResolveOption::DEFAULT,
ModuleRegistry::ResolveMethod::REQUIRE),
Error, "No such module \"", targetPath.toString(), "\".");
// Adding imported from suffix here not necessary like it is for resolveCallback, since we have a
// js stack that will include the parent module's name and location of the failed require().
Expand Down Expand Up @@ -590,7 +594,8 @@ v8::Local<v8::Value> NodeJsModuleContext::require(jsg::Lock& js, kj::String spec
// permitted to require worker bundle or built-in modules. Internal modules are
// excluded.
auto& info = JSG_REQUIRE_NONNULL(
modulesForResolveCallback->resolve(js, targetPath, resolveOption),
modulesForResolveCallback->resolve(js, targetPath, path, resolveOption,
ModuleRegistry::ResolveMethod::REQUIRE),
Error, "No such module \"", targetPath.toString(), "\".");
// Adding imported from suffix here not necessary like it is for resolveCallback, since we have a
// js stack that will include the parent module's name and location of the failed require().
Expand Down Expand Up @@ -668,4 +673,20 @@ void NodeJsModuleObject::setExports(jsg::Value value) {

kj::StringPtr NodeJsModuleObject::getPath() { return path; }

kj::Maybe<kj::OneOf<kj::String, ModuleRegistry::ModuleInfo>> tryResolveFromFallbackService(
Lock& js, const kj::Path& specifier,
kj::Maybe<const kj::Path&>& referrer,
CompilationObserver& observer,
ModuleRegistry::ResolveMethod method) {
auto& isolateBase = IsolateBase::from(js.v8Isolate);
KJ_IF_SOME(fallback, isolateBase.tryGetModuleFallback()) {
kj::Maybe<kj::String> maybeRef;
KJ_IF_SOME(ref, referrer) {
maybeRef = ref.toString(true);
}
return fallback(js, specifier.toString(true), kj::mv(maybeRef), observer, method);
}
return kj::none;
}

} // namespace workerd::jsg
Loading

0 comments on commit 292c015

Please sign in to comment.