From dd10a00825c58de112fca52977b4a2439583b543 Mon Sep 17 00:00:00 2001 From: Lazy <2818242447@qq.com> Date: Tue, 9 Jun 2026 15:14:12 +0800 Subject: [PATCH] Fix package .js.flow type discovery --- .../jsFlowPackageDefinitions.test.js | 76 +++++++++++++++++++ packages/flow-runtime-cli/src/findFiles.js | 43 ++++++++++- packages/flow-runtime-cli/src/importAST.js | 47 +++++++++++- 3 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 packages/flow-runtime-cli/src/__tests__/jsFlowPackageDefinitions.test.js diff --git a/packages/flow-runtime-cli/src/__tests__/jsFlowPackageDefinitions.test.js b/packages/flow-runtime-cli/src/__tests__/jsFlowPackageDefinitions.test.js new file mode 100644 index 00000000..f86e3e35 --- /dev/null +++ b/packages/flow-runtime-cli/src/__tests__/jsFlowPackageDefinitions.test.js @@ -0,0 +1,76 @@ +/* @flow */ + +import {equal, ok} from 'assert'; +import {fs} from '../util'; +import generate from 'babel-generator'; +import {format} from 'prettier'; +import os from 'os'; +import path from 'path'; + +import buildProgram from '../buildProgram'; +import crawlTypeDefinitions from '../crawlTypeDefinitions'; +import crawlTypeDependencies from '../crawlTypeDependencies'; + +const expected = ` +import t from "flow-runtime"; +t.declare( + t.module("fixture-package", t => { + t.declare(t.type("ExternalUser", t.object(t.property("name", t.string())))); + }) +); +`; + +describe('JS Flow Package Definitions', () => { + it('uses package index.js.flow definitions for imported package types', async () => { + const workspace = await createPackageFixture(); + try { + const dependencies = await crawlTypeDependencies([ + path.join(workspace, 'src') + ]); + const definitions = await crawlTypeDefinitions([ + path.join(workspace, 'node_modules') + ]); + + ok(definitions.get('fixture-package', 'ExternalUser')); + + const code = format(generate(buildProgram(null, dependencies, definitions)).code); + equal(code.trim(), expected.trim()); + } + finally { + await fs.unlinkAsync(path.join(workspace, 'node_modules/fixture-package/index.js')); + await fs.unlinkAsync(path.join(workspace, 'node_modules/fixture-package/index.js.flow')); + await fs.rmdirAsync(path.join(workspace, 'node_modules/fixture-package')); + await fs.rmdirAsync(path.join(workspace, 'node_modules')); + await fs.unlinkAsync(path.join(workspace, 'src/consumer.js')); + await fs.rmdirAsync(path.join(workspace, 'src')); + await fs.rmdirAsync(workspace); + } + }); +}); + +async function createPackageFixture () { + const workspace = await fs.mkdtempAsync(path.join(os.tmpdir(), 'flow-runtime-js-flow-')); + const packageDir = path.join(workspace, 'node_modules/fixture-package'); + const sourceDir = path.join(workspace, 'src'); + + await fs.mkdirAsync(path.join(workspace, 'node_modules')); + await fs.mkdirAsync(packageDir); + await fs.mkdirAsync(sourceDir); + await fs.writeFileAsync(path.join(packageDir, 'index.js'), 'exports.value = true;'); + await fs.writeFileAsync(path.join(packageDir, 'index.js.flow'), `/* @flow */ + +export type ExternalUser = { + name: string +}; +`); + await fs.writeFileAsync(path.join(sourceDir, 'consumer.js'), `/* @flow */ + +import type {ExternalUser} from 'fixture-package'; + +type LocalUser = { + user: ExternalUser +}; +`); + + return workspace; +} diff --git a/packages/flow-runtime-cli/src/findFiles.js b/packages/flow-runtime-cli/src/findFiles.js index d5dc233b..11e5ac77 100644 --- a/packages/flow-runtime-cli/src/findFiles.js +++ b/packages/flow-runtime-cli/src/findFiles.js @@ -22,8 +22,47 @@ async function collectFiles (fileOrDir: string, collected: string[]): Promise { + try { + await fs.statAsync(`${filename}.flow`); + return true; + } + catch (e) { + return false; + } +} diff --git a/packages/flow-runtime-cli/src/importAST.js b/packages/flow-runtime-cli/src/importAST.js index afcd52f3..4c5738a9 100644 --- a/packages/flow-runtime-cli/src/importAST.js +++ b/packages/flow-runtime-cli/src/importAST.js @@ -6,17 +6,26 @@ import {findIdentifiers, getTypeParameters} from 'babel-plugin-flow-runtime'; import shouldIgnoreType from './shouldIgnoreType'; import type {NodePath, Scope} from '@babel/traverse'; -import type {FlowModule, FlowEntity} from './Graph'; +import {FlowModule} from './Graph'; +import type {FlowEntity} from './Graph'; type Node = { type: string; + filename?: string; + program?: { + body: Object[]; + }; }; export default function importAST (graph: FlowModule, file: Node) { const nodeTypeParameters = new WeakMap(); + const implicitModuleName = getImplicitModuleName(file); const moduleStack = [graph]; + if (implicitModuleName && !hasExplicitDeclaredModule(file)) { + moduleStack.push(getOrCreateModule(graph, implicitModuleName)); + } function currentModule (): FlowModule { return moduleStack[moduleStack.length - 1]; @@ -108,6 +117,12 @@ export default function importAST (graph: FlowModule, file: Node) { } }, ExportNamedDeclaration (path: NodePath) { + if (implicitModuleName && path.has('declaration')) { + const declaration = path.get('declaration'); + if (declaration.isTypeAlias()) { + declaration.node.type = 'DeclareTypeAlias'; + } + } if (path.node.declare) { // this is not a real export, it's a DeclareExportDeclaration // which babylon doesn't support @@ -245,6 +260,36 @@ export default function importAST (graph: FlowModule, file: Node) { return graph; } +function getImplicitModuleName (file: Node): ?string { + const {filename} = file; + if (!filename) { + return; + } + const normalized = filename.replace(/\\/g, '/'); + const match = /\/node_modules\/((?:@[^/]+\/)?[^/]+)\/index\.js\.flow$/.exec(normalized); + return match && match[1]; +} + +function hasExplicitDeclaredModule (file: Node): boolean { + const program = file.program; + if (!program) { + return false; + } + return program.body.some(item => item.type === 'DeclareModule'); +} + +function getOrCreateModule (graph: FlowModule, name: string): FlowModule { + const existing = graph.modules[name]; + if (existing) { + return existing; + } + const child = new FlowModule(); + child.name = name; + child.parent = graph; + graph.modules[name] = child; + return child; +} + function findContainingPath (path: NodePath): ? NodePath { let child = path; let parent = path.parentPath;