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

Add a way to expose interfaces automatically #101

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
30 changes: 12 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,11 @@ jsdom does this for `Window`, which is written in custom, non-webidl2js-generate

This export is the wrapper class interface, suitable for example for putting on a global scope or exporting to module consumers who don't know anything about webidl2js.

#### `expose`
#### `expose(globalName, obj)`

This export contains information about where an interface is supposed to be exposed as a property. It takes into account the Web IDL extended attributes `[Expose]` and `[NoInterfaceObject]` to generate a data structure of the form:
This function allows the interface object to be automatically exposed on a global object `obj`, taking into account the Web IDL extended attributes `[Expose]`, `[NoInterfaceObject]`, and `[LegacyWindowAlias]`. The `globalName` parameter specifies the [global name](https://heycam.github.io/webidl/#dfn-global-name) of the interface the provided global object implements, such as `Window`, `Worker`, and `Worklet`.

```js
{
nameOfGlobal1: {
nameOfInterface: InterfaceClass
},
nameOfGlobal2: {
nameOfInterface: InterfaceClass
},
// etc.
}
```

This format may seem a bit verbose, but eventually when we support `[NamedConstructor]`, there will be potentially more than one key/value pair per global, and it will show its worth.
A limitation of the current implementation of `expose()` is that it does not yet support the `[SecureContext]` extended attribute, and all members of the interface are exposed regardless of the origin of that global object. This is expected to be remedied in the future.

### For dictionaries

Expand Down Expand Up @@ -311,6 +299,12 @@ This can be useful when you are given a wrapper, but need to modify its inaccess

Returns the corresponding impl class instance for a given wrapper class instance, or returns the argument back if it is not an implementation class instance.

## The generated `bundle-entry.js` file

webidl2js also generates a `bundle-entry.js` file in the same directory as all interface wrapper files and the utilities file. This file contains only one exported function, `bootstrap(globalName, globalObj, defaultPrivateData)`, which when called will expose all interfaces generated onto the provided `globalObj`. For interfaces annotated with `[WebIDL2JSFactory]` (documented below), it will automatically create the interface with the given `defaultPrivateData` and expose them the same way as ordinary interfaces. The return value is an object containing all interface wrappers, including the newly created ones for `[WebIDL2JSFactory]`.

As the name implies, this file is also fit for bundling all interface files through a code bundler such as Browserify and webpack, for faster loading of the initial interface wrappers. Care must be taken however to avoid bundling the implementation files as well.

## Web IDL features

webidl2js is implementing an ever-growing subset of the Web IDL specification. So far we have implemented:
Expand Down Expand Up @@ -342,9 +336,10 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- `[Clamp]`
- `[Constructor]`
- `[EnforceRange]`
- `[Exposed]` and `[NoInterfaceObject]` (by exporting metadata on where/whether it is exposed)
- `[Exposed]` and `[NoInterfaceObject]` (through the exported `expose()` function)
- `[LegacyArrayClass]`
- `[LegacyUnenumerableNamedProperties]`
- `[LegacyWindowAlias]`
- `[OverrideBuiltins]`
- `[PutForwards]`
- `[Replaceable]`
Expand All @@ -360,8 +355,7 @@ Notable missing features include:
- `maplike<>` and `setlike<>`
- `[AllowShared]`
- `[Default]` (for `toJSON()` operations)
- `[Global]` and `[PrimaryGlobal]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
- `[LegacyWindowAlias]`
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
- `[LenientSetter]`
- `[LenientThis]`
- `[NamedConstructor]`
Expand Down
86 changes: 58 additions & 28 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -1086,32 +1086,6 @@ class Interface {
}

generateIface() {
const shouldExposeRoot = !utils.getExtAttr(this.idl.extAttrs, "NoInterfaceObject");

const exposedMap = {};
if (shouldExposeRoot) {
let exposedOn = ["Window"];
const exposedAttrs = this.idl.extAttrs
.filter(attr => attr.name === "Exposed");
if (exposedAttrs.length !== 0) {
if (typeof exposedAttrs[0].rhs.value === "string") {
exposedAttrs[0].rhs.value = [exposedAttrs[0].rhs.value];
}
exposedOn = exposedAttrs[0].rhs.value;
}
for (let i = 0; i < exposedOn.length; ++i) {
if (!exposedMap[exposedOn[i]]) {
exposedMap[exposedOn[i]] = [];
}
exposedMap[exposedOn[i]].push(this.name);
}
}

const exposers = [];
for (let keys = Object.keys(exposedMap), i = 0; i < keys.length; ++i) {
exposers.push(keys[i] + ": { " + exposedMap[keys[i]].join(", ") + " }");
}

this.str += `
create(constructorArgs, privateData) {
let obj = Object.create(${this.name}.prototype);
Expand Down Expand Up @@ -1172,8 +1146,64 @@ class Interface {
return obj;
},
interface: ${this.name},
expose: {
${exposers.join(",\n ")}
expose(globalName, obj) {
`;

let exposedOn = [];
if (!utils.getExtAttr(this.idl.extAttrs, "NoInterfaceObject")) {
const exposedAttr = utils.getExtAttr(this.idl.extAttrs, "Exposed");
if (exposedAttr) {
exposedOn = utils.getIdentifiers(exposedAttr);
} else {
exposedOn = ["Window"];
}
}

if (exposedOn.length > 0) {
this.str += `
switch (globalName) {
`;

if (exposedOn.includes("Window")) {
this.str += `case "Window":`;

const legacyWindowAlias = utils.getExtAttr(this.idl.extAttrs, "LegacyWindowAlias");
if (legacyWindowAlias) {
const identifiers = utils.getIdentifiers(legacyWindowAlias);
for (const id of identifiers) {
this.str += `
Object.defineProperty(obj, ${JSON.stringify(id)}, {
value: ${this.name},
writable: true,
enumerable: false,
configurable: true
});
`;
}
this.str += `
// fall-through
`;
}
}

for (const globalName of exposedOn) {
if (globalName !== "Window") {
this.str += `case ${JSON.stringify(globalName)}:`;
}
}

this.str += `
Object.defineProperty(obj, ${JSON.stringify(this.name)}, {
value: ${this.name},
writable: true,
enumerable: false,
configurable: true
});
}
`;
}

this.str += `
}
`;
}
Expand Down
21 changes: 21 additions & 0 deletions lib/output/bundle-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use strict";

const allExports = Object.create(null);
exports.bootstrap = function (globalName, globalObj, defaultPrivateData) {
const exportsWithFactories = Object.assign(Object.create(null), allExports);
for (const name of Object.keys(exportsWithFactories)) {
const obj = exportsWithFactories[name];
if (typeof obj.expose === "function") {
obj.expose(globalName, globalObj);
} else if (typeof obj.createInterface === "function") {
const iface = obj.createInterface(defaultPrivateData);
// Mix in is()/isImpl().
Object.assign(iface, obj);
exportsWithFactories[name] = iface;
iface.expose(globalName, globalObj);
}
}
return exportsWithFactories;
};

// Below this line, exports will be added.
9 changes: 9 additions & 0 deletions lib/transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class Transformer {
yield fs.writeFile(this.utilPath, utilsText);

const { interfaces, dictionaries, enumerations } = this.ctx;
const allExports = [];

for (const obj of interfaces.values()) {
let source = obj.toString();
Expand Down Expand Up @@ -197,6 +198,7 @@ class Transformer {
source = this._prettify(source);

yield fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
allExports.push(obj.name);
}

for (const obj of dictionaries.values()) {
Expand All @@ -218,6 +220,7 @@ class Transformer {
source = this._prettify(source);

yield fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
allExports.push(obj.name);
}

for (const obj of enumerations.values()) {
Expand All @@ -227,7 +230,13 @@ class Transformer {
${obj.toString()}
`);
yield fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
allExports.push(obj.name);
}

const bundleEntryText = yield fs.readFile(path.resolve(__dirname, "output/bundle-entry.js"));
const out = path.join(outputDir, "bundle-entry.js");
yield fs.writeFile(out, bundleEntryText);
yield fs.appendFile(out, allExports.map(e => `allExports[${JSON.stringify(e)}] = require("./${e}");\n`).join(""));
}

_prettify(source) {
Expand Down
12 changes: 12 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ function getExtAttr(attrs, name) {
return null;
}

// https://heycam.github.io/webidl/#legacywindowalias-identifier
function getIdentifiers(extAttr) {
let identifiers;
if (typeof extAttr.rhs.value === "string") {
identifiers = [extAttr.rhs.value];
} else {
identifiers = extAttr.rhs.value;
}
return identifiers;
}

function isGlobal(idl) {
return Boolean(getExtAttr(idl.extAttrs, "Global"));
}
Expand Down Expand Up @@ -92,6 +103,7 @@ class RequiresMap extends Map {
module.exports = {
getDefault,
getExtAttr,
getIdentifiers,
isGlobal,
isOnInstance,
stringifyPropertyName,
Expand Down
Loading