import fs from 'fs'; import webpack, { Configuration } from 'webpack'; import path from 'path'; import rimraf from 'rimraf'; import nodeExternals from 'webpack-node-externals'; import { OrisonSettings } from '.'; function* flattenDir(dir: string, sub?: string): Generator { const items = fs.readdirSync(sub ? (dir + '/' + sub) : dir); items.sort((a, b) => { if (a.startsWith('by-')) { return 1; } if (b.startsWith('by-')) { return -1; } return a.localeCompare(b); }); for (const item of items) { const full = sub ? (dir + '/' + sub + '/' + item) : (dir + '/' + item); if (fs.lstatSync(full).isDirectory()) { yield* flattenDir(dir, sub ? (sub + '/' + item) : item); } else { yield sub ? (sub + '/' + item) : item; } } } async function buildOrisonDirectory(settings: OrisonSettings): Promise { if (fs.existsSync('.orison')) { rimraf.sync('.orison'); } console.log('Building .orison directory...'); const allPaths = Array.from(flattenDir('src')) .filter(page => page.endsWith('.ts') || page.endsWith('.tsx')) .map(page => 'src/' + page); const result = await compileTypeScriptFiles(settings, allPaths); if (fs.existsSync('.orison/pages/client')) { for (const item of flattenDir('.orison/pages/client')) { if (item.includes('__prewebpack')) { fs.unlinkSync('.orison/pages/client/' + item); } } } else { console.log('Info: no client scripts generated.'); } return result; } async function compile(settings: OrisonSettings, pages: Record, transformer: 'server' | 'client', transformerArgs?: any) { if (Object.keys(pages).length === 0) { console.log('Info: no input files for transformer: ' + transformer); return {}; } const cssLoader = { loader: 'css-loader', options: { importLoaders: 1, modules: { localIdentName: settings.production ? '[hash:base64:8]' : '[name]__[local]__[hash:base64:5]' }, } }; const cssLoaderLocals = { ...cssLoader, options: { ...cssLoader.options, modules: { ...cssLoader.options.modules }, onlyLocals: true } }; return await new Promise>(resolve => { let options: Configuration = { mode: settings.production ? 'production' : 'development', target: transformer === 'client' ? 'web' : 'node', context: process.cwd(), entry: pages, resolve: { enforceExtension: false, extensions: ['.tsx', '.ts', '.jsx', '.js'] }, module: { rules: [ { test: /\.css$/, use: transformer === 'server' ? [require.resolve(path.resolve(__dirname, 'server-module-loader')), cssLoader] : [cssLoaderLocals], include: /\.module\.css$/ }, { test: /\.css$/, use: transformer === 'server' ? [require.resolve(path.resolve(__dirname, 'server-style-loader')), 'css-loader'] : ['css-loader'], exclude: /\.module\.css$/ }, { test: /\.svg$/, use: ['@svgr/webpack'], }, { test: /\.tsx?$/, use: [ { loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], plugins: transformer ? [ [ require.resolve(path.resolve(__dirname, 'transformer')), { ...(transformerArgs || {}), side: transformer } ], '@babel/plugin-transform-runtime' ] : ['@babel/plugin-transform-runtime'] } } ] } ] }, externals: transformer === 'server' ? [ nodeExternals(), (context, request, callback) => { if (request.includes('../common/')) { return callback(null, 'commonjs2 ' + request); } callback(); } ] : undefined, output: { filename: '[name].js', path: path.resolve(process.cwd(), '.orison'), libraryTarget: transformer === 'server' ? 'commonjs2' : undefined, chunkFilename: 'pages/client/vendors-[contenthash:8].bundle.js', }, optimization: transformer === 'client' ? { splitChunks: { chunks: 'all', minSize: 50000, maxSize: 200000, name: false } } : undefined, plugins: transformer === 'client' ? [ new webpack.DefinePlugin({ 'process.env': { 'NODE_ENV': '"' + process.env.NODE_ENV + '"' } }) ] : [] }; if (settings.rewebpack) { options = settings.rewebpack(options, transformer); } webpack(options, (err, stats) => { if (err) { throw err; } console.log(stats.toString()); if (stats.hasErrors()) { process.exit(1); } const pageDependencies: Record = {}; /*for (const chunk of stats.compilation.chunks) { if (chunk.entryModule === undefined) { console.log(chunk); const assets = chunk.name.split('~'); for (const asset of assets) { if (pageDependencies[asset] === undefined) { pageDependencies[asset] = chunk.files; } else { pageDependencies[asset] = pageDependencies[asset].concat(chunk.files); } } } }*/ const assets = Object.keys(stats.compilation.assets); const vendorAssets = assets.filter(asset => /vendors-[0-9a-f]+\.bundle\.js/.test(asset)); for (const asset of assets) { if (asset.startsWith('pages/')) { pageDependencies['.orison/' + asset.replace(/~.+/, '.js')] = vendorAssets; } } for (const file of flattenDir('.orison/pages/client')) { if (file.includes('~')) { const name = file.replace(/~.+/, '.js'); fs.renameSync('.orison/pages/client/' + file, '.orison/pages/client/' + name); } } resolve(pageDependencies); }); }); } export async function compileTypeScriptFiles(settings: OrisonSettings, files: string[]): Promise { const pagesClientPoints: Record = {}; const pagesServerPoints: Record = {}; const commonServerPoints: Record = {}; // const serverPoints: Record = {}; const pages: PageResult[] = []; for (const entry of files) { if (entry === 'src/orison.ts') { continue; } const newFile = entry.substring(0, entry.indexOf('.')); if (entry.startsWith('src/pages')) { pagesServerPoints[newFile.substring(4) + '_server'] = './' + entry; } else if (entry.startsWith('src/api') || entry.startsWith('src/ws')) { pagesServerPoints[newFile.substring(4)] = './' + entry; } else if (entry.startsWith('src/common')) { commonServerPoints[newFile.substring(4)] = './' + entry; } } const clientPreWebpackPaths: string[] = []; for (const currentPath of files) { if (currentPath.startsWith('src/pages')) { let relative = currentPath.substring('src/pages'.length); relative = relative.substring(0, relative.indexOf('.')); if (relative.endsWith('index')) { relative = relative.substring(0, relative.length - 'index'.length); } if (relative.endsWith('/')) { relative = relative.substring(0, relative.length - 1); } if (relative.length === 0) { relative = '/'; } relative = relative.replace(/by-/g, ':'); const file = currentPath.substring('src/pages'.length); if (relative !== '/_app' && relative !== '/_master') { const fileNoExt = file.substring(0, file.indexOf('.')); if (settings.skipHydration !== undefined && settings.skipHydration.includes(relative)) { pages.push({ serverScriptPath: fileNoExt, clientScriptPath: null, path: relative }); continue; } const relativePages = '../../..' + '/..'.repeat(fileNoExt.match(/\//g)!.length - 1) + '/src/pages'; let js: string; if (fs.existsSync('src/pages/_master.tsx')) { js = ` import React from 'react'; import ReactDOM from 'react-dom'; import Master from '` + relativePages + `/_master.tsx'; import Component from '` + relativePages + fileNoExt + `.tsx'; ReactDOM.hydrate( React.createElement(Master, window.__ORISON_MASTER_PROPS, React.createElement(Component, window.__ORISON_PROPS)), document.getElementById('root') ); `; } else { js = ` import React from 'react'; import ReactDOM from 'react-dom'; import Component from '` + relativePages + fileNoExt + `.tsx'; ReactDOM.hydrate( React.createElement(Component, window.__ORISON_PROPS), document.getElementById('root') ); `; } const prepack = fileNoExt.substring(1) + '__prewebpack.js'; const full = '.orison/pages/client/' + prepack; const parentDir = path.dirname(full); if (!fs.existsSync(parentDir)) { fs.mkdirSync(parentDir, { recursive: true }); } fs.writeFileSync(full, js, 'utf-8'); clientPreWebpackPaths.push(prepack); pagesClientPoints['pages/client' + fileNoExt + '.bundle'] = './' + full; pages.push({ serverScriptPath: fileNoExt, clientScriptPath: '.orison/pages/client' + fileNoExt + '.bundle.js', path: relative }); } } } if (Object.keys(commonServerPoints).length > 0) { await compile({ ...settings, rewebpack: config => { const newConfig: webpack.Configuration = { ...config, externals: [ nodeExternals(), (context, request, callback) => { if (request.startsWith('NON_REQUIRE_')) { return callback(null, 'commonjs2 ' + request.substring('NON_REQUIRE_'.length)); } callback(); } ] }; if (settings.rewebpack) { return settings.rewebpack(newConfig); } return newConfig; } }, commonServerPoints, 'server', { nonRequire: true }); } await compile(settings, pagesServerPoints, 'server'); return { pages, pageDependencies: await compile(settings, pagesClientPoints, 'client') }; } interface PageResult { serverScriptPath: string; clientScriptPath: string | null; path: string; } export interface OrisonBuildResult { pages: PageResult[]; pageDependencies: Record; } export { buildOrisonDirectory };