Skip to content

Commit 877562d

Browse files
committed
Implement [LegacyFactoryFunction]
1 parent 8d92c30 commit 877562d

9 files changed

+6365
-3959
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
@@ -46,6 +46,7 @@ class Interface {
4646
this.attributes = new Map();
4747
this.staticAttributes = new Map();
4848
this.constants = new Map();
49+
this.legacyFactoryFunctions = [];
4950

5051
this.indexedGetter = null;
5152
this.indexedSetter = null;
@@ -386,6 +387,28 @@ class Interface {
386387
throw new Error(msg);
387388
}
388389
}
390+
391+
let legacyFactoryFunctionName;
392+
for (const attr of this.idl.extAttrs) {
393+
if (attr.name === "LegacyFactoryFunction") {
394+
if (!attr.rhs || attr.rhs.type !== "identifier" || !attr.arguments) {
395+
throw new Error(`[LegacyFactoryFunction] must take a named argument list`);
396+
}
397+
398+
const name = attr.rhs.value;
399+
if (legacyFactoryFunctionName === undefined) {
400+
legacyFactoryFunctionName = name;
401+
} else if (legacyFactoryFunctionName !== name) {
402+
// This is currently valid, but not used anywhere, and there are plans to disallow it:
403+
// https://github.com/jsdom/webidl2js/pull/213#issuecomment-621277733
404+
throw new Error(
405+
`Multiple [LegacyFactoryFunction] definitions with different names are not supported on ${this.name}`
406+
);
407+
}
408+
409+
this.legacyFactoryFunctions.push(attr);
410+
}
411+
}
389412
}
390413

391414
get supportsIndexedProperties() {
@@ -1194,17 +1217,25 @@ class Interface {
11941217

11951218
generateIface() {
11961219
this.str += `
1197-
function makeWrapper(globalObject) {
1220+
function makeWrapper(globalObject, newTarget) {
11981221
if (globalObject[ctorRegistrySymbol] === undefined) {
11991222
throw new Error('Internal error: invalid global object');
12001223
}
12011224
1202-
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1203-
if (ctor === undefined) {
1204-
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1225+
let prototype;
1226+
if (newTarget !== undefined) {
1227+
({ prototype } = newTarget);
1228+
}
1229+
1230+
if (!utils.isObject(prototype)) {
1231+
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1232+
if (ctor === undefined) {
1233+
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1234+
}
1235+
({ prototype } = ctor);
12051236
}
12061237
1207-
return Object.create(ctor.prototype);
1238+
return Object.create(prototype);
12081239
}
12091240
`;
12101241

@@ -1272,8 +1303,8 @@ class Interface {
12721303
return wrapper;
12731304
};
12741305
1275-
exports.new = globalObject => {
1276-
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject);
1306+
exports.new = (globalObject, newTarget) => {
1307+
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject, newTarget);
12771308
12781309
exports._internalSetup(wrapper, globalObject);
12791310
Object.defineProperty(wrapper, implSymbol, {
@@ -1547,6 +1578,65 @@ class Interface {
15471578
}
15481579
}
15491580

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

@@ -1617,6 +1707,8 @@ class Interface {
16171707
}
16181708
}
16191709

1710+
this.generateLegacyFactoryFunction();
1711+
16201712
this.str += `
16211713
};
16221714
`;

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)