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

Merged
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 @@ -311,6 +311,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 @@ -322,6 +326,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
Loading