mirror of
https://github.com/Microsoft/vscode
synced 2024-10-13 06:48:17 +00:00
398 lines
10 KiB
TypeScript
398 lines
10 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 = <any> 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 = <IBuildModuleInfo[]>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 = <any>(() => {
|
|
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 = <any>((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;
|
|
}
|