UNPKG

dts-bundle

Version:

Export TypeScript .d.ts files as an external module definition

660 lines (659 loc) 26 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var os = require("os"); var fs = require("fs"); var path = require("path"); var util = require("util"); var assert = require("assert"); var glob = require("glob"); var mkdirp = require("mkdirp"); var detectIndent = require("detect-indent"); var pkg = require('../package'); var dtsExp = /\.d\.ts$/; var bomOptExp = /^\uFEFF?/; var externalExp = /^([ \t]*declare module )(['"])(.+?)(\2[ \t]*{?.*)$/; var importExp = /^([ \t]*(?:export )?(?:import .+? )= require\()(['"])(.+?)(\2\);.*)$/; var importEs6Exp = /^([ \t]*(?:export|import) ?(?:(?:\* (?:as [^ ,]+)?)|.*)?,? ?(?:[^ ,]+ ?,?)(?:\{(?:[^ ,]+ ?,?)*\})? ?from )(['"])([^ ,]+)(\2;.*)$/; var referenceTagExp = /^[ \t]*\/\/\/[ \t]*<reference[ \t]+path=(["'])(.*?)\1?[ \t]*\/>.*$/; var identifierExp = /^\w+(?:[\.-]\w+)*$/; var fileExp = /^([\./].*|.:.*)$/; var privateExp = /^[ \t]*(?:static )?private (?:static )?/; var publicExp = /^([ \t]*)(static |)(public |)(static |)(.*)/; function bundle(options) { assert(typeof options === 'object' && options, 'options must be an object'); var allFiles = stringEndsWith(options.main, "**/*.d.ts"); var main = allFiles ? "*.d.ts" : options.main; var exportName = options.name; var _baseDir = (function () { var baseDir = optValue(options.baseDir, path.dirname(options.main)); if (allFiles) { baseDir = baseDir.substr(0, baseDir.length - 2); } return baseDir; })(); var out = optValue(options.out, exportName + '.d.ts').replace(/\//g, path.sep); var newline = optValue(options.newline, os.EOL); var indent = optValue(options.indent, ' '); var outputAsModuleFolder = optValue(options.outputAsModuleFolder, false); var prefix = optValue(options.prefix, ''); var separator = optValue(options.separator, '/'); var externals = optValue(options.externals, false); var exclude = optValue(options.exclude, null); var removeSource = optValue(options.removeSource, false); var referenceExternals = optValue(options.referenceExternals, false); var emitOnIncludedFileNotFound = optValue(options.emitOnIncludedFileNotFound, false); var emitOnNoIncludedFileNotFound = optValue(options.emitOnNoIncludedFileNotFound, false); var _headerPath = optValue(options.headerPath, null); var headerText = optValue(options.headerText, ''); var comments = false; var verbose = optValue(options.verbose, false); assert.ok(main, 'option "main" must be defined'); assert.ok(exportName, 'option "name" must be defined'); assert(typeof newline === 'string', 'option "newline" must be a string'); assert(typeof indent === 'string', 'option "indent" must be a string'); assert(typeof prefix === 'string', 'option "prefix" must be a string'); assert(separator.length > 0, 'option "separator" must have non-zero length'); var baseDir = path.resolve(_baseDir); var mainFile = allFiles ? path.resolve(baseDir, "**/*.d.ts") : path.resolve(main.replace(/\//g, path.sep)); var outFile = calcOutFilePath(out, baseDir); var headerData = '// Generated by dts-bundle v' + pkg.version + newline; var headerPath = _headerPath && _headerPath !== "none" ? path.resolve(_headerPath.replace(/\//g, path.sep)) : _headerPath; trace('### settings object passed ###'); traceObject(options); trace('### settings ###'); trace('main: %s', main); trace('name: %s', exportName); trace('out: %s', out); trace('baseDir: %s', baseDir); trace('mainFile: %s', mainFile); trace('outFile: %s', outFile); trace('externals: %s', externals ? 'yes' : 'no'); trace('exclude: %s', exclude); trace('removeSource: %s', removeSource ? 'yes' : 'no'); trace('comments: %s', comments ? 'yes' : 'no'); trace('emitOnIncludedFileNotFound: %s', emitOnIncludedFileNotFound ? "yes" : "no"); trace('emitOnNoIncludedFileNotFound: %s', emitOnNoIncludedFileNotFound ? "yes" : "no"); trace("headerPath %s", headerPath); trace("headerText %s", headerText); if (!allFiles) { assert(fs.existsSync(mainFile), 'main does not exist: ' + mainFile); } if (headerPath) { if (headerPath === "none") { headerData = ""; } else { assert(fs.existsSync(headerPath), 'header does not exist: ' + headerPath); headerData = fs.readFileSync(headerPath, 'utf8') + headerData; } } else if (headerText) { headerData = '/*' + headerText + '*/\n'; } var isExclude; if (typeof exclude === 'function') { isExclude = exclude; } else if (exclude instanceof RegExp) { isExclude = function (file) { return exclude.test(file); }; } else { isExclude = function () { return false; }; } var sourceTypings = glob.sync('**/*.d.ts', { cwd: baseDir }).map(function (file) { return path.resolve(baseDir, file); }); if (allFiles) { var mainFileContent_1 = ""; trace("## temporally main file ##"); sourceTypings.forEach(function (file) { var generatedLine = "export * from './" + path.relative(baseDir, file.substr(0, file.length - 5)).replace(path.sep, "/") + "';"; trace(generatedLine); mainFileContent_1 += generatedLine + "\n"; }); mainFile = path.resolve(baseDir, "dts-bundle.tmp." + exportName + ".d.ts"); fs.writeFileSync(mainFile, mainFileContent_1, 'utf8'); } trace('\n### find typings ###'); var inSourceTypings = function (file) { return sourceTypings.indexOf(file) !== -1 || sourceTypings.indexOf(path.join(file, 'index.d.ts')) !== -1; }; trace('source typings (will be included in output if actually used)'); sourceTypings.forEach(function (file) { return trace(' - %s ', file); }); trace('excluded typings (will always be excluded from output)'); var fileMap = Object.create(null); var globalExternalImports = []; var mainParse; var externalTypings = []; var inExternalTypings = function (file) { return externalTypings.indexOf(file) !== -1; }; { trace('\n### parse files ###'); var queue = [mainFile]; var queueSeen = Object.create(null); while (queue.length > 0) { var target = queue.shift(); if (queueSeen[target]) { continue; } queueSeen[target] = true; var parse = parseFile(target); if (!mainParse) { mainParse = parse; } fileMap[parse.file] = parse; pushUniqueArr(queue, parse.refs, parse.relativeImports); } } trace('\n### map exports ###'); var exportMap = Object.create(null); Object.keys(fileMap).forEach(function (file) { var parse = fileMap[file]; parse.exports.forEach(function (name) { assert(!(name in exportMap), 'already got export for: ' + name); exportMap[name] = parse; trace('- %s -> %s', name, parse.file); }); }); trace('\n### determine typings to include ###'); var excludedTypings = []; var usedTypings = []; var externalDependencies = []; { var queue_1 = [mainParse]; var queueSeen = Object.create(null); trace('queue'); trace(queue_1); while (queue_1.length > 0) { var parse = queue_1.shift(); if (queueSeen[parse.file]) { continue; } queueSeen[parse.file] = true; trace('%s (%s)', parse.name, parse.file); usedTypings.push(parse); parse.externalImports.forEach(function (name) { var p = exportMap[name]; if (!externals) { trace(' - exclude external %s', name); pushUnique(externalDependencies, !p ? name : p.file); return; } if (isExclude(path.relative(baseDir, p.file), true)) { trace(' - exclude external filter %s', name); pushUnique(excludedTypings, p.file); return; } trace(' - include external %s', name); assert(p, name); queue_1.push(p); }); parse.relativeImports.forEach(function (file) { var p = fileMap[file]; if (isExclude(path.relative(baseDir, p.file), false)) { trace(' - exclude internal filter %s', file); pushUnique(excludedTypings, p.file); return; } trace(' - import relative %s', file); assert(p, file); queue_1.push(p); }); } } trace('\n### rewrite global external modules ###'); usedTypings.forEach(function (parse) { trace(parse.name); parse.relativeRef.forEach(function (line, i) { line.modified = replaceExternal(line.original, getLibName); trace(' - %s ==> %s', line.original, line.modified); }); parse.importLineRef.forEach(function (line, i) { if (outputAsModuleFolder) { trace(' - %s was skipped.', line.original); line.skip = true; return; } if (importExp.test(line.original)) { line.modified = replaceImportExport(line.original, getLibName); } else { line.modified = replaceImportExportEs6(line.original, getLibName); } trace(' - %s ==> %s', line.original, line.modified); }); }); trace('\n### build output ###'); var content = headerData; if (externalDependencies.length > 0) { content += '// Dependencies for this module:' + newline; externalDependencies.forEach(function (file) { if (referenceExternals) { content += formatReference(path.relative(baseDir, file).replace(/\\/g, '/')) + newline; } else { content += '// ' + path.relative(baseDir, file).replace(/\\/g, '/') + newline; } }); } if (globalExternalImports.length > 0) { content += newline; content += globalExternalImports.join(newline) + newline; } content += newline; content += usedTypings.filter(function (parse) { parse.lines = parse.lines.filter(function (line) { return (true !== line.skip); }); return (parse.lines.length > 0); }).map(function (parse) { if (inSourceTypings(parse.file)) { return formatModule(parse.file, parse.lines.map(function (line) { return getIndenter(parse.indent, indent)(line); })); } else { return parse.lines.map(function (line) { return getIndenter(parse.indent, indent)(line); }).join(newline) + newline; } }).join(newline) + newline; if (removeSource) { trace('\n### remove source typings ###'); sourceTypings.forEach(function (p) { if (p !== outFile && dtsExp.test(p) && fs.statSync(p).isFile()) { trace(' - %s', p); fs.unlinkSync(p); } }); } var inUsed = function (file) { return usedTypings.filter(function (parse) { return parse.file === file; }).length !== 0; }; var bundleResult = { fileMap: fileMap, includeFilesNotFound: [], noIncludeFilesNotFound: [], options: options }; trace('## files not found ##'); for (var p in fileMap) { var parse = fileMap[p]; if (!parse.fileExists) { if (inUsed(parse.file)) { bundleResult.includeFilesNotFound.push(parse.file); warning(' X Included file NOT FOUND %s ', parse.file); } else { bundleResult.noIncludeFilesNotFound.push(parse.file); trace(' X Not used file not found %s', parse.file); } } } trace('\n### write output ###'); if ((bundleResult.includeFilesNotFound.length == 0 || (bundleResult.includeFilesNotFound.length > 0 && emitOnIncludedFileNotFound)) && (bundleResult.noIncludeFilesNotFound.length == 0 || (bundleResult.noIncludeFilesNotFound.length > 0 && emitOnNoIncludedFileNotFound))) { trace(outFile); { var outDir = path.dirname(outFile); if (!fs.existsSync(outDir)) { mkdirp.sync(outDir); } } fs.writeFileSync(outFile, content, 'utf8'); bundleResult.emitted = true; } else { warning(" XXX Not emit due to exist files not found."); trace("See documentation for emitOnIncludedFileNotFound and emitOnNoIncludedFileNotFound options."); bundleResult.emitted = false; } if (verbose) { trace('\n### statistics ###'); trace('used sourceTypings'); sourceTypings.forEach(function (p) { if (inUsed(p)) { trace(' - %s', p); } }); trace('unused sourceTypings'); sourceTypings.forEach(function (p) { if (!inUsed(p)) { trace(' - %s', p); } }); trace('excludedTypings'); excludedTypings.forEach(function (p) { trace(' - %s', p); }); trace('used external typings'); externalTypings.forEach(function (p) { if (inUsed(p)) { trace(' - %s', p); } }); trace('unused external typings'); externalTypings.forEach(function (p) { if (!inUsed(p)) { trace(' - %s', p); } }); trace('external dependencies'); externalDependencies.forEach(function (p) { trace(' - %s', p); }); } trace('\n### done ###\n'); if (allFiles) { fs.unlinkSync(mainFile); } return bundleResult; function stringEndsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } function stringStartsWith(str, prefix) { return str.slice(0, prefix.length) == prefix; } function calcOutFilePath(out, baseDir) { var result = path.resolve(baseDir, out); if (stringStartsWith(out, "~" + path.sep)) { result = path.resolve(".", out.substr(2)); } return result; } function traceObject(obj) { if (verbose) { console.log(obj); } } function trace() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } if (verbose) { console.log(util.format.apply(null, args)); } } function warning() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } console.log(util.format.apply(null, args)); } function getModName(file) { return path.relative(baseDir, path.dirname(file) + path.sep + path.basename(file).replace(/\.d\.ts$/, '')); } function getExpName(file) { if (file === mainFile) { return exportName; } return getExpNameRaw(file); } function getExpNameRaw(file) { return prefix + exportName + separator + cleanupName(getModName(file)); } function getLibName(ref) { return getExpNameRaw(mainFile) + separator + prefix + separator + ref; } function cleanupName(name) { return name.replace(/\.\./g, '--').replace(/[\\\/]/g, separator); } function mergeModulesLines(lines) { var i = (outputAsModuleFolder ? '' : indent); return (lines.length === 0 ? '' : i + lines.join(newline + i)) + newline; } function formatModule(file, lines) { var out = ''; if (outputAsModuleFolder) { return mergeModulesLines(lines); } out += 'declare module \'' + getExpName(file) + '\' {' + newline; out += mergeModulesLines(lines); out += '}' + newline; return out; } function parseFile(file) { var name = getModName(file); trace('%s (%s)', name, file); var res = { file: file, name: name, indent: indent, exp: getExpName(file), refs: [], externalImports: [], relativeImports: [], exports: [], lines: [], fileExists: true, importLineRef: [], relativeRef: [] }; if (!fs.existsSync(file)) { trace(' X - File not found: %s', file); res.fileExists = false; return res; } if (fs.lstatSync(file).isDirectory()) { file = path.join(file, 'index.d.ts'); } var code = fs.readFileSync(file, 'utf8').replace(bomOptExp, '').replace(/\s*$/, ''); res.indent = detectIndent(code) || indent; var multiComment = []; var queuedJSDoc; var inBlockComment = false; var popBlock = function () { if (multiComment.length > 0) { if (/^[ \t]*\/\*\*/.test(multiComment[0])) { queuedJSDoc = multiComment; } else if (comments) { multiComment.forEach(function (line) { return res.lines.push({ original: line }); }); } multiComment = []; } inBlockComment = false; }; var popJSDoc = function () { if (queuedJSDoc) { queuedJSDoc.forEach(function (line) { var match = line.match(/^([ \t]*)(\*.*)/); if (match) { res.lines.push({ original: match[1] + ' ' + match[2] }); } else { res.lines.push({ original: line }); } }); queuedJSDoc = null; } }; code.split(/\r?\n/g).forEach(function (line) { var match; if (/^[((=====)(=*)) \t]*\*+\//.test(line)) { multiComment.push(line); popBlock(); return; } if (/^[ \t]*\/\*/.test(line)) { multiComment.push(line); inBlockComment = true; if (/\*+\/[ \t]*$/.test(line)) { popBlock(); } return; } if (inBlockComment) { multiComment.push(line); return; } if (/^\s*$/.test(line)) { res.lines.push({ original: '' }); return; } if (/^\/\/\//.test(line)) { var ref = extractReference(line); if (ref) { var refPath = path.resolve(path.dirname(file), ref); if (inSourceTypings(refPath)) { trace(' - reference source typing %s (%s)', ref, refPath); } else { var relPath = path.relative(baseDir, refPath).replace(/\\/g, '/'); trace(' - reference external typing %s (%s) (relative: %s)', ref, refPath, relPath); if (!inExternalTypings(refPath)) { externalTypings.push(refPath); } } pushUnique(res.refs, refPath); return; } } if (/^\/\//.test(line)) { if (comments) { res.lines.push({ original: line }); } return; } if (privateExp.test(line)) { queuedJSDoc = null; return; } popJSDoc(); if ((line.indexOf("from") >= 0 && (match = line.match(importEs6Exp))) || (line.indexOf("require") >= 0 && (match = line.match(importExp)))) { var _ = match[0], lead = match[1], quote = match[2], moduleName = match[3], trail = match[4]; assert(moduleName); var impPath = path.resolve(path.dirname(file), moduleName); if (fileExp.test(moduleName)) { var modLine = { original: lead + quote + getExpName(impPath) + trail }; res.lines.push(modLine); var full = path.resolve(path.dirname(file), impPath); if (!fs.existsSync(full) || fs.existsSync(full + '.d.ts')) { full += '.d.ts'; } trace(' - import relative %s (%s)', moduleName, full); pushUnique(res.relativeImports, full); res.importLineRef.push(modLine); } else { var modLine = { original: line }; trace(' - import external %s', moduleName); pushUnique(res.externalImports, moduleName); if (externals) { res.importLineRef.push(modLine); } if (!outputAsModuleFolder) { res.lines.push(modLine); } else { pushUnique(globalExternalImports, line); } } } else if ((match = line.match(externalExp))) { var _ = match[0], declareModule = match[1], lead = match[2], moduleName = match[3], trail = match[4]; assert(moduleName); trace(' - declare %s', moduleName); pushUnique(res.exports, moduleName); var modLine = { original: line }; res.relativeRef.push(modLine); res.lines.push(modLine); } else { if ((match = line.match(publicExp))) { var _ = match[0], sp = match[1], static1 = match[2], pub = match[3], static2 = match[4], ident = match[5]; line = sp + static1 + static2 + ident; } if (inSourceTypings(file)) { res.lines.push({ original: line.replace(/^(export )?declare /g, '$1') }); } else { res.lines.push({ original: line }); } } }); return res; } } exports.bundle = bundle; function pushUnique(arr, value) { if (arr.indexOf(value) < 0) { arr.push(value); } return arr; } function pushUniqueArr(arr) { var values = []; for (var _i = 1; _i < arguments.length; _i++) { values[_i - 1] = arguments[_i]; } values.forEach(function (vs) { return vs.forEach(function (v) { return pushUnique(arr, v); }); }); return arr; } function formatReference(file) { return '/// <reference path="' + file.replace(/\\/g, '/') + '" />'; } function extractReference(tag) { var match = tag.match(referenceTagExp); if (match) { return match[2]; } return null; } function replaceImportExport(line, replacer) { var match = line.match(importExp); if (match) { assert(match[4]); if (identifierExp.test(match[3])) { return match[1] + match[2] + replacer(match[3]) + match[4]; } } return line; } function replaceImportExportEs6(line, replacer) { if (line.indexOf("from") < 0) { return line; } var match = line.match(importEs6Exp); if (match) { assert(match[4]); if (identifierExp.test(match[3])) { return match[1] + match[2] + replacer(match[3]) + match[4]; } } return line; } function replaceExternal(line, replacer) { var match = line.match(externalExp); if (match) { var _ = match[0], declareModule = match[1], beforeIndent = match[2], moduleName = match[3], afterIdent = match[4]; assert(afterIdent); if (identifierExp.test(moduleName)) { return declareModule + beforeIndent + replacer(moduleName) + afterIdent; } } return line; } function getIndenter(actual, use) { if (actual === use || !actual) { return function (line) { return line.modified || line.original; }; } return function (line) { return (line.modified || line.original).replace(new RegExp('^' + actual + '+', 'g'), function (match) { return match.split(actual).join(use); }); }; } function optValue(passed, def) { if (typeof passed === 'undefined') { return def; } return passed; } function regexEscape(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); }