Skip to content

Commit

Permalink
fix(build): move error-codes generation to transform-error-messages b…
Browse files Browse the repository at this point in the history
…abel plugin and always run with build-release
  • Loading branch information
etrepum committed May 3, 2024
1 parent 0fb96a6 commit 9252553
Show file tree
Hide file tree
Showing 8 changed files with 500 additions and 167 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 31 additions & 4 deletions packages/lexical-website/docs/maintainers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
17 changes: 1 addition & 16 deletions scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}),
Expand All @@ -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',
],
Expand Down
96 changes: 96 additions & 0 deletions scripts/error-codes/ErrorMap.js
Original file line number Diff line number Diff line change
@@ -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<string, string>}
*/
errorMap;
/**
* The map of error messages to the error code numbers (as integers)
*
* @type {Record<string, number}
*/
inverseErrorMap = {};
/**
* The largest known error code presently in the errorMap
* @type {number}
*/
maxId = -1;
/**
* true if the errorMap has been updated but not yet flushed
*
* @type {boolean}
*/
dirty = false;

/**
* @param {Record<string, string>} errorMap typically the result of `JSON.parse(fs.readFileSync('codes.json', 'utf8'))`
* @param {(errorMap: Record<string, string>) => 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;
105 changes: 105 additions & 0 deletions scripts/error-codes/__tests__/unit/ErrorMap.test.js
Original file line number Diff line number Diff line change
@@ -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<void>} */
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]),
),
});
});
});
});
});
Loading

0 comments on commit 9252553

Please sign in to comment.