Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect context.projectMetadata.hasRouter and send to the index #1112

31 changes: 31 additions & 0 deletions node-src/git/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import {
findFilesFromRepositoryRoot,
getCommit,
getCommittedFileCount,
getNumberOfComitters,
getRepositoryCreationDate,
getSlug,
hasPreviousCommit,
mergeQueueBranchMatch,
Expand Down Expand Up @@ -145,3 +148,31 @@ describe('findFilesFromRepositoryRoot', () => {
expect(results).toEqual(filesFound);
});
});

describe('getRepositoryCreationDate', () => {
it('parses the date successfully', async () => {
command.mockImplementation(() => Promise.resolve({ all: `2017-05-17 10:00:35 -0700` }) as any);
expect(await getRepositoryCreationDate()).toEqual(new Date('2017-05-17T17:00:35.000Z'));
});
});

describe('getNumberOfComitters', () => {
it('parses the count successfully', async () => {
command.mockImplementation(() => Promise.resolve({ all: ` 17` }) as any);
expect(await getNumberOfComitters()).toEqual(17);
});
});

describe('getCommittedFileCount', () => {
it('constructs the correct command', async () => {
await getCommittedFileCount(['page', 'screen'], ['js', 'ts']);
expect(command).toHaveBeenCalledWith(
'git ls-files -- "*page*.js" "*page*.ts" "*Page*.js" "*Page*.ts" "*screen*.js" "*screen*.ts" "*Screen*.js" "*Screen*.ts" | wc -l',
expect.anything()
);
});
it('parses the count successfully', async () => {
command.mockImplementation(() => Promise.resolve({ all: ` 17` }) as any);
expect(await getCommittedFileCount(['page', 'screen'], ['js', 'ts'])).toEqual(17);
});
});
44 changes: 44 additions & 0 deletions node-src/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,47 @@ export async function mergeQueueBranchMatch(branch: string) {

return match ? Number(match[1]) : undefined;
}

/**
* Determine the date the repository was created
*
* @returns Date The date the repository was created
*/
export async function getRepositoryCreationDate() {
const dateString = await execGitCommand(`git log --reverse --format=%cd --date=iso | head -1`);
return dateString ? new Date(dateString) : undefined;
}

/**
* Determine the number of committers in the last 6 months
*
* @returns number The number of committers
*/
export async function getNumberOfComitters() {
const numberString = await execGitCommand(
`git shortlog -sn --all --since="6 months ago" | wc -l`
);
return numberString ? Number.parseInt(numberString, 10) : undefined;
}

/**
* Find the number of files in the git index that include a name with the given prefixes.
*
* @param nameMatches The names to match - will be matched with upper and lowercase first letter
* @param extensions The filetypes to match
*
* @returns The number of files matching the above
*/
export async function getCommittedFileCount(nameMatches: string[], extensions: string[]) {
const bothCasesNameMatches = nameMatches.flatMap((match) => [
match,
[match[0].toUpperCase(), ...match.slice(1)].join(''),
]);

const globs = bothCasesNameMatches.flatMap((match) =>
extensions.map((extension) => `"*${match}*.${extension}"`)
);

const numberString = await execGitCommand(`git ls-files -- ${globs.join(' ')} | wc -l`);
return numberString ? Number.parseInt(numberString, 10) : undefined;
}
5 changes: 5 additions & 0 deletions node-src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ vi.mock('./git/git', () => ({
getUncommittedHash: () => Promise.resolve('abc123'),
getUserEmail: () => Promise.resolve('[email protected]'),
mergeQueueBranchMatch: () => Promise.resolve(undefined),
getRepositoryCreationDate: () => Promise.resolve(new Date('2024-11-01')),
getNumberOfComitters: () => Promise.resolve(17),
getCommittedFileCount: () => Promise.resolve(100),
}));

