-
Notifications
You must be signed in to change notification settings - Fork 24.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add transform that replaces requires with imports (#49228)
Summary: Pull Request resolved: #49228 Changelog: [Internal] Reviewed By: huntie Differential Revision: D69247874 fbshipit-source-id: 8b4ea4b358c1c8714237e0109cf5a9492528016b
- Loading branch information
1 parent
47a2174
commit c0415ad
Showing
3 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
136 changes: 136 additions & 0 deletions
136
scripts/build/build-types/transforms/__tests__/replaceRequiresWithImports-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/** | ||
* 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-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
const replaceRequiresWithImports = require('../replaceRequiresWithImports.js'); | ||
const {parse, print} = require('hermes-transform'); | ||
|
||
const prettierOptions = {parser: 'babel'}; | ||
|
||
async function translate(code: string): Promise<string> { | ||
const parsed = await parse(code); | ||
const result = await replaceRequiresWithImports(parsed); | ||
return print(result.ast, result.mutatedCode, prettierOptions); | ||
} | ||
|
||
describe('replaceRequiresWithImports', () => { | ||
test('should replace require().default with a default import', async () => { | ||
const code = ` | ||
const Foo: mixed = require('./Foo').default; | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import Foo from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should replace require() with a namespace import', async () => { | ||
const code = ` | ||
const Foo: mixed = require('./Foo'); | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import * as Foo from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should replace require().member with an import', async () => { | ||
const code = ` | ||
const Foo: mixed = require('./Foo').foo; | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import { foo as Foo } from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should replace require()["member"] with an import', async () => { | ||
const code = ` | ||
const Foo: mixed = require('./Foo')["foo"]; | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import { foo as Foo } from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should replace spread require() with an import', async () => { | ||
const code = ` | ||
const { foo, bar } = require('./Foo'); | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import { foo, bar } from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should replace aliased spread require() with an import', async () => { | ||
const code = ` | ||
const { foo: otherFoo, bar: otherBar } = require('./Foo'); | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import { foo as otherFoo, bar as otherBar } from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should handle aliased and non-aliased values in spread require() with an import', async () => { | ||
const code = ` | ||
const { default: defaultFoo, foo } = require('./Foo'); | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"import { default as defaultFoo, foo } from \\"./Foo\\"; | ||
" | ||
`); | ||
}); | ||
|
||
test('should ignore unbound requires', async () => { | ||
const code = ` | ||
require('./Foo'); | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"require(\\"./Foo\\"); | ||
" | ||
`); | ||
}); | ||
|
||
test('should ignore local requires', async () => { | ||
const code = ` | ||
function foo() { | ||
const bar = require('./Bar'); | ||
} | ||
`; | ||
const result = await translate(code); | ||
expect(result).toMatchInlineSnapshot(` | ||
"function foo() { | ||
const bar = require(\\"./Bar\\"); | ||
} | ||
" | ||
`); | ||
}); | ||
|
||
test('should throw when encountering spread operator', async () => { | ||
const tranlsateFn = async () => { | ||
const code = ` | ||
const { foo, ...rest } = require('./Foo'); | ||
`; | ||
await translate(code); | ||
}; | ||
await expect(tranlsateFn()).rejects.toThrow(); | ||
}); | ||
}); |
194 changes: 194 additions & 0 deletions
194
scripts/build/build-types/transforms/replaceRequiresWithImports.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
/** | ||
* 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-local | ||
* @format | ||
* @oncall react_native | ||
*/ | ||
|
||
import type {TransformVisitor} from 'hermes-transform'; | ||
import type {ParseResult} from 'hermes-transform/dist/transform/parse'; | ||
import type {TransformASTResult} from 'hermes-transform/dist/transform/transformAST'; | ||
|
||
const {transformAST} = require('hermes-transform/dist/transform/transformAST'); | ||
|
||
const visitors: TransformVisitor = context => ({ | ||
VariableDeclaration(node): void { | ||
if (node.parent?.type !== 'Program') { | ||
// Ignore declarations that are not in the top-level scope | ||
return; | ||
} | ||
|
||
if (node.declarations.length !== 1) { | ||
// Ignore mutliple declarations for now, those can be implemented if it | ||
// turns out to be necessary. | ||
return; | ||
} | ||
|
||
// Handle simple cases where `require` call is the only expression on the RHS | ||
if ( | ||
node.declarations[0].init?.type === 'CallExpression' && | ||
node.declarations[0].init.callee.type === 'Identifier' && | ||
node.declarations[0].init.callee.name === 'require' | ||
) { | ||
const requiredModule = node.declarations[0].init.arguments[0]; | ||
|
||
if (node.declarations[0].id.type === 'Identifier') { | ||
// $FlowExpectedError[incompatible-call] - we are replacing an expression with a statement but in the top-level scope | ||
context.replaceNode(node, { | ||
type: 'ImportDeclaration', | ||
source: requiredModule, | ||
specifiers: [ | ||
{ | ||
type: 'ImportNamespaceSpecifier', | ||
local: { | ||
type: 'Identifier', | ||
name: node.declarations[0].id.name, | ||
optional: false, | ||
}, | ||
}, | ||
], | ||
}); | ||
} else if (node.declarations[0].id.type === 'ObjectPattern') { | ||
// $FlowExpectedError[incompatible-call] - we are replacing an expression with a statement but in the top-level scope | ||
context.replaceNode(node, { | ||
type: 'ImportDeclaration', | ||
source: requiredModule, | ||
specifiers: node.declarations[0].id.properties.map(property => { | ||
if (property.type !== 'Property') { | ||
throw new Error('Unexpected property type: ' + property.type); | ||
} | ||
|
||
if (property.key.type !== 'Identifier') { | ||
throw new Error('Unexpected key type: ' + property.key.type); | ||
} | ||
|
||
if (property.value.type !== 'Identifier') { | ||
throw new Error('Unexpected value type: ' + property.value.type); | ||
} | ||
|
||
return { | ||
type: 'ImportSpecifier', | ||
local: { | ||
type: 'Identifier', | ||
name: property.value.name, | ||
optional: false, | ||
}, | ||
imported: { | ||
type: 'Identifier', | ||
name: property.key.name, | ||
optional: false, | ||
}, | ||
}; | ||
}), | ||
}); | ||
} | ||
} | ||
|
||
// Handle cases where `require` call is the first expression in a member expression | ||
if ( | ||
node.declarations[0].init?.type === 'MemberExpression' && | ||
node.declarations[0].init.object.type === 'CallExpression' && | ||
node.declarations[0].init.object.callee.type === 'Identifier' && | ||
node.declarations[0].init.object.callee.name === 'require' && | ||
node.declarations[0].id.type === 'Identifier' | ||
) { | ||
const requiredModule = node.declarations[0].init.object.arguments[0]; | ||
const variableName = node.declarations[0].id.name; | ||
|
||
// Handle member access via dot operator | ||
if (node.declarations[0].init.property.type === 'Identifier') { | ||
// Special treatment for `require().default` case to transform it to | ||
// a default import | ||
if (node.declarations[0].init.property.name === 'default') { | ||
// $FlowExpectedError[incompatible-call] - we are replacing an expression with a statement but in the top-level scope | ||
context.replaceNode(node, { | ||
type: 'ImportDeclaration', | ||
source: requiredModule, | ||
specifiers: [ | ||
{ | ||
type: 'ImportDefaultSpecifier', | ||
local: { | ||
type: 'Identifier', | ||
name: variableName, | ||
optional: false, | ||
}, | ||
}, | ||
], | ||
}); | ||
} else { | ||
// $FlowExpectedError[incompatible-call] - we are replacing an expression with a statement but in the top-level scope | ||
context.replaceNode(node, { | ||
type: 'ImportDeclaration', | ||
source: requiredModule, | ||
specifiers: [ | ||
{ | ||
type: 'ImportSpecifier', | ||
local: { | ||
type: 'Identifier', | ||
name: variableName, | ||
optional: false, | ||
}, | ||
imported: { | ||
type: 'Identifier', | ||
name: node.declarations[0].init.property.name, | ||
optional: false, | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
} else if (node.declarations[0].init.property.type === 'Literal') { | ||
// Handle member access via bracket notation | ||
// $FlowExpectedError[incompatible-call] - we are replacing an expression with a statement but in the top-level | ||
context.replaceNode(node, { | ||
type: 'ImportDeclaration', | ||
source: requiredModule, | ||
specifiers: [ | ||
{ | ||
type: 'ImportSpecifier', | ||
local: { | ||
type: 'Identifier', | ||
name: variableName, | ||
optional: false, | ||
}, | ||
imported: { | ||
type: 'Identifier', | ||
name: node.declarations[0].init.property.value, | ||
optional: false, | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
} | ||
}, | ||
}); | ||
|
||
/** | ||
* Replace `require` calls with `import` statements. | ||
* | ||
* In the type-land top-level requires can safely be replaced with import | ||
* statements without impacring the runtime. This allows the modern Flow toolkit | ||
* to be used in existing codebases without having to update each file still | ||
* relying on require syntax. | ||
* | ||
* It's expecially useful in more complex cases where a type comes from an import | ||
* but the implementation comes from a require, like so: | ||
* import typeof FooClassT from './Foo'; | ||
* const FooClass: FooClassT = require('./Foo').default; | ||
* const Foo: FooClass = new FooClass(); | ||
* | ||
* Where the types would diverge in the resulting TS output generated by | ||
* flow-api-translator. | ||
*/ | ||
async function replaceRequiresWithImports( | ||
source: ParseResult, | ||
): Promise<TransformASTResult> { | ||
return transformAST(source, visitors); | ||
} | ||
|
||
module.exports = replaceRequiresWithImports; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters