mirror of
https://github.com/fluencelabs/assemblyscript
synced 2025-04-26 15:32:16 +00:00
451 lines
21 KiB
JavaScript
451 lines
21 KiB
JavaScript
|
// © 2015-2019 SitePen, Inc. New BSD License.
|
||
|
// see: https://github.com/SitePen/dts-generator
|
||
|
(function (factory) {
|
||
|
var v = factory(require, exports);
|
||
|
if (v !== undefined) module.exports = v;
|
||
|
})(function (require, exports) {
|
||
|
"use strict";
|
||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||
|
const fs = require("fs");
|
||
|
const glob = require("glob");
|
||
|
const mkdirp = require("mkdirp");
|
||
|
const os = require("os");
|
||
|
const pathUtil = require("path");
|
||
|
const ts = require("typescript");
|
||
|
// declare some constants so we don't have magic integers without explanation
|
||
|
const DTSLEN = '.d.ts'.length;
|
||
|
const filenameToMid = (function () {
|
||
|
if (pathUtil.sep === '/') {
|
||
|
return function (filename) {
|
||
|
return filename;
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
const separatorExpression = new RegExp(pathUtil.sep.replace('\\', '\\\\'), 'g');
|
||
|
return function (filename) {
|
||
|
return filename.replace(separatorExpression, '/');
|
||
|
};
|
||
|
}
|
||
|
})();
|
||
|
/**
|
||
|
* A helper function that takes TypeScript diagnostic errors and returns an error
|
||
|
* object.
|
||
|
* @param diagnostics The array of TypeScript Diagnostic objects
|
||
|
*/
|
||
|
function getError(diagnostics) {
|
||
|
let message = 'Declaration generation failed';
|
||
|
diagnostics.forEach(function (diagnostic) {
|
||
|
// not all errors have an associated file: in particular, problems with a
|
||
|
// the tsconfig.json don't; the messageText is enough to diagnose in those
|
||
|
// cases.
|
||
|
if (diagnostic.file) {
|
||
|
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
||
|
message +=
|
||
|
`\n${diagnostic.file.fileName}(${position.line + 1},${position.character + 1}): ` +
|
||
|
`error TS${diagnostic.code}: ${diagnostic.messageText}`;
|
||
|
}
|
||
|
else {
|
||
|
message += `\nerror TS${diagnostic.code}: ${diagnostic.messageText}`;
|
||
|
}
|
||
|
});
|
||
|
const error = new Error(message);
|
||
|
error.name = 'EmitterError';
|
||
|
return error;
|
||
|
}
|
||
|
function getFilenames(baseDir, files) {
|
||
|
return files.map(function (filename) {
|
||
|
const resolvedFilename = pathUtil.resolve(filename);
|
||
|
if (resolvedFilename.indexOf(baseDir) === 0) {
|
||
|
return resolvedFilename;
|
||
|
}
|
||
|
return pathUtil.resolve(baseDir, filename);
|
||
|
});
|
||
|
}
|
||
|
function processTree(sourceFile, replacer) {
|
||
|
let code = '';
|
||
|
let cursorPosition = 0;
|
||
|
function skip(node) {
|
||
|
cursorPosition = node.end;
|
||
|
}
|
||
|
function readThrough(node) {
|
||
|
code += sourceFile.text.slice(cursorPosition, node.pos);
|
||
|
cursorPosition = node.pos;
|
||
|
}
|
||
|
function visit(node) {
|
||
|
readThrough(node);
|
||
|
const replacement = replacer(node);
|
||
|
if (replacement != null) {
|
||
|
code += replacement;
|
||
|
skip(node);
|
||
|
}
|
||
|
else {
|
||
|
ts.forEachChild(node, visit);
|
||
|
}
|
||
|
}
|
||
|
visit(sourceFile);
|
||
|
code += sourceFile.text.slice(cursorPosition);
|
||
|
return code;
|
||
|
}
|
||
|
/**
|
||
|
* Load and parse a TSConfig File
|
||
|
* @param options The dts-generator options to load config into
|
||
|
* @param fileName The path to the file
|
||
|
*/
|
||
|
function getTSConfig(fileName) {
|
||
|
// TODO this needs a better design than merging stuff into options.
|
||
|
// the trouble is what to do when no tsconfig is specified...
|
||
|
const configText = fs.readFileSync(fileName, { encoding: 'utf8' });
|
||
|
const result = ts.parseConfigFileTextToJson(fileName, configText);
|
||
|
if (result.error) {
|
||
|
throw getError([result.error]);
|
||
|
}
|
||
|
const configObject = result.config;
|
||
|
const configParseResult = ts.parseJsonConfigFileContent(configObject, ts.sys, pathUtil.dirname(fileName));
|
||
|
if (configParseResult.errors && configParseResult.errors.length) {
|
||
|
throw getError(configParseResult.errors);
|
||
|
}
|
||
|
return [
|
||
|
configParseResult.fileNames,
|
||
|
configParseResult.options
|
||
|
];
|
||
|
}
|
||
|
function isNodeKindImportDeclaration(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.ImportDeclaration;
|
||
|
}
|
||
|
function isNodeKindExternalModuleReference(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.ExternalModuleReference;
|
||
|
}
|
||
|
function isNodeKindStringLiteral(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.StringLiteral;
|
||
|
}
|
||
|
function isNodeKindExportDeclaration(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.ExportDeclaration;
|
||
|
}
|
||
|
function isNodeKindExportAssignment(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.ExportAssignment;
|
||
|
}
|
||
|
function isNodeKindModuleDeclaration(value) {
|
||
|
return value && value.kind === ts.SyntaxKind.ModuleDeclaration;
|
||
|
}
|
||
|
function generate(options) {
|
||
|
if (Boolean(options.main) !== Boolean(options.name)) {
|
||
|
if (Boolean(options.name)) {
|
||
|
// since options.name used to do double duty as the prefix, let's be
|
||
|
// considerate and point out that name should be replaced with prefix.
|
||
|
// TODO update this error message when we finalize which version this change
|
||
|
// will be released in.
|
||
|
throw new Error(`name and main must be used together. Perhaps you want prefix instead of
|
||
|
name? In dts-generator version 2.1, name did double duty as the option to
|
||
|
use to prefix module names with, but in >=2.2 the name option was split
|
||
|
into two; prefix is what is now used to prefix imports and module names
|
||
|
in the output.`);
|
||
|
}
|
||
|
else {
|
||
|
throw new Error('name and main must be used together.');
|
||
|
}
|
||
|
}
|
||
|
const noop = function () { };
|
||
|
const sendMessage = options.sendMessage || noop;
|
||
|
const verboseMessage = options.verbose ? sendMessage : noop;
|
||
|
let compilerOptions = {};
|
||
|
let files = options.files;
|
||
|
/* following tsc behaviour, if a project is specified, or if no files are specified then
|
||
|
* attempt to load tsconfig.json */
|
||
|
if (options.project || !options.files || options.files.length === 0) {
|
||
|
verboseMessage(`project = "${options.project || options.baseDir}"`);
|
||
|
// if project isn't specified, use baseDir. If it is and it's a directory,
|
||
|
// assume we want tsconfig.json in that directory. If it is a file, though
|
||
|
// use that as our tsconfig.json. This allows for projects that have more
|
||
|
// than one tsconfig.json file.
|
||
|
let tsconfigFilename;
|
||
|
if (Boolean(options.project)) {
|
||
|
if (fs.lstatSync(options.project).isDirectory()) {
|
||
|
tsconfigFilename = pathUtil.join(options.project, 'tsconfig.json');
|
||
|
}
|
||
|
else {
|
||
|
// project isn't a diretory, it's a file
|
||
|
tsconfigFilename = options.project;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
tsconfigFilename = pathUtil.join(options.baseDir, 'tsconfig.json');
|
||
|
}
|
||
|
if (fs.existsSync(tsconfigFilename)) {
|
||
|
verboseMessage(` parsing "${tsconfigFilename}"`);
|
||
|
[files, compilerOptions] = getTSConfig(tsconfigFilename);
|
||
|
}
|
||
|
else {
|
||
|
sendMessage(`No "tsconfig.json" found at "${tsconfigFilename}"!`);
|
||
|
return new Promise(function ({}, reject) {
|
||
|
reject(new SyntaxError('Unable to resolve configuration.'));
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
const eol = options.eol || os.EOL;
|
||
|
const nonEmptyLineStart = new RegExp(eol + '(?!' + eol + '|$)', 'g');
|
||
|
const indent = options.indent === undefined ? '\t' : options.indent;
|
||
|
// use input values if tsconfig leaves any of these undefined.
|
||
|
// this is for backwards compatibility
|
||
|
compilerOptions.declaration = true;
|
||
|
compilerOptions.target = compilerOptions.target || ts.ScriptTarget.Latest; // is this necessary?
|
||
|
compilerOptions.moduleResolution = compilerOptions.moduleResolution || options.moduleResolution;
|
||
|
compilerOptions.outDir = compilerOptions.outDir || options.outDir;
|
||
|
// TODO should compilerOptions.baseDir come into play?
|
||
|
const baseDir = pathUtil.resolve(compilerOptions.rootDir || options.project || options.baseDir);
|
||
|
const outDir = compilerOptions.outDir;
|
||
|
verboseMessage(`baseDir = "${baseDir}"`);
|
||
|
verboseMessage(`target = ${compilerOptions.target}`);
|
||
|
verboseMessage(`outDir = ${compilerOptions.outDir}`);
|
||
|
verboseMessage(`rootDir = ${compilerOptions.rootDir}`);
|
||
|
verboseMessage(`moduleResolution = ${compilerOptions.moduleResolution}`);
|
||
|
const filenames = getFilenames(baseDir, files);
|
||
|
verboseMessage('filenames:');
|
||
|
filenames.forEach(name => { verboseMessage(' ' + name); });
|
||
|
const excludesMap = {};
|
||
|
options.exclude = options.exclude || ['node_modules/**/*.d.ts'];
|
||
|
options.exclude && options.exclude.forEach(function (filename) {
|
||
|
glob.sync(filename, { cwd: baseDir }).forEach(function (globFileName) {
|
||
|
excludesMap[filenameToMid(pathUtil.resolve(baseDir, globFileName))] = true;
|
||
|
});
|
||
|
});
|
||
|
if (options.exclude) {
|
||
|
verboseMessage('exclude:');
|
||
|
options.exclude.forEach(name => { verboseMessage(' ' + name); });
|
||
|
}
|
||
|
if (!options.stdout) mkdirp.sync(pathUtil.dirname(options.out));
|
||
|
/* node.js typings are missing the optional mode in createWriteStream options and therefore
|
||
|
* in TS 1.6 the strict object literal checking is throwing, therefore a hammer to the nut */
|
||
|
const output = options.stdout || fs.createWriteStream(options.out, { mode: parseInt('644', 8) });
|
||
|
const host = ts.createCompilerHost(compilerOptions);
|
||
|
const program = ts.createProgram(filenames, compilerOptions, host);
|
||
|
function writeFile(filename, data) {
|
||
|
// Compiler is emitting the non-declaration file, which we do not care about
|
||
|
if (filename.slice(-DTSLEN) !== '.d.ts') {
|
||
|
return;
|
||
|
}
|
||
|
writeDeclaration(ts.createSourceFile(filename, data, compilerOptions.target, true), true);
|
||
|
}
|
||
|
let declaredExternalModules = [];
|
||
|
return new Promise(function (resolve, reject) {
|
||
|
output.on('close', () => { resolve(undefined); });
|
||
|
output.on('error', reject);
|
||
|
if (options.externs) {
|
||
|
options.externs.forEach(function (path) {
|
||
|
sendMessage(`Writing external dependency ${path}`);
|
||
|
output.write(`/// <reference path="${path}" />` + eol);
|
||
|
});
|
||
|
}
|
||
|
if (options.types) {
|
||
|
options.types.forEach(function (type) {
|
||
|
sendMessage(`Writing external @types package dependency ${type}`);
|
||
|
output.write(`/// <reference types="${type}" />` + eol);
|
||
|
});
|
||
|
}
|
||
|
sendMessage('processing:');
|
||
|
let mainExportDeclaration = false;
|
||
|
let mainExportAssignment = false;
|
||
|
let foundMain = false;
|
||
|
program.getSourceFiles().forEach(function (sourceFile) {
|
||
|
processTree(sourceFile, function (node) {
|
||
|
if (isNodeKindModuleDeclaration(node)) {
|
||
|
const name = node.name;
|
||
|
if (isNodeKindStringLiteral(name)) {
|
||
|
declaredExternalModules.push(name.text);
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
});
|
||
|
});
|
||
|
program.getSourceFiles().some(function (sourceFile) {
|
||
|
// Source file is a default library, or other dependency from another project, that should not be included in
|
||
|
// our bundled output
|
||
|
if (pathUtil.normalize(sourceFile.fileName).indexOf(baseDir + pathUtil.sep) !== 0) {
|
||
|
return;
|
||
|
}
|
||
|
if (excludesMap[filenameToMid(pathUtil.normalize(sourceFile.fileName))]) {
|
||
|
return;
|
||
|
}
|
||
|
sendMessage(` ${sourceFile.fileName}`);
|
||
|
// Source file is already a declaration file so should does not need to be pre-processed by the emitter
|
||
|
if (sourceFile.fileName.slice(-DTSLEN) === '.d.ts') {
|
||
|
writeDeclaration(sourceFile, false);
|
||
|
return;
|
||
|
}
|
||
|
// We can optionally output the main module if there's something to export.
|
||
|
if (options.main && options.main === (options.prefix + filenameToMid(sourceFile.fileName.slice(baseDir.length, -3)))) {
|
||
|
foundMain = true;
|
||
|
ts.forEachChild(sourceFile, function (node) {
|
||
|
mainExportDeclaration = mainExportDeclaration || isNodeKindExportDeclaration(node);
|
||
|
mainExportAssignment = mainExportAssignment || isNodeKindExportAssignment(node);
|
||
|
});
|
||
|
}
|
||
|
const emitOutput = program.emit(sourceFile, writeFile);
|
||
|
if (emitOutput.emitSkipped || emitOutput.diagnostics.length > 0) {
|
||
|
reject(getError(emitOutput.diagnostics
|
||
|
.concat(program.getSemanticDiagnostics(sourceFile))
|
||
|
.concat(program.getSyntacticDiagnostics(sourceFile))
|
||
|
.concat(program.getDeclarationDiagnostics(sourceFile))));
|
||
|
return true;
|
||
|
}
|
||
|
});
|
||
|
if (options.main && !foundMain) {
|
||
|
throw new Error(`main module ${options.main} was not found`);
|
||
|
}
|
||
|
if (options.main) {
|
||
|
output.write(`declare module '${options.name}' {` + eol + indent);
|
||
|
if (compilerOptions.target >= ts.ScriptTarget.ES2015) {
|
||
|
if (mainExportAssignment) {
|
||
|
output.write(`export {default} from '${options.main}';` + eol + indent);
|
||
|
}
|
||
|
if (mainExportDeclaration) {
|
||
|
output.write(`export * from '${options.main}';` + eol);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
output.write(`import main = require('${options.main}');` + eol + indent);
|
||
|
output.write('export = main;' + eol);
|
||
|
}
|
||
|
output.write('}' + eol);
|
||
|
sendMessage(`Aliased main module ${options.name} to ${options.main}`);
|
||
|
}
|
||
|
if (!options.stdout) {
|
||
|
sendMessage(`output to "${options.out}"`);
|
||
|
output.end();
|
||
|
}
|
||
|
});
|
||
|
function writeDeclaration(declarationFile, isOutput) {
|
||
|
// resolving is important for dealting with relative outDirs
|
||
|
const filename = pathUtil.resolve(declarationFile.fileName);
|
||
|
// use the outDir here, not the baseDir, because the declarationFiles are
|
||
|
// outputs of the build process; baseDir points instead to the inputs.
|
||
|
// However we have to account for .d.ts files in our inputs that this code
|
||
|
// is also used for. Also if no outDir is used, the compiled code ends up
|
||
|
// alongside the source, so use baseDir in that case too.
|
||
|
const outputDir = (isOutput && Boolean(outDir)) ? pathUtil.resolve(outDir) : baseDir;
|
||
|
const sourceModuleId = filenameToMid(filename.slice(outputDir.length + 1, -DTSLEN));
|
||
|
const currentModuleId = filenameToMid(filename.slice(outputDir.length + 1, -DTSLEN));
|
||
|
function resolveModuleImport(moduleId) {
|
||
|
const isDeclaredExternalModule = declaredExternalModules.indexOf(moduleId) !== -1;
|
||
|
let resolved;
|
||
|
if (options.resolveModuleImport) {
|
||
|
resolved = options.resolveModuleImport({
|
||
|
importedModuleId: moduleId,
|
||
|
currentModuleId: currentModuleId,
|
||
|
isDeclaredExternalModule: isDeclaredExternalModule
|
||
|
});
|
||
|
}
|
||
|
if (!resolved) {
|
||
|
// resolve relative imports relative to the current module id.
|
||
|
if (moduleId.charAt(0) === '.') {
|
||
|
resolved = filenameToMid(pathUtil.join(pathUtil.dirname(sourceModuleId), moduleId));
|
||
|
}
|
||
|
else {
|
||
|
resolved = moduleId;
|
||
|
}
|
||
|
// prefix the import with options.prefix, so that both non-relative imports
|
||
|
// and relative imports end up prefixed with options.prefix. We only
|
||
|
// do this when no resolveModuleImport function is given so that that
|
||
|
// function has complete control of the imports that get outputed.
|
||
|
// NOTE: we may want to revisit the isDeclaredExternalModule behavior.
|
||
|
// discussion is on https://github.com/SitePen/dts-generator/pull/94
|
||
|
// but currently there's no strong argument against this behavior.
|
||
|
if (Boolean(options.prefix) && !isDeclaredExternalModule) {
|
||
|
resolved = `${options.prefix}/${resolved}`;
|
||
|
}
|
||
|
}
|
||
|
return resolved;
|
||
|
}
|
||
|
/* For some reason, SourceFile.externalModuleIndicator is missing from 1.6+, so having
|
||
|
* to use a sledgehammer on the nut */
|
||
|
if (declarationFile.externalModuleIndicator) {
|
||
|
let resolvedModuleId = sourceModuleId;
|
||
|
if (options.resolveModuleId) {
|
||
|
const resolveModuleIdResult = options.resolveModuleId({
|
||
|
currentModuleId: currentModuleId
|
||
|
});
|
||
|
if (resolveModuleIdResult) {
|
||
|
resolvedModuleId = resolveModuleIdResult;
|
||
|
}
|
||
|
else if (options.prefix) {
|
||
|
resolvedModuleId = `${options.prefix}/${resolvedModuleId}`;
|
||
|
}
|
||
|
}
|
||
|
else if (options.prefix) {
|
||
|
resolvedModuleId = `${options.prefix}/${resolvedModuleId}`;
|
||
|
}
|
||
|
output.write('declare module \'' + resolvedModuleId + '\' {' + eol + indent);
|
||
|
const content = processTree(declarationFile, function (node) {
|
||
|
if (isNodeKindExternalModuleReference(node)) {
|
||
|
// TODO figure out if this branch is possible, and if so, write a test
|
||
|
// that covers it.
|
||
|
const expression = node.expression;
|
||
|
// convert both relative and non-relative module names in import = require(...)
|
||
|
// statements.
|
||
|
const resolved = resolveModuleImport(expression.text);
|
||
|
return ` require('${resolved}')`;
|
||
|
}
|
||
|
else if (node.kind === ts.SyntaxKind.DeclareKeyword) {
|
||
|
return '';
|
||
|
}
|
||
|
else if (isNodeKindStringLiteral(node) && node.parent &&
|
||
|
(isNodeKindExportDeclaration(node.parent) || isNodeKindImportDeclaration(node.parent))) {
|
||
|
// This block of code is modifying the names of imported modules
|
||
|
const text = node.text;
|
||
|
const resolved = resolveModuleImport(text);
|
||
|
if (resolved) {
|
||
|
return ` '${resolved}'`;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
output.write(content.replace(nonEmptyLineStart, '$&' + indent));
|
||
|
output.write(eol + '}' + eol);
|
||
|
}
|
||
|
else {
|
||
|
output.write(declarationFile.text);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
exports.default = generate;
|
||
|
});
|
||
|
|
||
|
const prelude = `declare type bool = boolean;
|
||
|
declare type i8 = number;
|
||
|
declare type i16 = number;
|
||
|
declare type i32 = number;
|
||
|
declare type isize = number;
|
||
|
declare type u8 = number;
|
||
|
declare type u16 = number;
|
||
|
declare type u32 = number;
|
||
|
declare type usize = number;
|
||
|
declare type f32 = number;
|
||
|
declare type f64 = number;
|
||
|
declare module 'assemblyscript' {
|
||
|
export * from 'assemblyscript/src/index';
|
||
|
}
|
||
|
`;
|
||
|
|
||
|
var path = require("path");
|
||
|
var fs = require("fs");
|
||
|
var stdout = fs.createWriteStream(path.resolve(__dirname, "..", "dist", "assemblyscript.d.ts"));
|
||
|
stdout.write(prelude);
|
||
|
stdout.write = (function(_write) {
|
||
|
return function(...args) {
|
||
|
if (typeof args[0] === "string") {
|
||
|
args[0] = args[0].replace(/\/\/\/ <reference[^>]*>\r?\n/g, "");
|
||
|
}
|
||
|
return _write.apply(stdout, args);
|
||
|
};
|
||
|
})(stdout.write);
|
||
|
|
||
|
module.exports.default({
|
||
|
project: path.resolve(__dirname, "..", "src"),
|
||
|
prefix: "assemblyscript",
|
||
|
exclude: [
|
||
|
"glue/js/index.ts",
|
||
|
"glue/js/node.d.ts"
|
||
|
],
|
||
|
verbose: true,
|
||
|
sendMessage: console.log,
|
||
|
stdout: stdout
|
||
|
});
|