/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ 'use strict'; const rollup = require('rollup'); const fs = require('fs-extra'); const path = require('path'); const argv = require('minimist')(process.argv.slice(2)); const babel = require('@rollup/plugin-babel').default; const nodeResolve = require('@rollup/plugin-node-resolve').default; const commonjs = require('@rollup/plugin-commonjs'); const replace = require('@rollup/plugin-replace'); const json = require('@rollup/plugin-json'); const extractErrorCodes = require('./error-codes/extract-errors'); const alias = require('@rollup/plugin-alias'); const compiler = require('@ampproject/rollup-plugin-closure-compiler'); const terser = require('@rollup/plugin-terser'); const {exec} = require('child-process-promise'); const license = ` * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree.`; const isProduction = argv.prod; const isRelease = argv.release; const isWWW = argv.www; const extractCodes = argv.codes; const closureOptions = { apply_input_source_maps: false, assume_function_wrapper: true, compilation_level: 'SIMPLE', inject_libraries: false, language_in: 'ECMASCRIPT_2019', language_out: 'ECMASCRIPT_2019', process_common_js_modules: false, rewrite_polyfills: false, use_types_for_optimization: false, warning_level: 'QUIET', }; const wwwMappings = { '@lexical/clipboard': 'LexicalClipboard', '@lexical/code': 'LexicalCode', '@lexical/devtools-core': 'LexicalDevtoolsCore', '@lexical/dragon': 'LexicalDragon', '@lexical/file': 'LexicalFile', '@lexical/hashtag': 'LexicalHashtag', '@lexical/headless': 'LexicalHeadless', '@lexical/history': 'LexicalHistory', '@lexical/html': 'LexicalHtml', '@lexical/link': 'LexicalLink', '@lexical/list': 'LexicalList', '@lexical/mark': 'LexicalMark', '@lexical/markdown': 'LexicalMarkdown', '@lexical/offset': 'LexicalOffset', '@lexical/overflow': 'LexicalOverflow', '@lexical/plain-text': 'LexicalPlainText', '@lexical/rich-text': 'LexicalRichText', '@lexical/selection': 'LexicalSelection', '@lexical/table': 'LexicalTable', '@lexical/text': 'LexicalText', '@lexical/utils': 'LexicalUtils', '@lexical/yjs': 'LexicalYjs', lexical: 'Lexical', 'prismjs/components/prism-c': 'prism-c', 'prismjs/components/prism-clike': 'prism-clike', 'prismjs/components/prism-core': 'prismjs', 'prismjs/components/prism-cpp': 'prism-cpp', 'prismjs/components/prism-css': 'prism-css', 'prismjs/components/prism-java': 'prism-java', 'prismjs/components/prism-javascript': 'prism-javascript', 'prismjs/components/prism-markdown': 'prism-markdown', 'prismjs/components/prism-markup': 'prism-markup', 'prismjs/components/prism-objectivec': 'prism-objectivec', 'prismjs/components/prism-python': 'prism-python', 'prismjs/components/prism-rust': 'prism-rust', 'prismjs/components/prism-sql': 'prism-sql', 'prismjs/components/prism-swift': 'prism-swift', 'prismjs/components/prism-typescript': 'prism-typescript', 'react-dom': 'ReactDOMComet', }; const lexicalReactModules = fs .readdirSync(path.resolve('./packages/lexical-react/src')) .filter( (str) => !str.includes('__tests__') && !str.includes('shared') && !str.includes('test-utils'), ); const lexicalReactModuleExternals = lexicalReactModules.map((module) => { const basename = path.basename(path.basename(module, '.ts'), '.tsx'); const external = `@lexical/react/${basename}`; wwwMappings[external] = basename; return external; }); function resolveExternalEsm(id) { if (/^prismjs\/components\/prism-/.test(id)) { return `${id}.js`; } return id; } const externals = [ 'lexical', 'prismjs/components/prism-core', 'prismjs/components/prism-clike', 'prismjs/components/prism-javascript', 'prismjs/components/prism-markup', 'prismjs/components/prism-markdown', 'prismjs/components/prism-c', 'prismjs/components/prism-css', 'prismjs/components/prism-objectivec', 'prismjs/components/prism-sql', 'prismjs/components/prism-python', 'prismjs/components/prism-rust', 'prismjs/components/prism-swift', 'prismjs/components/prism-typescript', 'prismjs/components/prism-java', 'prismjs/components/prism-cpp', '@lexical/list', '@lexical/table', '@lexical/file', '@lexical/clipboard', '@lexical/devtools-core', '@lexical/hashtag', '@lexical/headless', '@lexical/html', '@lexical/history', '@lexical/selection', '@lexical/text', '@lexical/offset', '@lexical/utils', '@lexical/code', '@lexical/yjs', '@lexical/plain-text', '@lexical/rich-text', '@lexical/mark', '@lexical/dragon', '@lexical/overflow', '@lexical/link', '@lexical/markdown', '@lexical/devtools-core', 'react-dom', 'react', 'yjs', 'y-websocket', ...lexicalReactModuleExternals, ...Object.values(wwwMappings), ]; const errorCodeOpts = { errorMapFilePath: 'scripts/error-codes/codes.json', }; const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); const strictWWWMappings = {}; // Add quotes around mappings to make them more strict. Object.keys(wwwMappings).forEach((mapping) => { strictWWWMappings[`'${mapping}'`] = `'${wwwMappings[mapping]}'`; }); function getExtension(format) { return `.${format === 'esm' ? 'm' : ''}js`; } async function build(name, inputFile, outputPath, outputFile, isProd, format) { const extensions = ['.js', '.jsx', '.ts', '.tsx']; const inputOptions = { external(modulePath, src) { return externals.includes(modulePath); }, input: inputFile, onwarn(warning) { if (warning.code === 'CIRCULAR_DEPENDENCY') { // Ignored } else if (warning.code === 'UNUSED_EXTERNAL_IMPORT') { // Important, but not enough to stop the build console.error(); console.error(warning.message || warning); console.error(); } else if ( warning.code === 'SOURCEMAP_ERROR' && warning.message.endsWith(`Can't resolve original location of error.`) ) { // Ignored } else if (typeof warning.code === 'string') { console.error(warning); // This is a warning coming from Rollup itself. // These tend to be important (e.g. clashes in namespaced exports) // so we'll fail the build on any of them. console.error(); console.error(warning.message || warning); console.error(); process.exit(1); } else { // The warning is from one of the plugins. // Maybe it's not important, so just print it. console.warn(warning.message || warning); } }, plugins: [ alias({ entries: [ {find: 'shared', replacement: path.resolve('packages/shared/src')}, ], }), // Extract error codes from invariant() messages into a file. { transform(source) { // eslint-disable-next-line no-unused-expressions extractCodes && findAndRecordErrorCodes(source); return source; }, }, nodeResolve({ extensions, }), babel({ babelHelpers: 'bundled', babelrc: false, configFile: false, exclude: '/**/node_modules/**', extensions, plugins: [ [ require('./error-codes/transform-error-messages'), {noMinify: !isProd}, ], '@babel/plugin-transform-optional-catch-binding', ], presets: [ [ '@babel/preset-typescript', { tsconfig: path.resolve('./tsconfig.build.json'), }, ], '@babel/preset-react', ], }), { resolveId(importee, importer) { if (importee === 'formatProdErrorMessage') { return path.resolve( './scripts/error-codes/formatProdErrorMessage.js', ); } }, }, commonjs(), json(), replace( Object.assign( { __DEV__: isProd ? 'false' : 'true', delimiters: ['', ''], preventAssignment: true, }, isWWW && strictWWWMappings, ), ), // terser is used for esm builds because // @ampproject/rollup-plugin-closure-compiler doesn't compile // `export default function X()` correctly isProd && (format === 'esm' ? terser({ecma: 2019, module: true}) : compiler(closureOptions)), { renderChunk(source) { // Assets pipeline might use "export" word in the beginning of the line // as a dependency, avoiding it with empty comment in front const patchedSource = isWWW ? source.replace(/^(export(?!s))/gm, '/**/$1') : source; return `${getComment()}\n${patchedSource}`; }, }, ], // This ensures PrismJS imports get included in the bundle treeshake: name !== 'Lexical Code' ? 'smallest' : false, }; const outputOptions = { esModule: false, exports: 'auto', externalLiveBindings: false, file: outputFile, format, // change between es and cjs modules freeze: false, interop: format === 'esm' ? 'esModule' : false, paths: format === 'esm' ? resolveExternalEsm : undefined, }; const result = await rollup.rollup(inputOptions); const {output} = await result.write(outputOptions); return output[0].exports; } function getComment() { const lines = ['/**', license]; if (isWWW) { lines.push( '*', '* @fullSyntaxTransform', '* @generated', '* @noflow', '* @nolint', '* @oncall lexical_web_text_editor', '* @preserve-invariant-messages', '* @preserve-whitespace', '* @preventMunge', ); } lines.push(' */'); return lines.join('\n'); } function getFileName(fileName, isProd, format) { const extension = getExtension(format); if (isWWW || isRelease) { return `${fileName}.${isProd ? 'prod' : 'dev'}${extension}`; } return `${fileName}${extension}`; } const packages = [ { modules: [ { outputFileName: 'Lexical', sourceFileName: 'index.ts', }, ], name: 'Lexical Core', outputPath: './packages/lexical/dist/', packageName: 'lexical', sourcePath: './packages/lexical/src/', }, { modules: [ { outputFileName: 'LexicalList', sourceFileName: 'index.ts', }, ], name: 'Lexical List', outputPath: './packages/lexical-list/dist/', packageName: 'lexical-list', sourcePath: './packages/lexical-list/src/', }, { modules: [ { outputFileName: 'LexicalTable', sourceFileName: 'index.ts', }, ], name: 'Lexical Table', outputPath: './packages/lexical-table/dist/', packageName: 'lexical-table', sourcePath: './packages/lexical-table/src/', }, { modules: [ { outputFileName: 'LexicalFile', sourceFileName: 'index.ts', }, ], name: 'Lexical File', outputPath: './packages/lexical-file/dist/', packageName: 'lexical-file', sourcePath: './packages/lexical-file/src/', }, { modules: [ { outputFileName: 'LexicalClipboard', sourceFileName: 'index.ts', }, ], name: 'Lexical File', outputPath: './packages/lexical-clipboard/dist/', packageName: 'lexical-clipboard', sourcePath: './packages/lexical-clipboard/src/', }, { modules: [ { outputFileName: 'LexicalHashtag', sourceFileName: 'index.ts', }, ], name: 'Lexical Hashtag', outputPath: './packages/lexical-hashtag/dist/', packageName: 'lexical-hashtag', sourcePath: './packages/lexical-hashtag/src/', }, { modules: [ { outputFileName: 'LexicalHistory', sourceFileName: 'index.ts', }, ], name: 'Lexical History', outputPath: './packages/lexical-history/dist/', packageName: 'lexical-history', sourcePath: './packages/lexical-history/src/', }, { modules: [ { outputFileName: 'LexicalSelection', sourceFileName: 'index.ts', }, ], name: 'Lexical Selection', outputPath: './packages/lexical-selection/dist/', packageName: 'lexical-selection', sourcePath: './packages/lexical-selection/src/', }, { modules: [ { outputFileName: 'LexicalText', sourceFileName: 'index.ts', }, ], name: 'Lexical Text', outputPath: './packages/lexical-text/dist/', packageName: 'lexical-text', sourcePath: './packages/lexical-text/src/', }, { modules: [ { outputFileName: 'LexicalOffset', sourceFileName: 'index.ts', }, ], name: 'Lexical Offset', outputPath: './packages/lexical-offset/dist/', packageName: 'lexical-offset', sourcePath: './packages/lexical-offset/src/', }, { modules: [ { outputFileName: 'LexicalUtils', sourceFileName: 'index.ts', }, ], name: 'Lexical Utils', outputPath: './packages/lexical-utils/dist/', packageName: 'lexical-utils', sourcePath: './packages/lexical-utils/src/', }, { modules: [ { outputFileName: 'LexicalCode', sourceFileName: 'index.ts', }, ], name: 'Lexical Code', outputPath: './packages/lexical-code/dist/', packageName: 'lexical-code', sourcePath: './packages/lexical-code/src/', }, { modules: [ { outputFileName: 'LexicalDragon', sourceFileName: 'index.ts', }, ], name: 'Lexical Dragon', outputPath: './packages/lexical-dragon/dist/', packageName: 'lexical-dragon', sourcePath: './packages/lexical-dragon/src/', }, { modules: [ { outputFileName: 'LexicalDevtoolsCore', sourceFileName: 'index.ts', }, ], name: 'Lexical Devtools Core', outputPath: './packages/lexical-devtools-core/dist/', packageName: 'lexical-devtools-core', sourcePath: './packages/lexical-devtools-core/src/', }, { modules: [ { outputFileName: 'LexicalLink', sourceFileName: 'index.ts', }, ], name: 'Lexical Link', outputPath: './packages/lexical-link/dist/', packageName: 'lexical-link', sourcePath: './packages/lexical-link/src/', }, { modules: [ { outputFileName: 'LexicalOverflow', sourceFileName: 'index.ts', }, ], name: 'Lexical Overflow', outputPath: './packages/lexical-overflow/dist/', packageName: 'lexical-overflow', sourcePath: './packages/lexical-overflow/src/', }, { modules: [ { outputFileName: 'LexicalPlainText', sourceFileName: 'index.ts', }, ], name: 'Lexical Plain Text', outputPath: './packages/lexical-plain-text/dist/', packageName: 'lexical-plain-text', sourcePath: './packages/lexical-plain-text/src/', }, { modules: [ { outputFileName: 'LexicalRichText', sourceFileName: 'index.ts', }, ], name: 'Lexical Rich Text', outputPath: './packages/lexical-rich-text/dist/', packageName: 'lexical-rich-text', sourcePath: './packages/lexical-rich-text/src/', }, { modules: [ { outputFileName: 'LexicalMarkdown', sourceFileName: 'index.ts', }, ], name: 'Lexical Markdown', outputPath: './packages/lexical-markdown/dist/', packageName: 'lexical-markdown', sourcePath: './packages/lexical-markdown/src/', }, { modules: [ { outputFileName: 'LexicalHeadless', sourceFileName: 'index.ts', }, ], name: 'Lexical Headless', outputPath: './packages/lexical-headless/dist/', packageName: 'lexical-headless', sourcePath: './packages/lexical-headless/src/', }, { modules: [ { outputFileName: 'LexicalHtml', sourceFileName: 'index.ts', }, ], name: 'Lexical HTML', outputPath: './packages/lexical-html/dist/', packageName: 'lexical-html', sourcePath: './packages/lexical-html/src/', }, { modules: [ { outputFileName: 'LexicalMark', sourceFileName: 'index.ts', }, ], name: 'Lexical Mark', outputPath: './packages/lexical-mark/dist/', packageName: 'lexical-mark', sourcePath: './packages/lexical-mark/src/', }, { modules: lexicalReactModules .filter((module) => { // We don't want to sync these modules, as they're bundled in the other // modules already. const ignoredModules = [ 'useLexicalDragonSupport', 'usePlainTextSetup', 'useRichTextSetup', 'useYjsCollaboration', ]; return !ignoredModules.includes(module); }) .map((module) => { const basename = path.basename(path.basename(module, '.ts'), '.tsx'); return { name: basename, outputFileName: basename, sourceFileName: module, }; }), name: 'Lexical React', outputPath: './packages/lexical-react/dist/', packageName: 'lexical-react', sourcePath: './packages/lexical-react/src/', }, { modules: [ { outputFileName: 'LexicalYjs', sourceFileName: 'index.ts', }, ], name: 'Lexical Yjs', outputPath: './packages/lexical-yjs/dist/', packageName: 'lexical-yjs', sourcePath: './packages/lexical-yjs/src/', }, ]; async function buildTSDeclarationFiles(packageName, outputPath) { await exec('tsc -p ./tsconfig.build.json'); } async function moveTSDeclarationFilesIntoDist(packageName, outputPath) { await fs.copy(`./.ts-temp/${packageName}/src`, outputPath); } function forkModuleContent( {devFileName, exports, outputFileName, prodFileName}, target, ) { const lines = [getComment()]; if (target === 'cjs') { lines.push( `'use strict'`, `const ${outputFileName} = process.env.NODE_ENV === 'development' ? require('${devFileName}') : require('${prodFileName}');`, `module.exports = ${outputFileName};`, ); } else { if (target === 'esm') { lines.push( `import * as modDev from '${devFileName}';`, `import * as modProd from '${prodFileName}';`, `const mod = process.env.NODE_ENV === 'development' ? modDev : modProd;`, ); } else if (target === 'node') { lines.push( `const mod = await (process.env.NODE_ENV === 'development' ? import('${devFileName}') : import('${prodFileName}'));`, ); } for (const name of exports) { lines.push( name === 'default' ? `export default mod.default;` : `export const ${name} = mod.${name};`, ); } } return lines.join('\n'); } function buildForkModules(outputPath, outputFileName, format, exports) { const extension = getExtension(format); const devFileName = `./${outputFileName}.dev${extension}`; const prodFileName = `./${outputFileName}.prod${extension}`; const opts = {devFileName, exports, outputFileName, prodFileName}; fs.outputFileSync( path.resolve(path.join(`${outputPath}${outputFileName}${extension}`)), forkModuleContent(opts, format), ); if (format === 'esm') { fs.outputFileSync( path.resolve( path.join(`${outputPath}${outputFileName}.node${extension}`), ), forkModuleContent(opts, 'node'), ); } } async function buildAll() { if (!isWWW && (isRelease || isProduction)) { await buildTSDeclarationFiles(); } for (const pkg of packages) { const {name, sourcePath, outputPath, packageName, modules} = pkg; for (const module of modules) { for (const format of ['cjs', 'esm']) { if (isWWW && format === 'esm') { break; } const {sourceFileName, outputFileName} = module; let inputFile = path.resolve( path.join(`${sourcePath}${sourceFileName}`), ); await build( `${name}${module.name ? '-' + module.name : ''}`, inputFile, outputPath, path.resolve( path.join( `${outputPath}${getFileName( outputFileName, isProduction, format, )}`, ), ), isProduction, format, ); if (isRelease) { const exports = await build( name, inputFile, outputPath, path.resolve( path.join( `${outputPath}${getFileName(outputFileName, false, format)}`, ), ), false, format, ); buildForkModules(outputPath, outputFileName, format, exports); } } } if (!isWWW && (isRelease || isProduction)) { await moveTSDeclarationFilesIntoDist(packageName, outputPath); } } } buildAll();