Skip to content

Commit 4f4fd30

Browse files
committed
Add webidl2js‑globals.js to automate install of [Exposed] globals
1 parent 5af205c commit 4f4fd30

File tree

14 files changed

+8004
-4125
lines changed

14 files changed

+8004
-4125
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
- `[LegacyWindowAlias]`
@@ -478,7 +479,6 @@ Notable missing features include:
478479
- `[AllowShared]`
479480
- `[Default]` (for `toJSON()` operations)
480481
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
481-
- `[Exposed]`
482482
- `[LenientSetter]`
483483
- `[LenientThis]`
484484
- `[NamedConstructor]`

lib/constructs/callback-interface.js

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@ 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 callback interface ${this.name}`);
32+
}
33+
34+
if (exposed.rhs.type === "identifier") {
35+
this.exposed = new Set([exposed.rhs.value]);
36+
} else {
37+
this.exposed = new Set(exposed.rhs.value.map(token => token.value));
38+
}
39+
} else {
40+
this.exposed = new Set();
41+
}
2342
}
2443

2544
_analyzeMembers() {
@@ -178,14 +197,41 @@ class CallbackInterface {
178197
}
179198

180199
generateInstall() {
200+
if (this.constants.size > 0) {
201+
this.str += `
202+
const exposed = new Set([
203+
`;
204+
205+
for (const globalName of this.exposed) {
206+
this.str += `"${globalName}",\n`;
207+
}
208+
209+
this.str += `
210+
]);
211+
`;
212+
}
213+
181214
this.str += `
182-
exports.install = function install(globalObject) {
215+
exports.install = (globalObject, globalNames) => {
183216
`;
184217

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

188221
this.str += `
222+
let isExposed = false;
223+
224+
for (const globalName of globalNames) {
225+
if (exposed.has(globalName)) {
226+
isExposed = true;
227+
break;
228+
}
229+
}
230+
231+
if (!isExposed) {
232+
return;
233+
}
234+
189235
const ${name} = () => {
190236
throw new TypeError("Illegal invocation");
191237
};
@@ -234,4 +280,6 @@ class CallbackInterface {
234280
}
235281
}
236282

283+
CallbackInterface.prototype.type = "callback interface";
284+
237285
module.exports = CallbackInterface;

lib/constructs/interface.js

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,18 @@ 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+
if (global.rhs.type === "identifier") {
82+
this.globalNames = new Set([global.rhs.value]);
83+
} else {
84+
this.globalNames = new Set(global.rhs.value.map(token => token.value));
85+
}
86+
} else {
87+
this.globalNames = null;
7888
}
7989