vi.mock('./git/getParentCommits', () => ({
Expand All @@ -321,6 +324,8 @@ const getSlug = vi.mocked(git.getSlug);

vi.mock('./lib/emailHash');

vi.mock('./lib/getHasRouter');

vi.mock('./lib/getFileHashes', () => ({
getFileHashes: (files: string[]) =>
Promise.resolve(Object.fromEntries(files.map((f) => [f, 'hash']))),
Expand Down
29 changes: 29 additions & 0 deletions node-src/lib/getHasRouter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, it } from 'vitest';

import { getHasRouter } from './getHasRouter';

it('returns true if there is a routing package in package.json', async () => {
expect(
getHasRouter({
dependencies: {
react: '^18',
'react-dom': '^18',
'react-router': '^6',
},
})
).toBe(true);
});

it('sreturns false if there is a routing package in package.json dependenices', async () => {
expect(
getHasRouter({
dependencies: {
react: '^18',
'react-dom': '^18',
},
devDependencies: {
'react-router': '^6',
},
})
).toBe(false);
});
38 changes: 38 additions & 0 deletions node-src/lib/getHasRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Context } from '../types';

const routerPackages = new Set([
'react-router',
'react-router-dom',
'remix',
'@tanstack/react-router',
'expo-router',
'@reach/router',
'react-easy-router',
'@remix-run/router',
'wouter',
'wouter-preact',
'preact-router',
'vue-router',
'unplugin-vue-router',
'@angular/router',
'@solidjs/router',

// metaframeworks that imply routing
'next',
'react-scripts',
'gatsby',
'nuxt',
'@sveltejs/kit',
]);

/**
* @param packageJson The package JSON of the project (from context)
*
* @returns boolean Does this project use a routing package?
*/
export function getHasRouter(packageJson: Context['packageJson']) {
// NOTE: we just check real dependencies; if it is in dev dependencies, it may just be an example
return Object.keys(packageJson?.dependencies ?? {}).some((depName) =>
routerPackages.has(depName)
);
}
22 changes: 22 additions & 0 deletions node-src/tasks/gitInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@ import { getChangedFilesWithReplacement as getChangedFilesWithReplacementUnmocke
import * as getCommitInfo from '../git/getCommitAndBranch';
import { getParentCommits as getParentCommitsUnmocked } from '../git/getParentCommits';
import * as git from '../git/git';
import { getHasRouter as getHasRouterUnmocked } from '../lib/getHasRouter';
import { setGitInfo } from './gitInfo';

vi.mock('../git/getCommitAndBranch');
vi.mock('../git/git');
vi.mock('../git/getParentCommits');
vi.mock('../git/getBaselineBuilds');
vi.mock('../git/getChangedFilesWithReplacement');
vi.mock('../lib/getHasRouter');

const getCommitAndBranch = vi.mocked(getCommitInfo.default);
const getChangedFilesWithReplacement = vi.mocked(getChangedFilesWithReplacementUnmocked);
const getSlug = vi.mocked(git.getSlug);
const getVersion = vi.mocked(git.getVersion);
const getUserEmail = vi.mocked(git.getUserEmail);
const getRepositoryCreationDate = vi.mocked(git.getRepositoryCreationDate);
const getNumberOfComitters = vi.mocked(git.getNumberOfComitters);
const getCommittedFileCount = vi.mocked(git.getCommittedFileCount);
const getUncommittedHash = vi.mocked(git.getUncommittedHash);
const getBaselineBuilds = vi.mocked(getBaselineBuildsUnmocked);
const getParentCommits = vi.mocked(getParentCommitsUnmocked);
const getHasRouter = vi.mocked(getHasRouterUnmocked);

const log = { info: vi.fn(), warn: vi.fn(), debug: vi.fn() };

Expand All @@ -47,6 +53,11 @@ beforeEach(() => {
getVersion.mockResolvedValue('Git v1.0.0');
getUserEmail.mockResolvedValue('[email protected]');
getSlug.mockResolvedValue('user/repo');
getRepositoryCreationDate.mockResolvedValue(new Date('2024-11-01'));
getNumberOfComitters.mockResolvedValue(17);
getCommittedFileCount.mockResolvedValue(100);
getHasRouter.mockReturnValue(true);

client.runQuery.mockReturnValue({ app: { isOnboarding: false } });
});

Expand Down Expand Up @@ -164,4 +175,15 @@ describe('setGitInfo', () => {
await setGitInfo(ctx, {} as any);
expect(ctx.git.branch).toBe('repo');
});

it('sets projectMetadata on context', async () => {
const ctx = { log, options: { isLocalBuild: true }, client } as any;
await setGitInfo(ctx, {} as any);
expect(ctx.projectMetadata).toMatchObject({
hasRouter: true,
creationDate: new Date('2024-11-01'),
numberOfCommitters: 17,
numberOfAppFiles: 100,
});
});
});
18 changes: 17 additions & 1 deletion node-src/tasks/gitInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import { getBaselineBuilds } from '../git/getBaselineBuilds';
import { getChangedFilesWithReplacement } from '../git/getChangedFilesWithReplacement';
import getCommitAndBranch from '../git/getCommitAndBranch';
import { getParentCommits } from '../git/getParentCommits';
import { getSlug, getUncommittedHash, getUserEmail, getVersion } from '../git/git';
import {
getCommittedFileCount,
getNumberOfComitters,
getRepositoryCreationDate,
getSlug,
getUncommittedHash,
getUserEmail,
getVersion,
} from '../git/git';
import { getHasRouter } from '../lib/getHasRouter';
import { exitCodes, setExitCode } from '../lib/setExitCode';
import { createTask, transitionTo } from '../lib/tasks';
import { isPackageMetadataFile, matchesFile } from '../lib/utils';
Expand Down Expand Up @@ -260,6 +269,13 @@ export const setGitInfo = async (ctx: Context, task: Task) => {
}
}

ctx.projectMetadata = {
hasRouter: getHasRouter(ctx.packageJson),
creationDate: await getRepositoryCreationDate(),
numberOfCommitters: await getNumberOfComitters(),
numberOfAppFiles: await getCommittedFileCount(['page', 'screen'], ['js', 'jsx', 'ts', 'tsx']),
};
tmeasday marked this conversation as resolved.
Show resolved Hide resolved

transitionTo(success, true)(ctx, task);
};

Expand Down
1 change: 1 addition & 0 deletions node-src/tasks/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const announceBuild = async (ctx: Context) => {
storybookAddons: ctx.storybook.addons,
storybookVersion: ctx.storybook.version,
storybookViewLayer: ctx.storybook.viewLayer,
projectMetadata: ctx.projectMetadata,
},
},
{ retries: 3 }
Expand Down
2 changes: 1 addition & 1 deletion node-src/tasks/storybookInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('storybookInfo', () => {
const storybook = { version: '1.0.0', viewLayer: 'react', addons: [] };
getStorybookInfo.mockResolvedValue(storybook);

const ctx = {} as any;
const ctx = { packageJson: {} } as any;
await setStorybookInfo(ctx);
expect(ctx.storybook).toEqual(storybook);
});
Expand Down
6 changes: 6 additions & 0 deletions node-src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ export interface Context {
};
mainConfigFilePath?: string;
};
projectMetadata: {
hasRouter?: boolean;
creationDate?: Date;
numberOfCommitters?: number;
numberOfAppFiles?: number;
};
storybookUrl?: string;
announcedBuild: {
id: string;
Expand Down
Loading