import * as fs from 'fs'; import * as path from 'path'; import * as glob from 'glob'; /** * Simple Promiseify function that takes a Node API and return a version that supports promises. * We use promises instead of synchronized functions to make the process less I/O bound and * faster. It also simplify the code. */ function promiseify(fn) { return function (...args) { return new Promise((resolve, reject) => { fn.apply(this, args.concat([function (err, value) { if (err) { reject(err); } else { resolve(value); } }])); }); }; } const readFile = promiseify(fs.readFile); const writeFile = promiseify(fs.writeFile); export function copyResources(globs) { if (typeof globs === 'string') { globs = [globs]; } /** * For every argument, inline the templates and styles under it and write the new file. */ return Promise.all(globs.map(pattern => { if (pattern.indexOf('*') < 0) { // Argument is a directory target, add glob patterns to include every files. pattern = path.join(pattern, '**', '*'); } const files = glob.sync(pattern, {}) .filter(name => /\.js$/.test(name)); // Matches only JavaScript files. // Generate all files content with inlined templates. return Promise.all(files.map(filePath => { return readFile(filePath, 'utf-8') .then(content => inlineResourcesFromString(content, url => { return path.join(path.dirname(filePath), url); })) .then(content => writeFile(filePath, content)) .catch(err => { console.error('An error occurred: ', err); }); })); })); } /** * Inline resources from a string content. * @param content {string} The source file's content. * @param urlResolver {Function} A resolver that takes a URL and return a path. * @returns {string} The content with resources inlined. */ function inlineResourcesFromString(content, urlResolver) { // Curry through the inlining functions. return [ inlineTemplate, inlineStyle, removeModuleId, ].reduce((с, fn) => fn(с, urlResolver), content); } if (require.main === module) { copyResources(process.argv.slice(2)); } /** * Inline the templates for a source file. Simply search for instances of `templateUrl: ...` and * replace with `template: ...` (with the content of the file included). * @param content {string} The source file's content. * @param urlResolver {Function} A resolver that takes a URL and return a path. * @return {string} The content with all templates inlined. */ function inlineTemplate(content, urlResolver) { return content.replace(/templateUrl:\s*'([^']+?\.html)'/g, function (_, templateUrl) { const templateFile = urlResolver(templateUrl); const templateContent = fs.readFileSync(templateFile, 'utf-8'); const shortenedTemplate = templateContent .replace(/([\n\r]\s*)+/gm, ' ') .replace(/"/g, '\\"'); return `template: "${shortenedTemplate}"`; }); } /** * Inline the styles for a source file. Simply search for instances of `styleUrls: [...]` and * replace with `styles: [...]` (with the content of the file included). * @param urlResolver {Function} A resolver that takes a URL and return a path. * @param content {string} The source file's content. * @return {string} The content with all styles inlined. */ function inlineStyle(content, urlResolver) { return content.replace(/styleUrls:\s*(\[[\s\S]*?\])/gm, function (_, styleUrls) { // tslint:disable-next-line const urls = eval(styleUrls); return 'styles: [' + urls.map(styleUrl => { const styleFile = urlResolver(styleUrl); const styleContent = fs.readFileSync(styleFile, 'utf-8'); const shortenedStyle = styleContent .replace(/([\n\r]\s*)+/gm, ' ') .replace(/"/g, '\\"'); return `"${shortenedStyle}"`; }) .join(',\n') + ']'; }); } /** * Remove every mention of `moduleId: module.id`. * @param content {string} The source file's content. * @returns {string} The content with all moduleId: mentions removed. */ function removeModuleId(content) { return content.replace(/\s*moduleId:\s*module\.id\s*,?\s*/gm, ''); }