Skip to content

Commit

Permalink
Add webidl2js‑globals.js to automate install of [Exposed] globals
Browse files Browse the repository at this point in the history
  • Loading branch information
ExE-Boss committed Apr 4, 2020
1 parent 5af205c commit 4f4fd30
Show file tree
Hide file tree
Showing 14 changed files with 8,004 additions and 4,125 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- Variadic arguments
- `[Clamp]`
- `[EnforceRange]`
- `[Exposed]`
- `[LegacyArrayClass]`
- `[LegacyUnenumerableNamedProperties]`
- `[LegacyWindowAlias]`
Expand All @@ -478,7 +479,6 @@ Notable missing features include:
- `[AllowShared]`
- `[Default]` (for `toJSON()` operations)
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
- `[Exposed]`
- `[LenientSetter]`
- `[LenientThis]`
- `[NamedConstructor]`
Expand Down
50 changes: 49 additions & 1 deletion lib/constructs/callback-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ class CallbackInterface {

this._analyzed = false;
this._outputStaticProperties = new Map();

const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
if (this.idl.members.some(member => member.type === "const") && !exposed) {
throw new Error(`Callback interface ${this.name} with defined constants lacks the [Exposed] extended attribute`);
}

if (exposed) {
if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) {
throw new Error(`[Exposed] must take an identifier or an identifier list in callback interface ${this.name}`);
}

if (exposed.rhs.type === "identifier") {
this.exposed = new Set([exposed.rhs.value]);
} else {
this.exposed = new Set(exposed.rhs.value.map(token => token.value));
}
} else {
this.exposed = new Set();
}
}

