1 | import * as fs from 'fs';
|
2 | import * as Promise from 'bluebird';
|
3 | import * as sysUtil from 'systemjs-builder/lib/utils.js';
|
4 | import * as Builder from 'systemjs-builder';
|
5 | import * as path from 'path';
|
6 | import * as _ from 'lodash';
|
7 | import * as utils from './utils';
|
8 | import * as serializer from './config-serializer';
|
9 | import * as minifier from 'html-minifier';
|
10 | import * as CleanCSS from 'clean-css';
|
11 | import { createBuilder } from './builder-factory';
|
12 | import { BundleConfig, FetchHook } from "./models";
|
13 | import * as mkdirp from 'mkdirp';
|
14 |
|
15 | function 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 |
|
31 | function 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 |
|
68 | export 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 |
|
83 | export function depCache(cfg: BundleConfig): Promise<any> {
|
84 | let buildExpression = createBuildExpression(cfg);
|
85 | return _depCache(buildExpression, cfg);
|
86 | }
|
87 |
|
88 | function _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 |
|
104 | function _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 |
|
123 | function createOutputPath(baseURL: string, outfile: string, outputPath?: string) {
|
124 | return outputPath ? path.resolve(outputPath, path.basename(outfile)) : path.resolve(baseURL, outfile);
|
125 | }
|
126 |
|
127 | export 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 |
|
142 | export 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 |
|
167 | export 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 |
|
179 | export function getFullModuleName(moduleName: string, map: any) {
|
180 | let cleanName = (n: string) => {
|
181 |
|
182 | let result = n.replace(/^.*:/, '');
|
183 |
|
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 | }
|