Skip to content

Commit

Permalink
Merge pull request #1112 from chromaui/tom/cap-2327-track-hasrouter-a…
Browse files Browse the repository at this point in the history
…nd-haspagecomponents-on-build-events

Detect `context.projectMetadata.hasRouter` and send to the index
  • Loading branch information
tmeasday authored Nov 7, 2024
2 parents cbf13a7 + 0c1a9b2 commit 7d7e8c3
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 2 deletions.
54 changes: 54 additions & 0 deletions node-src/git/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import {
findFilesFromRepositoryRoot,
getCommit,
getCommittedFileCount,
getNumberOfComitters,
getRepositoryCreationDate,
getSlug,
getStorybookCreationDate,
hasPreviousCommit,
mergeQueueBranchMatch,
NULL_BYTE,
Expand Down Expand Up @@ -145,3 +149,53 @@ 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('getStorybookCreationDate', () => {
it('passes the config dir to the git command', async () => {
await getStorybookCreationDate({ options: { storybookConfigDir: 'special-config-dir' } });
expect(command).toHaveBeenCalledWith(
expect.stringMatching(/special-config-dir/),
expect.anything()
);
});

it('defaults the config dir to the git command', async () => {
await getStorybookCreationDate({ options: {} });
expect(command).toHaveBeenCalledWith(expect.stringMatching(/.storybook/), expect.anything());
});

it('parses the date successfully', async () => {
command.mockImplementation(() => Promise.resolve({ all: `2017-05-17 10:00:35 -0700` }) as any);
expect(
await getStorybookCreationDate({ options: { storybookConfigDir: '.storybook' } })
).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);
});
});
65 changes: 65 additions & 0 deletions node-src/git/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,68 @@ 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 date the storybook was added to the repository
*
* @param ctx Context The context set when executing the CLI.
* @param ctx.options Object standard context options
* @param ctx.options.storybookConfigDir Configured Storybook config dir, if set
*
* @returns Date The date the storybook was added
*/
export async function getStorybookCreationDate(ctx: {
options: {
storybookConfigDir?: Context['options']['storybookConfigDir'];
};
}) {
const configDirectory = ctx.options.storybookConfigDir ?? '.storybook';
const dateString = await execGitCommand(
`git log --follow --reverse --format=%cd --date=iso -- ${configDirectory} | 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;
}
6 changes: 6 additions & 0 deletions node-src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ vi.mock('./git/git', () => ({
getUncommittedHash: () => Promise.resolve('abc123'),
getUserEmail: () => Promise.resolve('test@test.com'),
mergeQueueBranchMatch: () => Promise.resolve(undefined),
getRepositoryCreationDate: () => Promise.resolve(new Date('2024-11-01')),
getStorybookCreationDate: () => Promise.resolve(new Date('2025-11-01')),
getNumberOfComitters: () => Promise.resolve(17),
getCommittedFileCount: () => Promise.resolve(100),
}));

vi.mock('./git/getParentCommits', () => ({
Expand All @@ -325,6 +329,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)
);
}
25 changes: 25 additions & 0 deletions node-src/tasks/gitInfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@ 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 getStorybookCreationDate = vi.mocked(git.getStorybookCreationDate);
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 +54,12 @@ beforeEach(() => {
getVersion.mockResolvedValue('Git v1.0.0');
getUserEmail.mockResolvedValue('user@email.com');
getSlug.mockResolvedValue('user/repo');
getRepositoryCreationDate.mockResolvedValue(new Date('2024-11-01'));
getStorybookCreationDate.mockResolvedValue(new Date('2025-11-01'));
getNumberOfComitters.mockResolvedValue(17);
getCommittedFileCount.mockResolvedValue(100);
getHasRouter.mockReturnValue(true);

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

Expand Down Expand Up @@ -164,4 +177,16 @@ 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'),
storybookCreationDate: new Date('2025-11-01'),
numberOfCommitters: 17,
numberOfAppFiles: 100,
});
});
});
20 changes: 19 additions & 1 deletion node-src/tasks/gitInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ 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,
getStorybookCreationDate,
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 @@ -87,6 +97,14 @@ export const setGitInfo = async (ctx: Context, task: Task) => {
...commitAndBranchInfo,
};

ctx.projectMetadata = {
hasRouter: getHasRouter(ctx.packageJson),
creationDate: await getRepositoryCreationDate(),
storybookCreationDate: await getStorybookCreationDate(ctx),
numberOfCommitters: await getNumberOfComitters(),
numberOfAppFiles: await getCommittedFileCount(['page', 'screen'], ['js', 'jsx', 'ts', 'tsx']),
};

if (isLocalBuild && !ctx.git.gitUserEmail) {
throw new Error(gitUserEmailNotFound());
}
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
7 changes: 7 additions & 0 deletions node-src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ export interface Context {
};
mainConfigFilePath?: string;
};
projectMetadata: {
hasRouter?: boolean;
creationDate?: Date;
storybookCreationDate?: Date;
numberOfCommitters?: number;
numberOfAppFiles?: number;
};
storybookUrl?: string;
announcedBuild: {
id: string;
Expand Down

0 comments on commit 7d7e8c3

Please sign in to comment.