Skip to content

Commit

Permalink
Implements globalThis.Cloudflare.compatibilityFlags API
Browse files Browse the repository at this point in the history
A simple built-in module and API for determining if a given compat
flag is set.

```
const { compatibilityFlags } = globalThis.Cloudflare;

console.log(compatibilityFlags['url_standard']);  // 'on' or 'off'
console.log(compatibilityFlags['url_original']);  // 'on' or 'off'
```
  • Loading branch information
jasnell committed Aug 20, 2024
1 parent 1c54402 commit a68b472
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 1 deletion.
36 changes: 36 additions & 0 deletions src/workerd/api/global-scope.c++
Original file line number Diff line number Diff line change
Expand Up @@ -918,4 +918,40 @@ void ServiceWorkerGlobalScope::clearImmediate(kj::Maybe<jsg::Ref<Immediate>> may
}
}

jsg::JsObject Cloudflare::getCompatibilityFlags(jsg::Lock& js) {
auto flags = FeatureFlags::get(js);
auto obj = js.objNoProto();
auto dynamic = capnp::toDynamic(flags);
auto schema = dynamic.getSchema();

bool skipExperimental = !flags.getWorkerdExperimental();

for (auto field: schema.getFields()) {
// If this is an experimental flag, we expose it only if the experimental mode
// is enabled.
auto annotations = field.getProto().getAnnotations();
bool skip = false;
if (skipExperimental) {
for (auto annotation: annotations) {
if (annotation.getId() == EXPERIMENTAl_ANNOTATION_ID) {
skip = true;
break;
}
}
}
if (skip) continue;

// Note that disable flags are not exposed.
for (auto annotation: annotations) {
if (annotation.getId() == COMPAT_ENABLE_FLAG_ANNOTATION_ID) {
obj.setReadOnly(
js, annotation.getValue().getText(), js.boolean(dynamic.get(field).as<bool>()));
}
}
}

obj.seal(js);
return obj;
}

} // namespace workerd::api
23 changes: 22 additions & 1 deletion src/workerd/api/global-scope.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ class Performance: public jsg::Object {
}
};

// Exposed as a global to provide access to certain Cloudflare-specific
// configuration details. This is not a standard API and great care should
// be taken when deciding to expose new properties or methods here.
class Cloudflare: public jsg::Object {
public:
// Return an object containing the state of all compatibility flags known to the runtime.
jsg::JsObject getCompatibilityFlags(jsg::Lock& js);

JSG_RESOURCE_TYPE(Cloudflare) {
JSG_LAZY_READONLY_INSTANCE_PROPERTY(compatibilityFlags, getCompatibilityFlags);

JSG_TS_OVERRIDE({ readonly compatibilityFlags: Record<string, boolean>;
});
}
};

class PromiseRejectionEvent: public Event {
public:
PromiseRejectionEvent(
Expand Down Expand Up @@ -507,6 +523,10 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope {
return jsg::alloc<Performance>();
}

jsg::Ref<Cloudflare> getCloudflare() {
return jsg::alloc<Cloudflare>();
}

// The origin is unknown, return "null" as described in
// https://html.spec.whatwg.org/multipage/browsers.html#concept-origin-opaque.
kj::StringPtr getOrigin() {
Expand Down Expand Up @@ -570,6 +590,7 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope {
JSG_LAZY_INSTANCE_PROPERTY(caches, getCaches);
JSG_LAZY_INSTANCE_PROPERTY(scheduler, getScheduler);
JSG_LAZY_INSTANCE_PROPERTY(performance, getPerformance);
JSG_LAZY_INSTANCE_PROPERTY(Cloudflare, getCloudflare);
JSG_READONLY_INSTANCE_PROPERTY(origin, getOrigin);

JSG_NESTED_TYPE(Event);
Expand Down Expand Up @@ -824,6 +845,6 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope {
api::WorkerGlobalScope, api::ServiceWorkerGlobalScope, api::TestController, \
api::ExecutionContext, api::ExportedHandler, \
api::ServiceWorkerGlobalScope::StructuredCloneOptions, api::PromiseRejectionEvent, \
api::Navigator, api::Performance, api::AlarmInvocationInfo, api::Immediate
api::Navigator, api::Performance, api::AlarmInvocationInfo, api::Immediate, api::Cloudflare
// The list of global-scope.h types that are added to worker.c++'s JSG_DECLARE_ISOLATE_TYPE
} // namespace workerd::api
1 change: 1 addition & 0 deletions src/workerd/api/node/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <workerd/jsg/modules-new.h>
#include <capnp/dynamic.h>
#include <node/node.capnp.h>
#include <workerd/io/compatibility-date.h>

namespace workerd::api::node {

Expand Down
60 changes: 60 additions & 0 deletions src/workerd/api/tests/compat-flags-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
ok,
strictEqual,
throws,
} from 'node:assert';

const { compatibilityFlags } = globalThis.Cloudflare;
ok(Object.isSealed(compatibilityFlags));
ok(Object.isFrozen(compatibilityFlags));
ok(!Object.isExtensible(compatibilityFlags));

// It shuld be possible to shadow the Cloudflare global
const Cloudflare = 1;
strictEqual(Cloudflare, 1);

export const compatFlagsTest = {
test() {
// The compatibility flags object should be sealed, frozen, and not extensible.
throws(() => compatibilityFlags.no_nodejs_compat_v2 = "...");
throws(() => compatibilityFlags.not_a_real_compat_flag = "...");
throws(() => { delete compatibilityFlags['nodejs_compat_v2']; });

// The compatibility flags object should have no prototype.
strictEqual(Object.getPrototypeOf(compatibilityFlags), null);

// The compatibility flags object should have the expected properties...
// That is... the only keys that should appear on the compatibilityFlags
// object are the enable flags.
//
// If a key does not appear, it can mean one of three things:
// 1. It is a disable flag.
// 2. It is an experimental flag and the experimental option is not set.
// 3. The flag does not exist.
//
// At this level we make no attempt to differentiate between these cases
ok(compatibilityFlags['nodejs_compat_v2']);
ok(compatibilityFlags['url_standard']);

ok(!compatibilityFlags['no_nodejs_compat_v2']);
ok(!compatibilityFlags['url_original']);
strictEqual(compatibilityFlags['no_nodejs_compat_v2'], undefined);
strictEqual(compatibilityFlags['url_original'], undefined);

// Since we are not specifying the experimental flag, experimental flags should
// not be included in the output.
strictEqual(compatibilityFlags['durable_object_rename'], undefined);
strictEqual('durable_object_rename' in compatibilityFlags, false);

// If a flag does not exist, the value will be undefined.
strictEqual(compatibilityFlags['not-a-real-compat-flag'], undefined);
strictEqual('not-a-real-compat-flag' in compatibilityFlags, false);

// The compatibility flags object should have the expected keys.
const keys = Object.keys(compatibilityFlags);
ok(keys.includes('nodejs_compat_v2'));
ok(keys.includes('url_standard'));
ok(!keys.includes('url_original'));
ok(!keys.includes('not-a-real-compat-flag'));
}
}
15 changes: 15 additions & 0 deletions src/workerd/api/tests/compat-flags-test.wd-test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Workerd = import "/workerd/workerd.capnp";

const unitTests :Workerd.Config = (
services = [
( name = "compat-flags-test",
worker = (
modules = [
(name = "worker", esModule = embed "compat-flags-test.js")
],
compatibilityDate = "2023-01-15",
compatibilityFlags = ["nodejs_compat_v2"],
)
),
],
);
1 change: 1 addition & 0 deletions src/workerd/io/features.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

#include <workerd/io/compatibility-date.capnp.h>
#include <workerd/io/compatibility-date.h>
#include <workerd/jsg/jsg.h>

namespace workerd {
Expand Down
1 change: 1 addition & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -2541,6 +2541,7 @@ class Lock {
JsSymbol symbolShared(kj::StringPtr) KJ_WARN_UNUSED_RESULT;
JsSymbol symbolInternal(kj::StringPtr) KJ_WARN_UNUSED_RESULT;
JsObject obj() KJ_WARN_UNUSED_RESULT;
JsObject objNoProto() KJ_WARN_UNUSED_RESULT;
JsMap map() KJ_WARN_UNUSED_RESULT;
JsValue external(void*) KJ_WARN_UNUSED_RESULT;
JsValue error(kj::StringPtr message) KJ_WARN_UNUSED_RESULT;
Expand Down
14 changes: 14 additions & 0 deletions src/workerd/jsg/jsvalue.c++
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ void JsObject::set(Lock& js, kj::StringPtr name, const JsValue& value) {
set(js, js.strIntern(name), value);
}

void JsObject::setReadOnly(Lock& js, kj::StringPtr name, const JsValue& value) {
v8::Local<v8::String> nameStr = js.strIntern(name);
check(inner->DefineOwnProperty(js.v8Context(), nameStr, value,
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete)));
}

JsValue JsObject::get(Lock& js, const JsValue& name) {
return JsValue(check(inner->Get(js.v8Context(), name.inner)));
}
Expand Down Expand Up @@ -135,6 +141,10 @@ void JsObject::recursivelyFreeze(Lock& js) {
jsg::recursivelyFreeze(js.v8Context(), inner);
}

void JsObject::seal(Lock& js) {
check(inner->SetIntegrityLevel(js.v8Context(), v8::IntegrityLevel::kSealed));
}

JsObject JsObject::jsonClone(Lock& js) {
auto tmp = JsValue(inner).toJson(js);
auto obj = KJ_ASSERT_NONNULL(JsValue::fromJson(js, tmp).tryCast<jsg::JsObject>());
Expand Down Expand Up @@ -421,6 +431,10 @@ JsObject Lock::obj() {
return JsObject(v8::Object::New(v8Isolate));
}

JsObject Lock::objNoProto() {
return JsObject(v8::Object::New(v8Isolate, v8::Null(v8Isolate), nullptr, nullptr, 0));
}

JsMap Lock::map() {
return JsMap(v8::Map::New(v8Isolate));
}
Expand Down
2 changes: 2 additions & 0 deletions src/workerd/jsg/jsvalue.h
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ class JsObject final: public JsBase<v8::Object, JsObject> {

void set(Lock& js, const JsValue& name, const JsValue& value);
void set(Lock& js, kj::StringPtr name, const JsValue& value);
void setReadOnly(Lock& js, kj::StringPtr name, const JsValue& value);
JsValue get(Lock& js, const JsValue& name) KJ_WARN_UNUSED_RESULT;
JsValue get(Lock& js, kj::StringPtr name) KJ_WARN_UNUSED_RESULT;

Expand Down Expand Up @@ -377,6 +378,7 @@ class JsObject final: public JsBase<v8::Object, JsObject> {
using JsBase<v8::Object, JsObject>::JsBase;

void recursivelyFreeze(Lock&);
void seal(Lock&);
JsObject jsonClone(Lock&);
};

Expand Down

0 comments on commit a68b472

Please sign in to comment.