UNPKG

6.74 kBPlain TextView Raw
1import * as fs from 'fs';
2import * as Promise from 'bluebird';
3import * as sysUtil from 'systemjs-builder/lib/utils.js';
4import * as Builder from 'systemjs-builder';
5import * as path from 'path';
6import * as _ from 'lodash';
7import * as utils from './utils';
8import * as serializer from './config-serializer';
9import * as minifier from 'html-minifier';
10import * as CleanCSS from 'clean-css';
11import { createBuilder } from './builder-factory';
12import { BundleConfig, FetchHook } from "./models";
13import * as mkdirp from 'mkdirp';
14
15function createBuildExpression(cfg: BundleConfig) {
16 let appCfg = serializer.getAppConfig(cfg.configPath);
17 let includes = cfg.includes as string[];
18 let excludes = cfg.excludes;
19
20 let includeExpression = includes.map(m => getFullModuleName(m, appCfg.map)).join(' + ');
21 let excludeExpression = excludes.map(m => getFullModuleName(m, appCfg.map)).join(' - ');
22 let buildExpression = includeExpression;
23
24 if (excludeExpression && excludeExpression.length > 0) {
25 buildExpression = `${buildExpression} - ${excludeExpression}`;
26 }
27
28 return buildExpression;
29}
30
31function createFetchHook(cfg: BundleConfig): FetchHook {
32 return (load: any, fetch: (load: any) => any): string | any => {
33 let address = sysUtil.fromFileURL(load.address);
34 let ext = path.extname(address);
35
36 if (!(ext === '.html' || ext === '.css')) {
37 return fetch(load);
38 }
39
40 let plugin = path.basename(sysUtil.fromFileURL(load.name.split('!')[1]));
41
42 if (!plugin.startsWith('plugin-text')) {
43 return fetch(load);
44 }
45
46 if (ext === '.html' && cfg.options.minify) {
47 let content = fs.readFileSync(address, 'utf8');
48 let opts = utils.getHTMLMinOpts(cfg.options.htmlminopts);
49 return minifier.minify(content, opts);
50 }
51
52 if (ext === '.css' && cfg.options.minify) {
53 let content = fs.readFileSync(address, 'utf8');
54 let opts = utils.getCSSMinOpts(cfg.options.cssminopts);
55 let output = new CleanCSS(opts).minify(content);
56
57 if (output.errors.length) {
58 throw new Error('CSS Plugin:\n' + output.errors.join('\n'));
59 }
60
61 return output.styles;
62 }
63
64 return fetch(load);
65 };
66}
67
68export function bundle(cfg: BundleConfig) {
69 let buildExpression = createBuildExpression(cfg);
70 cfg.options.fetch = createFetchHook(cfg);
71
72 let tasks = [
73 _bundle(buildExpression, cfg)
74 ];
75
76 if (cfg.options.depCache) {
77 tasks.push(_depCache(buildExpression, cfg));
78 }
79
80 return Promise.all<any>(tasks);
81}
82
83export function depCache(cfg: BundleConfig): Promise<any> {
84 let buildExpression = createBuildExpression(cfg);
85 return _depCache(buildExpression, cfg);
86}
87
88function _depCache(buildExpression: string, cfg: BundleConfig) {
89 let builder = createBuilder(cfg);
90 return builder.trace(buildExpression, cfg.options)
91 .then(tree => {
92 let depCache = builder.getDepCache(tree);
93 let configPath = cfg.injectionConfigPath as string;
94 let appCfg = serializer.getAppConfig(configPath);
95 let dc = appCfg.depCache || {};
96
97 _.assign(dc, depCache);
98 appCfg.depCache = dc;
99 serializer.saveAppConfig(configPath, appCfg);
100 return Promise.resolve();
101 });
102}
103
104function _bundle(buildExpression: string, cfg: BundleConfig) {
105 let builder = createBuilder(cfg);
106 return builder.bundle(buildExpression, cfg.options)
107 .then((output) => {
108 let outfile = utils.getOutFileName(output.source, cfg.bundleName + '.js', cfg.options.rev as boolean);
109 let outPath = createOutputPath(cfg.baseURL, outfile, cfg.outputPath);
110 writeOutput(output, outPath, cfg.force as boolean, cfg.options.sourceMaps);
111
112 if (cfg.options.sourceMaps && cfg.options.sourceMaps !== 'inline') {
113 writeSourcemaps(output, `${outPath}.map`, cfg.force as boolean);
114 }
115
116 if (cfg.options.inject) {
117 injectBundle(builder, output, outfile, cfg);
118 }
119 return Promise.resolve();
120 });
121}
122
123function createOutputPath(baseURL: string, outfile: string, outputPath?: string) {
124 return outputPath ? path.resolve(outputPath, path.basename(outfile)) : path.resolve(baseURL, outfile);
125}
126
127export function writeSourcemaps(output: Builder.Output, outPath: string, force: boolean) {
128 if (fs.existsSync(outPath)) {
129 if (!force) {
130 throw new Error(`A source map named '${outPath}' already exists. Use the --force option to overwrite it.`);
131 }
132 fs.unlinkSync(outPath);
133 } else {
134 let dirPath = path.dirname(outPath);
135 if (!fs.existsSync(dirPath)) {
136 mkdirp.sync(dirPath);
137 }
138 }
139 fs.writeFileSync(outPath, output.sourceMap);
140}
141
142export function writeOutput(output: Builder.Output, outPath: string, force: boolean, sourceMap: boolean | string) {
143 if (fs.existsSync(outPath)) {
144
145 if (!force) {
146 throw new Error(`A bundle named '${outPath}' already exists. Use the --force option to overwrite it.`);
147 }
148
149 fs.unlinkSync(outPath);
150 } else {
151 let dirPath = path.dirname(outPath);
152
153 if (!fs.existsSync(dirPath)) {
154 mkdirp.sync(dirPath);
155 }
156 }
157 let source = output.source;
158
159 if (sourceMap && sourceMap !== 'inline') {
160 let sourceMapFileName = path.basename(outPath) + '.map';
161 source += '\n//# sourceMappingURL=' + sourceMapFileName;
162 }
163
164 fs.writeFileSync(outPath, source);
165}
166
167export function injectBundle(builder: Builder.BuilderInstance, output: Builder.Output, outfile: string, cfg: BundleConfig) {
168 let configPath = cfg.injectionConfigPath as string;
169 let bundleName = builder.getCanonicalName(sysUtil.toFileURL(path.resolve(cfg.baseURL, outfile)));
170 let appCfg = serializer.getAppConfig(configPath);
171
172 if (!appCfg.bundles) {
173 appCfg.bundles = {};
174 }
175 appCfg.bundles[bundleName] = output.modules.sort();
176 serializer.saveAppConfig(configPath, appCfg);
177}
178
179export function getFullModuleName(moduleName: string, map: any) {
180 let cleanName = (n: string) => {
181 // strip leading 'registry' prefixes
182 let result = n.replace(/^.*:/, '');
183 // strip trailing version info
184 if (result.charAt(0) === '@') {
185 result = '@' + result.substr(1).replace(/@.*$/, '');
186 } else {
187 result = result.replace(/@.*$/, '');
188 }
189
190 return result;
191 };
192
193 let matches = Object.keys(map).filter(m => m === moduleName);
194
195 if (matches.length === 1) {
196 return moduleName;
197 }
198
199 matches = Object.keys(map).filter(m => {
200 return cleanName(m) === cleanName(moduleName);
201 });
202
203 if (matches.length === 1) {
204 return matches[0];
205 }
206
207 if (matches.length === 0) {
208 return moduleName;
209 }
210
211 throw new Error(`A version conflict was found among the module names specified \
212 in the configuration for '${moduleName}'. Try including a full module name with a specific version \
213 number or resolve the conflict manually with jspm.`);
214}