Skip to content

Commit 3853cad

Browse files
committed
Add webidl2js‑globals.js to handle setup of [Global]s
1 parent 1455648 commit 3853cad

16 files changed

+6717
-3956
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
454454
- Variadic arguments
455455
- `[Clamp]`
456456
- `[EnforceRange]`
457+
- `[Exposed]`
457458
- `[LegacyArrayClass]`
458459
- `[LegacyUnenumerableNamedProperties]`
459460
- `[OverrideBuiltins]`
@@ -476,7 +477,6 @@ Notable missing features include:
476477
- `[AllowShared]`
477478
- `[Default]` (for `toJSON()` operations)
478479
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
479-
- `[Exposed]`
480480
- `[LegacyWindowAlias]`
481481
- `[LenientSetter]`
482482
- `[LenientThis]`

lib/constructs/callback-interface.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ class CallbackInterface {
2020

2121
this._analyzed = false;
2222
this._outputStaticProperties = new Map();
23+
24+
const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
25+
if (this.idl.members.some(member => member.type === "const") && !exposed) {
26+
throw new Error(`Callback interface ${this.name} with defined constants lacks the [Exposed] extended attribute`);
27+
}
28+
29+
if (exposed) {
30+
if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) {
31+
throw new Error(`[Exposed] must take an identifier or an identifier list in interface ${this.name}`);
32+
}
33+
34+
this.ctx.globals.addGlobalNames(exposed.rhs.value);
35+
}
2336
}
2437

2538
_analyzeMembers() {
@@ -234,4 +247,6 @@ class CallbackInterface {
234247
}
235248
}
236249

250+
CallbackInterface.prototype.type = "callback interface";
251+
237252
module.exports = CallbackInterface;

lib/constructs/interface.js

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,25 @@ class Interface {
7373

7474
const global = utils.getExtAttr(this.idl.extAttrs, "Global");
7575
this.isGlobal = Boolean(global);
76-
if (global && !global.rhs) {
77-
throw new Error(`[Global] must take an identifier or an identifier list in interface ${this.name}`);
76+
if (global) {
77+
if (!global.rhs || (global.rhs.type !== "identifier" && global.rhs.type !== "identifier-list")) {
78+
throw new Error(`[Global] must take an identifier or an identifier list in interface ${this.name}`);
79+
}
80+
81+
this.ctx.globals.addGlobalNames(global.rhs.value);
82+
}
83+
84+
const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
85+
if (exposed) {
86+
if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) {
87+
throw new Error(`[Exposed] must take an identifier or an identifier list in interface ${this.name}`);
88+
}
89+
90+
this.ctx.globals.addGlobalNames(exposed.rhs.value);
91+
}
92+
93+
if (!exposed && !utils.getExtAttr(this.idl.extAttrs, "NoInterfaceObject")) {
94+
throw new Error(`Interface ${this.name} has neither [Exposed] nor [NoInterfaceObject]`);
7895
}
7996
}
8097

@@ -1150,6 +1167,36 @@ class Interface {
11501167
return obj;
11511168
};
11521169
`;
1170+
1171+
if (this.isGlobal) {
1172+
const globalAttr = utils.getExtAttr(this.idl.extAttrs, "Global");
1173+
const globalNames = globalAttr.rhs.type === "identifier-list" ?
1174+
globalAttr.rhs.value.map(token => token.value) :
1175+
[globalAttr.rhs.value];
1176+
const isMultiGlobal = globalNames.length > 1;
1177+
1178+
const bundleEntry = this.requires.add("./webidl2js-globals.js");
1179+
this.str += `
1180+
/**
1181+
* Initialises the passed obj as a new global.
1182+
*
1183+
* The obj is expected to contain all the global object properties
1184+
* as specified in the ECMAScript specification.
1185+
*/
1186+
exports.setupGlobal = (obj, constructorArgs = [], privateData = {}`;
1187+
1188+
if (isMultiGlobal) {
1189+
this.str += `, globalName = "${globalNames[0]}"`;
1190+
}
1191+
1192+
this.str += `) => {
1193+
${bundleEntry}.setupGlobal(obj, ${isMultiGlobal ? "globalName" : `"${globalNames[0]}"`});
1194+
1195+
Object.setPrototypeOf(obj, obj[interfaceName].prototype);
1196+
obj = exports.setup(obj, obj, constructorArgs, privateData);
1197+
};
1198+
`;
1199+
}
11531200
}
11541201

11551202
addConstructor() {
@@ -1444,12 +1491,19 @@ class Interface {
14441491
globalObject[ctorRegistrySymbol] = Object.create(null);
14451492
}
14461493
globalObject[ctorRegistrySymbol][interfaceName] = ${name};
1494+
`;
14471495

1496+
if (!utils.getExtAttr(this.idl.extAttrs, "NoInterfaceObject")) {
1497+
this.str += `
14481498
Object.defineProperty(globalObject, interfaceName, {
14491499
configurable: true,
14501500
writable: true,
14511501
value: ${name}
14521502
});
1503+
`;
1504+
}
1505+
1506+
this.str += `
14531507
};
14541508
`;
14551509
}

