diff --git a/package.json b/package.json index 7d275ab31a03..91af961f4b2e 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,10 @@ "build": "node scripts/build.js", "build-prod": "npm run clean && npm run build -- --prod", "build-playground-prod": "npm run build-prod && npm run build-prod --prefix packages/lexical-playground", - "build-release": "npm run build-prod -- --release", + "build-release": "npm run build-prod -- --release --codes", "build-www": "npm run clean && npm run build -- --www && npm run build -- --www --prod && npm run prepare-www", "build-types": "tsc -p ./tsconfig.build.json && node ./scripts/validate-tsc-types.js", "clean": "node scripts/clean.js", - "extract-codes": "node scripts/build.js --codes", "flow": "node ./scripts/check-flow-types.js", "tsc": "tsc", "tsc-extension": "npm run compile -w @lexical/devtools", diff --git a/packages/lexical-website/docs/maintainers-guide.md b/packages/lexical-website/docs/maintainers-guide.md index 6e5f04f5a138..89feca11af97 100644 --- a/packages/lexical-website/docs/maintainers-guide.md +++ b/packages/lexical-website/docs/maintainers-guide.md @@ -196,6 +196,10 @@ This runs all of the pre-release steps and will let you inspect the artifacts that would be uploaded to npm. Each public package will have a npm directory, e.g. `packages/lexical/npm` that contains those artifacts. +This will also update scripts/error-codes/codes.json, the mapping of +production error codes to error messages. It's imperative to commit the result +of this before tagging a release. + ### npm run ci-check Check flow, TypeScript, prettier and eslint for issues. A good command to run @@ -227,11 +231,8 @@ Run eslint ### npm run increment-version Increment the monorepo version. Make sure to run `npm run update-packages` -after this. - -### npm run extract-codes - -Extract error codes for the production build. Essential to run before a release. +(to update all examples and sub-packages) and `npm run prepare-release` +(to update `scripts/error-codes/codes.json`) after this. ### npm run changelog diff --git a/scripts/build.js b/scripts/build.js index e35328e61d17..70f96e9d214e 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -17,7 +17,6 @@ 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'); @@ -103,12 +102,6 @@ const externals = [ 'y-websocket', ].sort(); -const errorCodeOpts = { - errorMapFilePath: 'scripts/error-codes/codes.json', -}; - -const findAndRecordErrorCodes = extractErrorCodes(errorCodeOpts); - const strictWWWMappings = {}; // Add quotes around mappings to make them more strict. @@ -175,14 +168,6 @@ async function build(name, inputFile, outputPath, outputFile, isProd, format) { {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, }), @@ -195,7 +180,7 @@ async function build(name, inputFile, outputPath, outputFile, isProd, format) { plugins: [ [ require('./error-codes/transform-error-messages'), - {noMinify: !isProd}, + {extractCodes, noMinify: !isProd}, ], '@babel/plugin-transform-optional-catch-binding', ], diff --git a/scripts/error-codes/extract-errors.js b/scripts/error-codes/extract-errors.js deleted file mode 100644 index 0720f8b20ada..000000000000 --- a/scripts/error-codes/extract-errors.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * 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 parser = require('@babel/parser'); -const fs = require('fs-extra'); -const path = require('path'); -const traverse = require('@babel/traverse').default; -const evalToString = require('./evalToString'); -const invertObject = require('./invertObject'); - -const plugins = [ - 'classProperties', - 'jsx', - 'trailingFunctionCommas', - 'objectRestSpread', - 'typescript', -]; - -const babylonOptions = { - // As a parser, babylon has its own options and we can't directly - // import/require a babel preset. It should be kept **the same** as - // the `babel-plugin-syntax-*` ones specified in - // https://github.com/facebook/fbjs/blob/master/packages/babel-preset-fbjs/configure.js - plugins, - sourceType: 'module', -}; - -module.exports = function (opts) { - if (!opts || !('errorMapFilePath' in opts)) { - throw new Error( - 'Missing options. Ensure you pass an object with `errorMapFilePath`.', - ); - } - - const errorMapFilePath = opts.errorMapFilePath; - let existingErrorMap; - try { - // Using `fs.readJsonSync` instead of `require` here, because `require()` - // calls are cached, and the cache map is not properly invalidated after - // file changes. - existingErrorMap = fs.readJsonSync( - path.join(__dirname, path.basename(errorMapFilePath)), - ); - } catch (e) { - existingErrorMap = {}; - } - - const allErrorIDs = Object.keys(existingErrorMap); - let currentID; - - if (allErrorIDs.length === 0) { - // Map is empty - currentID = 0; - } else { - currentID = Math.max.apply(null, allErrorIDs) + 1; - } - - // Here we invert the map object in memory for faster error code lookup - existingErrorMap = invertObject(existingErrorMap); - - function transform(source) { - const ast = parser.parse(source, babylonOptions); - - traverse(ast, { - CallExpression: { - exit(astPath) { - if (astPath.get('callee').isIdentifier({name: 'invariant'})) { - const node = astPath.node; - - // error messages can be concatenated (`+`) at runtime, so here's a - // trivial partial evaluator that interprets the literal value - const errorMsgLiteral = evalToString(node.arguments[1]); - addToErrorMap(errorMsgLiteral); - } - }, - }, - }); - } - - function addToErrorMap(errorMsgLiteral) { - if (existingErrorMap.hasOwnProperty(errorMsgLiteral)) { - return; - } - existingErrorMap[errorMsgLiteral] = '' + currentID++; - } - - function flush(cb) { - fs.writeJsonSync(errorMapFilePath, invertObject(existingErrorMap), { - spaces: 2, - }); - } - - return function extractErrors(source) { - transform(source); - flush(); - }; -}; diff --git a/scripts/error-codes/transform-error-messages.js b/scripts/error-codes/transform-error-messages.js index 4bf93a802842..896fdb1d0fb2 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -7,20 +7,85 @@ */ 'use strict'; +// @ts-check const fs = require('fs-extra'); const evalToString = require('./evalToString'); -const invertObject = require('./invertObject'); const helperModuleImports = require('@babel/helper-module-imports'); +const prettier = require('prettier'); -module.exports = function (babel) { - const t = babel.types; +class ErrorMap { + /** @type {Record} */ + errorMap; + /** @type {Record} errorMap + * @param {(errorMap: Record) => void} flushErrorMap + */ + constructor(errorMap, flushErrorMap) { + this.errorMap = errorMap; + this.flushErrorMap = flushErrorMap; + for (const k in this.errorMap) { + const id = parseInt(k, 10); + this.inverseErrorMap[this.errorMap[k]] = id; + this.maxId = id > this.maxId ? id : this.maxId; + } + } + + /** + * @param {string} message + * @param {boolean} extractCodes true if we are also writing to codes.json + * @returns {number | undefined} + */ + getOrAddToErrorMap(message, extractCodes) { + let id = this.inverseErrorMap[message]; + if (extractCodes && typeof id === 'undefined') { + id = ++this.maxId; + this.inverseErrorMap[message] = id; + this.errorMap[`${id}`] = message; + if (!this.dirty) { + queueMicrotask(this.flush.bind(this)); + this.dirty = true; + } + } + return id; + } + flush() { + if (this.dirty) { + this.flushErrorMap(this.errorMap); + this.dirty = false; + } + } +} + +/** + * @returns {Promise} + */ +module.exports = async function (babel) { + const t = babel.types; + const filepath = `${__dirname}/codes.json`; + const prettierConfig = { + ...((await prettier.resolveConfig('./')) || {}), + filepath, + }; + const errorMap = new ErrorMap(fs.readJsonSync(filepath), (newErrorMap) => + fs.writeFileSync( + filepath, + prettier.format(JSON.stringify(newErrorMap), prettierConfig), + ), + ); return { visitor: { CallExpression(path, file) { const node = path.node; - const noMinify = file.opts.noMinify; + const {extractCodes, noMinify} = file.opts; if (path.get('callee').isIdentifier({name: 'invariant'})) { // Turns this code: // @@ -82,10 +147,10 @@ module.exports = function (babel) { } // Avoid caching because we write it as we go. - const existingErrorMap = fs.readJsonSync(__dirname + '/codes.json'); - const errorMap = invertObject(existingErrorMap); - - let prodErrorId = errorMap[errorMsgLiteral]; + let prodErrorId = errorMap.getOrAddToErrorMap( + errorMsgLiteral, + extractCodes, + ); if (prodErrorId === undefined) { // There is no error code for this message. Add an inline comment @@ -113,7 +178,6 @@ module.exports = function (babel) { ); return; } - prodErrorId = parseInt(prodErrorId, 10); // Import ReactErrorProd const formatProdErrorMessageIdentifier =