diff --git a/scripts/www/__tests__/unit/transformFlowFileContents.test.js b/scripts/www/__tests__/unit/transformFlowFileContents.test.js new file mode 100644 index 00000000000..af3b931fede --- /dev/null +++ b/scripts/www/__tests__/unit/transformFlowFileContents.test.js @@ -0,0 +1,107 @@ +/** + * 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 transformFlowFileContents = require('../../transformFlowFileContents'); + +const HEADER_BEFORE = + ` +/** + * 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. + * + * @flow strict + */ +`.trim() + '\n'; + +const HEADER_AFTER = + ` +/** + * 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. + * + * @flow strict + * @generated + * @oncall lexical_web_text_editor + */ +`.trim() + '\n'; + +const IMPORTS_BEFORE = + ` +import type {Doc, RelativePosition, UndoManager, XmlText} from 'yjs'; +import type { + DecoratorNode, + EditorState, + ElementNode, + LexicalCommand, + LexicalEditor, + LexicalNode, + LineBreakNode, + NodeMap, + NodeKey, + TextNode, +} from 'lexical'; +`.trim() + '\n'; + +const IMPORTS_AFTER = + ` +import type {Doc, RelativePosition, UndoManager, XmlText} from 'yjs'; +import type { + DecoratorNode, + EditorState, + ElementNode, + LexicalCommand, + LexicalEditor, + LexicalNode, + LineBreakNode, + NodeMap, + NodeKey, + TextNode, +} from 'Lexical'; +`.trim() + '\n'; + +const EXTRA_BLOCK_COMMENT = + ` +/** + * LexicalDevToolsCore + */ +`.trim() + '\n'; + +describe('transformFlowFileContents', () => { + [ + { + input: [HEADER_BEFORE, IMPORTS_BEFORE, EXTRA_BLOCK_COMMENT].join('\n'), + output: [HEADER_AFTER, IMPORTS_AFTER, EXTRA_BLOCK_COMMENT].join('\n'), + title: 'header-imports-comment', + }, + { + input: [HEADER_BEFORE, EXTRA_BLOCK_COMMENT].join('\n'), + output: [HEADER_AFTER, EXTRA_BLOCK_COMMENT].join('\n'), + title: 'header-comment', + }, + { + input: [HEADER_BEFORE, IMPORTS_BEFORE].join('\n'), + output: [HEADER_AFTER, IMPORTS_AFTER].join('\n'), + title: 'header-imports', + }, + { + input: [HEADER_BEFORE].join('\n'), + output: [HEADER_AFTER].join('\n'), + title: 'header', + }, + ].forEach(({input, output, title}) => { + it(`transforms ${title}`, async () => { + expect(await transformFlowFileContents(input)).toBe(output); + }); + }); +}); diff --git a/scripts/www/rewriteImports.js b/scripts/www/rewriteImports.js index 076172cf0d2..6635300f5ad 100644 --- a/scripts/www/rewriteImports.js +++ b/scripts/www/rewriteImports.js @@ -12,59 +12,7 @@ const fs = require('fs-extra'); const glob = require('glob'); const path = require('node:path'); const {packagesManager} = require('../shared/packagesManager'); -const npmToWwwName = require('./npmToWwwName'); -const {t, transform} = require('hermes-transform'); - -const wwwMappings = Object.fromEntries( - packagesManager - .getPublicPackages() - .flatMap((pkg) => - pkg.getExportedNpmModuleNames().map((npm) => [npm, npmToWwwName(npm)]), - ), -); - -/** - * It would be nice to use jscodeshift for this but the flow sources are using - * ast features that are not supported in ast-types (as of 2024-04-11) so it's - * not possible to traverse the tree and replace the imports & comments. - * - * It might be possible going straight to flow-parser, but it was a slew of - * hardcoded regexps before and now it's at least automated based on the - * exports. - * - * @param {string} source - * @returns {Promise} transformed source - */ -async function transformFlowFileContents(source) { - return await transform( - source, - (context) => ({ - ImportDeclaration(node) { - const value = wwwMappings[node.source.value]; - if (value) { - context.replaceNode(node.source, t.StringLiteral({value})); - } - }, - Program(node) { - if ( - node.docblock && - node.docblock.comment && - node.docblock.comment.value.includes('@flow strict') - ) { - node.docblock.comment.value = node.docblock.comment.value.replace( - / \* @flow strict/g, - ' * @flow strict\n * @generated\n * @oncall lexical_web_text_editor', - ); - // Let the transform know we actually did something. - // Could not figure out the right way to update the - // docblock without an in-place update - context.addLeadingComments(node, ''); - } - }, - }), - {}, - ); -} +const transformFlowFileContents = require('./transformFlowFileContents'); // This script attempts to find all Flow definition modules, and makes // them compatible with www. Specifically, it finds any imports that diff --git a/scripts/www/transformFlowFileContents.js b/scripts/www/transformFlowFileContents.js new file mode 100644 index 00000000000..4dc505c3d36 --- /dev/null +++ b/scripts/www/transformFlowFileContents.js @@ -0,0 +1,90 @@ +/** + * 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 {packagesManager} = require('../shared/packagesManager'); +const npmToWwwName = require('./npmToWwwName'); +const {t, transform} = require('hermes-transform'); +const prettier = require('prettier'); + +const wwwMappings = Object.fromEntries( + packagesManager + .getPublicPackages() + .flatMap((pkg) => + pkg.getExportedNpmModuleNames().map((npm) => [npm, npmToWwwName(npm)]), + ), +); + +const prettierConfig = prettier.resolveConfig('./').then((cfg) => cfg || {}); + +/** + * Add a statement to the end of the code so the comments don't + * disappear. This is a workaround for a hermes transform issue. + * + * @param {string} code + */ +function wrapCode(code) { + return [code, 'export {};\n'].join('\n'); +} + +/** + * The inverse transform of wrapCode, removes the added statement. + * + * @param {string} code + */ +function unwrapCode(code) { + return code.replace(/\n+export {};\n?$/, '\n'); +} + +/** + * It would be nice to use jscodeshift for this but the flow sources are using + * ast features that are not supported in ast-types (as of 2024-04-11) so it's + * not possible to traverse the tree and replace the imports & comments. + * + * It might be possible going straight to flow-parser, but it was a slew of + * hardcoded regexps before and now it's at least automated based on the + * exports. + * + * @param {string} source + * @returns {Promise} transformed source + */ +module.exports = async function transformFlowFileContents(source) { + return unwrapCode( + await transform( + wrapCode(source), + (context) => ({ + ImportDeclaration(node) { + const value = wwwMappings[node.source.value]; + if (value) { + context.replaceNode(node.source, t.StringLiteral({value})); + } + }, + Program(node) { + if ( + node.docblock && + node.docblock.comment && + node.docblock.comment.value.includes('@flow strict') + ) { + // This is mutated in-place because I couldn't find a mutation that + // did not fail for replacing the Program node. + node.docblock.comment.value = node.docblock.comment.value.replace( + / \* @flow strict/g, + ' * @flow strict\n * @generated\n * @oncall lexical_web_text_editor', + ); + // We need the mutations array to be non-empty, so remove something + // that is not there. The AST traversals use object identity in a + // Set so we don't have to worry about some other line changing. + context.removeComments(t.LineComment({value: ''})); + } + }, + }), + await prettierConfig, + ), + ); +};