UNPKG

@v4fire/client

Version:

V4Fire client core library

430 lines (328 loc) • 9.12 kB
'use strict'; /*! * V4Fire Client Core * https://github.com/V4Fire/Client * * Released under the MIT license * https://github.com/V4Fire/Client/blob/master/LICENSE */ const $C = require('collection.js'), config = require('@config/config'); const fs = require('fs-extra'), path = require('upath'); const {src, build, webpack} = config, {resolve, block, entries} = require('@pzlr/build-core'); const componentParams = include('build/graph/component-params'); const { isLayerDep, HTML, RUNTIME, STANDALONE, STYLES, MIN_PROCESS, MAX_TASKS_PER_ONE_PROCESS, MAX_PROCESS } = include('build/const'); const { output, cacheDir, isStandalone } = include('build/helpers'); /** * The project graph * @type {Promise<{entry, components, processes, dependencies}>} */ module.exports = buildProjectGraph(); const needLoadStylesAsJS = webpack.dynamicPublicPath(); let styleIndex = 1; /** * Builds a project graph * @returns {!Promise<{entry, components, processes, dependencies}>} */ async function buildProjectGraph() { block.setObjToHash(config.componentDependencies()); const graphCacheFile = path.join(cacheDir, 'graph.json'); // Graph already exists in the cache and we can read it if (build.buildGraphFromCache && fs.existsSync(graphCacheFile)) { return loadFromCache(); } fs.mkdirpSync(cacheDir); fs.writeFileSync(graphCacheFile, ''); // All others process will use this cache process.env.BUILD_GRAPH_FROM_CACHE = 1; const tmpEntries = path.join(resolve.entry(), `tmp/${build.hash()}`); fs.mkdirpSync(tmpEntries); fs.mkdirpSync(path.join(src.clientOutput(), path.dirname(output))); let entriesFilter; // Filtering of build entries (if there are specified) if (build.entries) { entriesFilter = $C(build.entries).reduce((map, el) => { map[el] = true; return map; }, {}); entriesFilter.index = true; entriesFilter.std = true; } const monic = config.monic().javascript; const buildConfig = (await entries.getBuildConfig({monic})).filter((el, key) => !entriesFilter || entriesFilter[key]), components = await getComponents(); const graph = await buildConfig.getUnionEntryPoints({cache: components}), processes = $C(MIN_PROCESS).map(() => ({entries: {}})); // Generate dynamic entries to build with webpack const entry = await $C(graph.entry) .parallel() .to({}) .reduce(entryReducer); processes[HTML].name = 'html'; processes[RUNTIME].name = 'runtime'; processes[STANDALONE].name = 'standalone'; processes[STYLES].name = 'styles'; // Add to HTML task all other tasks as dependencies processes[HTML].dependencies = processes .map((proc) => proc.name) .filter((name) => name !== 'html'); const res = { entry, components, processes, dependencies: $C(graph.dependencies).map((el, key) => [...el, key]) }; fs.writeFileSync( graphCacheFile, JSON.stringify(Object.reject(res, 'components'), undefined, 2) ); console.log('The project graph is initialized'); return res; /** * Reducer to create an entry point object */ async function entryReducer(entry, list, name) { // JS / TS const componentsToIgnore = /^[iv]-/, cursor = isStandalone(name) ? STANDALONE : RUNTIME; const webpackRuntime = "require('core/prelude/webpack');", taskProcess = processes[cursor]; { const entrySrc = path.join(tmpEntries, `${name}.js`); const content = await $C(list).async.to('').reduce(async (str, {name}) => { const component = components.get(name), logic = await component?.logic; if (component) { $C(component.libs).forEach((el) => str += `require('${el}');\n`); } const needRequireAsLogic = component ? logic : /^$|^\.(?:js|ts)(?:\?|$)/.test(path.extname(name)); if (needRequireAsLogic) { let entry; if (logic) { entry = logic; } else if (resolve.isNodeModule(name)) { entry = name; } else { entry = path.resolve(tmpEntries, '../', name); } str += `require('${getEntryPath(entry)}');\n`; } return str; }); if (content) { fs.writeFileSync(entrySrc, content); entry[name] = entrySrc; taskProcess.entries[name] = entrySrc; } } // TEMPLATES { const entryName = `${name}_tpl`, entrySrc = path.join(tmpEntries, `${name}.ss.js`); const content = await $C(list).async.to('').reduce(async (str, {name, isParent}) => { const component = components.get(name), tpl = await component?.tpl; if (!isParent && tpl && !componentsToIgnore.test(name)) { const entry = getEntryPath(tpl); str += `Object.assign(TPLS, require('./${entry}'));\n`; } return str; }); if (content) { fs.writeFileSync( entrySrc, [ webpackRuntime, 'window.TPLS = window.TPLS || Object.create(null);', content ].join('\n') ); entry[entryName] = entrySrc; taskProcess.entries[entryName] = entrySrc; } } // CSS { const entryName = `${name}_style`, stylSrc = path.join(tmpEntries, `${name}.styl`); const content = await $C(list).async.to('').reduce(async (str, {name, isParent}) => { const component = components.get(name), styles = await component?.styles; const needRequireAsStyles = component ? !isParent && styles && styles.length && !componentsToIgnore.test(name) : /^\.(?:styl|css)(?:\?|$)/.test(path.extname(name)); if (needRequireAsStyles) { const getImport = (filePath) => `@import "${getEntryPath(filePath)}"\n`; if (component) { $C(styles).forEach((filePath) => { str += getImport(filePath); }); } else { str += getImport(name); } if (!needLoadStylesAsJS) { const normalizedName = path.basename(name, path.extname(name)); if (/^[bp]-/.test(normalizedName)) { str += ` .${normalizedName} extends($${normalizedName.camelize(false)}) `; } } } return str; }); if (content) { let entrySrc; if (needLoadStylesAsJS) { entrySrc = path.join(tmpEntries, `${name}.styl.js`); fs.writeFileSync(stylSrc, content); fs.writeFileSync( entrySrc, [webpackRuntime, `require('${stylSrc}');`].join('\n') ); } else { entrySrc = stylSrc; fs.writeFileSync( entrySrc, [content, 'generateImgClasses()'].join('\n') ); } entry[entryName] = entrySrc; let processStyleIndex = STYLES; const canMoveBuildToNewProcess = MAX_PROCESS > processes.length && Object.keys(processes[STYLES].entries).length > MAX_TASKS_PER_ONE_PROCESS; if (canMoveBuildToNewProcess) { processes.push({entries: {}, name: `styles_${styleIndex++}`}); processStyleIndex = processes.length - 1; } processes[processStyleIndex].entries[entryName] = entrySrc; } } // HTML { const entryName = `${name}_view`, entrySrc = path.join(tmpEntries, `${entryName}.html.js`); const content = await $C(list).async.to('').reduce(async (str, {name}) => { const component = components.get(name), html = await component?.etpl; if (html && !componentsToIgnore.test(name)) { str += [webpackRuntime, `require('./${getEntryPath(html)}');\n`].join('\n'); } return str; }); if (content) { fs.writeFileSync(entrySrc, content); entry[entryName] = entrySrc; // eslint-disable-next-line require-atomic-updates processes[HTML].entries[entryName] = entrySrc; } } return entry; } /** * Loads the dependency graph from a cache */ function loadFromCache() { const timeout = (1).minute(); let total = 0; return new Promise((r) => { const delay = 500; const f = () => { // Sometimes we can be caught in the situation when one of the multiple processes writes something // to the cache file and it breaks the cache for a moment. // To avoid this, we can sleep a little and try again. setTimeout(async () => { try { const graph = fs.readJSONSync(graphCacheFile); r({ ...graph, components: await getComponents() }); } catch (err) { total += delay; if (total > timeout) { build.buildGraphFromCache = false; return buildProjectGraph(); } f(); } }, delay); }; f(); }); } /** * Returns a map of all existed components */ async function getComponents() { const components = await block.getAll(null, { lockPrefix: build.componentLockPrefix() }); $C(components).forEach((component, name) => { component.params = componentParams[name.camelize(false)] ?? {}; }); return components; } /** * Returns a file path relative to the entry folder */ function getEntryPath(filePath) { if (resolve.isNodeModule(filePath)) { const resolvedEntry = src.lib(filePath); if (!isLayerDep.test(filePath) || !fs.existsSync(resolvedEntry)) { return path.normalize(filePath); } filePath = resolvedEntry; } return path.relative(tmpEntries, filePath); } }