UNPKG

@enact/dev-utils

Version:

A collection of development utilities for Enact apps.

287 lines (268 loc) 8.95 kB
const path = require('path'); const glob = require('glob'); const fs = require('graceful-fs'); const {DefinePlugin} = require('webpack'); function packageName(file) { try { return JSON.parse(fs.readFileSync(file, {encoding: 'utf8'})).name || ''; } catch (e) { return ''; } } function packageSearch(dir, pkg) { let pkgPath; while (dir.length > 0 && dir !== path.dirname(dir) && !pkgPath) { const full = path.join(dir, 'node_modules', pkg); if (fs.existsSync(full)) { pkgPath = path.relative(process.cwd(), full); } else { dir = path.dirname(dir); } } return pkgPath; } // Determine if it's a NodeJS output filesystem or if it's a foreign/virtual one. function isNodeOutputFS(compiler) { return ( compiler.outputFileSystem && compiler.outputFileSystem.constructor && compiler.outputFileSystem.constructor.name && compiler.outputFileSystem.constructor.name === 'NodeOutputFileSystem' ); } // Normalize a filepath to be relative to the webpack context, using forward-slashes, and // replace each '..' with '_', keeping in line with the file-loader and other webpack standards. function transformPath(context, file) { return path .relative(context, file) .replace(/\\/g, '/') .replace(/\.\.(\/)?/g, '_$1'); } function resolveBundle(dir, context) { const bundle = {resolved: dir, path: dir, emit: true}; if (path.isAbsolute(bundle.path)) { bundle.emit = false; bundle.resolved = JSON.stringify(bundle.path); } else { if (fs.existsSync(bundle.path)) { bundle.path = fs.realpathSync(bundle.path); } bundle.resolved = '__webpack_require__.p + ' + JSON.stringify(transformPath(context, bundle.path)); } return bundle; } // Read a manifest (creating a new one dynamically as applicable) and emit it, // returning its contents. function readManifest(compilation, manifest, opts) { let data; let files = []; if (typeof manifest === 'string') { if (fs.existsSync(manifest)) { manifest = fs.realpathSync(manifest); data = fs.readFileSync(manifest, {encoding: 'utf8'}); if (data) { files = JSON.parse(data).files || files; } } emitAsset(compilation, transformPath(opts.context, manifest), data); } else { files = manifest.value || files; data = JSON.stringify({files: files}, null, '\t'); emitAsset(compilation, transformPath(opts.context, manifest.generate), data); } return files; } // Read each manifest and process their contents. function handleBundles(compilation, manifests, opts, callback) { if (manifests.length === 0) { callback(); } else { const manifest = manifests.shift(); try { const files = readManifest(compilation, manifest, opts); if (fs.existsSync(manifest) || opts.create) { const dir = fs.realpathSync(path.dirname(manifest)); handleManifestFiles(compilation, dir, files, opts, () => { handleBundles(compilation, manifests, opts, callback); }); } else { handleBundles(compilation, manifests, opts, callback); } } catch (e) { compilation.errors.push(new Error('iLibPlugin: Unable to read localization manifest at ' + manifest)); handleBundles(compilation, manifests, opts, callback); } } } // Read and emit all the assets in a particular manifest. function handleManifestFiles(compilation, dir, files, opts, callback) { if (files.length === 0) { callback(); } else { const outfile = path.join(dir, files.shift()); if (shouldEmit(compilation.compiler, outfile, opts.cache)) { fs.readFile(outfile, (err, data) => { if (err) { compilation.errors.push(err); } else { emitAsset(compilation, transformPath(opts.context, outfile), data); } handleManifestFiles(compilation, dir, files, opts, callback); }); } else { handleManifestFiles(compilation, dir, files, opts, callback); } } } // Determine if the output file exists and if its newer to determine if it should be emitted. function shouldEmit(compiler, file, cache) { if (isNodeOutputFS(compiler)) { try { const src = fs.statSync(file); const dest = fs.statSync( path.join(compiler.options.output.path, transformPath(compiler.options.context, file)) ); return src.isDirectory() || src.mtime.getTime() > dest.mtime.getTime() || !cache; } catch (e) { return true; } } else { return true; } } // Add a given asset's data to the compilation array in a webpack-compatible source object. function emitAsset(compilation, name, data) { compilation.assets[name] = { size: function() { return data.length; }, source: function() { return data; }, updateHash: function(hash) { return hash.update(data); }, map: function() { return null; } }; } function ILibPlugin(options) { this.options = options || {}; this.options.ilib = this.options.ilib || process.env.ILIB_BASE_PATH; const pkgName = packageName('./package.json'); if (typeof this.options.ilib === 'undefined') { try { if (pkgName.indexOf('@enact') === 0) { this.options.resources = false; } if (pkgName === '@enact/i18n') { this.options.ilib = 'ilib'; } else { this.options.ilib = packageSearch(process.cwd(), '@enact/i18n/ilib'); } } catch (e) { console.error('ERROR: iLib locale not detected. Please ensure @enact/i18n is installed.'); process.exit(1); } } if (typeof this.options.resources === 'undefined') { this.options.resources = 'resources'; } this.options.bundles = this.options.bundles || {}; if (typeof this.options.bundles.moonstone === 'undefined') { if (pkgName === '@enact/moonstone') { this.options.bundles.moonstone = 'resources'; } else { const moonstone = packageSearch(process.cwd(), '@enact/moonstone'); if (moonstone) { this.options.bundles.moonstone = path.join(moonstone, 'resources'); } } } this.options.cache = typeof this.options.cache !== 'boolean' || this.options.cache; this.options.create = typeof this.options.create !== 'boolean' || this.options.create; this.options.emit = typeof this.options.emit !== 'boolean' || this.options.emit; } ILibPlugin.prototype.apply = function(compiler) { const opts = this.options; const created = []; let manifests = []; opts.context = compiler.options.context; if (opts.ilib) { // Resolve an accurate basepath for iLib. const ilib = resolveBundle(opts.ilib, opts.context); const definedConstants = { ILIB_BASE_PATH: ilib.resolved, ILIB_RESOURCES_PATH: resolveBundle(opts.resources || 'resources', opts.context).resolved, ILIB_CACHE_ID: '__webpack_require__.ilib_cache_id' }; for (const name in opts.bundles) { if (opts.bundles[name]) { const bundle = resolveBundle(opts.bundles[name], opts.context); definedConstants['ILIB_' + name.toUpperCase() + '_PATH'] = bundle.resolved; if (opts.emit && bundle.emit) { manifests.push(path.join(bundle.path, 'ilibmanifest.json')); } } } // Rewrite the iLib global constants to specific values corresponding to the build. compiler.apply(new DefinePlugin(definedConstants)); // Add a unique ID value to the webpack require-function, so that the value is correctly updated, // even when hot-reloading and serving. compiler.plugin('compilation', compilation => { compilation.mainTemplate.plugin('require-extensions', function(source) { const buf = [source]; buf.push(''); buf.push(this.requireFn + '.ilib_cache_id = ' + JSON.stringify('' + new Date().getTime()) + ';'); return this.asString(buf); }); }); // Prepare manifest list for usage. // Missing files will created if needed otherwise scanned. if (opts.emit && ilib.emit) { manifests.unshift(path.join(ilib.path, 'locale', 'ilibmanifest.json')); } if (opts.emit && opts.resources) { manifests.push(path.join(opts.resources, 'ilibmanifest.json')); } for (let i = 0; i < manifests.length; i++) { if (!fs.existsSync(manifests[i])) { const dir = path.dirname(manifests[i]); let files = []; if (fs.existsSync(dir)) { files = glob.sync('./**/!(appinfo).json', {nodir: true, cwd: dir}); for (let k = 0; k < files.length; k++) { files[k] = files[k].replace(/^\.\//, ''); } } if (opts.create) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir); } fs.writeFileSync(manifests[i], JSON.stringify({files: files}, null, '\t'), { encoding: 'utf8' }); created.push(manifests[i]); } else { manifests[i] = {generate: manifests[i], value: files}; } } } // Emit all bundles as applicable. compiler.plugin('emit', (compilation, callback) => { for (let j = 0; j < created.length; j++) { compilation.warnings.push( new Error( 'iLibPlugin: Localization resource manifest not found. Created ' + created[j] + ' to prevent future errors.' ) ); } manifests = compilation.applyPluginsWaterfall('ilib-manifest-list', manifests); handleBundles(compilation, manifests, opts, callback); }); } }; module.exports = ILibPlugin;