8090
const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
@@ -1193,6 +1203,37 @@ class Interface {
11931203
return obj;
11941204
};
11951205
`;
1206+
1207+
if (this.isGlobal) {
1208+
const bundleEntry = this.requires.addRelative("./webidl2js-globals.js");
1209+
1210+
this.str += `
1211+
const globalNames = new Set([
1212+
`;
1213+
1214+
for (const globalName of this.globalNames) {
1215+
this.str += `"${globalName}",\n`;
1216+
}
1217+
1218+
this.str += `
1219+
]);
1220+
1221+
/**
1222+
* Initialises the passed obj as a new global.
1223+
*
1224+
* The obj is expected to contain all the global object properties
1225+
* as specified in the ECMAScript specification.
1226+
*/
1227+
exports.setupGlobal = (obj, constructorArgs = [], privateData = {}`;
1228+
1229+
this.str += `) => {
1230+
${bundleEntry}.setupGlobal(obj, globalNames);
1231+
1232+
Object.setPrototypeOf(obj, obj[interfaceName].prototype);
1233+
obj = exports.setup(obj, obj, constructorArgs, privateData);
1234+
};
1235+
`;
1236+
}
11961237
}
11971238

11981239
addConstructor() {
@@ -1463,7 +1504,47 @@ class Interface {
14631504
const { idl, name } = this;
14641505

14651506
this.str += `
1466-
exports.install = (globalObject, globalName) => {
1507+
const exposed = new Set([
1508+
`;
1509+
1510+
for (const globalName of this.exposed) {
1511+
this.str += `"${globalName}",\n`;
1512+
}
1513+
1514+
this.str += `
1515+
]);
1516+
1517+
exports.install = (globalObject, globalNames) => {
1518+
let isExposed = false;
1519+
`;
1520+
1521+
if (this.legacyWindowAliases) {
1522+
this.str += "let isWindow = false;\n";
1523+
}
1524+
1525+
this.str += `
1526+
for (const globalName of globalNames) {
1527+
if (exposed.has(globalName)) {
1528+
isExposed = true;
1529+
`;
1530+
1531+
if (this.legacyWindowAliases) {
1532+
this.str += `
1533+
if (globalName === "Window") {
1534+
isWindow = true;
1535+
}
1536+
`;
1537+
} else {
1538+
this.str += "break;";
1539+
}
1540+
1541+
this.str += `
1542+
}
1543+
}
1544+
1545+
if (!isExposed) {
1546+
return;
1547+
}
14671548
`;
14681549

14691550
if (idl.inheritance) {
@@ -1500,7 +1581,7 @@ class Interface {
15001581

15011582
if (this.legacyWindowAliases) {
15021583
this.str += `
1503-
if (globalName === "Window") {
1584+
if (isWindow) {
15041585
`;
15051586

15061587
for (const legacyWindowAlias of this.legacyWindowAliases) {

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: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
11+
this._analyzed = false;
12+
this._constructs = null;
13+
}
14+
15+
_analyzeMembers() {
16+
const { ctx } = this;
17+
18+
const constructs = [];
19+
20+
// This is needed to ensure that the interface order is deterministic
21+
// regardless of filesystem case sensitivity:
22+
const ifaceNames = [...ctx.interfaces.keys(), ...ctx.callbackInterfaces.keys()].sort();
23+
24+
function addExtendingInterfaces(parent) {
25+
for (const ifaceName of ifaceNames) {
26+
const iface = ctx.interfaces.get(ifaceName);
27+
if (iface && iface.idl.inheritance === parent.name) {
28+
constructs.push(iface);
29+
addExtendingInterfaces(iface);
30+
}
31+
}
32+
}
33+
34+
for (const ifaceName of ifaceNames) {
35+
const cb = ctx.callbackInterfaces.get(ifaceName);
36+
if (cb) {
37+
// Callback interface
38+
if (cb.constants.size > 0) {
39+
constructs.push(cb);
40+
}
41+
continue;
42+
}
43+
44+
const iface = ctx.interfaces.get(ifaceName);
45+
if (!iface.idl.inheritance) {
46+
constructs.push(iface);
47+
addExtendingInterfaces(iface);
48+
}
49+
}
50+
51+
this._constructs = constructs;
52+
}
53+
54+
generate() {
55+
this.generateInterfaces();
56+
this.generateSetupGlobal();
57+
this.generateRequires();
58+
}
59+
60+
generateInterfaces() {
61+
this.str += `
62+
/**
63+
* This object defines the mapping between the interface name and the generated interface wrapper code.
64+
*
65+
* Note: The mapping needs to stay as-is in order due to interface evaluation.
66+
* We cannot "refactor" this to something less duplicative because that would break bundlers which depend
67+
* on static analysis of require()s.
68+
*/
69+
exports.interfaces = {
70+
`;
71+
72+
for (const { name } of this._constructs) {
73+
this.str += `${utils.stringifyPropertyKey(name)}: require("${name.startsWith(".") ? name : `./${name}`}.js"),`;
74+
}
75+
76+
this.str += `
77+
};
78+
`;
79+
}
80+
81+
generateSetupGlobal() {
82+
this.str += `
83+
/**
84+
* Initialises the passed object as a new global.
85+
*
86+
* The object is expected to contain all the global object properties
87+
* as specified in the ECMAScript specification.
88+
*
89+
* This function has to be added to the exports object
90+
* to avoid circular dependency issues.
91+
*
92+
* @param {object} globalObject
93+
* @param {Iterable<string>} globalNames
94+
* The identifiers specified in the \`[Global]\` extended attribute
95+
* on the interface's WebIDL definition.
96+
*/
97+
exports.installInterfaces = (globalObject, globalNames) => {
98+
for (const iface of Object.values(exports.interfaces)) {
99+
iface.install(globalObject, globalNames);
100+
}
101+
};
102+
`;
103+
}
104+
105+
generateRequires() {
106+
this.str = `
107+
${this.requires.generate()}
108+
109+
${this.str}
110+
`;
111+
}
112+
113+
toString() {
114+
this.str = "";
115+
if (!this._analyzed) {
116+
this._analyzed = true;
117+
this._analyzeMembers();
118+
}
119+
this.generate();
120+
return this.str;
121+
}
122+
}
123+
124+
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) {

lib/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ const defaultDefinePropertyDescriptor = {
9595
};
9696

9797
function toKey(type, func = "") {
98+
if (extname(type) === ".js") {
99+
type = type.slice(0, -3);
100+
}
101+
98102
return String(func + type).replace(/[./-]+/g, " ").trim().replace(/ /g, "_");
99103
}
100104

0 commit comments

Comments
 (0)