mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-15 16:06:41 +03:00
b679408630
This makes the output more stable and fixes a build issue in a Google-internal environment. Change-Id: Ifa3b78e43ced12419a750dbb0bbde19316338640
868 lines
26 KiB
JavaScript
Executable File
868 lines
26 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 '';
|
|
|
|
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));
|