_analyzeMembers() {
Expand Down Expand Up @@ -178,14 +197,41 @@ class CallbackInterface {
}

generateInstall() {
if (this.constants.size > 0) {
this.str += `
const exposed = new Set([
`;

for (const globalName of this.exposed) {
this.str += `"${globalName}",\n`;
}

this.str += `
]);
`;
}

this.str += `
exports.install = function install(globalObject) {
exports.install = (globalObject, globalNames) => {
`;

if (this.constants.size > 0) {
const { name } = this;

this.str += `
let isExposed = false;
for (const globalName of globalNames) {
if (exposed.has(globalName)) {
isExposed = true;
break;
}
}
if (!isExposed) {
return;
}
const ${name} = () => {
throw new TypeError("Illegal invocation");
};
Expand Down Expand Up @@ -234,4 +280,6 @@ class CallbackInterface {
}
}

CallbackInterface.prototype.type = "callback interface";

module.exports = CallbackInterface;
89 changes: 85 additions & 4 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,18 @@ class Interface {

const global = utils.getExtAttr(this.idl.extAttrs, "Global");
this.isGlobal = Boolean(global);
if (global && !global.rhs) {
throw new Error(`[Global] must take an identifier or an identifier list in interface ${this.name}`);
if (global) {
if (!global.rhs || (global.rhs.type !== "identifier" && global.rhs.type !== "identifier-list")) {
throw new Error(`[Global] must take an identifier or an identifier list in interface ${this.name}`);
}

if (global.rhs.type === "identifier") {
this.globalNames = new Set([global.rhs.value]);
} else {
this.globalNames = new Set(global.rhs.value.map(token => token.value));
}
} else {
this.globalNames = null;
}

const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
Expand Down Expand Up @@ -1193,6 +1203,37 @@ class Interface {
return obj;
};
`;

if (this.isGlobal) {
const bundleEntry = this.requires.addRelative("./webidl2js-globals.js");

this.str += `
const globalNames = new Set([
`;

for (const globalName of this.globalNames) {
this.str += `"${globalName}",\n`;
}

this.str += `
]);
/**
* Initialises the passed obj as a new global.
*
* The obj is expected to contain all the global object properties
* as specified in the ECMAScript specification.
*/
exports.setupGlobal = (obj, constructorArgs = [], privateData = {}`;

this.str += `) => {
${bundleEntry}.setupGlobal(obj, globalNames);
Object.setPrototypeOf(obj, obj[interfaceName].prototype);
obj = exports.setup(obj, obj, constructorArgs, privateData);
};
`;
}
}

addConstructor() {
Expand Down Expand Up @@ -1463,7 +1504,47 @@ class Interface {
const { idl, name } = this;

this.str += `
exports.install = (globalObject, globalName) => {
const exposed = new Set([
`;

for (const globalName of this.exposed) {
this.str += `"${globalName}",\n`;
}

this.str += `
]);
exports.install = (globalObject, globalNames) => {
let isExposed = false;
`;

if (this.legacyWindowAliases) {
this.str += "let isWindow = false;\n";
}

this.str += `
for (const globalName of globalNames) {
if (exposed.has(globalName)) {
isExposed = true;
`;

if (this.legacyWindowAliases) {
this.str += `
if (globalName === "Window") {
isWindow = true;
}
`;
} else {
this.str += "break;";
}

this.str += `
}
}
if (!isExposed) {
return;
}
`;

if (idl.inheritance) {
Expand Down Expand Up @@ -1500,7 +1581,7 @@ class Interface {

if (this.legacyWindowAliases) {
this.str += `
if (globalName === "Window") {
if (isWindow) {
`;

for (const legacyWindowAlias of this.legacyWindowAliases) {
Expand Down
2 changes: 2 additions & 0 deletions lib/context.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";
const webidl = require("webidl2");
const Globals = require("./globals.js");
const Typedef = require("./constructs/typedef");

const builtinTypedefs = webidl.parse(`
Expand Down Expand Up @@ -38,6 +39,7 @@ class Context {
this.callbackInterfaces = new Map();
this.dictionaries = new Map();
this.enumerations = new Map();
this.globals = new Globals(this);

for (const typedef of builtinTypedefs) {
this.typedefs.set(typedef.name, new Typedef(this, typedef));
Expand Down
124 changes: 124 additions & 0 deletions lib/globals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"use strict";
const utils = require("./utils.js");

class Globals {
constructor(ctx) {
this.ctx = ctx;
this.requires = new utils.RequiresMap(ctx);

this.str = null;

this._analyzed = false;
this._constructs = null;
}

_analyzeMembers() {
const { ctx } = this;

const constructs = [];

// This is needed to ensure that the interface order is deterministic
// regardless of filesystem case sensitivity:
const ifaceNames = [...ctx.interfaces.keys(), ...ctx.callbackInterfaces.keys()].sort();

function addExtendingInterfaces(parent) {
for (const ifaceName of ifaceNames) {
const iface = ctx.interfaces.get(ifaceName);
if (iface && iface.idl.inheritance === parent.name) {
constructs.push(iface);
addExtendingInterfaces(iface);
}
}
}

for (const ifaceName of ifaceNames) {
const cb = ctx.callbackInterfaces.get(ifaceName);
if (cb) {
// Callback interface
if (cb.constants.size > 0) {
constructs.push(cb);
}
continue;
}

const iface = ctx.interfaces.get(ifaceName);
if (!iface.idl.inheritance) {
constructs.push(iface);
addExtendingInterfaces(iface);
}
}

this._constructs = constructs;
}

generate() {
this.generateInterfaces();
this.generateSetupGlobal();
this.generateRequires();
}

generateInterfaces() {
this.str += `
/**
* This object defines the mapping between the interface name and the generated interface wrapper code.
*
* Note: The mapping needs to stay as-is in order due to interface evaluation.
* We cannot "refactor" this to something less duplicative because that would break bundlers which depend
* on static analysis of require()s.
*/
exports.interfaces = {
`;

for (const { name } of this._constructs) {
this.str += `${utils.stringifyPropertyKey(name)}: require("${name.startsWith(".") ? name : `./${name}`}.js"),`;
}

this.str += `
};
`;
}

generateSetupGlobal() {
this.str += `
/**
* Initialises the passed object as a new global.
*
* The object is expected to contain all the global object properties
* as specified in the ECMAScript specification.
*
* This function has to be added to the exports object
* to avoid circular dependency issues.
*
* @param {object} globalObject
* @param {Iterable<string>} globalNames
* The identifiers specified in the \`[Global]\` extended attribute
* on the interface's WebIDL definition.
*/
exports.installInterfaces = (globalObject, globalNames) => {
for (const iface of Object.values(exports.interfaces)) {
iface.install(globalObject, globalNames);
}
};
`;
}

generateRequires() {
this.str = `
${this.requires.generate()}
${this.str}
`;
}

toString() {
this.str = "";
if (!this._analyzed) {
this._analyzed = true;
this._analyzeMembers();
}
this.generate();
return this.str;
}
}

module.exports = Globals;
8 changes: 8 additions & 0 deletions lib/transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ class Transformer {
`);
await fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
}

const source = this._prettify(`
"use strict";
const utils = require("${relativeUtils}");
${this.ctx.globals.toString()}
`);
await fs.writeFile(path.join(outputDir, "webidl2js-globals.js"), source);
}

_prettify(source) {
Expand Down
4 changes: 4 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ const defaultDefinePropertyDescriptor = {
};

function toKey(type, func = "") {
if (extname(type) === ".js") {
type = type.slice(0, -3);
}

return String(func + type).replace(/[./-]+/g, " ").trim().replace(/ /g, "_");
}

Expand Down
Loading

0 comments on commit 4f4fd30

Please sign in to comment.