lib/context.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use strict";
22
const webidl = require("webidl2");
3+
const Globals = require("./globals.js");
34
const Typedef = require("./constructs/typedef");
45

56
const builtinTypedefs = webidl.parse(`
@@ -38,6 +39,7 @@ class Context {
3839
this.callbackInterfaces = new Map();
3940
this.dictionaries = new Map();
4041
this.enumerations = new Map();
42+
this.globals = new Globals(this);
4143

4244
for (const typedef of builtinTypedefs) {
4345
this.typedefs.set(typedef.name, new Typedef(this, typedef));

lib/globals.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"use strict";
2+
const utils = require("./utils.js");
3+
4+
class Globals {
5+
constructor(ctx) {
6+
this.ctx = ctx;
7+
this.requires = new utils.RequiresMap(ctx);
8+
9+
this.str = null;
10+
this._globalNames = new Set();
11+
}
12+
13+
addGlobalNames(globalNames) {
14+
if (typeof globalNames === "string") {
15+
this._globalNames.add(globalNames);
16+
} else {
17+
for (const globalName of globalNames) {
18+
this._globalNames.add(globalName.value);
19+
}
20+
}
21+
}
22+
23+
getConstructsForGlobal(globalName) {
24+
const { ctx } = this;
25+
26+
const constructs = [];
27+
28+
for (const iface of [...ctx.interfaces.values(), ...ctx.callbackInterfaces.values()]) {
29+
const exposed = utils.getExtAttr(iface.idl.extAttrs, "Exposed");
30+
31+
if (!exposed) {
32+
continue;
33+
} else {
34+
const { rhs } = exposed;
35+
switch (rhs.type) {
36+
case "identifier":
37+
if (rhs.value !== globalName) {
38+
continue;
39+
}
40+
break;
41+
case "identifier-list":
42+
if (!rhs.value.reduce((r, t) => (t.value === globalName) || r, false)) {
43+
continue;
44+
}
45+
break;
46+
default:
47+
if (!ctx.options.suppressErrors) {
48+
throw new Error(
49+
`Unsupported ${iface.type} ${iface.name}'s [Exposed] extended attribute value type "${rhs.type}"`
50+
);
51+
}
52+
continue;
53+
}
54+
}
55+
56+
constructs.push(iface);
57+
}
58+
59+
// FIXME: `Element` currently gets incorrectly placed before `Node` and `EventTarget`
60+
constructs.sort((a, b) => {
61+
if (a.idl.inheritance === b.name) {
62+
return 1;
63+
}
64+
65+
if (b.idl.inheritance === a.name) {
66+
return -1;
67+
}
68+
69+
return 0;
70+
});
71+
72+
return constructs;
73+
}
74+
75+
generate() {
76+
this.generateGlobals();
77+
this.generateRequires();
78+
}
79+
80+
generateGlobals() {
81+
this.str += `
82+
/**
83+
* Initialises the passed object as a new global.
84+
*
85+
* The object is expected to contain all the global object properties
86+
* as specified in the ECMAScript specification.
87+
*
88+
* This function has to be added to the exports object
89+
* to avoid circular dependency issues.
90+
*
91+
* @param {object} globalObject
92+
* @param {string} globalName
93+
*/
94+
exports.setupGlobal = (globalObject, globalName) => {
95+
Object.defineProperty(globalObject, utils.ctorRegistrySymbol, { value: Object.create(null) });
96+
97+
switch(globalName) {
98+
`;
99+
100+
101+
for (const globalName of this._globalNames) {
102+
this.generateGlobal(globalName);
103+
}
104+
105+
this.str += `
106+
default:
107+
throw new Error(\`Internal error: Unknown global name "\${globalName}"\`);
108+
}
109+
};
110+
`;
111+
}
112+
113+
generateGlobal(globalName) {
114+
this.str += `
115+
case "${globalName}": {
116+
`;
117+
118+
for (const construct of this.getConstructsForGlobal(globalName)) {
119+
const imported = this.requires.addRelative(construct.name);
120+
this.str += `${imported}.install(globalObject);\n`;
121+
}
122+
123+
this.str += `break;
124+
}
125+
`;
126+
}
127+
128+
generateRequires() {
129+
this.str = `
130+
${this.requires.generate()}
131+
132+
${this.str}
133+
`;
134+
}
135+
136+
toString() {
137+
this.str = "";
138+
this.generate();
139+
return this.str;
140+
}
141+
}
142+
143+
module.exports = Globals;

lib/transformer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,14 @@ class Transformer {
252252
`);
253253
await fs.writeFile(path.join(outputDir, obj.name + ".js"), source);
254254
}
255+
256+
const source = this._prettify(`
257+
"use strict";
258+
259+
const utils = require("${relativeUtils}");
260+
${this.ctx.globals.toString()}
261+
`);
262+
await fs.writeFile(path.join(outputDir, "webidl2js-globals.js"), source);
255263
}
256264

257265
_prettify(source) {

0 commit comments

Comments
 (0)