UNPKG

apostrophe

Version:
908 lines (831 loc) • 33 kB
const path = require('node:path'); const util = require('node:util'); const Promise = require('bluebird'); const fs = require('fs-extra'); const { stripIndent } = require('common-tags'); const webpackModule = require('webpack'); const { mergeWithCustomize: webpackMerge } = require('webpack-merge'); const { pathToFileURL } = require('node:url'); const { getBundlesNames, writeBundlesImportFiles, findNodeModulesSymlinks } = require('../webpack/utils'); const Concat = require('concat-with-sourcemaps'); // The internal Webpack build task. module.exports = (self) => ({ async task(argv = {}) { if (self.hasBuildModule()) { return self.build(argv); } if (self.options.es5 && !self.es5TaskFn) { self.apos.util.warnDev(stripIndent` es5: true is set. IE11 compatibility builds now require that you install the optional @apostrophecms/asset-es5 module. Until then, for backwards compatibility, your build will succeed but will not be IE11 compatible. `); self.options.es5 = false; } // The lock could become huge, cache it, see computeCacheMeta() let packageLockContentCached; const req = self.apos.task.getReq(); const namespace = self.getNamespace(); const buildDir = `${self.apos.rootDir}/apos-build/${namespace}`; const bundleDir = `${self.apos.rootDir}/public/apos-frontend/${namespace}`; const modulesToInstantiate = self.apos.modulesToBeInstantiated(); const symLinkModules = await findNodeModulesSymlinks(self.apos.npmRootDir); // Make it clear if builds should detect changes const detectChanges = typeof argv.changes === 'string'; // Remove invalid changes. `argv.changes` is a comma separated list of // relative to `apos.rootDir` files or folders const sourceChanges = detectChanges ? filterValidChanges( argv.changes.split(',').map(p => p.trim()), Object.keys(self.apos.modules) ) : []; // Keep track of the executed builds const buildsExecuted = []; // Don't clutter up with previous builds. await fs.remove(buildDir); await fs.mkdirp(buildDir); // Static asset files in `public` subdirs of each module are copied // to the same relative path // `/public/apos-frontend/namespace/modules/modulename`. Inherited files are // also copied, with the deepest subclass overriding in the event of a // conflict if (self.options.publicBundle) { await moduleOverrides(`${bundleDir}/modules`, 'public'); } for (const [ name, options ] of Object.entries(self.builds)) { // If the option is not present always rebuild everything... let rebuild = argv && !argv['check-apos-build']; // ...but only when not in a watcher mode if (detectChanges) { rebuild = shouldRebuildFor(name, options, sourceChanges); } else if (!rebuild) { let checkTimestamp = false; // If options.publicBundle, only builds contributing // to the apos admin UI (currently just "apos") // are candidates to skip the build simply because package-lock.json is // older than the bundle. All other builds frequently contain // project level code // Else we can skip also for the src bundle if (options.apos || !self.options.publicBundle) { const bundleExists = await fs.pathExists(bundleDir); if (!bundleExists) { rebuild = true; } if (!process.env.APOS_DEV) { checkTimestamp = await fs.pathExists(`${bundleDir}/${name}-build-timestamp.txt`); } if (checkTimestamp) { // If we have a UI build timestamp file compare against the app's // package.json modified time. if (await lockFileIsNewer(name)) { rebuild = true; } } else { rebuild = true; } } else { // Always redo the other builds, // which are quick and typically contain // project level code not detectable by // comparing package-lock timestamps rebuild = true; } } if (rebuild) { await fs.mkdirp(bundleDir); await build({ name, options }); buildsExecuted.push(name); } } // No need of deploy if in a watcher mode. // Also we return an array of build names that // have been triggered - this is required by the watcher // so that page refresh is issued only when needed. if (detectChanges) { // merge the scenes that have been built const scenes = [ ...new Set(buildsExecuted.map(name => self.builds[name].scenes).flat()) ]; merge(scenes); return buildsExecuted; } // Discover the set of unique asset scenes that exist (currently // just `public` and `apos`) by examining those specified as // targets for the various builds const scenes = [ ...new Set(Object.values(self.builds).map(options => options.scenes).flat()) ]; // enumerate public assets and include them in deployment if appropriate const publicAssets = self.apos.util.glob('modules/**/*', { cwd: bundleDir, mark: true }).filter(match => !match.endsWith('/')); const deployFiles = [ ...publicAssets, ...merge(scenes), ...getBundlesNames(self.extraBundles, self.options.es5) ]; await deploy(deployFiles); async function moduleOverrides(modulesDir, source, pnpmPaths) { await fs.remove(modulesDir); await fs.mkdirp(modulesDir); let names = {}; const directories = {}; const pnpmOnly = {}; // Most other modules are not actually instantiated yet, but // we can access their metadata, which is sufficient for (const name of modulesToInstantiate) { const ancestorDirectories = []; const metadata = await self.apos.synth.getMetadata(name); for (const entry of metadata.__meta.chain) { const effectiveName = entry.name.replace(/^my-/, ''); names[effectiveName] = true; if (entry.npm && !entry.bundled && !entry.my) { pnpmOnly[entry.dirname] = true; } ancestorDirectories.push(entry.dirname); directories[effectiveName] = directories[effectiveName] || []; for (const dir of ancestorDirectories) { if (!directories[effectiveName].includes(dir)) { directories[effectiveName].push(dir); } } } } names = Object.keys(names); for (const name of names) { const moduleDir = `${modulesDir}/${name}`; for (const dir of directories[name]) { const srcDir = `${dir}/${source}`; if (fs.existsSync(srcDir)) { if ( // pnpmPaths is provided pnpmPaths && // is pnpm installation self.apos.isPnpm && // is npm module and not bundled pnpmOnly[dir] && // isn't apos core module !dir.startsWith(path.join(self.apos.npmRootDir, 'node_modules/apostrophe/')) ) { // Ignore further attempts to register this path (performance) pnpmOnly[dir] = false; // resolve symlinked pnpm path const resolved = fs.realpathSync(dir); // go up to the pnpm node_modules directory pnpmPaths.add(resolved.split(name)[0]); } await fs.copy(srcDir, moduleDir); } } } } async function build({ name, options }) { self.apos.util.log(req.t('apostrophe:assetTypeBuilding', { label: req.t(options.label) })); const modulesDir = `${buildDir}/${name}/modules`; const source = options.source || name; // Gather pnpm modules that are used in the build to be added as resolve // paths const pnpmModules = new Set(); await moduleOverrides(modulesDir, `ui/${source}`, pnpmModules); let iconImports, componentImports, tiptapExtensionImports, appImports, indexJsImports, indexSassImports; if (options.apos) { iconImports = await getIcons(); componentImports = await getImports(`${source}/components`, '*.vue', { registerComponents: true, importLastVersion: true }); /* componentImports = getGlobalVueComponents(self); */ tiptapExtensionImports = await getImports( `${source}/tiptap-extensions`, '*.js', { registerTiptapExtensions: true } ); appImports = await getImports(`${source}/apps`, '*.js', { invokeApps: true, enumerateImports: true, importSuffix: 'App' }); } if (options.index) { // Gather modules with non-main, catch-all bundles const ignoreModules = self.rebundleModules .filter(entry => !entry.main && !entry.source) .reduce((acc, entry) => ({ ...acc, [entry.name]: true }), {}); indexJsImports = await getImports(source, 'index.js', { invokeApps: true, enumerateImports: true, importSuffix: 'App', requireDefaultExport: true, mainModuleBundles: getMainModuleBundleFiles('js'), ignoreModules }); indexSassImports = await getImports(source, 'index.scss', { importSuffix: 'Stylesheet', enumerateImports: true, mainModuleBundles: getMainModuleBundleFiles('scss'), ignoreModules }); } if (options.webpack) { const importFile = `${buildDir}/${name}-import.js`; writeImportFile({ importFile, prologue: options.prologue, icon: iconImports, components: componentImports, tiptap: tiptapExtensionImports, app: appImports, indexJs: indexJsImports, indexSass: indexSassImports }); const outputFilename = `${name}-build.js`; // Remove previous build artifacts, as some pipelines won't build all // artifacts if there is no input, and we don't want stale output in the // bundle fs.removeSync(`${bundleDir}/${outputFilename}`); const cssPath = `${bundleDir}/${outputFilename}`.replace(/\.js$/, '.css'); fs.removeSync(cssPath); const webpack = Promise.promisify(webpackModule); const webpackBaseConfig = require(`../webpack/${name}/webpack.config`); const webpackExtraBundles = writeBundlesImportFiles({ name, buildDir, mainBundleName: outputFilename.replace('.js', ''), verifiedBundles: getVerifiedBundlesInEffect(), getImportFileOutput, writeImportFile }); const webpackInstanceConfig = webpackBaseConfig({ importFile, modulesDir, outputPath: bundleDir, outputFilename, bundles: webpackExtraBundles, pnpmModulesResolvePaths: pnpmModules, // Added on the fly by the // @apostrophecms/asset-es5 module, // if it is present es5TaskFn: self.es5TaskFn }, self.apos); const webpackInstanceConfigMerged = !options.apos && self.webpackExtensions ? webpackMerge({ customizeArray: self.srcCustomizeArray, customizeObject: self.srcCustomizeObject })(webpackInstanceConfig, ...Object.values(self.webpackExtensions)) : webpackInstanceConfig; // Inject the cache location at the end - we need the merged config const cacheMeta = await computeCacheMeta( name, webpackInstanceConfigMerged, symLinkModules ); webpackInstanceConfigMerged.cache.cacheLocation = cacheMeta.location; // Exclude symlinked modules from the cache managedPaths, no other way // for now https://github.com/webpack/webpack/issues/12112 if (cacheMeta.managedPathsRegex) { webpackInstanceConfigMerged.snapshot = { managedPaths: [ cacheMeta.managedPathsRegex ] }; } const result = await webpack(webpackInstanceConfigMerged); await writeCacheMeta(name, cacheMeta); if (result.compilation.errors.length) { // Throwing a string is appropriate in a command line task throw cleanErrors(result.toString('errors')); } else if (result.compilation.warnings.length) { self.apos.util.warn(result.toString('errors-warnings')); } else if (process.env.APOS_WEBPACK_VERBOSE) { self.apos.util.info(result.toString('verbose')); } if (fs.existsSync(cssPath)) { fs.writeFileSync(cssPath, self.filterCss(fs.readFileSync(cssPath, 'utf8'), { modulesPrefix: `${self.getAssetBaseUrl()}/modules` })); } if (options.apos || !self.options.publicBundle) { const now = Date.now().toString(); fs.writeFileSync(`${bundleDir}/${name}-build-timestamp.txt`, now); } } else { if (options.outputs.includes('js')) { // We do not use an import file here because import is not // an ES5 feature and it is contrary to the spirit of ES5 code // to force-fit that type of code. We do not mandate ES6 in // "public" code (loaded for logged-out users who might have // old browsers). // // Of course, developers can push an "public" asset that is // the output of an ES6 pipeline. const publicImports = await getImports(name, '*.js'); fs.writeFileSync(`${bundleDir}/${name}-build.js`, (((options.prologue || '') + '\n') || '') + publicImports.paths.map(path => { return fs.readFileSync(path, 'utf8'); }).join('\n') ); } if (options.outputs.includes('css')) { const publicImports = await getImports(name, '*.css'); fs.writeFileSync(`${bundleDir}/${name}-build.css`, publicImports.paths.map(path => { return self.filterCss(fs.readFileSync(path, 'utf8'), { modulesPrefix: `${self.getAssetBaseUrl()}/modules` }); }).join('\n') ); } } self.apos.util.log(req.t('apostrophe:assetTypeBuildComplete', { label: req.t(options.label) })); } function getMainModuleBundleFiles(ext) { return Object.values(self.verifiedBundles) .reduce((acc, entry) => { if (!entry.main) { return acc; }; return [ ...acc, ...(entry[ext] || []) ]; }, []); } function getVerifiedBundlesInEffect() { return Object.entries(self.verifiedBundles) .reduce((acc, [ key, entry ]) => { if (entry.main) { return acc; }; return { ...acc, [key]: entry }; }, {}); } function writeImportFile ({ importFile, prologue, icon, components, tiptap, app, indexJs, indexSass }) { fs.writeFileSync(importFile, (prologue || '') + stripIndent` ${(icon && icon.importCode) || ''} ${(components && components.importCode) || ''} ${(tiptap && tiptap.importCode) || ''} ${(app && app.importCode) || ''} ${(indexJs && indexJs.importCode) || ''} ${(indexSass && indexSass.importCode) || ''} ${(icon && icon.registerCode) || ''} ${(components && components.registerCode) || ''} ${(tiptap && tiptap.registerCode) || ''} ` + (app ? stripIndent` if (document.readyState !== 'loading') { setTimeout(invoke, 0); } else { window.addEventListener('DOMContentLoaded', invoke); } function invoke() { ${app.invokeCode} } ` : '') + // No delay on these, they expect to run early like ui/public code // and the first ones invoked set up expected stuff like apos.http (indexJs ? stripIndent` ${indexJs.invokeCode} ` : '') ); } async function getIcons() { for (const name of modulesToInstantiate) { const metadata = await self.apos.synth.getMetadata(name); // icons is an unparsed section, so getMetadata gives it back // to us as an object with a property for each class in the // inheritance tree, root first. Just keep merging in // icons from that for (const [ name, layer ] of Object.entries(metadata.icons)) { if ((typeof layer) === 'function') { // We should not support invoking a function to define the icons // because the developer would expect `(self)` to behave // normally, and they won't during an asset build. So we only // accept a simple object with the icon mappings throw new Error(`Error in ${name} module: the "icons" property may not be a function.`); } Object.assign(self.iconMap, layer || {}); } } // Load global vue icon components. const output = { importCode: '', registerCode: 'window.apos.iconComponents = window.apos.iconComponents || {};\n' }; const importIndex = []; for (const [ registerAs, importFrom ] of Object.entries(self.iconMap)) { let importName = importFrom; if (!importIndex.includes(importFrom)) { if (importFrom.substring(0, 1) === '~') { importName = self.apos.util.slugify(importFrom).replaceAll('-', ''); output.importCode += `import ${importName}Icon from '${importFrom.substring(1)}';\n`; } else { output.importCode += `import ${importName}Icon from '@apostrophecms/vue-material-design-icons/${importFrom}.vue';\n`; } importIndex.push(importFrom); } output.registerCode += `window.apos.iconComponents['${registerAs}'] = ${importName}Icon;\n`; } return output; } function merge(scenes) { return scenes.reduce((acc, scene) => { const jsModules = `${scene}-module-bundle.js`; const jsNoModules = `${scene}-nomodule-bundle.js`; const css = `${scene}-bundle.css`; writeSceneBundle({ scene, filePath: jsModules, jsCondition: 'module' }); writeSceneBundle({ scene, filePath: jsNoModules, jsCondition: 'nomodule' }); writeSceneBundle({ scene, filePath: css, checkForFile: true }); return [ ...acc, jsModules, jsNoModules, css ]; }, []); } function writeSceneBundle ({ scene, filePath, jsCondition, checkForFile = false }) { const [ _ext, fileExt ] = filePath.match(/\.(\w+)$/); const filterBuilds = ({ scenes, outputs, condition }) => { return outputs.includes(fileExt) && ((!condition || !jsCondition) || condition === jsCondition) && scenes.includes(scene); }; const files = Object.entries(self.builds) .filter(([ _, options ]) => filterBuilds(options)) .map(([ name ]) => `${bundleDir}/${name}-build.${fileExt}`) .filter(file => { return (!checkForFile) || fs.existsSync(file); }); const bundlePath = `${bundleDir}/${filePath}`; const sourceMapDir = self.options.productionSourceMapsDir || bundleDir; const sourceMapPath = `${sourceMapDir}/${filePath}.map`; const needSourcemap = (process.env.NODE_ENV !== 'production') || self.options.productionSourceMaps; if (!needSourcemap) { return fs.writeFileSync(bundlePath, files.map(file => fs.readFileSync(file, 'utf8')).join('\n')); } // Concatenate in a way that preserves sitemaps const concat = new Concat(true, bundlePath, '\n'); for (const file of files) { const map = `${file}.map`; // concat-with-sourcemaps does not strip old sourcemap comments for us const source = stripSourceMapComment(fs.readFileSync(file, 'utf8')); if (!fs.existsSync(map)) { concat.add( file, source ); } else { concat.add( file, source, // Per docs for concat-source-maps: this one should be read as a string fs.readFileSync(map, 'utf8') ); } } // Normally a production build does not include a sourcemap in the // final deliverable // concat-with-sourcemaps does not do this either const content = concat.content.toString('utf8') + `\n//# sourceMappingURL=${filePath}.map\n`; fs.writeFileSync(bundlePath, content); fs.writeFileSync(sourceMapPath, concat.sourceMap); } // If NODE_ENV is production, this function will copy the given // array of asset files from `${bundleDir}/${file}` to // the same relative location in the appropriate release subdirectory in // `/public/apos-frontend/releases`, or in `/apos-frontend/releases` in // uploadfs if `APOS_UPLOADFS_ASSETS` is present. // // If NODE_ENV is not production this function does nothing and // the assets are served directly from `/public/apos-frontend/${file}`. // // The namespace (e.g. default) should be part of each filename given. // A leading slash should NOT be passed. async function deploy(files) { if (process.env.NODE_ENV !== 'production') { return; } let copyIn; let releaseDir; const releaseId = self.getReleaseId(); if (process.env.APOS_UPLOADFS_ASSETS) { // The right choice if uploadfs is mapped to S3, Azure, etc., // not the local filesystem copyIn = util.promisify(self.uploadfs.copyIn); releaseDir = `/apos-frontend/releases/${releaseId}/${namespace}`; } else { // The right choice with Docker if uploadfs is just the local filesystem // mapped to a volume (a Docker build step can't access that) copyIn = fsCopyIn; releaseDir = `${self.apos.rootDir}/public/apos-frontend/releases/${releaseId}/${namespace}`; await fs.mkdirp(releaseDir); } for (const file of files) { const src = `${bundleDir}/${file}`; await copyIn(src, `${releaseDir}/${file}`); const srcMap = `${src}.map`; if (fs.existsSync(srcMap)) { await copyIn(srcMap, `${releaseDir}/${file}.map`); } await fs.remove(src); await fs.remove(srcMap); } } async function fsCopyIn(from, to) { const base = path.dirname(to); await fs.mkdirp(base); return fs.copyFile(from, to); } async function getImports(folder, pattern, options = {}) { let components = []; const seen = {}; for (const name of modulesToInstantiate) { const metadata = await self.apos.synth.getMetadata(name); for (const entry of metadata.__meta.chain) { if (options.ignoreModules?.[entry.name]) { seen[entry.dirname] = true; } if (seen[entry.dirname]) { continue; } components = components.concat(self.apos.util.glob(`${entry.dirname}/ui/${folder}/${pattern}`)); seen[entry.dirname] = true; } } if (options.importLastVersion) { // Reverse the list so we can easily find the last configured import // of a given component, allowing "improve" modules to win over // the originals when shipping an override of a Vue component // with the same name, and filter out earlier versions components.reverse(); const seen = new Set(); components = components.filter(component => { const name = getComponentName(component, options); if (seen.has(name)) { return false; } seen.add(name); return true; }); // Put the components back in their original order components.reverse(); } if (options.mainModuleBundles) { components.push(...options.mainModuleBundles); } return getImportFileOutput(components, options); } function getImportFileOutput (components, options = {}) { let registerCode; if (options.registerComponents) { registerCode = 'window.apos.vueComponents = window.apos.vueComponents || {};\n'; } else if (options.registerTiptapExtensions) { registerCode = 'window.apos.tiptapExtensions = window.apos.tiptapExtensions || [];\n'; } else { registerCode = ''; } const output = { importCode: '', registerCode, invokeCode: '', paths: [] }; components.forEach((component, i) => { if (options.requireDefaultExport) { if (!fs.readFileSync(component, 'utf8').match(/export[\s\n]+default/)) { throw new Error(stripIndent` The file ${component} does not have a default export. Any ui/src/index.js file that does not have a function as its default export will cause the build to fail in production. `); } } // We know component is a file path at this point const importUrl = JSON.stringify(pathToFileURL(component)); const name = getComponentName(component, options, i); const jsName = JSON.stringify(name); const importName = `${name}${options.importSuffix || ''}`; const importCode = ` import ${importName} from ${importUrl}; `; output.paths.push(component); output.importCode += `${importCode}\n`; if (options.registerComponents) { output.registerCode += `window.apos.vueComponents[${jsName}] = ${importName};\n`; } if (options.registerTiptapExtensions) { output.registerCode += stripIndent` apos.tiptapExtensions.push(${importName}); `; } if (options.invokeApps) { output.invokeCode += `${name}${options.importSuffix || ''}();\n`; } }); return output; } async function lockFileIsNewer(name) { const timestamp = fs.readFileSync(`${bundleDir}/${name}-build-timestamp.txt`, 'utf8'); let pkgStats; const packageLock = await findPackageLock(); if (packageLock) { pkgStats = await fs.stat(packageLock); } const pkgTimestamp = pkgStats && pkgStats.mtimeMs; return pkgTimestamp > parseInt(timestamp); } async function findPackageLock() { const packageLockPath = path.join(self.apos.npmRootDir, 'package-lock.json'); const yarnPath = path.join(self.apos.npmRootDir, 'yarn.lock'); const pnpmPath = path.join(self.apos.npmRootDir, 'pnpm-lock.yaml'); if (await fs.pathExists(packageLockPath)) { return packageLockPath; } else if (await fs.pathExists(yarnPath)) { return yarnPath; } else if (await fs.pathExists(pnpmPath)) { return pnpmPath; } else { return false; } } function getComponentName(component, { enumerateImports } = {}, i) { return path .basename(component) .replace(/-/g, '_') .replace(/\.\w+/, '') + (enumerateImports ? `_${i}` : ''); } function cleanErrors(errors) { // Dev experience: remove confusing and inaccurate webpack warning about // module loaders when straightforward JS parse errors occur, // stackoverflow is full of people confused by this return errors.replace(/(ERROR in[\s\S]*?Module parse failed[\s\S]*)You may need an appropriate loader.*/, '$1'); } // A (CPU intensive) webpack cache helper to compute a hash and build paths. // Cache the result when possible. // The base cache path is by default `data/temp/webpack-cache` // but it can be overridden by an APOS_ASSET_CACHE environment. // In order to compute an accurate hash, this helper needs // the final, merged webpack configuration. async function computeCacheMeta(name, webpackConfig, symLinkModules) { const cacheBase = self.getCacheBasePath(); if (!packageLockContentCached) { const packageLock = await findPackageLock(); if (packageLock === false) { // this should happen only when testing and // we don't want to break all non-core module tests packageLockContentCached = 'none'; } else { packageLockContentCached = await fs.readFile(packageLock, 'utf8'); } } // Plugins can output timestamps or other random information as // configuration (e.g. StylelintWebpackPlugin). As we don't have // control over plugins, we need to remove their configuration values. // We keep only the plugin name and config keys sorted list. // Additionally plugins are sorted by their constructor names. // A shallow clone is enough to avoid modification of the original // config. const config = { ...webpackConfig }; config.plugins = (config.plugins || []).map(p => { const result = []; if (p.constructor && p.constructor.name) { result.push(p.constructor.name); } const keys = Object.keys(p); keys.sort(); result.push(...keys); return result; }); config.plugins.sort((a, b) => (a[0] || '').localeCompare(b[0])); const configString = util.inspect(config, { depth: 10, compact: true, breakLength: Infinity }); const hash = self.apos.util.md5( `${self.getNamespace()}:${name}:${packageLockContentCached}:${configString}` ); const location = path.resolve(cacheBase, hash); // Retrieve symlinkModules and convert them to managedPaths regex rule let managedPathsRegex; if (symLinkModules.length > 0) { const regex = symLinkModules .map(m => self.apos.util.regExpQuote(m)) .join('|'); managedPathsRegex = new RegExp( '^(.+?[\\/]node_modules)[\\/]((?!' + regex + ')).*[\\/]*' ); } return { base: cacheBase, hash, location, managedPathsRegex }; } // Add .apos, useful for debugging, testing and cache invalidation. // It also keeps in sync the modified time of the cache folder. async function writeCacheMeta(name, cacheMeta) { try { const cachePath = path.join(cacheMeta.location, '.apos'); const lastModified = new Date(); await fs.writeFile( cachePath, `${lastModified.toISOString()} ${self.getNamespace()}:${name}`, 'utf8' ); // should be the same as the meta await fs.utimes(cachePath, lastModified, lastModified); } catch (e) { // Build probably failed, path is missing, ignore } } // Given a set of changes, leave only those that belong to an active // Apostrophe module. This would avoid unnecessary builds for non-active // watched files (e.g. in multi instance mode). // It's an expensive brute force O(n^2), so we do it once for all builds // and we rely on the fact that mass changes happen rarely. function filterValidChanges(all, modules) { return all.filter(c => { for (const module of modules) { if (c.includes(module)) { return true; } } return false; }); } // Detect if a build should be executed based on the changed // paths. This function is invoked only when the appropriate `argv.changes` // is passed to the task. function shouldRebuildFor(buildName, buildOptions, changes) { const name = buildOptions.source || buildName; const id = `/ui/${name}/`; for (const change of changes) { if (change.includes(id)) { return true; } } return false; } } }); // Helper to remove existing sourceMappingURL comments function stripSourceMapComment(code) { return code.replace(/\/\/[@#]\s*sourceMappingURL=.*$/gm, ''); }