diff --git a/package.json b/package.json index 7d275ab31a0..a666d265278 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "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", @@ -103,7 +103,7 @@ "update-flowconfig": "node ./scripts/update-flowconfig", "create-www-stubs": "node ./scripts/create-www-stubs", "update-packages": "npm run update-version && npm run update-tsconfig && npm run update-flowconfig && npm run create-docs && npm run create-www-stubs", - "postversion": "git checkout -b ${npm_package_version}__release && npm install && npm run update-version && npm run update-changelog && git add -A && git commit -m v${npm_package_version} && git tag -a v${npm_package_version} -m v${npm_package_version}", + "postversion": "git checkout -b ${npm_package_version}__release && npm run update-version && npm install && npm run update-packages && npm run extract-codes && npm run update-changelog && git add -A && git commit -m v${npm_package_version} && git tag -a v${npm_package_version} -m v${npm_package_version}", "publish-extension": "npm run zip -w @lexical/devtools && npm run publish -w @lexical/devtools", "release": "npm run prepare-release && node ./scripts/npm/release.js", "size": "npm run build-prod && size-limit" diff --git a/packages/lexical-website/docs/maintainers-guide.md b/packages/lexical-website/docs/maintainers-guide.md index 6e5f04f5a13..a21ac2b0d82 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 @@ -224,14 +228,37 @@ Run eslint ## Scripts for release managers +### npm run extract-codes + +This will run a build that also extracts the generated error codes.json file. + +This should be done, at minimum, before each release, but not in any PR as +it would cause conflicts between serial numbers. + +It's safe and probably advisable to do this more often, possibly any time a +branch is merged to main. + +The codes.json file is also updated any time a release build is generated +as a failsafe to ensure that these codes are up to date in a release. +This command runs a development build to extract the codes which is much +faster as it is not doing any optimization/minification steps. + ### npm run increment-version -Increment the monorepo version. Make sure to run `npm run update-packages` -after this. +Increment the monorepo version. The `-i` argument must be one of +`minor` | `patch` | `prerelease`. -### npm run extract-codes +The postversion script will: +- Create a local `${npm_package_version}__release` branch +- `npm run update-version` to update example and sub-package monorepo dependencies +- `npm install` to update the package-lock.json +- `npm run update-packages` to update other generated config +- `npm run extract-codes` to extract the error codes +- `npm run update-changelog` to update the changelog (if it's not a prerelease) +- Create a version commit and tag from the branch -Extract error codes for the production build. Essential to run before a release. +This is typically executed through the `version.yml` GitHub Workflow which +will also push the tag. ### npm run changelog diff --git a/scripts/build.js b/scripts/build.js index e35328e61d1..70f96e9d214 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/ErrorMap.js b/scripts/error-codes/ErrorMap.js new file mode 100644 index 00000000000..969bd9939c5 --- /dev/null +++ b/scripts/error-codes/ErrorMap.js @@ -0,0 +1,96 @@ +/** + * 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'; +// @ts-check + +/** + * Data structure to manage reading from and writing to codes.json + */ +class ErrorMap { + /** + * The map of error code numbers (as String(number)) to the error messages + * + * @type {Record} + */ + errorMap; + /** + * The map of error messages to the error code numbers (as integers) + * + * @type {Record} errorMap typically the result of `JSON.parse(fs.readFileSync('codes.json', 'utf8'))` + * @param {(errorMap: Record) => void} flushErrorMap the callback to persist the errorMap back to disk + */ + 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; + } + } + + /** + * Fetch the error code for a given error message. If the error message is + * present in the errorMap, it will return the corresponding numeric code. + * + * If the message is not present, and extractCodes is not true, it will + * return false. + * + * Otherwise, it will generate a new error code and queue a microtask to + * flush it back to disk (so multiple updates can be batched together). + * + * @param {string} message the error 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; + } + + /** + * If dirty is true, this will call flushErrorMap with the current errorMap + * and reset dirty to false. + * + * Normally this is automatically queued to a microtask as necessary, but + * it may be called manually in test scenarios. + */ + flush() { + if (this.dirty) { + this.flushErrorMap(this.errorMap); + this.dirty = false; + } + } +} + +module.exports = ErrorMap; diff --git a/scripts/error-codes/__tests__/unit/ErrorMap.test.js b/scripts/error-codes/__tests__/unit/ErrorMap.test.js new file mode 100644 index 00000000000..b72499c0b3c --- /dev/null +++ b/scripts/error-codes/__tests__/unit/ErrorMap.test.js @@ -0,0 +1,105 @@ +/** + * 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. + * + */ +// @ts-check +'use strict'; + +const ErrorMap = require('../../ErrorMap'); + +/** @returns {Promise} */ +function waitTick() { + return new Promise((resolve) => queueMicrotask(resolve)); +} + +describe('ErrorMap', () => { + [ + {initialMessages: []}, + { + initialMessages: ['known message', 'another known message'], + }, + ].forEach(({name, initialMessages}) => { + const initialMap = Object.fromEntries( + initialMessages.map((message, i) => [`${i}`, message]), + ); + describe(`with ${initialMessages.length} message(s)`, () => { + test('does not insert unless extractCodes is true', async () => { + const flush = jest.fn(); + const errorMap = new ErrorMap(initialMap, flush); + expect(errorMap.getOrAddToErrorMap('unknown message', false)).toBe( + undefined, + ); + await waitTick(); + expect(flush).not.toBeCalled(); + expect(Object.keys(errorMap.errorMap).length).toEqual( + initialMessages.length, + ); + }); + if (initialMessages.length > 0) { + test('looks up existing messages', async () => { + const flush = jest.fn(); + const errorMap = new ErrorMap(initialMap, flush); + initialMessages.forEach((msg, i) => { + expect(errorMap.getOrAddToErrorMap(msg, false)).toBe(i); + }); + expect(errorMap.dirty).toBe(false); + initialMessages.forEach((msg, i) => { + expect(errorMap.getOrAddToErrorMap(msg, true)).toBe(i); + }); + expect(errorMap.dirty).toBe(false); + await waitTick(); + expect(flush).not.toBeCalled(); + }); + } + test('inserts with extractCodes true', async () => { + const flush = jest.fn(); + const errorMap = new ErrorMap(initialMap, flush); + const msg = 'unknown message'; + const beforeSize = initialMessages.length; + expect(errorMap.getOrAddToErrorMap(msg, true)).toBe(beforeSize); + expect(Object.keys(errorMap.errorMap).length).toEqual(1 + beforeSize); + expect(Object.keys(errorMap.inverseErrorMap).length).toEqual( + 1 + beforeSize, + ); + expect(errorMap.errorMap[beforeSize]).toBe(msg); + expect(errorMap.inverseErrorMap[msg]).toBe(beforeSize); + expect(errorMap.maxId).toBe(beforeSize); + expect(flush).not.toBeCalled(); + expect(errorMap.dirty).toBe(true); + await waitTick(); + expect(errorMap.dirty).toBe(false); + expect(flush).toBeCalledWith({...initialMap, [`${beforeSize}`]: msg}); + }); + test('inserts two messages with extractCodes true', async () => { + const flush = jest.fn(); + const errorMap = new ErrorMap(initialMap, flush); + const msgs = ['unknown message', 'another unknown message']; + msgs.forEach((msg, i) => { + const beforeSize = i + initialMessages.length; + expect(errorMap.getOrAddToErrorMap(msg, true)).toBe(beforeSize); + expect(Object.keys(errorMap.errorMap).length).toEqual(1 + beforeSize); + expect(Object.keys(errorMap.inverseErrorMap).length).toEqual( + 1 + beforeSize, + ); + expect(errorMap.errorMap[beforeSize]).toBe(msg); + expect(errorMap.inverseErrorMap[msg]).toBe(beforeSize); + expect(errorMap.maxId).toBe(beforeSize); + expect(flush).not.toBeCalled(); + }); + expect(errorMap.dirty).toBe(true); + await waitTick(); + expect(errorMap.dirty).toBe(false); + expect(flush).toBeCalledTimes(1); + expect(flush).toBeCalledWith({ + ...initialMap, + ...Object.fromEntries( + msgs.map((msg, i) => [`${initialMessages.length + i}`, msg]), + ), + }); + }); + }); + }); +}); diff --git a/scripts/error-codes/__tests__/unit/transform-error-messages.test.js b/scripts/error-codes/__tests__/unit/transform-error-messages.test.js new file mode 100644 index 00000000000..7611e67bf87 --- /dev/null +++ b/scripts/error-codes/__tests__/unit/transform-error-messages.test.js @@ -0,0 +1,183 @@ +/** + * 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. + * + */ +// @ts-check +'use strict'; + +const fs = require('fs-extra'); +const path = require('node:path'); +const transformErrorMessages = require('../../transform-error-messages'); +const babel = require('@babel/core'); +const prettier = require('prettier'); + +const prettierConfig = prettier.resolveConfig.sync('./') || {}; + +/** @returns {Promise} */ +function waitTick() { + return new Promise((resolve) => queueMicrotask(resolve)); +} + +/** + * @param {Record} before + * @param {Record} after + * @param {(errorCodesPath: string) => Promise | void} cb + */ +async function withCodes(before, after, cb) { + const tmpdir = fs.mkdtempSync('transform-error-messages'); + try { + const errorCodesPath = path.join(tmpdir, 'codes.json'); + fs.writeJsonSync(errorCodesPath, before); + await cb(errorCodesPath); + await waitTick(); + expect(fs.readJsonSync(errorCodesPath)).toEqual(after); + } finally { + fs.removeSync(tmpdir); + } +} + +function fmt(strings: TemplateStringsArray, ...keys: unknown[]) { + const result = [strings[0]]; + keys.forEach((key, i) => { + result.push(String(key), strings[i + 1]); + }); + // Normalize some of the stuff that babel will inject so the examples are + // stable and easier to read: + // - use strict header + // - @babel/helper-module-imports interop + const before = result + .join('') + .replace(/.use strict.;\n/g, '') + .replace(/var _[^;]+;\n/g, '') + .replace(/function _interopRequireDefault\(obj\) {[^;]+?;[\s\n]*}\n/g, '') + .replace(/_formatProdErrorMessage\d+/g, 'formatProdErrorMessage') + .replace( + /\(0,\s*formatProdErrorMessage\.default\)/g, + 'formatProdErrorMessage', + ) + .trim(); + return prettier.format(before, { + ...prettierConfig, + filepath: 'test.js', + }); +} + +const NEW_MSG = 'A new invariant'; +const KNOWN_MSG = 'A %s message that contains %s'; +const KNOWN_MSG_MAP = Object.fromEntries( + [KNOWN_MSG].map((msg, i) => [String(i), msg]), +); +const NEW_MSG_MAP = Object.fromEntries( + [KNOWN_MSG, NEW_MSG].map((msg, i) => [String(i), msg]), +); + +/** + * @typedef {Object} ExpectTransformOptions + * @property {string} codeBefore + * @property {string} codeExpect + * @property {Record} messageMapBefore + * @property {Record} messageMapExpect + * @property {Partial} opts + */ + +/** @param {ExpectTransformOptions} opts */ +async function expectTransform(opts) { + return await withCodes( + opts.messageMapBefore, + opts.messageMapExpect, + async (errorCodesPath) => { + const {code} = babel.transform(fmt`${opts.codeBefore}`, { + plugins: [[transformErrorMessages, {errorCodesPath, ...opts.opts}]], + }); + const afterCode = fmt`${code}`; + console.log({afterCode, code}); + expect(fmt`${code}`).toEqual(fmt`${opts.codeExpect}`); + }, + ); +} + +describe('transform-error-messages', () => { + describe('{extractCodes: true, noMinify: false}', () => { + const opts = {extractCodes: true, noMinify: false}; + it('inserts known and extracts unknown message codes', async () => { + await expectTransform({ + codeBefore: ` + invariant(condition, ${JSON.stringify(NEW_MSG)}); + invariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun); + `, + codeExpect: ` + if (!condition) { + { + formatProdErrorMessage(1); + } + } + if (!condition) { + { + formatProdErrorMessage(0, adj, noun); + } + }`, + messageMapBefore: KNOWN_MSG_MAP, + messageMapExpect: NEW_MSG_MAP, + opts, + }); + }); + }); + describe('{extractCodes: true, noMinify: true}', () => { + const opts = {extractCodes: true, noMinify: true}; + it('inserts known and extracts unknown message codes', async () => { + await expectTransform({ + codeBefore: ` + invariant(condition, ${JSON.stringify(NEW_MSG)}); + invariant(condition, ${JSON.stringify(KNOWN_MSG)}, adj, noun); + `, + codeExpect: ` + if (!condition) { + throw Error(\`A new invariant\`); + } + if (!condition) { + throw Error(\`A \${adj} message that contains \${noun}\`); + }`, + messageMapBefore: KNOWN_MSG_MAP, + messageMapExpect: NEW_MSG_MAP, + opts, + }); + }); + }); + describe('{extractCodes: false, noMinify: false}', () => { + const opts = {extractCodes: false, noMinify: false}; + it('inserts known message', async () => { + await expectTransform({ + codeBefore: `invariant(condition, ${JSON.stringify( + KNOWN_MSG, + )}, adj, noun)`, + codeExpect: ` + if (!condition) { + { + formatProdErrorMessage(0, adj, noun); + } + } + `, + messageMapBefore: KNOWN_MSG_MAP, + messageMapExpect: KNOWN_MSG_MAP, + opts, + }); + }); + it('inserts warning comment for unknown messages', async () => { + await expectTransform({ + codeBefore: `invariant(condition, ${JSON.stringify(NEW_MSG)})`, + codeExpect: ` + /*FIXME (minify-errors-in-prod): Unminified error message in production build!*/ + if (!condition) { + throw Error(\`A new invariant\`); + } + `, + messageMapBefore: KNOWN_MSG_MAP, + messageMapExpect: KNOWN_MSG_MAP, + opts, + }); + }); + }); +}); diff --git a/scripts/error-codes/extract-errors.js b/scripts/error-codes/extract-errors.js deleted file mode 100644 index 0720f8b20ad..00000000000 --- 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 4bf93a80284..fc5cc667e2a 100644 --- a/scripts/error-codes/transform-error-messages.js +++ b/scripts/error-codes/transform-error-messages.js @@ -7,20 +7,65 @@ */ 'use strict'; +// @ts-check const fs = require('fs-extra'); +const ErrorMap = require('./ErrorMap'); 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; +/** @type {Map} */ +const errorMaps = new Map(); +/** + * Get a module-global ErrorMap instance so that all instances of this + * plugin are working with the same data structure. Typically there is + * at most one entry in this map (`${__dirname}/codes.json`). + * + * @param {string} filepath + * @returns {ErrorMap} + */ +function getErrorMap(filepath) { + let errorMap = errorMaps.get(filepath); + if (!errorMap) { + const prettierConfig = { + ...(prettier.resolveConfig.sync('./') || {}), + filepath, + }; + errorMap = new ErrorMap(fs.readJsonSync(filepath), (newErrorMap) => + fs.writeFileSync( + filepath, + prettier.format(JSON.stringify(newErrorMap), prettierConfig), + ), + ); + errorMaps.set(filepath, errorMap); + } + return errorMap; +} +/** + * @typedef {Object} TransformErrorMessagesOptions + * @property {string} errorCodesPath + * @property {boolean} extractCodes + * @property {boolean} noMinify + */ + +/** + * @param {import('@babel/core')} babel + * @param {Partial} opts + * @returns {import('@babel/core').PluginObj} + */ +module.exports = function (babel, opts) { + const t = babel.types; + const errorMap = getErrorMap( + (opts && opts.errorCodesPath) || `${__dirname}/codes.json`, + ); return { visitor: { CallExpression(path, file) { const node = path.node; - const noMinify = file.opts.noMinify; + const {extractCodes, noMinify} = + /** @type Partial */ (file.opts); if (path.get('callee').isIdentifier({name: 'invariant'})) { // Turns this code: // @@ -61,6 +106,13 @@ module.exports = function (babel) { ); } + // We extract the prodErrorId even if we are not using it + // so we can extractCodes in a non-production build. + let prodErrorId = errorMap.getOrAddToErrorMap( + errorMsgLiteral, + extractCodes, + ); + if (noMinify) { // Error minification is disabled for this build. // @@ -78,16 +130,7 @@ module.exports = function (babel) { ]), ), ); - return; - } - - // Avoid caching because we write it as we go. - const existingErrorMap = fs.readJsonSync(__dirname + '/codes.json'); - const errorMap = invertObject(existingErrorMap); - - let prodErrorId = errorMap[errorMsgLiteral]; - - if (prodErrorId === undefined) { + } else if (prodErrorId === undefined) { // There is no error code for this message. Add an inline comment // that flags this as an unminified error. This allows the build // to proceed, while also allowing a post-build linter to detect it. @@ -111,35 +154,33 @@ module.exports = function (babel) { 'leading', 'FIXME (minify-errors-in-prod): Unminified error message in production build!', ); - return; - } - prodErrorId = parseInt(prodErrorId, 10); - - // Import ReactErrorProd - const formatProdErrorMessageIdentifier = - helperModuleImports.addDefault(path, 'formatProdErrorMessage', { - nameHint: 'formatProdErrorMessage', - }); + } else { + // Import ReactErrorProd + const formatProdErrorMessageIdentifier = + helperModuleImports.addDefault(path, 'formatProdErrorMessage', { + nameHint: 'formatProdErrorMessage', + }); - // Outputs: - // formatProdErrorMessage(ERR_CODE, adj, noun); - const prodMessage = t.callExpression( - formatProdErrorMessageIdentifier, - [t.numericLiteral(prodErrorId), ...errorMsgExpressions], - ); + // Outputs: + // formatProdErrorMessage(ERR_CODE, adj, noun); + const prodMessage = t.callExpression( + formatProdErrorMessageIdentifier, + [t.numericLiteral(prodErrorId), ...errorMsgExpressions], + ); - // Outputs: - // if (!condition) { - // formatProdErrorMessage(ERR_CODE, adj, noun) - // } - parentStatementPath.replaceWith( - t.ifStatement( - t.unaryExpression('!', condition), - t.blockStatement([ - t.blockStatement([t.expressionStatement(prodMessage)]), - ]), - ), - ); + // Outputs: + // if (!condition) { + // formatProdErrorMessage(ERR_CODE, adj, noun) + // } + parentStatementPath.replaceWith( + t.ifStatement( + t.unaryExpression('!', condition), + t.blockStatement([ + t.blockStatement([t.expressionStatement(prodMessage)]), + ]), + ), + ); + } } }, },