/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import fs = require('fs'); import path = require('path'); import vm = require('vm'); interface IPosition { line: number; col: number; } interface IBuildModuleInfo { id: string; path: string; defineLocation: IPosition; dependencies: string[]; shim: string; exports: any; } interface IBuildModuleInfoMap { [moduleId:string]: IBuildModuleInfo; } interface ILoaderPlugin { write(pluginName:string, moduleName:string, write:ILoaderPluginWriteFunc): void; writeFile(pluginName:string, entryPoint:string, req:ILoaderPluginReqFunc, write:(filename:string, contents:string)=>void, config:any): void; finishBuild(write:(filename:string, contents:string)=>void): void; } interface ILoaderPluginWriteFunc { (something:string): void; getEntryPoint(): string; asModule(moduleId:string, code:string): void; } interface ILoaderPluginReqFunc { (something:string): void; toUrl(something:string): string; } export interface IEntryPoint { name: string; include: string[]; exclude: string[]; } interface IEntryPointMap { [moduleId:string]: IEntryPoint; } interface IGraph { [node:string]: string[]; } interface INodeSet { [node:string]: boolean; } export interface IFile { path: string; contents: string; } export interface IConcatFile { dest: string; sources: IFile[]; } export interface ILoaderConfig { isBuild?: boolean; } /** * Bundle `entryPoints` given config `config`. */ export function bundle(entryPoints:IEntryPoint[], config:ILoaderConfig, callback:(err:any, result:IConcatFile[]) => void): void { let entryPointsMap:IEntryPointMap = {}; entryPoints.forEach((module:IEntryPoint) => { entryPointsMap[module.name] = module; }); var code = require('fs').readFileSync(path.join(__dirname, '../../src/vs/loader.js')); var r: Function = vm.runInThisContext('(function(require, module, exports) { ' + code + '\n});'); var loaderModule = { exports: {} }; r.call({}, require, loaderModule, loaderModule.exports); var loader:any = loaderModule.exports; config.isBuild = true; loader.config(config); loader(Object.keys(entryPointsMap), () => { let modules = loader.getBuildInfo(); callback(null, emitEntryPoints(modules, entryPointsMap)); }, (err) => callback(err, null)) } function emitEntryPoints(modules:IBuildModuleInfo[], entryPoints:IEntryPointMap): IConcatFile[] { let modulesMap: IBuildModuleInfoMap = {}; modules.forEach((m:IBuildModuleInfo) => { modulesMap[m.id] = m; }); let modulesGraph:IGraph = {}; modules.forEach((m:IBuildModuleInfo) => { modulesGraph[m.id] = m.dependencies; }); let sortedModules = topologicalSort(modulesGraph); let result: IConcatFile[] = []; let usedPlugins: IPluginMap = {}; Object.keys(entryPoints).forEach((moduleToBundle:string) => { let info = entryPoints[moduleToBundle]; let rootNodes = [moduleToBundle].concat(info.include || []); let allDependencies = visit(rootNodes, modulesGraph); let excludes:string[] = ['require', 'exports', 'module'].concat(info.exclude || []); excludes.forEach((excludeRoot:string) => { let allExcludes = visit([excludeRoot], modulesGraph); Object.keys(allExcludes).forEach((exclude:string) => { delete allDependencies[exclude]; }); }); let includedModules = sortedModules.filter((module:string) => { return allDependencies[module]; }); let res = emitEntryPoint(modulesMap, modulesGraph, moduleToBundle, includedModules); result = result.concat(res.files); for (let pluginName in res.usedPlugins) { usedPlugins[pluginName] = usedPlugins[pluginName] || res.usedPlugins[pluginName]; } }); Object.keys(usedPlugins).forEach((pluginName:string) => { let plugin = usedPlugins[pluginName]; if (typeof plugin.finishBuild === 'function') { let write = (filename:string, contents:string) => { result.push({ dest: filename, sources: [{ path: null, contents: contents }] }); }; plugin.finishBuild(write); } }); return result; } interface IPluginMap { [moduleId:string]:ILoaderPlugin; } interface IEmitEntryPointResult { files: IConcatFile[]; usedPlugins: IPluginMap; } function emitEntryPoint(modulesMap:IBuildModuleInfoMap, deps:IGraph, entryPoint:string, includedModules:string[]): IEmitEntryPointResult { let mainResult: IConcatFile = { sources: [], dest: entryPoint + '.js' }, results: IConcatFile[] = [mainResult]; let usedPlugins: IPluginMap = {}; let getLoaderPlugin = (pluginName:string):ILoaderPlugin => { if (!usedPlugins[pluginName]) { usedPlugins[pluginName] = modulesMap[pluginName].exports; } return usedPlugins[pluginName]; } includedModules.forEach((c:string) => { let bangIndex = c.indexOf('!'); if (bangIndex >= 0) { let pluginName = c.substr(0, bangIndex); let plugin = getLoaderPlugin(pluginName); mainResult.sources.push(emitPlugin(entryPoint, plugin, pluginName, c.substr(bangIndex + 1))); return; } let module = modulesMap[c]; if (module.path === 'empty:') { return; } let contents = readFileAndRemoveBOM(module.path); if (module.shim) { mainResult.sources.push(emitShimmedModule(c, deps[c], module.shim, module.path, contents)); } else { mainResult.sources.push(emitNamedModule(c, deps[c], module.defineLocation, module.path, contents)); } }); Object.keys(usedPlugins).forEach((pluginName:string) => { let plugin = usedPlugins[pluginName]; if (typeof plugin.writeFile === 'function') { let req:ILoaderPluginReqFunc = (() => { throw new Error('no-no!'); }); req.toUrl = something => something; let write = (filename:string, contents:string) => { results.push({ dest: filename, sources: [{ path: null, contents: contents }] }); }; plugin.writeFile(pluginName, entryPoint, req, write, {}); } }); return { files: results, usedPlugins: usedPlugins }; } function readFileAndRemoveBOM(path:string): string { var BOM_CHAR_CODE = 65279; var contents = fs.readFileSync(path, 'utf8'); // Remove BOM if (contents.charCodeAt(0) === BOM_CHAR_CODE) { contents = contents.substring(1); } return contents; } function emitPlugin(entryPoint:string, plugin:ILoaderPlugin, pluginName:string, moduleName:string): IFile { let result = ''; if (typeof plugin.write === 'function') { let write: ILoaderPluginWriteFunc = ((what) => { result += what; }); write.getEntryPoint = () => { return entryPoint; }; write.asModule = (moduleId:string, code:string) => { code = code.replace(/^define\(/, 'define("'+moduleId+'",'); result += code; }; plugin.write(pluginName, moduleName, write); } return { path: null, contents: result }; } function emitNamedModule(moduleId:string, myDeps:string[], defineCallPosition:IPosition, path:string, contents:string): IFile { // `defineCallPosition` is the position in code: |define() let defineCallOffset = positionToOffset(contents, defineCallPosition.line, defineCallPosition.col); // `parensOffset` is the position in code: define|() let parensOffset = contents.indexOf('(', defineCallOffset); let insertStr = '"' + moduleId + '", '; return { path: path, contents: contents.substr(0, parensOffset + 1) + insertStr + contents.substr(parensOffset + 1) }; } function emitShimmedModule(moduleId:string, myDeps:string[], factory:string, path:string, contents:string): IFile { let strDeps = (myDeps.length > 0 ? '"' + myDeps.join('", "') + '"' : ''); let strDefine = 'define("' + moduleId + '", [' + strDeps + '], ' + factory + ');'; return { path: path, contents: contents + '\n;\n' + strDefine }; } /** * Convert a position (line:col) to (offset) in string `str` */ function positionToOffset(str:string, desiredLine:number, desiredCol:number): number { if (desiredLine === 1) { return desiredCol - 1; } let line = 1, lastNewLineOffset = -1; do { if (desiredLine === line) { return lastNewLineOffset + 1 + desiredCol - 1; } lastNewLineOffset = str.indexOf('\n', lastNewLineOffset + 1); line++; } while (lastNewLineOffset >= 0); return -1; } /** * Return a set of reachable nodes in `graph` starting from `rootNodes` */ function visit(rootNodes:string[], graph:IGraph):INodeSet { let result:INodeSet = {}, queue = rootNodes; rootNodes.forEach((node) => { result[node] = true; }); while (queue.length > 0) { let el = queue.shift(); let myEdges = graph[el] || []; myEdges.forEach((toNode) => { if (!result[toNode]) { result[toNode] = true; queue.push(toNode); } }); } return result; } /** * Perform a topological sort on `graph` */ function topologicalSort(graph:IGraph): string[] { let allNodes:INodeSet = {}, outgoingEdgeCount:{[node:string]:number;} = {}, inverseEdges:IGraph = {}; Object.keys(graph).forEach((fromNode:string) => { allNodes[fromNode] = true; outgoingEdgeCount[fromNode] = graph[fromNode].length; graph[fromNode].forEach((toNode) => { allNodes[toNode] = true; outgoingEdgeCount[toNode] = outgoingEdgeCount[toNode] || 0; inverseEdges[toNode] = inverseEdges[toNode] || []; inverseEdges[toNode].push(fromNode); }); }); // https://en.wikipedia.org/wiki/Topological_sorting let S: string[] = [], L: string[] = []; Object.keys(allNodes).forEach((node:string) => { if (outgoingEdgeCount[node] === 0) { delete outgoingEdgeCount[node]; S.push(node); } }); while (S.length > 0) { // Ensure the exact same order all the time with the same inputs S.sort(); let n:string = S.shift(); L.push(n); let myInverseEdges = inverseEdges[n] || []; myInverseEdges.forEach((m:string) => { outgoingEdgeCount[m]--; if (outgoingEdgeCount[m] === 0) { delete outgoingEdgeCount[m]; S.push(m); } }); } if (Object.keys(outgoingEdgeCount).length > 0) { throw new Error('Cannot do topological sort on cyclic graph, remaining nodes: ' + Object.keys(outgoingEdgeCount)); } return L; }