mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
169b3e2583
lib/player.js was being updated separately because: 1. Originally, we didn't have support for updating arbitrary files with release-please. 2. When we did get that support in release-please, it would trash the "-uncompiled" tag we have in uncompiled mode. By separating the uncompiled version string into two parts and using the extra-files feature of release-please, we can get the updater to preserve the "-uncompiled" tag and simplify the release workflow to only update the PR once per change instead of twice.
873 lines
27 KiB
JavaScript
Executable File
873 lines
27 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview
|
|
*
|
|
* A node script that generates externs automatically from the uncompiled
|
|
* source of a Closure project. Designed for Shaka Player, but may be usable
|
|
* in other projects as well. Does not depend on the Closure compiler itself.
|
|
*
|
|
* We were not able to get our externs generated by the Closure compiler. There
|
|
* were many issues with the Closure-generated externs, including the order of
|
|
* the externs and the replacement of record types and enums with their
|
|
* underlying types.
|
|
*
|
|
* This uses a node module called esprima to parse JavaScript, then explores the
|
|
* abstract syntax tree from esprima. It finds exported symbols and generates
|
|
* an appropriate extern definition for it.
|
|
*
|
|
* The generated externs are then topologically sorted according to the
|
|
* goog.provide and goog.require calls in the sources. No sorting is done
|
|
* within source files, and no sorting is done based on parameter types.
|
|
* Circular deps between source files will not be resolved, and deps not
|
|
* represented in goog.provide/goog.require will not be discovered.
|
|
*
|
|
* Arguments: --output <EXTERNS> <INPUT> [<INPUT> ...]
|
|
*/
|
|
|
|
// Load required modules.
|
|
let assert = require('assert');
|
|
if (assert.strict) {
|
|
// The "strict" mode was added in v9.9, use that if available.
|
|
assert = assert.strict;
|
|
}
|
|
const esprima = require('esprima');
|
|
const fs = require('fs');
|
|
|
|
// The annotations we will consider "exporting" a symbol.
|
|
const EXPORT_REGEX = /@(?:export|exportInterface|expose)\b/;
|
|
|
|
// TODO: revisit this when Closure Compiler supports partially-exported classes.
|
|
let partiallyExportedClassesDetected = false;
|
|
|
|
/**
|
|
* Topological sort of general objects using a DFS approach.
|
|
* Will add a __mark field to each object as part of the sorting process.
|
|
* @param {!Array.<T>} list
|
|
* @param {function(T):!Array.<T>} getDeps
|
|
* @return {!Array.<T>}
|
|
* @template T
|
|
* @see https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search
|
|
*/
|
|
function topologicalSort(list, getDeps) {
|
|
const sorted = [];
|
|
const NOT_VISITED = 0;
|
|
const MID_VISIT = 1;
|
|
const COMPLETELY_VISITED = 2;
|
|
|
|
// Mark all objects as not visited.
|
|
for (const object of list) {
|
|
object.__mark = NOT_VISITED;
|
|
}
|
|
|
|
// Visit each object.
|
|
for (const object of list) {
|
|
visit(object);
|
|
}
|
|
|
|
// Return the sorted list.
|
|
return sorted;
|
|
|
|
/**
|
|
* @param {T} object
|
|
* @template T
|
|
*/
|
|
function visit(object) {
|
|
if (object.__mark == MID_VISIT) {
|
|
assert.fail('Dependency cycle detected!');
|
|
} else if (object.__mark == NOT_VISITED) {
|
|
object.__mark = MID_VISIT;
|
|
|
|
// Visit all dependencies.
|
|
for (const dep of getDeps(object)) {
|
|
visit(dep);
|
|
}
|
|
|
|
object.__mark = COMPLETELY_VISITED;
|
|
|
|
// Push this object onto the list. All transitive dependencies have
|
|
// already been added to the list.
|
|
sorted.push(object);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is a call node.
|
|
*/
|
|
function isCallNode(node) {
|
|
// Example node: {
|
|
// type: 'ExpressionStatement',
|
|
// expression: { type: 'CallExpression', callee: {...}, arguments: [...] },
|
|
// }
|
|
return node.type == 'ExpressionStatement' &&
|
|
node.expression.type == 'CallExpression';
|
|
}
|
|
|
|
|
|
/**
|
|
* Pretty-print a node via console.log. Useful for debugging and development
|
|
* to see what the AST looks like.
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
*/
|
|
function dumpNode(node) {
|
|
console.log(JSON.stringify(node, null, ' '));
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is a call to goog.provide.
|
|
*/
|
|
function isProvideNode(node) {
|
|
return isCallNode(node) &&
|
|
getIdentifierString(node.expression.callee) == 'goog.provide';
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is a call to goog.require.
|
|
*/
|
|
function isRequireNode(node) {
|
|
return isCallNode(node) &&
|
|
getIdentifierString(node.expression.callee) == 'goog.require';
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is an exported symbol or property.
|
|
*/
|
|
function isExportNode(node) {
|
|
const doc = getLeadingBlockComment(node);
|
|
return doc && EXPORT_REGEX.test(doc);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is a class assignment.
|
|
*/
|
|
function isClassAssignmentNode(node) {
|
|
return node.type == 'ExpressionStatement' &&
|
|
node.expression.type == 'AssignmentExpression' &&
|
|
node.expression.right.type == 'ClassExpression';
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {boolean} true if this is a class assignment with exported members.
|
|
*/
|
|
function isPartiallyExportedClassAssignmentNode(node) {
|
|
if (!isClassAssignmentNode(node)) {
|
|
return false;
|
|
}
|
|
|
|
const rightSide = node.expression.right;
|
|
// Example code: foo.bar = class bar2 extends foo.baz { /* ... */ };
|
|
// Example right side: {
|
|
// id: { name: 'bar' }, // or null
|
|
// superClass: { type: 'MemberExpression', ... }, // or null
|
|
// body: { body: [ ... ] },
|
|
// }
|
|
|
|
for (const member of rightSide.body.body) {
|
|
// Only look at exported members. Constructors are exported implicitly
|
|
// when the class is exported.
|
|
const comment = getLeadingBlockComment(member);
|
|
if (EXPORT_REGEX.test(comment)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {string} A reconstructed leading comment block for the node.
|
|
* If there are multiple comments before this node, we will take the most
|
|
* recent block comment, as that is the one that would contain any applicable
|
|
* jsdoc/closure annotations for this symbol.
|
|
*/
|
|
function getLeadingBlockComment(node) {
|
|
// Example code: /** @summary blah */ /** @export */ foo.bar = ...;
|
|
// Example node: {
|
|
// type: 'ExpressionStatement',
|
|
// expression: { ... },
|
|
// leadingComments: [
|
|
// { type: 'Block', value: '* @summary blah ' },
|
|
// { type: 'Block', value: '* @export ' },
|
|
// ],
|
|
// }
|
|
if (!node.leadingComments || !node.leadingComments.length) {
|
|
return null;
|
|
}
|
|
|
|
// Ignore non-block comments, since those are not jsdoc/Closure comments.
|
|
const blockComments = node.leadingComments.filter((comment) => {
|
|
return comment.type == 'Block';
|
|
});
|
|
if (!blockComments.length) {
|
|
return null;
|
|
}
|
|
|
|
// In case there are multiple (for example, a file-level comment that also
|
|
// preceeds the node), take the most recent one, which is closest to the node.
|
|
const mostRecentComment = blockComments[blockComments.length - 1];
|
|
|
|
// Reconstruct the original block comment by adding back /* and */.
|
|
return '/*' + mostRecentComment.value + '*/';
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {number} idx An argument index from the call node.
|
|
* @param {ASTNode} node A node from the abstract syntax tree.
|
|
* @return {string} The argument value as a string.
|
|
*/
|
|
function getArgumentFromCallNode(idx, node) {
|
|
// Example node: {
|
|
// type: 'ExpressionStatement',
|
|
// expression: { type: 'CallExpression', callee: {...}, arguments: [...] },
|
|
// }
|
|
assert(isCallNode(node));
|
|
return node.expression.arguments[idx].value;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node An identifier or member node from the abstract syntax
|
|
* tree.
|
|
* @return {string} The identifier as a string.
|
|
*/
|
|
function getIdentifierString(node) {
|
|
if (node.type == 'Identifier') {
|
|
// Example code: foo
|
|
// Example node: { type: 'Identifier', name: 'foo' }
|
|
return node.name;
|
|
}
|
|
|
|
assert.equal(node.type, 'MemberExpression');
|
|
// Example code: foo.bar.baz
|
|
// Example node: {
|
|
// type: 'MemberExpression',
|
|
// object: {
|
|
// type: 'MemberExpression',
|
|
// object: { type: 'Identifier', name: 'foo' },
|
|
// property: { type: 'Identifier', name: 'bar' },
|
|
// },
|
|
// property: { type: 'Identifier', name: 'baz' },
|
|
// }
|
|
return getIdentifierString(node.object) + '.' +
|
|
getIdentifierString(node.property);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A function definition node from the abstract syntax
|
|
* tree.
|
|
* @return {!Array.<string>} a list of the parameter names.
|
|
*/
|
|
function getFunctionParameters(node) {
|
|
assert(node.type == 'FunctionExpression' ||
|
|
node.type == 'ArrowFunctionExpression');
|
|
// Example code: function(x, y, z = null, ...varArgs) {...}
|
|
// Example node: {
|
|
// params: [
|
|
// { type: 'Identifier', name: 'x' },
|
|
// { type: 'Identifier', name: 'y' },
|
|
// {
|
|
// type: 'AssignmentPattern',
|
|
// left: { type: 'Identifier', name: 'z' },
|
|
// right: { type: 'Literal', raw: 'null' },
|
|
// },
|
|
// {
|
|
// type: 'RestElement',
|
|
// argument: { type: 'Identifier', name: 'varArgs' },
|
|
// },
|
|
// ],
|
|
// body: {...},
|
|
// }
|
|
return node.params.map((param) => {
|
|
if (param.type == 'Identifier') {
|
|
return param.name;
|
|
} else if (param.type == 'AssignmentPattern') {
|
|
return param.left.name;
|
|
} else {
|
|
assert.equal(param.type, 'RestElement');
|
|
return '...' + param.argument.name;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Take the original block comment and prep it for the externs by removing
|
|
* export annotations and blank lines.
|
|
*
|
|
* @param {string}
|
|
* @return {string}
|
|
*/
|
|
function removeExportAnnotationsFromComment(comment) {
|
|
// Remove @export annotations.
|
|
comment = comment.replace(EXPORT_REGEX, '');
|
|
|
|
// Split into lines, remove empty comment lines, then recombine.
|
|
comment = comment.split('\n')
|
|
.filter((line) => !/^ *\*? *$/.test(line))
|
|
.join('\n');
|
|
|
|
return comment;
|
|
}
|
|
|
|
|
|
/**
|
|
* Recursively find all expression statements in all block nodes.
|
|
* @param {ASTNode} node
|
|
* @return {!Array.<ASTNode>}
|
|
*/
|
|
function getAllExpressionStatements(node) {
|
|
assert(node.body && node.body.body);
|
|
const expressionStatements = [];
|
|
for (const childNode of node.body.body) {
|
|
if (childNode.type == 'ExpressionStatement') {
|
|
expressionStatements.push(childNode);
|
|
} else if (childNode.body) {
|
|
const childExpressions = getAllExpressionStatements(childNode);
|
|
expressionStatements.push(...childExpressions);
|
|
}
|
|
}
|
|
return expressionStatements;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {!Set.<string>} names A set of the names of exported nodes.
|
|
* @param {ASTNode} node An exported node from the abstract syntax tree.
|
|
* @return {string} An extern string for this node.
|
|
*/
|
|
function createExternFromExportNode(names, node) {
|
|
assert.equal(node.type, 'ExpressionStatement', 'Unknown node type');
|
|
|
|
let comment = getLeadingBlockComment(node);
|
|
comment = removeExportAnnotationsFromComment(comment);
|
|
|
|
let name;
|
|
let assignment;
|
|
|
|
switch (node.expression.type) {
|
|
case 'AssignmentExpression':
|
|
// Example code: /** @export */ foo.bar = function(...) { ... };
|
|
// Example node.expression: {
|
|
// operator: '=',
|
|
// left: {
|
|
// type: 'MemberExpression',
|
|
// object: { type: 'Identifier', name: 'foo' },
|
|
// property: { type: 'Identifier', name: 'bar' },
|
|
// }, right: {
|
|
// type: 'FunctionExpression', params: [ ... ], body: {...}
|
|
// }
|
|
// }
|
|
name = getIdentifierString(node.expression.left);
|
|
assignment = createExternAssignment(name, node.expression.right,
|
|
/* alwaysIncludeConstructor= */ true);
|
|
break;
|
|
|
|
case 'MemberExpression':
|
|
// Example code: /** @export */ foo.bar;
|
|
// Example node.expression: {
|
|
// object: { type: 'Identifier', name: 'foo' },
|
|
// property: { type: 'Identifier', name: 'bar' },
|
|
// }
|
|
name = getIdentifierString(node.expression);
|
|
assignment = '';
|
|
break;
|
|
|
|
default:
|
|
assert.fail('Unexpected expression type: ' + node.expression.type);
|
|
}
|
|
|
|
// Keep track of the names we've externed.
|
|
names.add(name);
|
|
// Generate the actual extern string.
|
|
let externString = comment + '\n' + name + assignment + ';\n';
|
|
|
|
// Find this.foo = bar in the constructor, and potentially generate externs
|
|
// for that, too.
|
|
if (node.expression.type == 'AssignmentExpression') {
|
|
const rightSide = node.expression.right;
|
|
|
|
if (rightSide.type == 'FunctionExpression' &&
|
|
comment.includes('@constructor')) {
|
|
externString += createExternsFromConstructor(name, rightSide);
|
|
} else if (rightSide.type == 'ClassExpression') {
|
|
const ctor = getClassConstructor(node.expression.right);
|
|
if (ctor) {
|
|
externString += createExternsFromConstructor(name, ctor);
|
|
}
|
|
}
|
|
}
|
|
return externString;
|
|
}
|
|
|
|
|
|
/**
|
|
* Some classes are not exported, but contain exported members. These need to
|
|
* have externs generated, too.
|
|
*
|
|
* @param {!Set.<string>} names A set of the names of exported nodes.
|
|
* @param {ASTNode} node An exported node from the abstract syntax tree.
|
|
* @return {string} An extern string for this node.
|
|
*/
|
|
function createExternFromPartiallyExportedClassAssignmentNode(names, node) {
|
|
assert.equal(node.type, 'ExpressionStatement', 'Unknown node type');
|
|
assert.equal(node.expression.type, 'AssignmentExpression',
|
|
'Should be assignment node');
|
|
assert.equal(node.expression.right.type, 'ClassExpression',
|
|
'Should be class assignment');
|
|
|
|
const name = getIdentifierString(node.expression.left);
|
|
const assignment = createExternAssignment(name, node.expression.right,
|
|
/* alwaysIncludeConstructor= */ false);
|
|
|
|
let externString = name + assignment + ';\n';
|
|
|
|
// Find this.foo = bar in the constructor, and potentially generate externs
|
|
// for that, too.
|
|
const rightSide = node.expression.right;
|
|
const ctor = getClassConstructor(node.expression.right);
|
|
if (ctor) {
|
|
externString += createExternsFromConstructor(name, ctor);
|
|
}
|
|
|
|
// Keep track of the names we've externed.
|
|
names.add(name);
|
|
|
|
return externString;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {ASTNode} node A method node from the abstract syntax tree.
|
|
* @return {string} The extern string for this method.
|
|
*/
|
|
function createExternMethod(node) {
|
|
// Example code: foo.bar = class {
|
|
// baz() { ... }
|
|
// };
|
|
// Example node: {
|
|
// leadingComments: [ ... ],
|
|
// static: false,
|
|
// key: Identifier,
|
|
// value: FunctionExpression,
|
|
// }
|
|
const id = getIdentifierString(node.key);
|
|
let comment = getLeadingBlockComment(node);
|
|
if (!comment) {
|
|
if (id == 'constructor') {
|
|
// ES6 constructors don't necessarily need comments; a comment along the
|
|
// lines of "Creates a Foo object." doesn't really add anything.
|
|
comment = '';
|
|
} else {
|
|
throw new Error('No leading block comment for: ' + id);
|
|
}
|
|
}
|
|
comment = removeExportAnnotationsFromComment(comment);
|
|
|
|
const params = getFunctionParameters(node.value);
|
|
|
|
let methodString = (comment ? ' ' + comment + '\n' : '') + ' ';
|
|
if (node.static) {
|
|
methodString += 'static ';
|
|
}
|
|
methodString += id + '(' + params.join(', ') + ') {}';
|
|
return methodString;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find the constructor of an ES6 class, if it exists.
|
|
*
|
|
* @param {ASTNode} className
|
|
* @return {ASTNode}
|
|
*/
|
|
function getClassConstructor(classNode) {
|
|
// Example class node: {
|
|
// type: 'ClassExpression',
|
|
// body: {
|
|
// type: 'ClassBody',
|
|
// body: [ MethodDefinition, ... ],
|
|
// }
|
|
// }
|
|
//
|
|
// Example method node: {
|
|
// type: 'MethodDefinition',
|
|
// key: { type: 'Identifier', name: 'constructor' },
|
|
// value: {
|
|
// type: 'FunctionExpression',
|
|
// params: [ [Identifier], [Identifier], [Identifier] ],
|
|
// body: { type: 'BlockStatement', body: [Array] },
|
|
// }
|
|
|
|
assert.equal(classNode.type, 'ClassExpression');
|
|
|
|
for (const member of classNode.body.body) {
|
|
if (member.type == 'MethodDefinition' && member.key.name == 'constructor') {
|
|
return member.value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {string} name The name of the thing we are assigning.
|
|
* @param {ASTNode} node An assignment node from the abstract syntax tree.
|
|
* @param {boolean} alwaysIncludeConstructor Include the constructor of a class
|
|
* expression, even if there is no export annotation.
|
|
* @return {string} The assignment part of the extern string for this node.
|
|
*/
|
|
function createExternAssignment(name, node, alwaysIncludeConstructor) {
|
|
switch (node.type) {
|
|
case 'ClassExpression': {
|
|
// Example code: foo.bar = class bar2 extends foo.baz { /* ... */ };
|
|
// Example node: {
|
|
// id: { name: 'bar' }, // or null
|
|
// superClass: { type: 'MemberExpression', ... }, // or null
|
|
// body: { body: [ ... ] },
|
|
// }
|
|
let classString = ' = class ';
|
|
if (node.id) {
|
|
classString += getIdentifierString(node.id) + ' ';
|
|
}
|
|
if (node.superClass) {
|
|
classString += 'extends ' + getIdentifierString(node.superClass) + ' ';
|
|
}
|
|
classString += '{\n';
|
|
for (const member of node.body.body) {
|
|
const comment = getLeadingBlockComment(member);
|
|
|
|
if (EXPORT_REGEX.test(comment)) {
|
|
// This has an export annotation, so fall through and generate
|
|
// externs.
|
|
} else {
|
|
// If there's no export annotation, we may make an exception for the
|
|
// constructor in some situations.
|
|
if (member.key.name == 'constructor' && alwaysIncludeConstructor) {
|
|
// Fall through and generate externs.
|
|
} else {
|
|
// Skip extern generation.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
assert.equal(
|
|
member.type, 'MethodDefinition',
|
|
'Unexpected exported member type in exported class!');
|
|
|
|
classString += createExternMethod(member) + '\n';
|
|
}
|
|
classString += '}';
|
|
return classString;
|
|
}
|
|
|
|
case 'ArrowFunctionExpression':
|
|
case 'FunctionExpression': {
|
|
// Example code: foo.square = function(x) { return x * x; };
|
|
// Example node: { params: [ { type: 'Identifier', name: 'x' } ] }
|
|
const params = getFunctionParameters(node);
|
|
return ' = function(' + params.join(', ') + ') {}';
|
|
}
|
|
|
|
case 'ObjectExpression': {
|
|
// Example code: foo.Bar = { 'ABC': 1, DEF: 2 };
|
|
// Example node: {
|
|
// properties: [ {
|
|
// kind: 'init',
|
|
// key: { type: 'Literal', value: 'ABC' }
|
|
// value: { type: 'Literal', value: 1 }
|
|
// }, {
|
|
// kind: 'init',
|
|
// key: { type: 'Identifier', name: 'DEF' }
|
|
// value: { type: 'Literal', value: 2 }
|
|
// } ]
|
|
// }
|
|
const propertyStrings = node.properties.map((prop) => {
|
|
assert.equal(prop.kind, 'init');
|
|
assert(prop.key.type == 'Literal' || prop.key.type == 'Identifier');
|
|
// Literal indicates a quoted name in the source, while Identifier is
|
|
// an unquoted name. In the case of Literal, key.raw gets us the
|
|
// unquoted name, we end up with an unquoted name in both cases.
|
|
const name = prop.key.type == 'Literal' ? prop.key.raw : prop.key.name;
|
|
assert.equal(prop.value.type, 'Literal');
|
|
return ' ' + name + ': ' + prop.value.raw;
|
|
});
|
|
return ' = {\n' + propertyStrings.join(',\n') + '\n}';
|
|
}
|
|
|
|
case 'Identifier':
|
|
// Example code: /** @const {string} @export */ foo.version = VERSION;
|
|
// Example extern: /** @const {string} */ foo.version;
|
|
return '';
|
|
|
|
case 'Literal':
|
|
// Example code: /** @const {string} @export */ foo.version = 'v1.0.0';
|
|
// Example extern: /** @const {string} */ foo.version;
|
|
return '';
|
|
|
|
case 'BinaryExpression':
|
|
// Example code: /** @const {string} @export */ foo.version = 'a' + 'b';
|
|
// Example extern: /** @const {string} */ foo.version;
|
|
return '';
|
|
|
|
default:
|
|
assert.fail('Unexpected export type: ' + node.type);
|
|
return ''; // Shouldn't be hit, but linter wants a return statement.
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Look for exports in a constructor body. If we don't do this, we may end up
|
|
* with errors about classes not fully implementing their interfaces. In
|
|
* reality, the interface is implemented by assigning members on "this".
|
|
*
|
|
* @param {string} className
|
|
* @param {ASTNode} constructorNode
|
|
* @return {string}
|
|
*/
|
|
function createExternsFromConstructor(className, constructorNode) {
|
|
// Example code:
|
|
//
|
|
// /** @interface @exportInterface */
|
|
// FooLike = function() {};
|
|
//
|
|
// /** @exportInterface @type {number} */
|
|
// FooLike.prototype.bar;
|
|
//
|
|
// /** @export @implements {FooLike} */
|
|
// class Foo {
|
|
// constructor() {
|
|
// /** @override @exportInterface */
|
|
// this.bar = 10;
|
|
// }
|
|
// };
|
|
//
|
|
// Example externs:
|
|
//
|
|
// /**
|
|
// * Generated by createExternFromExportNode:
|
|
// * @implements {FooLike}
|
|
// */
|
|
// class Foo {
|
|
// constructor() {}
|
|
// }
|
|
//
|
|
// /**
|
|
// * Generated by createExternsFromConstructor:
|
|
// * @override
|
|
// */
|
|
// Foo.prototype.bar;
|
|
|
|
const expressionStatements = getAllExpressionStatements(constructorNode);
|
|
let externString = '';
|
|
|
|
for (const statement of expressionStatements) {
|
|
const left = statement.expression.left;
|
|
const right = statement.expression.right;
|
|
|
|
// Skip anything that isn't an assignment to a member of "this".
|
|
if (statement.expression.type != 'AssignmentExpression' ||
|
|
left.type != 'MemberExpression' ||
|
|
left.object.type != 'ThisExpression') {
|
|
continue;
|
|
}
|
|
|
|
assert(left);
|
|
assert(right);
|
|
|
|
// Skip anything that isn't exported.
|
|
let comment = getLeadingBlockComment(statement);
|
|
if (!EXPORT_REGEX.test(comment)) {
|
|
continue;
|
|
}
|
|
|
|
comment = removeExportAnnotationsFromComment(comment);
|
|
|
|
assert.equal(left.property.type, 'Identifier');
|
|
const name = className + '.prototype.' + left.property.name;
|
|
externString += comment + '\n' + name + ';\n';
|
|
}
|
|
|
|
return externString;
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {!Set.<string>} names A set of the names of exported nodes.
|
|
* @param {string} inputPath
|
|
* @return {{
|
|
* path: string,
|
|
* provides: !Array.<string>,
|
|
* requires: !Array.<string>,
|
|
* externs: string,
|
|
* }}
|
|
*/
|
|
function generateExterns(names, inputPath) {
|
|
// Load and parse the code, with comments attached to the nodes.
|
|
const code = fs.readFileSync(inputPath, 'utf-8');
|
|
const program = esprima.parse(code, {attachComment: true});
|
|
assert.equal(program.type, 'Program');
|
|
|
|
const body = program.body;
|
|
const provides = program.body.filter(isProvideNode)
|
|
.map((node) => getArgumentFromCallNode(0, node));
|
|
const requires = program.body.filter(isRequireNode)
|
|
.map((node) => getArgumentFromCallNode(0, node));
|
|
|
|
// Get all exported nodes and all classes, in order.
|
|
const rawExterns = program.body.map((node) => {
|
|
if (isExportNode(node)) {
|
|
// Explicitly-exported nodes are handled here.
|
|
return createExternFromExportNode(names, node);
|
|
} else if (isPartiallyExportedClassAssignmentNode(node)) {
|
|
// Some classes are not exported, but contain exported members. These
|
|
// need to have externs generated, too.
|
|
|
|
// But wait! The latest compiler won't actually export those correctly!
|
|
// TODO: File a bug against the Closure Compiler.
|
|
// In the mean time, log these now and throw an error at the end to make
|
|
// sure we are generating usable releases. This tends to affect our
|
|
// plugin registration APIs, and apps should definitely be able to use
|
|
// those!
|
|
if (!partiallyExportedClassesDetected) {
|
|
partiallyExportedClassesDetected = true;
|
|
console.log('The Closure Compiler does not handle partially-exported ' +
|
|
'classes correctly! The following classes need to be exported:');
|
|
}
|
|
|
|
const name = getIdentifierString(node.expression.left);
|
|
console.log(' * ' + name);
|
|
|
|
return createExternFromPartiallyExportedClassAssignmentNode(names, node);
|
|
} else {
|
|
// Ignore anything else, and don't generate any externs.
|
|
return '';
|
|
}
|
|
});
|
|
|
|
const externs = rawExterns.join('');
|
|
|
|
return {
|
|
path: inputPath,
|
|
provides: provides,
|
|
requires: requires,
|
|
externs: externs,
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate externs from exported code.
|
|
* Arguments: --output <EXTERNS> <INPUT> [<INPUT> ...]
|
|
*
|
|
* @param {!Array.<string>} args The args to this script, not counting node and
|
|
* the script name itself.
|
|
*/
|
|
function main(args) {
|
|
const inputPaths = [];
|
|
let outputPath;
|
|
|
|
for (let i = 0; i < args.length; ++i) {
|
|
if (args[i] == '--output') {
|
|
outputPath = args[i + 1];
|
|
++i;
|
|
} else {
|
|
inputPaths.push(args[i]);
|
|
}
|
|
}
|
|
assert(outputPath, 'You must specify output file with --output <EXTERNS>');
|
|
assert(inputPaths.length, 'You must specify at least one input file.');
|
|
|
|
// Generate externs for all input paths.
|
|
const names = new Set();
|
|
const results = inputPaths.map((path) => generateExterns(names, path));
|
|
|
|
// TODO: revisit this when the compiler supports partially-exported classes.
|
|
if (partiallyExportedClassesDetected) {
|
|
throw new Error(
|
|
'Partially exported classes are not supported in the compiler!');
|
|
}
|
|
|
|
// Sort them in dependency order.
|
|
const sorted = topologicalSort(results, /* getDeps= */ (object) => {
|
|
return object.requires.map((id) => {
|
|
const dep = results.find((x) => x.provides.includes(id));
|
|
assert(dep, 'Cannot find dependency: ' + id);
|
|
return dep;
|
|
});
|
|
});
|
|
|
|
// Generate namespaces for all externs. For example, if we extern
|
|
// foo.bar.baz, foo and foo.bar will both need to be declared first.
|
|
const namespaces = new Set();
|
|
const namespaceDeclarations = [];
|
|
for (const name of Array.from(names).sort()) {
|
|
// Add the full name "foo.bar.baz" and its prototype ahead of time. We
|
|
// should never generate these as namespaces.
|
|
namespaces.add(name);
|
|
namespaces.add(name + '.prototype');
|
|
|
|
// For name "foo.bar.baz", iterate over partialName "foo" and "foo.bar".
|
|
const pieces = name.split('.');
|
|
for (let i = 1; i < pieces.length; ++i) {
|
|
const partialName = pieces.slice(0, i).join('.');
|
|
if (!namespaces.has(partialName)) {
|
|
let declaration;
|
|
if (i == 1) {
|
|
declaration = '/** @namespace */\n';
|
|
declaration += 'window.';
|
|
} else {
|
|
declaration = '/** @const */\n';
|
|
}
|
|
declaration += partialName + ' = {};\n';
|
|
namespaceDeclarations.push(declaration);
|
|
namespaces.add(partialName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get externs.
|
|
const externs = sorted.map((x) => x.externs).join('');
|
|
|
|
// Get license header.
|
|
const licenseHeader = fs.readFileSync(__dirname + '/license-header', 'utf-8');
|
|
|
|
// Output generated externs, with an appropriate header.
|
|
fs.writeFileSync(outputPath,
|
|
licenseHeader +
|
|
'/**\n' +
|
|
' * @fileoverview Generated externs. DO NOT EDIT!\n' +
|
|
' * @externs\n' +
|
|
' * @suppress {duplicate} To prevent compiler errors with the\n' +
|
|
' * namespace being declared both here and by goog.provide in the\n' +
|
|
' * library.\n' +
|
|
' */\n\n' +
|
|
namespaceDeclarations.join('') + '\n' + externs);
|
|
}
|
|
|
|
|
|
// Skip argv[0], which is the node binary, and argv[1], which is the script.
|
|
main(process.argv.slice(2));
|