// © 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(`/// ` + eol); }); } if (options.types) { options.types.forEach(function (type) { sendMessage(`Writing external @types package dependency ${type}`); output.write(`/// ` + 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(/\/\/\/ ]*>\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 });