UNPKG

@discoveryjs/cli

Version:

CLI tools to serve & build projects based on Discovery.js

413 lines (358 loc) 15.1 kB
const fs = require('fs'); const path = require('path'); const esbuild = require('esbuild'); const gen = require('./gen'); const staticSrc = path.join(__dirname, '../static'); function booleanStr(value) { return value ? 'true' : 'false'; } function selectAssets(modelConfig, includeServeOnlyAssets) { const view = modelConfig.view || {}; const serveOnlyAssets = includeServeOnlyAssets && Array.isArray(view.serveOnlyAssets) ? view.serveOnlyAssets : []; const assets = Array.isArray(view.assets) ? view.assets : []; return [...new Set([...serveOnlyAssets, ...assets])]; } function selectJsAssets(modelConfig, includeServeOnlyAssets) { return ( 'export default [\n' + selectAssets(modelConfig, includeServeOnlyAssets) .filter(fn => /\.[cm]?[jt]sx?$/.test(path.extname(fn))) .map(fn => ' require(' + JSON.stringify(fn + ':discovery') + ')') .join(',\n') + '\n]' ); } function selectCssAssets(modelConfig, includeServeOnlyAssets) { return selectAssets(modelConfig, includeServeOnlyAssets) .filter(fn => path.extname(fn) === '.css') .map(fn => '@import url(' + JSON.stringify(fn) + ');') .join('\n'); } function selectScriptModules(modelConfig) { const script = modelConfig.script || {}; const modules = Array.isArray(script.modules) ? script.modules : []; return ( 'export default [\n' + modules .filter(fn => /\.[cm]?[jt]sx?$/.test(path.extname(fn))) .map(fn => ' require(' + JSON.stringify(fn + ':discovery') + ')') .join(',\n') + '\n]' ); } function modelSetup(modelConfig) { if (modelConfig.setup) { return `export { default } from ${JSON.stringify(modelConfig.setup)};\n`; } return 'export default null;\n'; } function prepare(modelConfig) { const prepares = [ ['commonPrepare', modelConfig.commonPrepare], ['modelPrepare', modelConfig.prepare] ].filter(([, path]) => path); return prepares .map(([name, path]) => `import ${name} from ${JSON.stringify(path)};\n` ).join('') + [ 'export default function(host) {', ` const prepares = [${prepares.map(([name]) => name).join(', ')}].filter(p => {`, ' if (typeof p === "function") return true;', ' console.warn("[discovery-cli] \\"prepare\\" module should return a function, but got " + typeof p);', ' });', ' if (prepares.length) {', ' host.setPrepare(async (data, ...args) => {', ' for (const prepare of prepares) {', ' data = await prepare(data, ...args) || data;', ' }', ' return data;', ' });', ' }', '}' ].join('\n'); } function encodings(modelConfig) { const encodingList = [ ['commonEncodings', modelConfig.commonEncodings], ['modelEncodings', modelConfig.encodings] ].filter(([, path]) => path); return encodingList .map(([name, path]) => `import ${name} from ${JSON.stringify(path)};\n` ).join('') + `export default [${encodingList.map(([name]) => '...' + name).join(', ')}]`; } function selectBundles(modelConfig) { const view = modelConfig.view || {}; return view.bundles || {}; } function dirname(filepath) { const dir = path.dirname(filepath); return dir === '.' ? '' : `${dir}/`; } function getDiscoveryDir(workingDir) { const pkgJson = path.join(workingDir, 'package.json'); const discoveryDir = fs.existsSync(pkgJson) && require(pkgJson).name === '@discoveryjs/discovery' ? workingDir : path.dirname(require.resolve('@discoveryjs/discovery/package.json', { paths: [workingDir] })); const discoveryDev = fs.existsSync(path.join(discoveryDir, 'src')); return { discoveryDir, discoveryDev }; } const pluginDiscoveryPaths = (discoveryDir) => ({ name: 'discovery-paths', setup({ onResolve, resolve }) { // rewrite path to @discoveryjs/dicovery to ensure a single instance is used, // since @discoveryjs/discovery-cli might has its own copy onResolve({ filter: /^@discoveryjs\/discovery(\/|$)/ }, args => { if (args.resolveDir !== discoveryDir) { return resolve(args.path, { kind: args.kind, resolveDir: discoveryDir }); } }); } }); const pluginDiscoveryCli = (options, bundleEntryPoints, mainModules, files) => ({ name: 'discovery-cli', setup({ onResolve, onLoad }) { // entry points onResolve({ namespace: '', filter: /.*/ }, args => { if (args.kind === 'entry-point') { if (bundleEntryPoints.has(args.path)) { return { path: bundleEntryPoints.get(args.path) }; } return { namespace: 'discovery-cli', path: args.path }; } }); // entry point imports onResolve({ namespace: 'discovery-cli', filter: /^discovery-cli:(setup|setup-script|model-setup|prepare|extensions|extensions-script|encodings)$/ }, args => ({ namespace: 'discovery-cli', path: dirname(args.importer) + args.path.split(':')[1] + path.extname(args.importer) })); onResolve({ namespace: 'discovery-cli', filter: /\/(model|index).js$/ }, (args) => { if (mainModules.has(path.join(args.resolveDir, args.path))) { if (!options.singleFile) { return { external: true }; } return { namespace: 'discovery-cli', path: args.importer.replace(/-loader/, '') }; } }); onLoad({ namespace: 'discovery-cli', filter: /.*/ }, async args => { const getContents = files.get(args.path); return { loader: path.extname(args.path) === '.css' ? 'css' : 'js', resolveDir: staticSrc, contents: await getContents() }; }); } }); const pluginDiscoveryWrapper = { name: 'discovery-wrapper', setup({ onResolve, onLoad }) { onResolve({ filter: /:discovery$/ }, args => ({ namespace: 'discovery-wrapper', path: args.path.replace(/:discovery$/, '') })); onLoad({ namespace: 'discovery-wrapper', filter: /$/ }, args => { const content = fs.readFileSync(args.path, 'utf8'); const prelude = content.match(/^(?:(?:\/\/.*\n|\/\*.*?\*\/|\s+)*import.*;\n?)+/); const imports = prelude ? prelude[0] + '\n' : ''; const body = prelude ? content.slice(prelude[0].length) : content; return { resolveDir: path.dirname(args.path), contents: imports + 'export default function(discovery) {\n' + body + '\n}\n' }; }); } }; module.exports = async function(config, options, esbuildConfig, { cacheDispatcher, filter } = {}) { const { discoveryDir, discoveryDev } = getDiscoveryDir(process.cwd()); const outputDir = options.output; const files = new Map(); const entryPoints = []; const scriptFormat = options.scriptFormat ?? 'esm'; const scriptEntryPointsEsm = []; const scriptEntryPointsCjs = []; const bundleEntryPoints = new Map(); const mainModules = new Set([ path.join(staticSrc, '/index.js'), path.join(staticSrc, '/model.js') ]); for (const modelConfig of config.models) { const { slug } = modelConfig; files.set(`${slug}/model.js`, () => fs.readFileSync(staticSrc + '/model.js')); files.set(`${slug}/model-loader.js`, () => fs.readFileSync(staticSrc + '/model-loader.js')); files.set(`${slug}/model-setup.js`, () => modelSetup(modelConfig)); files.set(`${slug}/setup.js`, () => gen['/setup.js'](modelConfig, options, config, cacheDispatcher)); files.set(`${slug}/prepare.js`, () => prepare(modelConfig)); files.set(`${slug}/encodings.js`, () => encodings(modelConfig)); files.set(`${slug}/extensions.js`, () => selectJsAssets(modelConfig, options.serveOnlyAssets)); files.set(`${slug}/model.css`, () => fs.readFileSync(staticSrc + '/model.css')); files.set(`${slug}/model-loader.css`, () => fs.readFileSync(staticSrc + '/model-loader.css')); files.set(`${slug}/extensions.css`, () => selectCssAssets(modelConfig, options.serveOnlyAssets)); entryPoints.push( `${slug}/model.js`, `${slug}/model-loader.js`, `${slug}/model.css`, `${slug}/model-loader.css` ); for (const [relpath, entrypoint] of Object.entries(selectBundles(modelConfig))) { const ref = `${slug}/${relpath}`; bundleEntryPoints.set(ref, entrypoint); entryPoints.push(ref); } if (modelConfig.script) { files.set(`${slug}/setup-script.js`, () => gen['/setup-script.js'](modelConfig, options, config, cacheDispatcher)); files.set(`${slug}/model-script.js`, () => fs.readFileSync(staticSrc + '/model-script.js')); files.set(`${slug}/extensions-script.js`, () => selectScriptModules(modelConfig)); if (scriptFormat === 'esm' || scriptFormat === 'both') { scriptEntryPointsEsm.push(`${slug}/model-script.js`); } if (scriptFormat === 'cjs' || scriptFormat === 'both') { scriptEntryPointsCjs.push(`${slug}/model-script.js`); } } } if (config.mode === 'multi') { files.set('index.js', () => fs.readFileSync(staticSrc + '/index.js')); files.set('index-loader.js', () => fs.readFileSync(staticSrc + '/index-loader.js')); files.set('setup.js', () => gen['/setup.js'](null, options, config, cacheDispatcher)); files.set('encodings.js', () => encodings(config)); files.set('extensions.js', () => selectJsAssets(config, options.serveOnlyAssets)); files.set('index.css', () => fs.readFileSync(staticSrc + '/index.css')); files.set('index-loader.css', () => fs.readFileSync(staticSrc + '/index-loader.css')); files.set('extensions.css', () => selectCssAssets(config, options.serveOnlyAssets)); entryPoints.push( 'index.js', 'index-loader.js', 'index.css', 'index-loader.css' ); for (const [relpath, entrypoint] of Object.entries(selectBundles(config))) { const ref = `${relpath}`; bundleEntryPoints.set(ref, entrypoint); entryPoints.push(ref); } } esbuildConfig = { conditions: options.dev && discoveryDev ? ['discovery-dev'] : [], plugins: [ pluginDiscoveryPaths(discoveryDir), pluginDiscoveryCli(options, bundleEntryPoints, mainModules, files), pluginDiscoveryWrapper ], bundle: true, // metafile: true, format: options.singleFile ? 'iife' : 'esm', define: { global: 'globalThis', SINGLE_FILE: booleanStr(options.singleFile), INLINE_DATA: booleanStr(options.singleFile && options.data && ( !options.singleFileData || ['inline', 'both'].includes(options.singleFileData) )), MODEL_DOWNLOAD: booleanStr(options.modelDownload), MODEL_RESET_CACHE: booleanStr(options.modelResetCache) }, loader: { '.ico': 'dataurl', '.png': 'dataurl', '.gif': 'dataurl', '.jpg': 'dataurl', '.svg': 'dataurl', '.wasm': 'base64' }, write: false, // splitting: true, minify: true, outdir: outputDir, outbase: '.', target: 'esnext', ...esbuildConfig }; const assets = await esbuild.build({ ...esbuildConfig, entryPoints: typeof filter === 'function' ? entryPoints.filter(ref => filter(ref)) : entryPoints }); const scriptExternal = Array.isArray(options.scriptExternal) ? options.scriptExternal .map(pattern => pattern === 'all' ? './node_modules/*' : pattern) .filter(Boolean) : []; const scriptsEsm = await esbuild.build({ ...esbuildConfig, platform: 'node', format: 'esm', external: scriptExternal, outExtension: scriptFormat === 'both' ? { '.js': '.mjs' } : {}, entryPoints: typeof filter === 'function' ? scriptEntryPointsEsm.filter(ref => filter(ref)) : scriptEntryPointsEsm }); const scriptsCjs = await esbuild.build({ ...esbuildConfig, platform: 'node', format: 'cjs', external: scriptExternal, outExtension: scriptFormat === 'both' ? { '.js': '.cjs' } : {}, entryPoints: typeof filter === 'function' ? scriptEntryPointsCjs.filter(ref => filter(ref)) : scriptEntryPointsCjs }); scriptsEsm.outputFiles.forEach(file => file.format = 'esm'); scriptsCjs.outputFiles.forEach(file => file.format = 'cjs'); const result = [assets, scriptsEsm, scriptsCjs].reduce( (res, buildResult) => Object.fromEntries(Object.entries(res) .map(([key, value]) => [key, [...value, ...buildResult[key]]]) ), { warnings: [], errors: [], outputFiles: [] } ); if (esbuildConfig.sourcemap) { // rewrite source maps references to real module paths for (const file of result.outputFiles) { if (file.path.endsWith('.js.map')) { const text = file.text; const sourceMap = JSON.parse(text); delete file.text; // since it's not writable sourceMap.sources = sourceMap.sources.map(fn => { if (!fn.startsWith('discovery-cli:')) { return fn.replace(/^..\//, '').replace(/^discovery-wrapper:\/?/, ''); } const realpath = fn.replace(/^discovery-cli:/, ''); let dir = path.dirname(realpath); if (dir === '.') { dir = ''; } return 'discovery-cli:' + (!dir ? 'index/' : '') + realpath; }); const stringifiedSourceMap = JSON.stringify(sourceMap); file.contents = stringifiedSourceMap; file.text = stringifiedSourceMap; } } } return result; };