Skip to content

Commit f8cb248

Browse files
committed
Implement [LegacyFactoryFunction]
1 parent 1933ade commit f8cb248

File tree

9 files changed

+6344
-3958
lines changed

9 files changed

+6344
-3958
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,11 @@ Creates a new instance of the wrapper class and corresponding implementation cla
282282

283283
This is useful inside implementation class files, where it is easiest to only deal with impls, not wrappers.
284284

285-
#### `new(globalObject)`
285+
#### `new(globalObject, newTarget)`
286286

287287
Creates a new instance of the wrapper class and corresponding implementation class, but without invoking the implementation class constructor logic. Then returns the implementation class.
288288

289-
This corresponds to the [Web IDL "new" algorithm](https://heycam.github.io/webidl/#new), and is useful when implementing specifications that initialize objects in different ways than their constructors do.
289+
This corresponds to the [WebIDL "create a new object implementing the interface"](https://heycam.github.io/webidl/#new) and ["internally create a new object implementing the interface"](https://heycam.github.io/webidl/#internally-create-a-new-object-implementing-the-interface) algorithms, and is useful when implementing specifications that initialize objects in different ways than their constructors do.
290290

291291
#### `setup(obj, globalObject, constructorArgs, privateData)`
292292

@@ -421,6 +421,16 @@ It is often useful for implementation classes to inherit from each other, if the
421421

422422
However, it is not required! The wrapper classes will have a correct inheritance chain, regardless of the implementation class inheritance chain. Just make sure that, either via inheritance or manual implementation, you implement all of the expected operations and attributes.
423423

424+
### The `[LegacyFactoryFunction]` extended attribute
425+
426+
For interfaces which have the `[LegacyFactoryFunction]` extended attribute, the implementation class file must contain the `legacyFactoryFunction` export, with the signature `(globalObject, ...legacyFactoryFunctionArgs)`, which is used for:
427+
428+
- Setting up initial state that will always be used, such as caches or default values
429+
- Keep a reference to the relevant `globalObject` for later consumption.
430+
- Processing constructor arguments `legacyFactoryFunctionArgs` passed to the legacy factory function constructor, if the legacy factory function takes arguments.
431+
432+
The `legacyFactoryFunction` export is called with a `this` value of a new uninitialized implementation instance, which may be ignored by returning a different object, similarly to how constructors with overridden return values are implemented.
433+
424434
### The init export
425435

426436
In addition to the `implementation` export, for interfaces, your implementation class file can contain an `init` export. This would be a function taking as an argument an instance of the implementation class, and is called when any wrapper/implementation pairs are constructed (such as by the exports of the [generated wrapper module](https://github.com/jsdom/webidl2js#for-interfaces)). In particular, it is called even if they are constructed by [`new()`](newglobalobject), which does not invoke the implementation class constructor.
@@ -484,6 +494,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
484494
- `[Clamp]`
485495
- `[EnforceRange]`
486496
- `[Exposed]`
497+
- `[LegacyFactoryFunction]`
487498
- `[LegacyLenientThis]`
488499
- `[LegacyLenientSetter]`
489500
- `[LegacyNoInterfaceObject]`
@@ -510,7 +521,6 @@ Notable missing features include:
510521
- `[AllowShared]`
511522
- `[Default]` (for `toJSON()` operations)
512523
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
513-
- `[LegacyFactoryFunction]`
514524
- `[LegacyNamespace]`
515525
- `[LegacyTreatNonObjectAsNull]`
516526
- `[SecureContext]`

lib/constructs/interface.js

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Interface {
5050
this.attributes = new Map();
5151
this.staticAttributes = new Map();
5252
this.constants = new Map();
53+
this.legacyFactoryFunctions = [];
5354

5455
this.indexedGetter = null;
5556
this.indexedSetter = null;
@@ -394,6 +395,28 @@ class Interface {
394395
throw new Error(msg);
395396
}
396397
}
398+
399+
let legacyFactoryFunctionName;
400+
for (const attr of this.idl.extAttrs) {
401+
if (attr.name === "LegacyFactoryFunction") {
402+
if (!attr.rhs || attr.rhs.type !== "identifier" || !attr.arguments) {
403+
throw new Error(`[LegacyFactoryFunction] must take a named argument list`);
404+
}
405+
406+
const name = attr.rhs.value;
407+
if (legacyFactoryFunctionName === undefined) {
408+
legacyFactoryFunctionName = name;
409+
} else if (legacyFactoryFunctionName !== name) {
410+
// This is currently valid, but not used anywhere, and there are plans to disallow it:
411+
// https://github.com/jsdom/webidl2js/pull/213#issuecomment-621277733
412+
throw new Error(
413+
`Multiple [LegacyFactoryFunction] definitions with different names are not supported on ${this.name}`
414+
);
415+
}
416+
417+
this.legacyFactoryFunctions.push(attr);
418+
}
419+
}
397420
}
398421

399422
get supportsIndexedProperties() {
@@ -1202,17 +1225,25 @@ class Interface {
12021225

12031226
generateIface() {
12041227
this.str += `
1205-
function makeWrapper(globalObject) {
1228+
function makeWrapper(globalObject, newTarget) {
12061229
if (globalObject[ctorRegistrySymbol] === undefined) {
12071230
throw new Error('Internal error: invalid global object');
12081231
}
12091232
1210-
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1211-
if (ctor === undefined) {
1212-
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1233+
let prototype;
1234+
if (newTarget !== undefined) {
1235+
({ prototype } = newTarget);
1236+
}
1237+
1238+
if (!utils.isObject(prototype)) {
1239+
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1240+
if (ctor === undefined) {
1241+
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1242+
}
1243+
({ prototype } = ctor);
12131244
}
12141245
1215-
return Object.create(ctor.prototype);
1246+
return Object.create(prototype);
12161247
}
12171248
`;
12181249

@@ -1280,8 +1311,8 @@ class Interface {
12801311
return wrapper;
12811312
};
12821313
1283-
exports.new = globalObject => {
1284-
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject);
1314+
exports.new = (globalObject, newTarget) => {
1315+
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject, newTarget);
12851316
12861317
exports._internalSetup(wrapper, globalObject);
12871318
Object.defineProperty(wrapper, implSymbol, {
@@ -1555,6 +1586,65 @@ class Interface {
15551586
}
15561587
}
15571588

1589+
generateLegacyFactoryFunction() {
1590+
const { legacyFactoryFunctions } = this;
1591+
if (legacyFactoryFunctions.length === 0) {
1592+
return;
1593+
}
1594+
1595+
const name = legacyFactoryFunctions[0].rhs.value;
1596+
1597+
if (!name) {
1598+
throw new Error(`Internal error: The legacy factory function does not have a name (in interface ${this.name})`);
1599+
}
1600+
1601+
const overloads = Overloads.getEffectiveOverloads("legacy factory function", name, 0, this);
1602+
let minOp = overloads[0];
1603+
for (let i = 1; i < overloads.length; ++i) {
1604+
if (overloads[i].nameList.length < minOp.nameList.length) {
1605+
minOp = overloads[i];
1606+
}
1607+
}
1608+
1609+
const args = minOp.nameList;
1610+
const conversions = Parameters.generateOverloadConversions(
1611+
this.ctx, "legacy factory function", name, this, `Failed to construct '${name}': `);
1612+
this.requires.merge(conversions.requires);
1613+
1614+
const argsSpread = conversions.hasArgs ? "...args" : "";
1615+
1616+
this.str += `
1617+
function ${name}(${utils.formatArgs(args)}) {
1618+
if (new.target === undefined) {
1619+
throw new TypeError("Class constructor ${name} cannot be invoked without 'new'");
1620+
}
1621+
1622+
${conversions.body}
1623+
`;
1624+
1625+
// This implements the WebIDL legacy factory function behavior, as well as support for overridding
1626+
// the return type, which is used by HTML's element legacy factory functions:
1627+
this.str += `
1628+
const thisArgument = exports.new(globalObject, new.target);
1629+
const result = Impl.legacyFactoryFunction.call(thisArgument, globalObject, ${argsSpread});
1630+
return utils.tryWrapperForImpl(utils.isObject(result) ? result : thisArgument);
1631+
}
1632+
1633+
Object.defineProperty(${name}, "prototype", {
1634+
configurable: false,
1635+
enumerable: false,
1636+
writable: false,
1637+
value: ${this.name}.prototype
1638+
})
1639+
1640+
Object.defineProperty(globalObject, "${name}", {
1641+
configurable: true,
1642+
writable: true,
1643+
value: ${name}
1644+
});
1645+
`;
1646+
}
1647+
15581648
generateInstall() {
15591649
const { idl, name } = this;
15601650

@@ -1620,6 +1710,8 @@ class Interface {
16201710
}
16211711
}
16221712

1713+
this.generateLegacyFactoryFunction();
1714+
16231715
this.str += `
16241716
};
16251717
`;

lib/overloads.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ function getOperations(type, A, I) {
1111
case "constructor": {
1212
return I.constructorOperations;
1313
}
14+
case "legacy factory function":
15+
return I.legacyFactoryFunctions;
1416
}
1517
throw new RangeError(`${type}s are not yet supported`);
1618
}

0 commit comments

Comments
 (0)