From 9508320ea790fa728d85e1d393f8f942925c57b8 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 30 Oct 2024 14:44:26 +1100 Subject: [PATCH 1/8] Add `ctx.projectMetadata.hasRouter` This reverts commit 961bf6fd5e1d6045296ac63bd7c46db57004bfcc. --- node-src/lib/getHasRouter.ts | 33 ++++++++++++++++++++++++++++ node-src/tasks/storybookInfo.test.ts | 32 ++++++++++++++++++++++++++- node-src/tasks/storybookInfo.ts | 6 +++++ node-src/types.ts | 3 +++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 node-src/lib/getHasRouter.ts diff --git a/node-src/lib/getHasRouter.ts b/node-src/lib/getHasRouter.ts new file mode 100644 index 000000000..41d0bc0a6 --- /dev/null +++ b/node-src/lib/getHasRouter.ts @@ -0,0 +1,33 @@ +import { Context } from '../../dist/node'; + +const routerPackages = [ + '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', +]; + +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 ?? {}).find((depName) => + routerPackages.includes(depName) + ); +} diff --git a/node-src/tasks/storybookInfo.test.ts b/node-src/tasks/storybookInfo.test.ts index b1bc59a14..b925bbfb7 100644 --- a/node-src/tasks/storybookInfo.test.ts +++ b/node-src/tasks/storybookInfo.test.ts @@ -12,8 +12,38 @@ 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); }); + + it('sets hasRouter=true if there is a routing package in package.json', async () => { + const ctx = { + packageJson: { + dependencies: { + react: '^18', + 'react-dom': '^18', + 'react-router': '^6', + }, + }, + } as any; + await setStorybookInfo(ctx); + expect(ctx.projectMetadata.hasRouter).toEqual(true); + }); + + it('sets hasRouter=false if there is a routing package in package.json dependenices', async () => { + const ctx = { + packageJson: { + dependencies: { + react: '^18', + 'react-dom': '^18', + }, + devDependencies: { + 'react-router': '^6', + }, + }, + } as any; + await setStorybookInfo(ctx); + expect(ctx.projectMetadata.hasRouter).toEqual(false); + }); }); diff --git a/node-src/tasks/storybookInfo.ts b/node-src/tasks/storybookInfo.ts index 74552c994..ec59a4d3b 100644 --- a/node-src/tasks/storybookInfo.ts +++ b/node-src/tasks/storybookInfo.ts @@ -4,6 +4,7 @@ import getStorybookInfo from '../lib/getStorybookInfo'; import { createTask, transitionTo } from '../lib/tasks'; import { Context } from '../types'; import { initial, pending, success } from '../ui/tasks/storybookInfo'; +import { getHasRouter } from '../lib/getHasRouter'; export const setStorybookInfo = async (ctx: Context) => { ctx.storybook = (await getStorybookInfo(ctx)) as Context['storybook']; @@ -17,6 +18,11 @@ export const setStorybookInfo = async (ctx: Context) => { } Sentry.setContext('storybook', ctx.storybook); } + + // Also get some project-level data for analytics + ctx.projectMetadata = { + hasRouter: getHasRouter(ctx.packageJson), + }; }; /** diff --git a/node-src/types.ts b/node-src/types.ts index 8e34eab82..b668335c2 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -250,6 +250,9 @@ export interface Context { }; mainConfigFilePath?: string; }; + projectMetadata: { + hasRouter: boolean; + }; storybookUrl?: string; announcedBuild: { id: string; From 7ea346708bf70babf4a37e529cbe2d228b12e510 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 30 Oct 2024 14:39:04 +1100 Subject: [PATCH 2/8] Send `projectMetadata` to `AnnounceBuild` --- node-src/tasks/initialize.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/node-src/tasks/initialize.ts b/node-src/tasks/initialize.ts index 3bf69c80f..116ce4b84 100644 --- a/node-src/tasks/initialize.ts +++ b/node-src/tasks/initialize.ts @@ -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 } From b17a49c31ab8dee54ed14953e8e4530959018f72 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 30 Oct 2024 14:47:36 +1100 Subject: [PATCH 3/8] Fix type import --- node-src/lib/getHasRouter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node-src/lib/getHasRouter.ts b/node-src/lib/getHasRouter.ts index 41d0bc0a6..7df1388ec 100644 --- a/node-src/lib/getHasRouter.ts +++ b/node-src/lib/getHasRouter.ts @@ -1,4 +1,4 @@ -import { Context } from '../../dist/node'; +import type { Context } from '../types'; const routerPackages = [ 'react-router', From 8b732e6e6ea41760cc101fc5f541880fd7c2f7b0 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 30 Oct 2024 15:21:09 +1100 Subject: [PATCH 4/8] Linting fixes --- node-src/lib/getHasRouter.ts | 10 +++++++--- node-src/tasks/storybookInfo.ts | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/node-src/lib/getHasRouter.ts b/node-src/lib/getHasRouter.ts index 7df1388ec..41249fcf9 100644 --- a/node-src/lib/getHasRouter.ts +++ b/node-src/lib/getHasRouter.ts @@ -1,6 +1,6 @@ import type { Context } from '../types'; -const routerPackages = [ +const routerPackages = new Set([ 'react-router', 'react-router-dom', 'remix', @@ -23,11 +23,15 @@ const routerPackages = [ '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 ?? {}).find((depName) => - routerPackages.includes(depName) + routerPackages.has(depName) ); } diff --git a/node-src/tasks/storybookInfo.ts b/node-src/tasks/storybookInfo.ts index ec59a4d3b..795b5b06c 100644 --- a/node-src/tasks/storybookInfo.ts +++ b/node-src/tasks/storybookInfo.ts @@ -1,10 +1,10 @@ import * as Sentry from '@sentry/node'; +import { getHasRouter } from '../lib/getHasRouter'; import getStorybookInfo from '../lib/getStorybookInfo'; import { createTask, transitionTo } from '../lib/tasks'; import { Context } from '../types'; import { initial, pending, success } from '../ui/tasks/storybookInfo'; -import { getHasRouter } from '../lib/getHasRouter'; export const setStorybookInfo = async (ctx: Context) => { ctx.storybook = (await getStorybookInfo(ctx)) as Context['storybook']; From 390a78d252cc8751d77489792785dc187192a11c Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 1 Nov 2024 17:09:15 +1100 Subject: [PATCH 5/8] Add three new fields to project metadata --- node-src/git/git.test.ts | 31 ++++++++++++++++++++ node-src/git/git.ts | 44 ++++++++++++++++++++++++++++ node-src/lib/getHasRouter.test.ts | 29 ++++++++++++++++++ node-src/tasks/gitInfo.test.ts | 22 ++++++++++++++ node-src/tasks/gitInfo.ts | 18 +++++++++++- node-src/tasks/storybookInfo.test.ts | 30 ------------------- node-src/tasks/storybookInfo.ts | 6 ---- node-src/types.ts | 5 +++- 8 files changed, 147 insertions(+), 38 deletions(-) create mode 100644 node-src/lib/getHasRouter.test.ts diff --git a/node-src/git/git.test.ts b/node-src/git/git.test.ts index 31ea11d8c..fce38a766 100644 --- a/node-src/git/git.test.ts +++ b/node-src/git/git.test.ts @@ -4,6 +4,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { findFilesFromRepositoryRoot, getCommit, + getCommittedFileCount, + getNumberOfComitters, + getRepositoryCreationDate, getSlug, hasPreviousCommit, mergeQueueBranchMatch, @@ -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); + }); +}); diff --git a/node-src/git/git.ts b/node-src/git/git.ts index 5621f1c3f..bd7dfb0ee 100644 --- a/node-src/git/git.ts +++ b/node-src/git/git.ts @@ -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; +} diff --git a/node-src/lib/getHasRouter.test.ts b/node-src/lib/getHasRouter.test.ts new file mode 100644 index 000000000..baea9caa7 --- /dev/null +++ b/node-src/lib/getHasRouter.test.ts @@ -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); +}); diff --git a/node-src/tasks/gitInfo.test.ts b/node-src/tasks/gitInfo.test.ts index ba122dbe7..8698ffb1d 100644 --- a/node-src/tasks/gitInfo.test.ts +++ b/node-src/tasks/gitInfo.test.ts @@ -5,6 +5,7 @@ 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'); @@ -12,15 +13,20 @@ 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() }; @@ -47,6 +53,11 @@ beforeEach(() => { getVersion.mockResolvedValue('Git v1.0.0'); getUserEmail.mockResolvedValue('user@email.com'); 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 } }); }); @@ -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, + }); + }); }); diff --git a/node-src/tasks/gitInfo.ts b/node-src/tasks/gitInfo.ts index 1a5e411b8..5b32d3cc0 100644 --- a/node-src/tasks/gitInfo.ts +++ b/node-src/tasks/gitInfo.ts @@ -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'; @@ -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']), + }; + transitionTo(success, true)(ctx, task); }; diff --git a/node-src/tasks/storybookInfo.test.ts b/node-src/tasks/storybookInfo.test.ts index b925bbfb7..677a2d79c 100644 --- a/node-src/tasks/storybookInfo.test.ts +++ b/node-src/tasks/storybookInfo.test.ts @@ -16,34 +16,4 @@ describe('storybookInfo', () => { await setStorybookInfo(ctx); expect(ctx.storybook).toEqual(storybook); }); - - it('sets hasRouter=true if there is a routing package in package.json', async () => { - const ctx = { - packageJson: { - dependencies: { - react: '^18', - 'react-dom': '^18', - 'react-router': '^6', - }, - }, - } as any; - await setStorybookInfo(ctx); - expect(ctx.projectMetadata.hasRouter).toEqual(true); - }); - - it('sets hasRouter=false if there is a routing package in package.json dependenices', async () => { - const ctx = { - packageJson: { - dependencies: { - react: '^18', - 'react-dom': '^18', - }, - devDependencies: { - 'react-router': '^6', - }, - }, - } as any; - await setStorybookInfo(ctx); - expect(ctx.projectMetadata.hasRouter).toEqual(false); - }); }); diff --git a/node-src/tasks/storybookInfo.ts b/node-src/tasks/storybookInfo.ts index 795b5b06c..74552c994 100644 --- a/node-src/tasks/storybookInfo.ts +++ b/node-src/tasks/storybookInfo.ts @@ -1,6 +1,5 @@ import * as Sentry from '@sentry/node'; -import { getHasRouter } from '../lib/getHasRouter'; import getStorybookInfo from '../lib/getStorybookInfo'; import { createTask, transitionTo } from '../lib/tasks'; import { Context } from '../types'; @@ -18,11 +17,6 @@ export const setStorybookInfo = async (ctx: Context) => { } Sentry.setContext('storybook', ctx.storybook); } - - // Also get some project-level data for analytics - ctx.projectMetadata = { - hasRouter: getHasRouter(ctx.packageJson), - }; }; /** diff --git a/node-src/types.ts b/node-src/types.ts index b668335c2..20f0e8797 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -251,7 +251,10 @@ export interface Context { mainConfigFilePath?: string; }; projectMetadata: { - hasRouter: boolean; + hasRouter?: boolean; + creationDate?: Date; + numberOfCommitters?: number; + numberOfAppFiles?: number; }; storybookUrl?: string; announcedBuild: { From 6c79ed15c2a254678e1ce3055e745560218318b7 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 1 Nov 2024 17:10:21 +1100 Subject: [PATCH 6/8] Fix linting --- node-src/lib/getHasRouter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/node-src/lib/getHasRouter.ts b/node-src/lib/getHasRouter.ts index 41249fcf9..f03203a69 100644 --- a/node-src/lib/getHasRouter.ts +++ b/node-src/lib/getHasRouter.ts @@ -27,11 +27,12 @@ const routerPackages = new Set([ /** * @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 ?? {}).find((depName) => + return Object.keys(packageJson?.dependencies ?? {}).some((depName) => routerPackages.has(depName) ); } From b338a3fc6a5edeb57d4b550a08dbefc8b1b01226 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 1 Nov 2024 17:31:56 +1100 Subject: [PATCH 7/8] Fix index.test.ts --- node-src/index.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/node-src/index.test.ts b/node-src/index.test.ts index 0ba0b7c3c..3f9f96e3a 100644 --- a/node-src/index.test.ts +++ b/node-src/index.test.ts @@ -310,6 +310,9 @@ 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')), + getNumberOfComitters: () => Promise.resolve(17), + getCommittedFileCount: () => Promise.resolve(100), })); vi.mock('./git/getParentCommits', () => ({ @@ -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']))), From db9eb1497335d17c1b55f68968da9645f4567e2a Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 6 Nov 2024 11:36:30 +1100 Subject: [PATCH 8/8] Add `getStorybookCreationDate` to project metadata --- node-src/git/git.test.ts | 23 +++++++++++++++++++++++ node-src/git/git.ts | 21 +++++++++++++++++++++ node-src/index.test.ts | 1 + node-src/tasks/gitInfo.test.ts | 3 +++ node-src/tasks/gitInfo.ts | 16 +++++++++------- node-src/types.ts | 1 + 6 files changed, 58 insertions(+), 7 deletions(-) diff --git a/node-src/git/git.test.ts b/node-src/git/git.test.ts index fce38a766..746de1edf 100644 --- a/node-src/git/git.test.ts +++ b/node-src/git/git.test.ts @@ -8,6 +8,7 @@ import { getNumberOfComitters, getRepositoryCreationDate, getSlug, + getStorybookCreationDate, hasPreviousCommit, mergeQueueBranchMatch, NULL_BYTE, @@ -156,6 +157,28 @@ describe('getRepositoryCreationDate', () => { }); }); +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); diff --git a/node-src/git/git.ts b/node-src/git/git.ts index bd7dfb0ee..36e716f63 100644 --- a/node-src/git/git.ts +++ b/node-src/git/git.ts @@ -433,6 +433,27 @@ export async function getRepositoryCreationDate() { 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 * diff --git a/node-src/index.test.ts b/node-src/index.test.ts index 3f9f96e3a..258ba2f07 100644 --- a/node-src/index.test.ts +++ b/node-src/index.test.ts @@ -311,6 +311,7 @@ vi.mock('./git/git', () => ({ 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), })); diff --git a/node-src/tasks/gitInfo.test.ts b/node-src/tasks/gitInfo.test.ts index 8698ffb1d..64fb705ee 100644 --- a/node-src/tasks/gitInfo.test.ts +++ b/node-src/tasks/gitInfo.test.ts @@ -21,6 +21,7 @@ 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); @@ -54,6 +55,7 @@ beforeEach(() => { 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); @@ -182,6 +184,7 @@ describe('setGitInfo', () => { expect(ctx.projectMetadata).toMatchObject({ hasRouter: true, creationDate: new Date('2024-11-01'), + storybookCreationDate: new Date('2025-11-01'), numberOfCommitters: 17, numberOfAppFiles: 100, }); diff --git a/node-src/tasks/gitInfo.ts b/node-src/tasks/gitInfo.ts index 5b32d3cc0..2b5a0e8c2 100644 --- a/node-src/tasks/gitInfo.ts +++ b/node-src/tasks/gitInfo.ts @@ -9,6 +9,7 @@ import { getNumberOfComitters, getRepositoryCreationDate, getSlug, + getStorybookCreationDate, getUncommittedHash, getUserEmail, getVersion, @@ -96,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()); } @@ -269,13 +278,6 @@ 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']), - }; - transitionTo(success, true)(ctx, task); }; diff --git a/node-src/types.ts b/node-src/types.ts index 20f0e8797..043ddf979 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -253,6 +253,7 @@ export interface Context { projectMetadata: { hasRouter?: boolean; creationDate?: Date; + storybookCreationDate?: Date; numberOfCommitters?: number; numberOfAppFiles?: number; };