Skip to content

Commit

Permalink
[cli] Install a dependency's environment definitions (#4541)
Browse files Browse the repository at this point in the history
* return up list of envs needed

* complete changes

* add tests
  • Loading branch information
Brianzchen committed Oct 25, 2023
1 parent b30ed37 commit 908dc38
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 28 deletions.
@@ -0,0 +1,3 @@
declare type jsx$HTMLElementProps = {|
test: boolean,
|};
124 changes: 118 additions & 6 deletions cli/src/commands/__tests__/install-test.js
Expand Up @@ -67,7 +67,7 @@ describe('install (command)', () => {

it('errors if unable to find a project root (.flowconfig)', () => {
return testProject(async ROOT_DIR => {
const result = await installNpmLibDefs({
const {status} = await installNpmLibDefs({
cwd: ROOT_DIR,
flowVersion: parseFlowDirString('flow_v0.40.0'),
explicitLibDefs: [],
Expand All @@ -79,7 +79,7 @@ describe('install (command)', () => {
ignoreDeps: [],
useCacheUntil: 1000 * 60,
});
expect(result).toBe(1);
expect(status).toBe(1);
expect(_mock(console.error).mock.calls).toEqual([
[
'Error: Unable to find a flow project in the current dir or any of ' +
Expand All @@ -102,7 +102,7 @@ describe('install (command)', () => {
'flow-bin': '^0.40.0',
},
});
const result = await installNpmLibDefs({
const {status} = await installNpmLibDefs({
cwd: ROOT_DIR,
flowVersion: parseFlowDirString('flow_v0.40.0'),
explicitLibDefs: ['INVALID'],
Expand All @@ -114,7 +114,7 @@ describe('install (command)', () => {
ignoreDeps: [],
useCacheUntil: 1000 * 60,
});
expect(result).toBe(1);
expect(status).toBe(1);
expect(_mock(console.error).mock.calls).toEqual([
[
'ERROR: Package not found from package.json.\n' +
Expand All @@ -133,7 +133,7 @@ describe('install (command)', () => {
name: 'test',
}),
]);
const result = await installNpmLibDefs({
const {status} = await installNpmLibDefs({
cwd: ROOT_DIR,
flowVersion: parseFlowDirString('flow_v0.40.0'),
explicitLibDefs: [],
Expand All @@ -145,7 +145,7 @@ describe('install (command)', () => {
ignoreDeps: [],
useCacheUntil: 1000 * 60,
});
expect(result).toBe(0);
expect(status).toBe(0);
expect(_mock(console.error).mock.calls).toEqual([
["No dependencies were found in this project's package.json!"],
]);
Expand Down Expand Up @@ -1504,6 +1504,118 @@ declare type jsx$HTMLElementProps = {||}`;
).not.toEqual(installedDef);
});
});

it('installs envs from dependency flow-typed.config even if no local flow-typed.config', () => {
return fakeProjectEnv(async FLOWPROJ_DIR => {
// Create some dependencies
await Promise.all([
writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
name: 'test',
devDependencies: {
'flow-bin': '^0.140.0',
untyped: '^1.0.0',
},
}),
mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'untyped')),
]);
await touchFile(path.join(FLOWPROJ_DIR, '.flowconfig'));

await touchFile(
path.join(FLOWPROJ_DIR, 'node_modules', 'untyped', 'index.js.flow'),
);
await touchFile(
path.join(
FLOWPROJ_DIR,
'node_modules',
'untyped',
'flow-typed.config.json',
),
);
await fs.writeJson(
path.join(
FLOWPROJ_DIR,
'node_modules',
'untyped',
'flow-typed.config.json',
),
{env: ['jsx']},
);

// Run the install command
await run({
...defaultRunProps,
rootDir: path.join(FLOWPROJ_DIR),
});

// Installs env definitions
expect(
await fs.exists(
path.join(FLOWPROJ_DIR, 'flow-typed', 'environments', 'jsx.js'),
),
).toEqual(true);
});
});

it('installs envs that exist in both dependency and own flow-typed.config', () => {
return fakeProjectEnv(async FLOWPROJ_DIR => {
// Create some dependencies
await Promise.all([
writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), {
name: 'test',
devDependencies: {
'flow-bin': '^0.140.0',
untyped: '^1.0.0',
},
}),
mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'untyped')),
]);
await touchFile(path.join(FLOWPROJ_DIR, '.flowconfig'));
await touchFile(path.join(FLOWPROJ_DIR, 'flow-typed.config.json'));
await fs.writeJson(
path.join(FLOWPROJ_DIR, 'flow-typed.config.json'),
{env: ['jsx']},
);

await touchFile(
path.join(FLOWPROJ_DIR, 'node_modules', 'untyped', 'index.js.flow'),
);
await touchFile(
path.join(
FLOWPROJ_DIR,
'node_modules',
'untyped',
'flow-typed.config.json',
),
);
await fs.writeJson(
path.join(
FLOWPROJ_DIR,
'node_modules',
'untyped',
'flow-typed.config.json',
),
{env: ['jsx', 'react']},
);

// Run the install command
await run({
...defaultRunProps,
rootDir: path.join(FLOWPROJ_DIR),
});

// Installs env definitions
expect(
await fs.exists(
path.join(FLOWPROJ_DIR, 'flow-typed', 'environments', 'jsx.js'),
),
).toEqual(true);
expect(
await fs.exists(
path.join(FLOWPROJ_DIR, 'flow-typed', 'environments', 'react.js'),
),
).toEqual(true);
});
});
});

describe('definitions with dependencies', () => {
Expand Down
66 changes: 52 additions & 14 deletions cli/src/commands/install.js
Expand Up @@ -180,7 +180,7 @@ export async function run(args: Args): Promise<number> {

const useCacheUntil = Number(args.useCacheUntil) || CACHE_REPO_EXPIRY;

const npmLibDefResult = await installNpmLibDefs({
const {status: npmLibDefResult, dependencyEnvs} = await installNpmLibDefs({
cwd,
flowVersion,
explicitLibDefs,
Expand All @@ -198,9 +198,9 @@ export async function run(args: Args): Promise<number> {
}

// Must be after `installNpmLibDefs` to ensure cache is updated first
if (ftConfig) {
if (ftConfig?.env || dependencyEnvs.length > 0) {
const envLibDefResult = await installEnvLibDefs(
ftConfig,
[...new Set([...(ftConfig?.env ?? []), ...dependencyEnvs])],
flowVersion,
cwd,
libdefDir,
Expand All @@ -226,7 +226,7 @@ export async function run(args: Args): Promise<number> {
}

async function installEnvLibDefs(
{env}: FtConfig,
env: Array<string>,
flowVersion: FlowVersion,
flowProjectRoot,
libdefDir,
Expand Down Expand Up @@ -354,15 +354,21 @@ async function installNpmLibDefs({
ignoreDeps,
useCacheUntil,
ftConfig = {},
}: installNpmLibDefsArgs): Promise<number> {
}: installNpmLibDefsArgs): Promise<{|
status: number,
dependencyEnvs: Array<string>,
|}> {
const flowProjectRoot = await findFlowRoot(cwd);
if (flowProjectRoot === null) {
console.error(
'Error: Unable to find a flow project in the current dir or any of ' +
"it's parent dirs!\n" +
'Please run this command from within a Flow project.',
);
return 1;
return {
status: 1,
dependencyEnvs: [],
};
}

const libdefsToSearchFor: Map<string, string> = new Map();
Expand Down Expand Up @@ -415,7 +421,10 @@ async function installNpmLibDefs({
'ERROR: Package not found from package.json.\n' +
'Please specify version for the package in the format of `foo@1.2.3`',
);
return 1;
return {
status: 1,
dependencyEnvs: [],
};
}
} else {
const [_, npmScope, pkgName, pkgVerStr] = termMatches;
Expand Down Expand Up @@ -445,7 +454,10 @@ async function installNpmLibDefs({
console.error(
"No dependencies were found in this project's package.json!",
);
return 0;
return {
status: 0,
dependencyEnvs: [],
};
}

if (verbose) {
Expand Down Expand Up @@ -528,20 +540,37 @@ async function installNpmLibDefs({

const pnpResolver = await loadPnpResolver(await getPackageJsonData(cwd));

const dependencyEnvs: Array<string> = [];

// If a package that's missing a flow-typed libdef has any .flow files,
// we'll skip generating a stub for it.
const untypedMissingLibDefs: Array<[string, string]> = [];
const typedMissingLibDefs: Array<[string, string, string]> = [];
await Promise.all(
unavailableLibDefs.map(async ({name: pkgName, ver: pkgVer}) => {
const hasFlowFiles = await pkgHasFlowFiles(
const {isFlowTyped, pkgPath} = await pkgHasFlowFiles(
cwd,
pkgName,
pnpResolver,
workspacesPkgJsonData,
);
if (hasFlowFiles.flowTyped && hasFlowFiles.path) {
typedMissingLibDefs.push([pkgName, pkgVer, hasFlowFiles.path]);
if (isFlowTyped && pkgPath) {
typedMissingLibDefs.push([pkgName, pkgVer, pkgPath]);

try {
// If the flow typed dependency has a flow-typed config
// check if they have env's defined and if so keep them in
// a list so we can later install them along with project
// defined envs.
const pkgFlowTypedConfig: FtConfig = await fs.readJson(
path.join(pkgPath, 'flow-typed.config.json'),
);
if (pkgFlowTypedConfig.env) {
dependencyEnvs.push(...pkgFlowTypedConfig.env);
}
} catch (e) {
// Ignore if we cannot read flow-typed config
}
} else {
untypedMissingLibDefs.push([pkgName, pkgVer]);
}
Expand Down Expand Up @@ -663,7 +692,10 @@ async function installNpmLibDefs({
);

if (results.some(res => !res)) {
return 1;
return {
status: 1,
dependencyEnvs: [],
};
}
}

Expand Down Expand Up @@ -714,7 +746,10 @@ async function installNpmLibDefs({
colors.bold(`flow-typed create-stub ${explicitLibDefs.join(' ')}`),
);

return 1;
return {
status: 1,
dependencyEnvs: [],
};
} else {
if (untypedMissingLibDefs.length > 0 && !skip) {
console.log('• Generating stubs for untyped dependencies...');
Expand Down Expand Up @@ -755,7 +790,10 @@ async function installNpmLibDefs({
}
}

return 0;
return {
status: 0,
dependencyEnvs,
};
}

async function installNpmLibDef(
Expand Down
2 changes: 1 addition & 1 deletion cli/src/lib/ftConfig.js
Expand Up @@ -2,7 +2,7 @@
import {fs, path} from './node';

export type FtConfig = {
env?: mixed, // Array<string>,
env?: Array<string>,
ignore?: Array<string>,
workspaces?: Array<string>,
};
Expand Down
14 changes: 7 additions & 7 deletions cli/src/lib/stubUtils.js
Expand Up @@ -377,8 +377,8 @@ export async function pkgHasFlowFiles(
pnpjs: PnpResolver | null,
workspacesPkgJsonData: Array<PkgJson>,
): Promise<{|
flowTyped: boolean,
path?: string,
isFlowTyped: boolean,
pkgPath?: string,
|}> {
const findTypedFiles = async (path: string): Promise<void | string> => {
try {
Expand All @@ -401,8 +401,8 @@ export async function pkgHasFlowFiles(

if (rootTypedPath) {
return {
flowTyped: true,
path: rootTypedPath,
isFlowTyped: true,
pkgPath: rootTypedPath,
};
}

Expand All @@ -414,13 +414,13 @@ export async function pkgHasFlowFiles(
const workspacePath = typedWorkspacePaths.find(o => !!o);
if (workspacePath) {
return {
flowTyped: true,
path: path.dirname(workspacePath),
isFlowTyped: true,
pkgPath: workspacePath,
};
}

return {
flowTyped: false,
isFlowTyped: false,
};
}

Expand Down

0 comments on commit 908dc38

Please sign in to comment.