├── .codacy.yaml ├── .env.example ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── check-labels.yml │ ├── chromatic-main-and-prs.yml │ ├── chromatic-prod.yml │ ├── lint-and-test.yml │ ├── package-size-main.yml │ ├── package-size.yml │ ├── pull-request-workflow.yml │ ├── release.yml │ ├── smoke-test-action-next.yml │ ├── smoke-test-action.yml │ ├── smoke-test-node-api.yml │ ├── smoke-test-node18.yml │ ├── smoke-test-node20.yml │ ├── smoke-test-npx.yml │ ├── smoke-test-windows.yml │ ├── smoke-test-yarn-berry.yml │ ├── smoke-test-yarn-canary.yml │ ├── smoke-test-yarn-classic.yml │ └── smoke-test-yarn.yml ├── .gitignore ├── .storybook ├── main.ts ├── preview-head.html └── preview.tsx ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-4.2.2.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── action-src ├── LICENSE ├── README.md ├── main.ts └── register.js ├── action.yml ├── bin-src ├── __mocks__ │ ├── malformedProjectJson │ │ └── project.json │ ├── normalProjectJson │ │ └── project.json │ ├── previewStatsJson │ │ ├── preview-stats.json │ │ └── preview-stats.trimmed.json │ ├── sb6ProjectJson │ │ └── project.json │ ├── sb6ProjectJsonMissingBuilder │ │ └── project.json │ └── unsupportedAddons │ │ └── project.json ├── init.test.ts ├── init.ts ├── main.ts ├── register.js ├── trace.test.ts ├── trace.ts ├── trimStatsFile.test.ts └── trimStatsFile.ts ├── docs └── DEVELOPMENT.md ├── eslint.config.mjs ├── isChromatic.d.ts ├── isChromatic.js ├── isChromatic.mjs ├── isChromatic.test.ts ├── node-src ├── __mocks__ │ ├── dependencyChanges │ │ ├── berry-chalk │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── berry │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── plain │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── react-async-10 │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ └── react-async-9 │ │ │ ├── package.json │ │ │ └── yarn.lock │ ├── dependencyParsing │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ └── yarn.lock │ ├── iframe.html │ ├── index.html │ ├── invalidPackageJson │ │ └── package.json │ └── storybookBaseDir │ │ ├── subdir │ │ ├── test.js │ │ ├── test.jsx │ │ └── test.tsx │ │ └── test.ts ├── errorMonitoring.test.ts ├── errorMonitoring.ts ├── git │ ├── execGit.test.ts │ ├── execGit.ts │ ├── findAncestorBuildWithCommit.test.ts │ ├── findAncestorBuildWithCommit.ts │ ├── generateGitRepository.ts │ ├── getBaselineBuilds.ts │ ├── getBranchFromMergeQueuePullRequestNumber.ts │ ├── getChangedFilesWithReplacement.test.ts │ ├── getChangedFilesWithReplacement.ts │ ├── getCommitAndBranch.test.ts │ ├── getCommitAndBranch.ts │ ├── getParentCommits.test.ts │ ├── getParentCommits.ts │ ├── git.test.ts │ ├── git.ts │ └── mocks │ │ ├── doubleLoop.ts │ │ ├── longLine.ts │ │ ├── longLoop.ts │ │ ├── mockIndex.ts │ │ ├── simpleLoop.ts │ │ ├── threeParents.ts │ │ └── twoRoots.ts ├── index.test.ts ├── index.ts ├── io │ ├── getDNSResolveAgent.ts │ ├── getProxyAgent.ts │ ├── graphqlClient.ts │ └── httpClient.ts ├── lib │ ├── builders.ts │ ├── checkForUpdates.ts │ ├── checkPackageJson.ts │ ├── checkStorybookBaseDirectory.test.ts │ ├── checkStorybookBaseDirectory.ts │ ├── compress.test.ts │ ├── compress.ts │ ├── e2e.ts │ ├── e2eUtils.ts │ ├── emailHash.ts │ ├── fileReaderBlob.ts │ ├── getConfiguration.test.ts │ ├── getConfiguration.ts │ ├── getEnvironment.ts │ ├── getFileHashes.test.ts │ ├── getFileHashes.ts │ ├── getHasRouter.test.ts │ ├── getHasRouter.ts │ ├── getOptions.test.ts │ ├── getOptions.ts │ ├── getPackageManager.ts │ ├── getPrebuiltStorybookMetadata.test.ts │ ├── getPrebuiltStorybookMetadata.ts │ ├── getStorybookBaseDirectory.test.ts │ ├── getStorybookBaseDirectory.ts │ ├── getStorybookInfo.test.ts │ ├── getStorybookInfo.ts │ ├── getStorybookMetadata.ts │ ├── installDependencies.ts │ ├── localBuildsSpecifier.ts │ ├── log.test.ts │ ├── log.ts │ ├── logSerializers.test.ts │ ├── logSerializers.ts │ ├── loggingRenderer.ts │ ├── nonTTYRenderer.ts │ ├── parseArguments.ts │ ├── posix.ts │ ├── promises.ts │ ├── setExitCode.ts │ ├── spawn.ts │ ├── tasks.ts │ ├── testLogger.ts │ ├── turbosnap │ │ ├── compareBaseline.test.ts │ │ ├── compareBaseline.ts │ │ ├── findChangedDependencies.test.ts │ │ ├── findChangedDependencies.ts │ │ ├── findChangedPackageFiles.test.ts │ │ ├── findChangedPackageFiles.ts │ │ ├── getDependencies.test.ts │ │ ├── getDependencies.ts │ │ ├── getDependentStoryFiles.test.ts │ │ ├── getDependentStoryFiles.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── upload.ts │ ├── uploadFiles.ts │ ├── uploadMetadataFiles.ts │ ├── uploadZip.ts │ ├── utils.test.ts │ ├── utils.ts │ ├── viewLayers.ts │ ├── waitForSentinel.ts │ ├── writeChromaticDiagnostics.test.ts │ └── writeChromaticDiagnostics.ts ├── tasks │ ├── auth.test.ts │ ├── auth.ts │ ├── build.test.ts │ ├── build.ts │ ├── gitInfo.test.ts │ ├── gitInfo.ts │ ├── index.ts │ ├── initialize.test.ts │ ├── initialize.ts │ ├── prepare.test.ts │ ├── prepare.ts │ ├── prepareWorkspace.test.ts │ ├── prepareWorkspace.ts │ ├── readStatsFile.ts │ ├── report.test.ts │ ├── report.ts │ ├── restoreWorkspace.ts │ ├── snapshot.test.ts │ ├── snapshot.ts │ ├── storybookInfo.test.ts │ ├── storybookInfo.ts │ ├── upload.test.ts │ ├── upload.ts │ ├── verify.test.ts │ └── verify.ts ├── types.ts ├── typings.d.ts └── ui │ ├── components │ ├── activity.ts │ ├── icons.stories.ts │ ├── icons.ts │ ├── link.stories.ts │ ├── link.ts │ ├── task.stories.ts │ └── task.ts │ ├── html │ ├── metadata.html.stories.ts │ └── metadata.html.ts │ ├── messages │ ├── errors │ │ ├── brokenStorybook.stories.ts │ │ ├── brokenStorybook.ts │ │ ├── buildCanceled.stories.ts │ │ ├── buildCanceled.ts │ │ ├── buildFailed.stories.ts │ │ ├── buildFailed.ts │ │ ├── buildHasChanges.stories.ts │ │ ├── buildHasChanges.ts │ │ ├── buildHasErrors.stories.ts │ │ ├── buildHasErrors.ts │ │ ├── dependentOption.stories.ts │ │ ├── dependentOption.ts │ │ ├── duplicatePatchBuild.stories.ts │ │ ├── duplicatePatchBuild.ts │ │ ├── e2eBuildFailed.stories.ts │ │ ├── e2eBuildFailed.ts │ │ ├── fatalError.stories.ts │ │ ├── fatalError.ts │ │ ├── fetchError.stories.ts │ │ ├── fetchError.ts │ │ ├── forksUnsupported.stories.ts │ │ ├── forksUnsupported.ts │ │ ├── gitNoCommits.stories.ts │ │ ├── gitNoCommits.ts │ │ ├── gitNotInitialized.stories.ts │ │ ├── gitNotInitialized.ts │ │ ├── gitNotInstalled.stories.ts │ │ ├── gitNotInstalled.ts │ │ ├── gitOneCommit.stories.ts │ │ ├── gitOneCommit.ts │ │ ├── gitUserEmailNotFound.stories.ts │ │ ├── gitUserEmailNotFound.ts │ │ ├── graphqlError.stories.ts │ │ ├── graphqlError.ts │ │ ├── incompatibleOptions.stories.ts │ │ ├── incompatibleOptions.ts │ │ ├── invalidConfigurationFile.stories.ts │ │ ├── invalidConfigurationFile.ts │ │ ├── invalidExitOnceUploaded.stories.ts │ │ ├── invalidExitOnceUploaded.ts │ │ ├── invalidOnlyChanged.stories.ts │ │ ├── invalidOnlyChanged.ts │ │ ├── invalidOnlyStoryNames.stories.ts │ │ ├── invalidOnlyStoryNames.ts │ │ ├── invalidOwnerName.stories.ts │ │ ├── invalidOwnerName.ts │ │ ├── invalidPackageJson.stories.ts │ │ ├── invalidPackageJson.ts │ │ ├── invalidPatchBuild.stories.ts │ │ ├── invalidPatchBuild.ts │ │ ├── invalidProjectId.stories.ts │ │ ├── invalidProjectId.ts │ │ ├── invalidProjectToken.stories.ts │ │ ├── invalidProjectToken.ts │ │ ├── invalidReportPath.stories.ts │ │ ├── invalidReportPath.ts │ │ ├── invalidRepositorySlug.stories.ts │ │ ├── invalidRepositorySlug.ts │ │ ├── invalidSingularOptions.stories.ts │ │ ├── invalidSingularOptions.ts │ │ ├── invalidStorybookBaseDirectory.stories.ts │ │ ├── invalidStorybookBaseDirectory.ts │ │ ├── maxFileCountExceeded.stories.ts │ │ ├── maxFileCountExceeded.ts │ │ ├── maxFileSizeExceeded.stories.ts │ │ ├── maxFileSizeExceeded.ts │ │ ├── mergeBaseNotFound.stories.ts │ │ ├── mergeBaseNotFound.ts │ │ ├── missingBuildScriptName.stories.ts │ │ ├── missingBuildScriptName.ts │ │ ├── missingConfigurationFile.stories.ts │ │ ├── missingConfigurationFile.ts │ │ ├── missingDependency.stories.ts │ │ ├── missingDependency.ts │ │ ├── missingGitHubInfo.stories.ts │ │ ├── missingGitHubInfo.ts │ │ ├── missingProjectToken.stories.ts │ │ ├── missingProjectToken.ts │ │ ├── missingStatsFile.stories.ts │ │ ├── missingStatsFile.ts │ │ ├── missingStories.stories.ts │ │ ├── missingStories.ts │ │ ├── missingTravisInfo.stories.ts │ │ ├── missingTravisInfo.ts │ │ ├── noCSFGlobs.stories.ts │ │ ├── noCSFGlobs.ts │ │ ├── noPackageJson.stories.ts │ │ ├── noPackageJson.ts │ │ ├── noViewLayerPackage.stories.ts │ │ ├── noViewLayerPackage.ts │ │ ├── runtimeError.stories.ts │ │ ├── runtimeError.ts │ │ ├── sentinelFileErrors.stories.ts │ │ ├── sentinelFileErrors.ts │ │ ├── taskError.stories.ts │ │ ├── taskError.ts │ │ ├── unparseableConfigurationFile.stories.ts │ │ ├── unparseableConfigurationFile.ts │ │ ├── uploadFailed.stories.ts │ │ ├── uploadFailed.ts │ │ ├── workspaceNotClean.stories.ts │ │ ├── workspaceNotClean.ts │ │ ├── workspaceNotUpToDate.stories.ts │ │ └── workspaceNotUpToDate.ts │ ├── info │ │ ├── addedScript.stories.ts │ │ ├── addedScript.ts │ │ ├── buildPassed.stories.ts │ │ ├── buildPassed.ts │ │ ├── buildPassedE2E.stories.ts │ │ ├── customGitHubAction.stories.ts │ │ ├── customGitHubAction.ts │ │ ├── forceRebuildHint.stories.ts │ │ ├── forceRebuildHint.ts │ │ ├── intro.stories.ts │ │ ├── intro.ts │ │ ├── listingStories.stories.ts │ │ ├── listingStories.ts │ │ ├── notAddedScript.stories.ts │ │ ├── notAddedScript.ts │ │ ├── replacedBuild.stories.ts │ │ ├── replacedBuild.ts │ │ ├── speedUpCI.stories.ts │ │ ├── speedUpCI.ts │ │ ├── storybookPublished.stories.ts │ │ ├── storybookPublished.ts │ │ ├── storybookPublishedE2E.stories.ts │ │ ├── tracedAffectedFiles.stories.ts │ │ ├── tracedAffectedFiles.ts │ │ ├── turboSnapEnabled.stories.ts │ │ ├── turboSnapEnabled.ts │ │ ├── uploadingMetadata.stories.ts │ │ ├── uploadingMetadata.ts │ │ ├── wroteReport.stories.ts │ │ └── wroteReport.ts │ └── warnings │ │ ├── bailFile.stories.ts │ │ ├── bailFile.ts │ │ ├── buildLimited.stories.ts │ │ ├── buildLimited.ts │ │ ├── deprecatedOption.stories.ts │ │ ├── deprecatedOption.ts │ │ ├── deviatingOutputDirectory.stories.ts │ │ ├── deviatingOutputDirectory.ts │ │ ├── externalsChanged.stories.ts │ │ ├── externalsChanged.ts │ │ ├── invalidChangedFiles.stories.ts │ │ ├── invalidChangedFiles.ts │ │ ├── isRebuild.stories.ts │ │ ├── isRebuild.ts │ │ ├── noAncestorBuild.stories.ts │ │ ├── noAncestorBuild.ts │ │ ├── noCommitDetails.stories.ts │ │ ├── noCommitDetails.ts │ │ ├── outdatedPackage.stories.ts │ │ ├── outdatedPackage.ts │ │ ├── paymentRequired.stories.ts │ │ ├── paymentRequired.ts │ │ ├── scriptNotFound.stories.ts │ │ ├── scriptNotFound.ts │ │ ├── snapshotQuotaReached.stories.ts │ │ ├── snapshotQuotaReached.ts │ │ ├── travisInternalBuild.stories.ts │ │ ├── travisInternalBuild.ts │ │ ├── turboSnapUnavailable.stories.ts │ │ ├── turboSnapUnavailable.ts │ │ ├── undefinedBranchOwner.stories.ts │ │ └── undefinedBranchOwner.ts │ ├── tasks │ ├── auth.stories.ts │ ├── auth.ts │ ├── build.stories.ts │ ├── build.ts │ ├── buildE2E.stories.ts │ ├── gitInfo.stories.ts │ ├── gitInfo.ts │ ├── initialize.stories.ts │ ├── initialize.ts │ ├── prepare.stories.ts │ ├── prepare.ts │ ├── prepareE2E.stories.ts │ ├── prepareWorkspace.stories.ts │ ├── prepareWorkspace.ts │ ├── report.stories.ts │ ├── report.ts │ ├── restoreWorkspace.stories.ts │ ├── restoreWorkspace.ts │ ├── snapshot.stories.ts │ ├── snapshot.ts │ ├── snapshotE2E.stories.ts │ ├── storybookInfo.stories.ts │ ├── storybookInfo.ts │ ├── storybookInfoE2E.stories.ts │ ├── upload.stories.ts │ ├── upload.ts │ ├── uploadE2E.stories.ts │ ├── utils.ts │ ├── verify.stories.ts │ ├── verify.ts │ └── verifyE2E.stories.ts │ └── workflows │ ├── uploadBuild.stories.ts │ └── uploadBuildE2E.stories.ts ├── package.json ├── prettier.config.js ├── scripts ├── publishAction.mjs ├── release.mjs ├── releaseNext.mjs ├── rename.js └── run-via-node.mjs ├── static └── css │ └── global.css ├── storybook-addon.d.ts ├── storybook-addon.js ├── subdir ├── .storybook │ └── main.js ├── One.js ├── One.stories.js ├── README.md ├── Two.js ├── Two.stories.js ├── package.json └── yarn.lock ├── test-stories ├── a.js ├── aWrap.js ├── b.js ├── star.js ├── tests.stories.js └── timing.stories-disabled.js ├── tsconfig.json ├── tsup.config.ts ├── vitest.config.mts ├── vitest.no-threads.config.ts └── yarn.lock /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - 'CHANGELOG.md' 4 | - 'vitest.no-threads.config.ts' 5 | - '**/__mocks__/**' 6 | engines: 7 | duplication: 8 | exclude_paths: 9 | - '**/*.test.js' 10 | - '**/*.test.ts' 11 | - '**/*.stories.js' 12 | - '**/*.stories.ts' 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # This is just an example file. 2 | # Rename this to `.env` and replace the app_code 3 | 4 | # If you need an app_code for local testing, we'll give you one for free: 5 | # opensource@hichroma.com 6 | 7 | CHROMATIC_PROJECT_TOKEN= 8 | 9 | # CHROMATIC_INDEX_URL=https://www.chromatic.com 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐞 3 | about: If you're experiencing a problem with Chromatic, please contact Chromatic support. Found a bug in our CLI? Please open an issue. 4 | labels: needs triage, bug, Empathy, CLI 5 | --- 6 | **Chromatic Issues** 7 | If you're experiencing an issue with Chromatic itself, such as a build showing a component error that 8 | you don't see when running Storybook locally, you should [contact Chromatic support](https://www.chromatic.com/docs/support/). 9 | If you're seeing an issue with the Chromatic command-line interface (the CLI tool itself), then this 10 | is the appropriate forum to file a ticket. 11 | 12 | **Bug report** 13 | 14 | A clear and concise description of what the bug is. Include screenshots if you can, but be aware this is a public medium. If you need to share private information, please contact the Chromatic support team [here](mailto:support@chromatic.com). 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentation 📚 4 | url: https://www.chromatic.com/docs/ 5 | about: Read the official docs for answers to common questions. 6 | - name: Get support 💬 7 | url: https://www.chromatic.com/docs/support 8 | about: Contact us in the in-app chat, or reach out via email. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 💡 3 | about: Suggest an idea for Chromatic. 4 | labels: needs triage, feature request, CLI 5 | --- 6 | 7 | **Feature request** 8 | 9 | Provide clear and concise description of the problem you'd like to solve or the use case you have. 10 | 11 | **Proposed solution** 12 | 13 | What would you like to see added to Chromatic? 14 | 15 | **Alternative solutions** 16 | 17 | Any alternative solutions or workarounds you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context or screenshots about the feature request here. 22 | 23 | Contact customer support if you need to share private information you'd rather not disclose here. 24 | -------------------------------------------------------------------------------- /.github/workflows/check-labels.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Labels 2 | on: 3 | pull_request: 4 | types: [opened, labeled, unlabeled, synchronize] 5 | 6 | permissions: {} 7 | 8 | jobs: 9 | check-labels: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 20 15 | - uses: chromaui/pr-label-checker-action@main 16 | with: 17 | one-of: | 18 | major, minor, patch 19 | release, skip-release 20 | none-of: DO NOT MERGE 21 | -------------------------------------------------------------------------------- /.github/workflows/chromatic-main-and-prs.yml: -------------------------------------------------------------------------------- 1 | name: Chromatic Main and Active PRs only 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [assigned, ready_for_review, review_requested] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | chromatic: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: corepack enable 23 | - name: install 24 | run: yarn install --immutable 25 | - uses: chromaui/action@latest 26 | with: 27 | # 👇 Chromatic projectToken, refer to the manage page to obtain it. 28 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 29 | exitOnceUploaded: true 30 | onlyChanged: true 31 | traceChanged: true 32 | diagnostics: true 33 | skip: ${{ github.event.pull_request.draft == true }} 34 | - uses: actions/upload-artifact@v4 35 | if: always() 36 | with: 37 | name: chromatic-build-artifacts-${{ github.run_id }} 38 | path: | 39 | chromatic-diagnostics.json 40 | **/build-storybook.log 41 | -------------------------------------------------------------------------------- /.github/workflows/chromatic-prod.yml: -------------------------------------------------------------------------------- 1 | name: Chromatic 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | chromatic: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: corepack enable 23 | - name: install 24 | run: yarn install --immutable 25 | - uses: chromaui/action@latest 26 | with: 27 | exitOnceUploaded: true 28 | onlyChanged: true 29 | traceChanged: true 30 | diagnostics: true 31 | debug: true 32 | env: 33 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 34 | - uses: actions/upload-artifact@v4 35 | if: always() 36 | with: 37 | name: chromatic-build-artifacts-${{ github.run_id }} 38 | path: | 39 | chromatic-diagnostics.json 40 | **/build-storybook.log 41 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_call: 8 | secrets: 9 | CODECOV_TOKEN: 10 | required: true 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | lint-and-test: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 22 25 | - run: corepack enable 26 | - name: install 27 | run: yarn install --immutable 28 | 29 | # check types and linting issues 30 | - run: yarn typescript:check 31 | - run: yarn lint 32 | 33 | # test if dist is correctly generated from src 34 | - run: yarn build && git status --porcelain 35 | 36 | # unit test 37 | - run: yarn test 38 | 39 | - name: Upload coverage reports to Codecov 40 | uses: codecov/codecov-action@v5 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/package-size-main.yml: -------------------------------------------------------------------------------- 1 | name: Chromatic Package Size Update 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | chromatic: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 22 20 | - run: corepack enable 21 | - name: install 22 | run: yarn install --immutable 23 | - name: build 24 | run: yarn build 25 | - name: Remove Sourcemaps 26 | run: | 27 | yarn clean:sourcemaps 28 | - name: Get Package Size 29 | id: package_size 30 | run: | 31 | export DIST_SIZE="$(du -k ./dist | cut -f1)" 32 | echo "Package Size: $DIST_SIZE KB" 33 | echo "size=$DIST_SIZE" >> "$GITHUB_OUTPUT" 34 | - name: Update Database 35 | run: | 36 | curl "${{ secrets.UPSTASH_REDIS_REST_URL }}/set/$GITHUB_SHA/${{ steps.package_size.outputs.size }}" -H "Authorization: Bearer ${{ secrets.UPSTASH_API_KEY }}" 37 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-action-next.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via action next 2 | on: merge_group 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | smoke-test-action-next: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 22 17 | - run: corepack enable 18 | - run: yarn 19 | - name: Push to action-next 20 | run: yarn run release-next 21 | env: 22 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 23 | - name: Run build against action-next 24 | uses: chromaui/action-next@latest 25 | with: 26 | buildScriptName: build-test-storybook 27 | exitZeroOnChanges: true 28 | forceRebuild: true 29 | env: 30 | LOG_LEVEL: debug 31 | DEBUG: chromatic-cli 32 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-action.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via action 2 | on: 3 | workflow_call: 4 | secrets: 5 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 6 | required: true 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | self-test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 22 21 | - run: corepack enable 22 | - run: yarn 23 | - run: yarn build 24 | - uses: ./ 25 | with: 26 | buildScriptName: build-test-storybook 27 | exitZeroOnChanges: true 28 | forceRebuild: true 29 | env: 30 | LOG_LEVEL: debug 31 | DEBUG: chromatic-cli 32 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-node-api.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via node api 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 'lts/*' 22 | - run: corepack enable 23 | - run: yarn 24 | - run: yarn build 25 | - name: run chromatic via node 26 | run: ./scripts/run-via-node.mjs 27 | env: 28 | LOG_LEVEL: debug 29 | DEBUG: chromatic-cli 30 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-node18.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via Node 18 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 18 22 | - run: corepack enable 23 | - run: yarn 24 | - run: yarn build 25 | - uses: ./ 26 | with: 27 | buildScriptName: build-test-storybook 28 | exitZeroOnChanges: true 29 | forceRebuild: true 30 | onlyChanged: true 31 | env: 32 | LOG_LEVEL: debug 33 | DEBUG: chromatic-cli 34 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-node20.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via Node 20 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 20 22 | - run: corepack enable 23 | - run: yarn 24 | - run: yarn build 25 | - uses: ./ 26 | with: 27 | buildScriptName: build-test-storybook 28 | exitZeroOnChanges: true 29 | forceRebuild: true 30 | onlyChanged: true 31 | env: 32 | LOG_LEVEL: debug 33 | DEBUG: chromatic-cli 34 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-npx.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via npx 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: corepack enable 23 | - name: install 24 | run: yarn && git status --porcelain 25 | - run: yarn build 26 | - name: run chromatic 27 | run: npx -p . chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 28 | env: 29 | LOG_LEVEL: debug 30 | DEBUG: chromatic-cli 31 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-windows.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via yarn on Windows 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: windows-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: corepack enable 23 | - name: install 24 | run: yarn && git status --porcelain 25 | - name: prep package 26 | run: ./scripts/rename.js storybook-chromatic 27 | - run: yarn build 28 | - name: run chromatic 29 | run: npx -p . chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 30 | env: 31 | LOG_LEVEL: debug 32 | DEBUG: chromatic-cli 33 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-yarn-berry.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via yarn berry 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: rm -rf subdir # remove conflicting subproject 23 | - run: corepack enable 24 | - name: install 25 | run: yarn install 26 | env: 27 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 28 | - name: run chromatic 29 | run: npx -p . chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 30 | env: 31 | LOG_LEVEL: debug 32 | DEBUG: chromatic-cli 33 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-yarn-canary.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via yarn canary 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: rm -rf subdir # remove conflicting subproject 23 | - run: corepack enable 24 | - run: yarn set version canary 25 | - name: install 26 | run: yarn install 27 | env: 28 | YARN_ENABLE_IMMUTABLE_INSTALLS: false 29 | - name: run chromatic 30 | run: npx -p . chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 31 | env: 32 | LOG_LEVEL: debug 33 | DEBUG: chromatic-cli 34 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-yarn-classic.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via yarn classic 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: rm -rf subdir # remove conflicting subproject 23 | - run: corepack enable 24 | - run: yarn set version 1.22.22 25 | - run: yarn install --ignore-engines 26 | - name: run chromatic 27 | run: npx -p . chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 28 | env: 29 | LOG_LEVEL: debug 30 | DEBUG: chromatic-cli 31 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/smoke-test-yarn.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test via yarn 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN: 7 | required: true 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | self-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 22 22 | - run: corepack enable 23 | - run: yarn 24 | - run: yarn build 25 | - run: yarn chromatic --build-script-name build-test-storybook --exit-zero-on-changes --force-rebuild 26 | env: 27 | LOG_LEVEL: debug 28 | DEBUG: chromatic-cli 29 | CHROMATIC_PROJECT_TOKEN: ${{ secrets.SMOKE_TESTS_CHROMATIC_PROJECT_TOKEN }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .env 4 | *.log 5 | *.tgz 6 | .editorconfig 7 | 8 | # yarn 4 9 | **/.pnp.* 10 | **/.yarn/* 11 | !.yarn/patches 12 | !.yarn/plugins 13 | !.yarn/releases 14 | !.yarn/sdks 15 | !.yarn/versions 16 | 17 | storybook-static 18 | subdir-static 19 | test-storybook 20 | chromatic-build-*.xml 21 | chromatic-diagnostics.json 22 | dist 23 | bin 24 | action 25 | node 26 | storybook-out 27 | coverage 28 | package.json.backup 29 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { StorybookConfig } from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | stories: process.env.SMOKE_TEST 5 | ? ['../test-stories/*.stories.*'] 6 | : ['../node-src/**/*.@(mdx|stories.*)'], 7 | addons: ['@storybook/addon-essentials', '@storybook/addon-webpack5-compiler-swc'], 8 | framework: { 9 | name: '@storybook/react-webpack5', 10 | options: {}, 11 | }, 12 | webpackFinal: async (config) => { 13 | config.resolve = { 14 | ...config.resolve, 15 | fallback: { 16 | ...config?.resolve?.fallback, 17 | os: require.resolve('os-browserify/browser'), 18 | }, 19 | }; 20 | 21 | return config; 22 | }, 23 | docs: {}, 24 | typescript: { 25 | reactDocgen: 'react-docgen-typescript', 26 | }, 27 | staticDirs: ['../static'], 28 | }; 29 | 30 | export default config; 31 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deepscan.enable": true, 3 | "cSpell.words": [ 4 | "hichroma", 5 | "opensource", 6 | "rebased" 7 | ] 8 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: false 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.2.2.cjs 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Chroma Software Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | 👉 https://www.chromatic.com/docs/security 4 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | 👉 4 | -------------------------------------------------------------------------------- /action-src/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Chroma Software Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /action-src/README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action for Chromatic 2 | 3 | Builds and publishes your Storybook to Chromatic and runs visual regression tests. 4 | 5 | 📋 [Source code](https://github.com/chromaui/chromatic-cli) ∙ 📚 [Documentation](https://www.chromatic.com/docs/github-actions) ∙ 💬 [Support](https://www.chromatic.com/docs/support) 6 | 7 | > ⚠️ This repository is just a deployment target for the GitHub Action. Do not fork or create issues/PRs here. 8 | -------------------------------------------------------------------------------- /action-src/register.js: -------------------------------------------------------------------------------- 1 | require('./main'); 2 | -------------------------------------------------------------------------------- /bin-src/__mocks__/malformedProjectJson/project.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /bin-src/__mocks__/normalProjectJson/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "generatedAt": 1716299331397, 3 | "hasCustomBabel": false, 4 | "hasCustomWebpack": true, 5 | "hasStaticDirs": true, 6 | "hasStorybookEslint": false, 7 | "refCount": 0, 8 | "packageManager": { "type": "yarn", "version": "1.22.19" }, 9 | "typescriptOptions": { "reactDocgen": "react-docgen-typescript" }, 10 | "preview": { "usesGlobals": false }, 11 | "framework": { "name": "@storybook/react-webpack5", "options": {}, "version": "8.1.5" }, 12 | "builder": "@storybook/builder-webpack5", 13 | "renderer": "@storybook/react", 14 | "storybookVersion": "8.1.5", 15 | "storybookVersionSpecifier": "^8.1.5", 16 | "language": "typescript", 17 | "storybookPackages": { 18 | "@storybook/csf-tools": { "version": "8.1.5" }, 19 | "@storybook/linter-config": { "version": "4.0.0" }, 20 | "@storybook/react": { "version": "8.1.5" }, 21 | "@storybook/react-webpack5": { "version": "8.1.5" }, 22 | "storybook": { "version": "8.1.5" } 23 | }, 24 | "addons": { 25 | "@storybook/addon-essentials": { "version": "8.1.5" }, 26 | "@storybook/addon-webpack5-compiler-swc": { "version": "1.0.2" }, 27 | "chromatic": { "version": "11.4.0", "versionSpecifier": "latest" } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /bin-src/__mocks__/sb6ProjectJsonMissingBuilder/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "generatedAt": 1717341501873, 3 | "hasCustomBabel": false, 4 | "hasCustomWebpack": true, 5 | "hasStaticDirs": true, 6 | "hasStorybookEslint": false, 7 | "refCount": 0, 8 | "monorepo": "Turborepo", 9 | "packageManager": { 10 | "type": "yarn", 11 | "version": "1.22.19" 12 | }, 13 | "features": { 14 | "postcss": false, 15 | "interactionsDebugger": true, 16 | "warnOnLegacyHierarchySeparator": false 17 | }, 18 | "storybookVersion": "6.5.16", 19 | "language": "javascript", 20 | "storybookPackages": { 21 | "@storybook/addon-actions": { 22 | "version": "6.5.16" 23 | }, 24 | "@storybook/addons": { 25 | "version": "6.5.16" 26 | }, 27 | "@storybook/builder-webpack4": { 28 | "version": "6.5.16" 29 | }, 30 | "@storybook/manager-webpack4": { 31 | "version": "6.5.16" 32 | }, 33 | "@storybook/react": { 34 | "version": "6.5.16" 35 | }, 36 | "@storybook/testing-library": { 37 | "version": "0.0.13" 38 | }, 39 | "@storybook/theming": { 40 | "version": "6.5.16" 41 | }, 42 | "msw-storybook-addon": { 43 | "version": "1.7.0" 44 | }, 45 | "storybook-mock-date-decorator": { 46 | "version": "1.0.0" 47 | } 48 | }, 49 | "framework": { 50 | "name": "react" 51 | }, 52 | "addons": { 53 | "@storybook/addon-links": { 54 | "version": "6.5.16" 55 | }, 56 | "@storybook/addon-essentials": { 57 | "version": "6.5.16" 58 | }, 59 | "@storybook/addon-interactions": { 60 | "version": "6.5.16" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bin-src/__mocks__/unsupportedAddons/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "generatedAt": 1716299331397, 3 | "hasCustomBabel": false, 4 | "hasCustomWebpack": true, 5 | "hasStaticDirs": true, 6 | "hasStorybookEslint": false, 7 | "refCount": 0, 8 | "packageManager": { "type": "yarn", "version": "1.22.19" }, 9 | "typescriptOptions": { "reactDocgen": "react-docgen-typescript" }, 10 | "preview": { "usesGlobals": false }, 11 | "framework": { "name": "@storybook/react-webpack5", "options": {} }, 12 | "builder": "@storybook/builder-webpack5", 13 | "renderer": "@storybook/react", 14 | "storybookVersion": "8.1.5", 15 | "storybookVersionSpecifier": "^8.1.5", 16 | "language": "typescript", 17 | "storybookPackages": { 18 | "@storybook/csf-tools": { "version": "8.1.5" }, 19 | "@storybook/linter-config": { "version": "4.0.0" }, 20 | "@storybook/react": { "version": "8.1.5" }, 21 | "@storybook/react-webpack5": { "version": "8.1.5" }, 22 | "storybook": { "version": "8.1.5" } 23 | }, 24 | "addons": { 25 | "@storybook/addon-essentials": { "version": "8.1.5" }, 26 | "@storybook/addon-webpack5-compiler-swc": { "version": "1.0.2" }, 27 | "chromatic": { "version": "11.4.0", "versionSpecifier": "latest" }, 28 | "storybook-addon-apollo-client": { 29 | "version": "4.0.11" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bin-src/main.ts: -------------------------------------------------------------------------------- 1 | import '../node-src/errorMonitoring'; 2 | 3 | import * as Sentry from '@sentry/node'; 4 | 5 | import { run } from '../node-src'; 6 | 7 | /** 8 | * The main entrypoint for the CLI. 9 | * 10 | * @param argv A list of arguments passed. 11 | */ 12 | export async function main(argv: string[]) { 13 | try { 14 | const { code } = await run({ argv }); 15 | process.exitCode = code; 16 | } catch (err) { 17 | Sentry.captureException(err); 18 | } finally { 19 | await Sentry.flush(2500); 20 | process.exit(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin-src/register.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import 'dotenv/config'; 4 | 5 | const commands = { 6 | init: () => import('./init').then(({ main: initMain }) => initMain(process.argv.slice(3))), 7 | main: () => import('./main').then(({ main }) => main(process.argv.slice(2))), 8 | trace: () => import('./trace').then(({ main: traceMain }) => traceMain(process.argv.slice(3))), 9 | 'trim-stats-file': () => 10 | import('./trimStatsFile').then(({ main: trimMain }) => trimMain(process.argv.slice(3))), 11 | }; 12 | 13 | (commands[process.argv[2]] || commands.main)(); 14 | -------------------------------------------------------------------------------- /bin-src/trimStatsFile.test.ts: -------------------------------------------------------------------------------- 1 | import mockfs from 'mock-fs'; 2 | import { afterEach, describe, expect, it } from 'vitest'; 3 | 4 | import { readStatsFile } from '../node-src/tasks/readStatsFile'; 5 | import * as trimmedFile from './__mocks__/previewStatsJson/preview-stats.trimmed.json'; 6 | 7 | mockfs({ 8 | './storybook-static/preview-stats.json': JSON.stringify(trimmedFile), 9 | }); 10 | 11 | afterEach(() => { 12 | mockfs.restore(); 13 | }); 14 | 15 | describe('Trim Stats File', () => { 16 | it('readStatsFile returns expected output', async () => { 17 | const result = await readStatsFile('./storybook-static/preview-stats.json'); 18 | expect( 19 | result.modules.some(({ id }) => id === './node-src/ui/components/icons.stories.ts') 20 | ).toBe(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /isChromatic.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns `true` if running within Chromatic, `false` otherwise. 3 | * @argument window - The window object whose `navigator` and/or `location` is 4 | * used to determine if running in Chromatic. 5 | */ 6 | declare function isChromatic(window?: Window): boolean; 7 | export = isChromatic; 8 | -------------------------------------------------------------------------------- /isChromatic.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/prefer-module */ 2 | /* eslint-env browser */ 3 | 4 | module.exports = function isChromatic(windowArgument) { 5 | const windowToCheck = windowArgument || (typeof window !== 'undefined' && window); 6 | return !!( 7 | windowToCheck && 8 | (/Chromatic/.test(windowToCheck.navigator.userAgent) || 9 | /chromatic=true/.test(windowToCheck.location.href)) 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /isChromatic.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export default function isChromatic(windowArgument) { 4 | const windowToCheck = windowArgument || (typeof window !== 'undefined' && window); 5 | return !!( 6 | windowToCheck && 7 | (/Chromatic/.test(windowToCheck.navigator.userAgent) || 8 | /chromatic=true/.test(windowToCheck.location.href)) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /isChromatic.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | import isChromatic from './isChromatic'; 5 | 6 | describe('with window arg', () => { 7 | it('returns false', () => { 8 | expect( 9 | isChromatic({ 10 | navigator: { 11 | userAgent: 'Chrome', 12 | }, 13 | location: new URL('https://example.com'), 14 | } as any as Window) 15 | ).toBe(false); 16 | }); 17 | 18 | it('returns true if location.href contains chromatic=true queryparam', () => { 19 | expect( 20 | isChromatic({ 21 | navigator: { 22 | userAgent: 'Chrome', 23 | }, 24 | location: new URL('https://example.com?chromatic=true'), 25 | } as any as Window) 26 | ).toBe(true); 27 | }); 28 | 29 | it('returns true if userAgent contains Chromatic', () => { 30 | expect( 31 | isChromatic({ 32 | navigator: { 33 | userAgent: 'Chromium(Chromatic)', 34 | }, 35 | location: new URL('https://example.com'), 36 | } as any as Window) 37 | ).toBe(true); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /node-src/__mocks__/dependencyChanges/plain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-demo", 3 | "test": "4", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Gert Hengeveld ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@emotion/core": "10.0.22", 10 | "@emotion/styled": "10.0.23", 11 | "polished": "3.4.2", 12 | "react": "16.12.0", 13 | "react-dom": "16.12.0", 14 | "relative-deps": "1.0.7" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "7.7.5", 18 | "@storybook/addon-actions": "^6.4.22", 19 | "@storybook/addon-essentials": "^6.4.22", 20 | "@storybook/addon-links": "^6.4.22", 21 | "@storybook/react": "^6.4.22", 22 | "@types/lodash": "^4.14.191", 23 | "babel-loader": "8.0.6", 24 | "chromatic": "5.6.1", 25 | "react-docgen-typescript-loader": "3.6.0", 26 | "react-is": "^17.0.2", 27 | "ts-loader": "6.2.1", 28 | "typescript": "3.7.3" 29 | }, 30 | "relativeDependencies": { 31 | "chromatic": "../chromatic-cli" 32 | }, 33 | "scripts": { 34 | "prepare": "relative-deps", 35 | "chromatic": "CHROMATIC_INDEX_URL=https://index.chromatic.com chromatic --project-token 2gsw6ht6am", 36 | "chromatic-dev": "CHROMATIC_INDEX_URL=https://index.dev-chromatic.com chromatic", 37 | "chromatic-staging": "CHROMATIC_INDEX_URL=https://index.staging-chromatic.com chromatic --project-token wu94j15cmd7", 38 | "storybook": "start-storybook -p 6006 --no-dll", 39 | "build-storybook": "build-storybook --no-dll" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /node-src/__mocks__/dependencyChanges/react-async-10/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-demo", 3 | "test": "4", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Gert Hengeveld ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@emotion/core": "10.0.22", 10 | "@emotion/styled": "10.0.23", 11 | "polished": "3.4.2", 12 | "react": "16.12.0", 13 | "react-async": "10.0.0", 14 | "react-dom": "16.12.0", 15 | "relative-deps": "1.0.7" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "7.7.5", 19 | "@storybook/addon-actions": "^6.4.22", 20 | "@storybook/addon-essentials": "^6.4.22", 21 | "@storybook/addon-links": "^6.4.22", 22 | "@storybook/react": "^6.4.22", 23 | "@types/lodash": "^4.14.191", 24 | "babel-loader": "8.0.6", 25 | "chromatic": "5.6.1", 26 | "react-docgen-typescript-loader": "3.6.0", 27 | "react-is": "^17.0.2", 28 | "ts-loader": "6.2.1", 29 | "typescript": "3.7.3" 30 | }, 31 | "relativeDependencies": { 32 | "chromatic": "../chromatic-cli" 33 | }, 34 | "scripts": { 35 | "prepare": "relative-deps", 36 | "chromatic": "CHROMATIC_INDEX_URL=https://index.chromatic.com chromatic --project-token 2gsw6ht6am", 37 | "chromatic-dev": "CHROMATIC_INDEX_URL=https://index.dev-chromatic.com chromatic", 38 | "chromatic-staging": "CHROMATIC_INDEX_URL=https://index.staging-chromatic.com chromatic --project-token wu94j15cmd7", 39 | "storybook": "start-storybook -p 6006 --no-dll", 40 | "build-storybook": "build-storybook --no-dll" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /node-src/__mocks__/dependencyChanges/react-async-9/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-demo", 3 | "test": "4", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Gert Hengeveld ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@emotion/core": "10.0.22", 10 | "@emotion/styled": "10.0.23", 11 | "polished": "3.4.2", 12 | "react": "16.12.0", 13 | "react-async": "9.0.0", 14 | "react-dom": "16.12.0", 15 | "relative-deps": "1.0.7" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "7.7.5", 19 | "@storybook/addon-actions": "^6.4.22", 20 | "@storybook/addon-essentials": "^6.4.22", 21 | "@storybook/addon-links": "^6.4.22", 22 | "@storybook/react": "^6.4.22", 23 | "@types/lodash": "^4.14.191", 24 | "babel-loader": "8.0.6", 25 | "chromatic": "5.6.1", 26 | "react-docgen-typescript-loader": "3.6.0", 27 | "react-is": "^17.0.2", 28 | "ts-loader": "6.2.1", 29 | "typescript": "3.7.3" 30 | }, 31 | "relativeDependencies": { 32 | "chromatic": "../chromatic-cli" 33 | }, 34 | "scripts": { 35 | "prepare": "relative-deps", 36 | "chromatic": "CHROMATIC_INDEX_URL=https://index.chromatic.com chromatic --project-token 2gsw6ht6am", 37 | "chromatic-dev": "CHROMATIC_INDEX_URL=https://index.dev-chromatic.com chromatic", 38 | "chromatic-staging": "CHROMATIC_INDEX_URL=https://index.staging-chromatic.com chromatic --project-token wu94j15cmd7", 39 | "storybook": "start-storybook -p 6006 --no-dll", 40 | "build-storybook": "build-storybook --no-dll" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /node-src/__mocks__/dependencyParsing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-demo", 3 | "test": "4", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "Gert Hengeveld ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@emotion/core": "10.0.22", 10 | "@emotion/styled": "10.0.23", 11 | "moment": "^2.30.0", 12 | "polished": "3.4.2", 13 | "react": "16.12.0", 14 | "react-dom": "16.12.0", 15 | "relative-deps": "1.0.7" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "7.7.5", 19 | "@storybook/addon-actions": "^6.4.22", 20 | "@storybook/addon-essentials": "^6.4.22", 21 | "@storybook/addon-links": "^6.4.22", 22 | "@storybook/react": "^6.4.22", 23 | "@types/lodash": "^4.14.191", 24 | "babel-loader": "8.0.6", 25 | "chromatic": "5.6.1", 26 | "react-docgen-typescript-loader": "3.6.0", 27 | "react-is": "^17.0.2", 28 | "ts-loader": "6.2.1", 29 | "typescript": "3.7.3" 30 | }, 31 | "relativeDependencies": { 32 | "chromatic": "../chromatic-cli" 33 | }, 34 | "scripts": { 35 | "prepare": "relative-deps", 36 | "chromatic": "CHROMATIC_INDEX_URL=https://index.chromatic.com chromatic --project-token 2gsw6ht6am", 37 | "chromatic-dev": "CHROMATIC_INDEX_URL=https://index.dev-chromatic.com chromatic", 38 | "chromatic-staging": "CHROMATIC_INDEX_URL=https://index.staging-chromatic.com chromatic --project-token wu94j15cmd7", 39 | "storybook": "start-storybook -p 6006 --no-dll", 40 | "build-storybook": "build-storybook --no-dll" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /node-src/__mocks__/invalidPackageJson/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chromatic", 3 | "version": "invalid-semver" 4 | } 5 | -------------------------------------------------------------------------------- /node-src/__mocks__/storybookBaseDir/subdir/test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-empty-file */ 2 | // This file is intentionally left blank 3 | -------------------------------------------------------------------------------- /node-src/__mocks__/storybookBaseDir/subdir/test.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-empty-file */ 2 | // This file is intentionally left blank 3 | -------------------------------------------------------------------------------- /node-src/__mocks__/storybookBaseDir/subdir/test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-empty-file */ 2 | // This file is intentionally left blank 3 | -------------------------------------------------------------------------------- /node-src/__mocks__/storybookBaseDir/test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/no-empty-file */ 2 | // This file is intentionally left blank 3 | -------------------------------------------------------------------------------- /node-src/git/getBranchFromMergeQueuePullRequestNumber.ts: -------------------------------------------------------------------------------- 1 | import gql from 'fake-tag'; 2 | 3 | import { Context } from '../types'; 4 | 5 | const MergeQueueOriginalBranchQuery = gql` 6 | query MergeQueueOriginalBranchQuery($number: Int!) { 7 | app { 8 | pullRequest(number: $number) { 9 | branch: headRefName 10 | } 11 | } 12 | } 13 | `; 14 | interface MergeQueueOriginalBranchQueryResult { 15 | app: { 16 | pullRequest: { 17 | branch: string; 18 | }; 19 | }; 20 | } 21 | 22 | /** 23 | * Get branch name from a pull request number via the Index service. 24 | * 25 | * @param ctx The context set when executing the CLI. 26 | * @param options Options to pass to the Index query. 27 | * @param options.number The pull request number. 28 | * 29 | * @returns The branch name, if available. 30 | */ 31 | export async function getBranchFromMergeQueuePullRequestNumber( 32 | ctx: Pick, 33 | { number }: { number: number } 34 | ) { 35 | const { app } = await ctx.client.runQuery( 36 | MergeQueueOriginalBranchQuery, 37 | { 38 | number, 39 | } 40 | ); 41 | 42 | return app?.pullRequest?.branch; 43 | } 44 | -------------------------------------------------------------------------------- /node-src/git/mocks/doubleLoop.ts: -------------------------------------------------------------------------------- 1 | // A 2 | // | \ 3 | // B C 4 | // | | \ 5 | // D E F 6 | // | | / 7 | // G H 8 | // | / 9 | // I 10 | 11 | // [commit, parent(s)] 12 | export default [ 13 | // prettier-ignore 14 | ['A', false], 15 | ['B', 'A'], 16 | ['C', 'A'], 17 | ['D', 'B'], 18 | ['E', 'C'], 19 | ['F', 'C'], 20 | ['G', 'D'], 21 | ['H', 'E'], 22 | ['I', ['G', 'H']], 23 | ]; 24 | -------------------------------------------------------------------------------- /node-src/git/mocks/longLine.ts: -------------------------------------------------------------------------------- 1 | // A z 2 | // | 3 | // B 4 | // | 5 | // ... 6 | // | 7 | // Y 8 | // | 9 | // Z 10 | 11 | const ACode = 'A'.codePointAt(0) as number; 12 | 13 | // [commit, parent(s)] 14 | export default [ 15 | ['A', false], 16 | ['z', false], 17 | ...Array.from({ length: 25 }).map((_, index) => [ 18 | String.fromCodePoint(index + 1 + ACode), // e.g. 'B' 19 | String.fromCodePoint(index + ACode), // e.g. 'A' 20 | ]), 21 | ]; 22 | -------------------------------------------------------------------------------- /node-src/git/mocks/longLoop.ts: -------------------------------------------------------------------------------- 1 | // A 2 | // | \ 3 | // B C 4 | // | | 5 | // D E 6 | // | | 7 | // F G 8 | // | / 9 | // H 10 | 11 | // [commit, parent(s)] 12 | export default [ 13 | // prettier-ignore 14 | ['A', false], 15 | ['B', 'A'], 16 | ['C', 'A'], 17 | ['D', 'B'], 18 | ['E', 'C'], 19 | ['F', 'D'], 20 | ['G', 'E'], 21 | ['H', ['F', 'G']], 22 | ]; 23 | -------------------------------------------------------------------------------- /node-src/git/mocks/simpleLoop.ts: -------------------------------------------------------------------------------- 1 | // A 2 | // | 3 | // B 4 | // | 5 | // C 6 | // | \ 7 | // D E 8 | // | / 9 | // F 10 | 11 | // [commit, parent(s)] 12 | export default [ 13 | // prettier-ignore 14 | ['A', false], 15 | ['B', 'A'], 16 | ['C', 'B'], 17 | ['D', 'C'], 18 | ['E', 'C'], 19 | ['F', ['D', 'E']], 20 | ]; 21 | -------------------------------------------------------------------------------- /node-src/git/mocks/threeParents.ts: -------------------------------------------------------------------------------- 1 | // A 2 | // /|\ 3 | // B C D 4 | // \|/ 5 | // E 6 | 7 | // [commit, parent(s)] 8 | export default [ 9 | // prettier-ignore 10 | ['A', false], 11 | ['B', 'A'], 12 | ['C', 'A'], 13 | ['D', 'A'], 14 | ['E', ['B', 'C', 'D']], 15 | ]; 16 | -------------------------------------------------------------------------------- /node-src/git/mocks/twoRoots.ts: -------------------------------------------------------------------------------- 1 | // A B 2 | // | 3 | // C 4 | // | 5 | // D 6 | 7 | // [commit, parent(s)] 8 | export default [ 9 | // prettier-ignore 10 | ['A', false], 11 | ['B', false], 12 | ['C', 'A'], 13 | ['D', 'C'], 14 | ]; 15 | -------------------------------------------------------------------------------- /node-src/io/getDNSResolveAgent.ts: -------------------------------------------------------------------------------- 1 | import dns from 'dns'; 2 | import { Agent, AgentOptions } from 'https'; 3 | 4 | /** 5 | * A DNS resolver for interacting with a custom DNS server, if provided. 6 | */ 7 | export class DNSResolveAgent extends Agent { 8 | constructor(options: AgentOptions = {}) { 9 | super({ 10 | ...options, 11 | lookup( 12 | hostname: string, 13 | _options: dns.LookupOptions, 14 | callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void 15 | ) { 16 | dns.resolve(hostname, (err, addresses) => callback(err, addresses?.[0], 4)); 17 | }, 18 | }); 19 | } 20 | } 21 | 22 | const getDNSResolveAgent = () => new DNSResolveAgent(); 23 | 24 | export default getDNSResolveAgent; 25 | -------------------------------------------------------------------------------- /node-src/io/getProxyAgent.ts: -------------------------------------------------------------------------------- 1 | import { HttpsProxyAgent, HttpsProxyAgentOptions } from 'https-proxy-agent'; 2 | import noProxy from 'no-proxy'; 3 | import { URL } from 'url'; 4 | 5 | import { Context } from '../types'; 6 | 7 | const agents = {}; 8 | 9 | const getProxyAgent = ( 10 | { env, log }: Pick, 11 | url: string, 12 | options?: HttpsProxyAgentOptions 13 | ) => { 14 | const proxy = env.HTTPS_PROXY || env.HTTP_PROXY; 15 | if (!proxy || noProxy(url)) return undefined; 16 | 17 | log.debug({ url, proxy, options }, 'Using proxy agent'); 18 | const requestHost = new URL(url).host; 19 | if (!agents[requestHost]) { 20 | agents[requestHost] = new HttpsProxyAgent(proxy, options); 21 | } 22 | return agents[requestHost]; 23 | }; 24 | 25 | export default getProxyAgent; 26 | -------------------------------------------------------------------------------- /node-src/lib/builders.ts: -------------------------------------------------------------------------------- 1 | export const builders = { 2 | webpack4: '@storybook/builder-webpack4', 3 | webpack5: '@storybook/builder-webpack5', 4 | '@storybook/vite-builder': '@storybook/builder-vite', 5 | '@storybook/builder-webpack5': '@storybook/react-webpack5', 6 | '@storybook/react-vite': '@storybook/builder-vite', 7 | } as Record; 8 | -------------------------------------------------------------------------------- /node-src/lib/compress.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import mockFs from 'mock-fs'; 3 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; 4 | 5 | import makeZipFile from './compress'; 6 | import TestLogger from './testLogger'; 7 | 8 | beforeEach(() => { 9 | vi.clearAllMocks(); 10 | }); 11 | 12 | afterEach(() => { 13 | mockFs.restore(); 14 | }); 15 | 16 | const testContext = { sourceDir: '/chromatic-tmp', log: new TestLogger() } as any; 17 | const files = [{ localPath: '/chromatic-tmp/file1', targetPath: 'file1', contentLength: 1 }]; 18 | 19 | describe.skip('makeZipFile', () => { 20 | it('adds files to an archive', async () => { 21 | mockFs({ 22 | '/chromatic-tmp': { 23 | file1: 'Storybook', 24 | }, 25 | }); 26 | 27 | const result = await makeZipFile(testContext, files); 28 | 29 | expect(existsSync(result.path)).toBeTruthy(); 30 | expect(result.size).toBeGreaterThan(0); 31 | }); 32 | 33 | it('rejects on error signals', () => { 34 | return expect(makeZipFile(testContext, files)).rejects.toThrow( 35 | `ENOENT: no such file or directory, open '/chromatic-tmp/file1'` 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /node-src/lib/e2eUtils.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../types'; 2 | 3 | /** 4 | * Determine if the build is an E2E build. 5 | * 6 | * @param options Parsed options when executing the CLI (usually from the context). 7 | * 8 | * @returns true if the build is an E2E build. 9 | */ 10 | export function isE2EBuild(options: Options) { 11 | return options.playwright || options.cypress; 12 | } 13 | -------------------------------------------------------------------------------- /node-src/lib/emailHash.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | /** 4 | * Create a hash of the provided email address. 5 | * 6 | * Inspired by https://en.gravatar.com/site/implement/hash 7 | * 8 | * @param email The plaintext email address. 9 | * 10 | * @returns A hashed version of the plaintext email address. 11 | */ 12 | export function emailHash(email: string) { 13 | return createHash('md5').update(email.trim().toLowerCase()).digest('hex'); 14 | } 15 | -------------------------------------------------------------------------------- /node-src/lib/fileReaderBlob.ts: -------------------------------------------------------------------------------- 1 | import { createReadStream, ReadStream } from 'fs'; 2 | 3 | /** 4 | * A file reader which offers a callback for tracking progress updates. 5 | */ 6 | export class FileReaderBlob { 7 | readStream: ReadStream; 8 | size: number; 9 | 10 | readonly [Symbol.toStringTag] = 'Blob'; 11 | constructor(filePath: string, contentLength: number, onProgress: (delta: number) => void) { 12 | this.size = contentLength; 13 | this.readStream = createReadStream(filePath); 14 | this.readStream.on('data', (chunk: Buffer | string) => onProgress(chunk.length)); 15 | } 16 | 17 | stream() { 18 | return this.readStream; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /node-src/lib/getFileHashes.test.ts: -------------------------------------------------------------------------------- 1 | import { describe } from 'node:test'; 2 | 3 | import { expect, it } from 'vitest'; 4 | 5 | import { getFileHashes } from './getFileHashes'; 6 | 7 | describe('getFileHashes', () => { 8 | it('should return a map of file paths to hashes', async () => { 9 | const hashes = await getFileHashes(['iframe.html', 'index.html'], 'node-src/__mocks__', 2); 10 | 11 | expect(hashes).toEqual({ 12 | 'iframe.html': '80b7ac41594507e8', 13 | 'index.html': '0e98fd69b0b01605', 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /node-src/lib/getHasRouter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest'; 2 | 3 | import { getHasRouter } from './getHasRouter'; 4 | 5 | it('returns true if there is a routing package in package.json', async () => { 6 | expect( 7 | getHasRouter({ 8 | dependencies: { 9 | react: '^18', 10 | 'react-dom': '^18', 11 | 'react-router': '^6', 12 | }, 13 | }) 14 | ).toBe(true); 15 | }); 16 | 17 | it('sreturns false if there is a routing package in package.json dependenices', async () => { 18 | expect( 19 | getHasRouter({ 20 | dependencies: { 21 | react: '^18', 22 | 'react-dom': '^18', 23 | }, 24 | devDependencies: { 25 | 'react-router': '^6', 26 | }, 27 | }) 28 | ).toBe(false); 29 | }); 30 | -------------------------------------------------------------------------------- /node-src/lib/getHasRouter.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from '../types'; 2 | 3 | const routerPackages = new Set([ 4 | 'react-router', 5 | 'react-router-dom', 6 | 'remix', 7 | '@tanstack/react-router', 8 | 'expo-router', 9 | '@reach/router', 10 | 'react-easy-router', 11 | '@remix-run/router', 12 | 'wouter', 13 | 'wouter-preact', 14 | 'preact-router', 15 | 'vue-router', 16 | 'unplugin-vue-router', 17 | '@angular/router', 18 | '@solidjs/router', 19 | 20 | // metaframeworks that imply routing 21 | 'next', 22 | 'react-scripts', 23 | 'gatsby', 24 | 'nuxt', 25 | '@sveltejs/kit', 26 | ]); 27 | 28 | /** 29 | * @param packageJson The package JSON of the project (from context) 30 | * 31 | * @returns boolean Does this project use a routing package? 32 | */ 33 | export function getHasRouter(packageJson: Context['packageJson']) { 34 | // NOTE: we just check real dependencies; if it is in dev dependencies, it may just be an example 35 | return Object.keys(packageJson?.dependencies ?? {}).some((depName) => 36 | routerPackages.has(depName) 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /node-src/lib/getPackageManager.ts: -------------------------------------------------------------------------------- 1 | import { getCliCommand, parseNa, parseNr } from '@antfu/ni'; 2 | import { execa } from 'execa'; 3 | 4 | // 'npm' | 'pnpm' | 'yarn' | 'bun' 5 | export const getPackageManagerName = async () => { 6 | return getCliCommand(parseNa, [], { programmatic: true }); 7 | }; 8 | 9 | // e.g. `npm run build-storybook` 10 | export const getPackageManagerRunCommand = async (args: string[]) => { 11 | return getCliCommand(parseNr, args, { programmatic: true }); 12 | }; 13 | 14 | // e.g. `8.19.2` 15 | export const getPackageManagerVersion = async (packageManager: string) => { 16 | if (!packageManager) { 17 | throw new Error('No package manager provided'); 18 | } 19 | 20 | const { stdout } = await execa(packageManager, ['--version']); 21 | const [output] = (stdout.toString() as string).trim().split('\n', 1); 22 | return output.trim().replace(/^v/, ''); 23 | }; 24 | -------------------------------------------------------------------------------- /node-src/lib/getPrebuiltStorybookMetadata.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | 3 | import { getStorybookMetadataFromProjectJson } from './getPrebuiltStorybookMetadata'; 4 | 5 | describe('getStorybookMetadataFromProjectJson', () => { 6 | it('should return the metadata from the project.json file', async () => { 7 | const projectJsonPath = 'bin-src/__mocks__/normalProjectJson/project.json'; 8 | const metadata = await getStorybookMetadataFromProjectJson(projectJsonPath); 9 | 10 | expect(metadata).toEqual({ 11 | version: '8.1.5', 12 | builder: { 13 | name: '@storybook/builder-webpack5', 14 | packageVersion: '8.1.5', 15 | }, 16 | }); 17 | }); 18 | 19 | it('should return the metadata from a Storybook 6 project.json file', async () => { 20 | const projectJsonPath = 'bin-src/__mocks__/sb6ProjectJson/project.json'; 21 | const metadata = await getStorybookMetadataFromProjectJson(projectJsonPath); 22 | 23 | expect(metadata).toEqual({ 24 | version: '6.5.16', 25 | builder: { 26 | name: 'webpack4', 27 | packageVersion: '6.5.16', 28 | }, 29 | }); 30 | }); 31 | 32 | it('should return the metadata from the project.json file when the builder is missing', async () => { 33 | const projectJsonPath = 'bin-src/__mocks__/sb6ProjectJsonMissingBuilder/project.json'; 34 | const metadata = await getStorybookMetadataFromProjectJson(projectJsonPath); 35 | 36 | expect(metadata).toEqual({ 37 | version: '6.5.16', 38 | builder: { 39 | name: 'webpack4', 40 | packageVersion: '6.5.16', 41 | }, 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /node-src/lib/getStorybookBaseDirectory.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { Context } from '../types'; 4 | import { posix } from './posix'; 5 | 6 | /** 7 | * Get the storybook base directory, relative to the git root. 8 | * This is where you run SB from, NOT the config dir. 9 | * 10 | * @param ctx Context Regular context 11 | * 12 | * @returns string The base directory 13 | */ 14 | export function getStorybookBaseDirectory(ctx: Context) { 15 | const { storybookBaseDir } = ctx.options || {}; 16 | if (storybookBaseDir) { 17 | return storybookBaseDir; 18 | } 19 | 20 | const { rootPath } = ctx.git || {}; 21 | if (!rootPath) { 22 | return '.'; 23 | } 24 | 25 | // NOTE: 26 | // - path.relative does not have a leading '.', unless it starts with '../' 27 | // - path.join('.', '') === '.' and path.join('.', '../x') = '../x' 28 | return posix(path.join('.', path.relative(rootPath, ''))); 29 | } 30 | -------------------------------------------------------------------------------- /node-src/lib/getStorybookInfo.ts: -------------------------------------------------------------------------------- 1 | import { pathExistsSync } from 'fs-extra'; 2 | import path from 'path'; 3 | 4 | import { Context } from '../types'; 5 | import { getStorybookMetadataFromProjectJson } from './getPrebuiltStorybookMetadata'; 6 | import { getStorybookMetadata } from './getStorybookMetadata'; 7 | 8 | /** 9 | * Get Storybook information from the user's local project. 10 | * 11 | * @param ctx The context set when executing the CLI. 12 | * 13 | * @returns Any Storybook information we can find from the user's local project (which may be 14 | * nothing). 15 | */ 16 | export default async function getStorybookInfo( 17 | ctx: Context 18 | ): Promise> { 19 | try { 20 | if (ctx.options.storybookBuildDir) { 21 | const projectJsonPath = path.resolve(ctx.options.storybookBuildDir, 'project.json'); 22 | // This test makes sure we fall through if the file does not exist. 23 | if (pathExistsSync(projectJsonPath)) { 24 | /* 25 | This await is needed in order to for the catch block 26 | to get the result in the case that this function fails. 27 | */ 28 | return await getStorybookMetadataFromProjectJson(projectJsonPath); 29 | } 30 | } 31 | // Same for this await. 32 | return await getStorybookMetadata(ctx); 33 | } catch (err) { 34 | ctx.log.debug(err); 35 | return {}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /node-src/lib/installDependencies.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions } from 'child_process'; 2 | import { spawn } from 'yarn-or-npm'; 3 | 4 | const installDependencies = (options?: SpawnOptions) => 5 | new Promise((resolve, reject) => { 6 | let stdout = ''; 7 | let stderr = ''; 8 | const child = spawn(['install'], options); 9 | child.stdout?.on('data', (chunk) => { 10 | stdout += chunk; 11 | }); 12 | child.stderr?.on('data', (chunk) => { 13 | stderr += chunk; 14 | }); 15 | child.on('error', reject); 16 | child.on('close', (code) => { 17 | if (code === 0) resolve(stdout); 18 | else reject(stderr); 19 | }); 20 | }); 21 | 22 | export default installDependencies; 23 | -------------------------------------------------------------------------------- /node-src/lib/localBuildsSpecifier.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../types'; 2 | import { emailHash } from './emailHash'; 3 | 4 | /** 5 | * Include local build information when querying Chromatic for build information. 6 | * 7 | * @param ctx The context set when executing the CLI. 8 | * 9 | * @returns Local build information used in GraphQL queries for build information. 10 | */ 11 | export function localBuildsSpecifier(ctx: Pick) { 12 | if (ctx.options.isLocalBuild) 13 | return { localBuildEmailHash: emailHash(ctx.git.gitUserEmail || '') }; 14 | 15 | // For global builds, we only want local builds from the committer (besides global builds) 16 | if (ctx.git.committerEmail) return { localBuildEmailHash: emailHash(ctx.git.committerEmail) }; 17 | 18 | // If we don't know, we fall back to *no local builds at all* 19 | return { isLocalBuild: false }; 20 | } 21 | -------------------------------------------------------------------------------- /node-src/lib/logSerializers.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { expect, it } from 'vitest'; 3 | 4 | import { errorSerializer } from './logSerializers'; 5 | 6 | it('strips off envPairs', () => { 7 | let err; 8 | try { 9 | execSync('some hot garbage', { stdio: 'ignore' }); 10 | } catch (execError) { 11 | err = execError; 12 | } 13 | expect((errorSerializer(err) as any).envPairs).toBeUndefined(); 14 | }); 15 | 16 | it('does not add random things to the error', () => { 17 | const err = new Error('error'); 18 | expect(errorSerializer(err).options).toBeUndefined(); 19 | }); 20 | -------------------------------------------------------------------------------- /node-src/lib/logSerializers.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'node-fetch'; 2 | 3 | function responseSerializer({ status, statusText, headers, url, _raw }: Response & { _raw?: any }) { 4 | return { 5 | status, 6 | statusText, 7 | headers, 8 | url, 9 | ...(_raw && { _raw: _raw.toString() }), 10 | }; 11 | } 12 | 13 | // We *don't* want to log the envPairs key -- this is added by node and includes 14 | // all of our environment variables! See https://github.com/chromaui/chromatic/issues/1993 15 | // Note it is added to both err.envPairs *and* err.options.envPairs :facepalm: 16 | function stripEnvironmentPairs(err: any) { 17 | // @ts-expect-error Ignore the _ property 18 | const { envPairs, options: { envPairs: _, ...options } = {}, ...sanitizedError } = err; 19 | return { sanitizedErr: sanitizedError, ...(err.options && { options }) }; 20 | } 21 | 22 | export const errorSerializer = (err: any) => ({ 23 | ...stripEnvironmentPairs(err), 24 | // Serialize the response part of err with the response serializer 25 | ...(err.response && { response: responseSerializer(err.response) }), 26 | }); 27 | -------------------------------------------------------------------------------- /node-src/lib/loggingRenderer.ts: -------------------------------------------------------------------------------- 1 | import UpdateRenderer from 'listr-update-renderer'; 2 | 3 | /** 4 | * The default Listr renderer to show the TUI. This also updates a log file at the same time. 5 | */ 6 | export default class LoggingRenderer { 7 | static readonly nonTTY = false; 8 | tasks; 9 | options; 10 | updateRenderer; 11 | 12 | constructor(tasks: any, options: any) { 13 | this.tasks = tasks; 14 | this.options = options; 15 | this.updateRenderer = new UpdateRenderer(tasks, options); 16 | } 17 | 18 | render() { 19 | this.updateRenderer.render(); 20 | for (const task of this.tasks) { 21 | let lastData; 22 | task.subscribe((event) => { 23 | if (event.type === 'TITLE') this.options.log.file(`${task.title}`); 24 | if (event.type === 'DATA' && lastData !== event.data) { 25 | lastData = event.data; 26 | this.options.log.file(` → ${event.data}`); 27 | } 28 | }); 29 | } 30 | } 31 | 32 | end() { 33 | this.updateRenderer.end(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /node-src/lib/nonTTYRenderer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Listr renderer to handle non-TTY environments. 3 | */ 4 | export default class NonTTYRenderer { 5 | static readonly nonTTY = true; 6 | tasks; 7 | options; 8 | 9 | constructor(tasks: any, options: any) { 10 | this.tasks = tasks; 11 | this.options = options; 12 | } 13 | 14 | render() { 15 | for (const task of this.tasks) { 16 | let lastData; 17 | task.subscribe((event) => { 18 | if (event.type === 'TITLE') this.options.log.info(`${task.title}`); 19 | if (event.type === 'DATA' && lastData !== event.data) { 20 | lastData = event.data; 21 | this.options.log.info(` → ${event.data}`); 22 | } 23 | }); 24 | } 25 | } 26 | 27 | end() { 28 | // do nothing 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /node-src/lib/posix.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | // Replaces Windows-style backslash path separators with POSIX-style forward slashes, because the 4 | // Webpack stats use forward slashes in the `name` and `moduleName` fields. Note `changedFiles` 5 | // already contains forward slashes, because that's what git yields even on Windows. 6 | export const posix = (localPath: string) => 7 | localPath.split(path.sep).filter(Boolean).join(path.posix.sep); 8 | -------------------------------------------------------------------------------- /node-src/lib/promises.ts: -------------------------------------------------------------------------------- 1 | // Double inversion on Promise.all means fulfilling with the first fulfilled promise, or rejecting 2 | // when _everything_ rejects. This is different from Promise.race, which immediately rejects on the 3 | // first rejection. 4 | const invert = (promise: Promise) => 5 | new Promise((resolve, reject) => promise.then(reject, resolve)); 6 | 7 | export const raceFulfilled = (promises: Promise[]): Promise => 8 | invert(Promise.all(promises.map((promise) => invert(promise))).then((promises) => promises[0])); 9 | 10 | export const timeout = (ms: number) => 11 | new Promise((_, rej) => { 12 | setTimeout(() => rej(new Error('Timeout while resolving Storybook view layer package')), ms); 13 | }); 14 | -------------------------------------------------------------------------------- /node-src/lib/setExitCode.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../types'; 2 | 3 | /** 4 | * Keep this in sync with https://github.com/chromaui/chromatic-docs/blob/main/cli.md#exit-codes 5 | */ 6 | export const exitCodes = { 7 | // Generic results 8 | OK: 0, 9 | UNKNOWN_ERROR: 255, 10 | 11 | // Chromatic build results 12 | BUILD_HAS_CHANGES: 1, 13 | BUILD_HAS_ERRORS: 2, 14 | BUILD_FAILED: 3, 15 | BUILD_NO_STORIES: 4, 16 | BUILD_WAS_LIMITED: 5, 17 | BUILD_WAS_CANCELED: 6, 18 | 19 | // Chromatic account issues 20 | ACCOUNT_QUOTA_REACHED: 11, 21 | ACCOUNT_PAYMENT_REQUIRED: 12, 22 | 23 | // Storybook issues 24 | STORYBOOK_BUILD_FAILED: 21, 25 | STORYBOOK_START_FAILED: 22, 26 | STORYBOOK_BROKEN: 23, 27 | 28 | // E2E errors 29 | E2E_BUILD_FAILED: 51, 30 | 31 | // Subprocess errors 32 | GIT_NOT_CLEAN: 101, 33 | GIT_OUT_OF_DATE: 102, 34 | GIT_NO_MERGE_BASE: 103, 35 | NPM_INSTALL_FAILED: 104, 36 | NPM_BUILD_STORYBOOK_FAILED: 105, 37 | 38 | // I/O errors 39 | FETCH_ERROR: 201, 40 | GRAPHQL_ERROR: 202, 41 | MISSING_DEPENDENCY: 210, 42 | VERIFICATION_TIMEOUT: 220, 43 | INVALID_OPTIONS: 254, 44 | }; 45 | 46 | export const setExitCode = (ctx: Partial, exitCode: number, userError = false) => { 47 | const [exitCodeKey] = Object.entries(exitCodes).find(([_, code]) => code === exitCode) || []; 48 | if (!exitCodeKey) throw new Error(`Invalid exitCode: ${exitCode}`); 49 | ctx.exitCode = exitCode; 50 | ctx.exitCodeKey = exitCodeKey; 51 | ctx.userError = userError; 52 | }; 53 | -------------------------------------------------------------------------------- /node-src/lib/spawn.ts: -------------------------------------------------------------------------------- 1 | import { spawn as packageCommand } from 'yarn-or-npm'; 2 | 3 | /** 4 | * Spawn a subprocess to interact with the user's package manager. 5 | * 6 | * @param args Command arguments to pass to the package manager. 7 | * @param options Options to pass to the package manager. 8 | 9 | * @returns The result from the package manager command. 10 | */ 11 | export default function spawn( 12 | args: Parameters[0], 13 | options: Parameters[1] = {} 14 | ) { 15 | return new Promise((resolve, reject) => { 16 | let stdout = ''; 17 | let stderr = ''; 18 | const child = packageCommand(args, options); 19 | child.stdout?.on('data', (chunk) => { 20 | stdout += chunk; 21 | }); 22 | child.stderr?.on('data', (chunk) => { 23 | stderr += chunk; 24 | }); 25 | child.on('error', reject); 26 | child.on('close', (code) => { 27 | if (code === 0) resolve(stdout.trim()); 28 | else reject(new Error(stderr)); 29 | }); 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /node-src/lib/testLogger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A noop logger used during tests. 3 | */ 4 | export default class TestLogger { 5 | entries: any[]; 6 | 7 | errors: any[]; 8 | 9 | warnings: any[]; 10 | 11 | constructor() { 12 | this.entries = []; 13 | this.errors = []; 14 | this.warnings = []; 15 | } 16 | 17 | error(...args) { 18 | this.entries.push(...args); 19 | this.errors.push(...args); 20 | } 21 | 22 | warn(...args) { 23 | this.entries.push(...args); 24 | this.warnings.push(...args); 25 | } 26 | 27 | info(...args) { 28 | this.entries.push(...args); 29 | } 30 | 31 | log(...args) { 32 | this.entries.push(...args); 33 | } 34 | 35 | debug(...args) { 36 | this.entries.push(...args); 37 | } 38 | 39 | queue() { 40 | // do nothing 41 | } 42 | 43 | flush() { 44 | // do nothing 45 | } 46 | 47 | setLevel() { 48 | // do nothing 49 | } 50 | 51 | setInteractive() { 52 | // do nothing 53 | } 54 | 55 | setLogFile() { 56 | // do nothing 57 | } 58 | 59 | file() { 60 | // do nothing 61 | } 62 | 63 | getLevel(): any { 64 | // do nothing 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /node-src/lib/turbosnap/compareBaseline.ts: -------------------------------------------------------------------------------- 1 | import { DepGraph } from '@snyk/dep-graph'; 2 | import { createChangedPackagesGraph } from '@snyk/dep-graph'; 3 | 4 | export const compareBaseline = async ( 5 | headDependencies: DepGraph, 6 | baselineDependencies: DepGraph 7 | ) => { 8 | const changedDependencyNames = new Set(); 9 | 10 | // createChangedPackagesGraph creates a graph of the dependencies that have changed between the 11 | // two dependency graphs but only finds removed dependencies based on the first graph argument. 12 | // Therefore, we need to run this twice to capture everything that changed. 13 | const changedPackagesFromBase = await createChangedPackagesGraph( 14 | baselineDependencies, 15 | headDependencies 16 | ); 17 | 18 | const changedPackagesFromHead = await createChangedPackagesGraph( 19 | headDependencies, 20 | baselineDependencies 21 | ); 22 | 23 | for (const pkg of changedPackagesFromBase.getDepPkgs()) { 24 | changedDependencyNames.add(pkg.name); 25 | } 26 | 27 | for (const pkg of changedPackagesFromHead.getDepPkgs()) { 28 | changedDependencyNames.add(pkg.name); 29 | } 30 | 31 | return changedDependencyNames; 32 | }; 33 | -------------------------------------------------------------------------------- /node-src/lib/viewLayers.ts: -------------------------------------------------------------------------------- 1 | export const viewLayers = { 2 | '@storybook/react': 'react', 3 | '@storybook/vue': 'vue', 4 | '@storybook/vue3': 'vue3', 5 | '@storybook/angular': 'angular', 6 | '@storybook/html': 'html', 7 | '@storybook/web-components': 'web-components', 8 | '@storybook/polymer': 'polymer', 9 | '@storybook/ember': 'ember', 10 | '@storybook/marko': 'marko', 11 | '@storybook/mithril': 'mithril', 12 | '@storybook/riot': 'riot', 13 | '@storybook/svelte': 'svelte', 14 | '@storybook/preact': 'preact', 15 | '@storybook/rax': 'rax', 16 | '@storybook/react-webpack5': '@storybook/react-webpack5', 17 | '@storybook/react-vite': 'react', 18 | } as Record; 19 | -------------------------------------------------------------------------------- /node-src/tasks/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | 3 | import { setAuthorizationToken } from './auth'; 4 | 5 | describe('setAuthorizationToken', () => { 6 | it('updates the GraphQL client with an app token from the index', async () => { 7 | const client = { runQuery: vi.fn(), setAuthorization: vi.fn() }; 8 | client.runQuery.mockReturnValue({ appToken: 'app-token' }); 9 | 10 | await setAuthorizationToken({ client, options: { projectToken: 'test' } } as any); 11 | expect(client.setAuthorization).toHaveBeenCalledWith('app-token'); 12 | }); 13 | 14 | it('supports projectId + userToken', async () => { 15 | const client = { runQuery: vi.fn(), setAuthorization: vi.fn() }; 16 | client.runQuery.mockReturnValue({ cliToken: 'cli-token' }); 17 | 18 | await setAuthorizationToken({ 19 | client, 20 | env: { CHROMATIC_INDEX_URL: 'https://index.chromatic.com' }, 21 | options: { projectId: 'Project:abc123', userToken: 'user-token' }, 22 | } as any); 23 | expect(client.setAuthorization).toHaveBeenCalledWith('cli-token'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /node-src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | import Listr from 'listr'; 2 | 3 | import { Context } from '../types'; 4 | import auth from './auth'; 5 | import build from './build'; 6 | import gitInfo from './gitInfo'; 7 | import initialize from './initialize'; 8 | import prepare from './prepare'; 9 | import prepareWorkspace from './prepareWorkspace'; 10 | import report from './report'; 11 | import restoreWorkspace from './restoreWorkspace'; 12 | import snapshot from './snapshot'; 13 | import storybookInfo from './storybookInfo'; 14 | import upload from './upload'; 15 | import verify from './verify'; 16 | 17 | export const runUploadBuild = [ 18 | auth, 19 | gitInfo, 20 | storybookInfo, 21 | initialize, 22 | build, 23 | prepare, 24 | upload, 25 | verify, 26 | snapshot, 27 | ]; 28 | 29 | export const runPatchBuild = [prepareWorkspace, ...runUploadBuild, restoreWorkspace]; 30 | 31 | /** 32 | * Prepare the list of tasks to run for a new build. 33 | * 34 | * @param ctx The context set when executing the CLI. 35 | * 36 | * @returns The list of tasks to be completed. 37 | */ 38 | export default function index(ctx: Context): Listr.ListrTask[] { 39 | const tasks = 40 | ctx.options.patchHeadRef && ctx.options.patchBaseRef ? runUploadBuild : runUploadBuild; 41 | 42 | if (ctx.options.junitReport) { 43 | tasks.push(report); 44 | } 45 | 46 | return tasks.map((task) => task(ctx)); 47 | } 48 | -------------------------------------------------------------------------------- /node-src/tasks/readStatsFile.ts: -------------------------------------------------------------------------------- 1 | import { parseChunked } from '@discoveryjs/json-ext'; 2 | import { createReadStream } from 'fs'; 3 | 4 | import { Stats } from '../types'; 5 | 6 | export const readStatsFile = async (filePath: string): Promise => { 7 | return parseChunked(createReadStream(filePath)); 8 | }; 9 | -------------------------------------------------------------------------------- /node-src/tasks/restoreWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { checkoutPrevious, discardChanges } from '../git/git'; 2 | import installDependencies from '../lib/installDependencies'; 3 | import { createTask, transitionTo } from '../lib/tasks'; 4 | import { Context } from '../types'; 5 | import { initial, pending, success } from '../ui/tasks/restoreWorkspace'; 6 | 7 | export const runRestoreWorkspace = async (ctx: Context) => { 8 | await discardChanges(ctx); // we need a clean state before checkout 9 | await checkoutPrevious(ctx); 10 | await installDependencies(); 11 | await discardChanges(ctx); // drop lockfile changes 12 | }; 13 | 14 | export default createTask({ 15 | name: 'restoreWorkspace', 16 | title: initial.title, 17 | steps: [transitionTo(pending), runRestoreWorkspace, transitionTo(success, true)], 18 | }); 19 | -------------------------------------------------------------------------------- /node-src/tasks/storybookInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | 3 | import { getStorybookBaseDirectory } from '../lib/getStorybookBaseDirectory'; 4 | import storybookInfo from '../lib/getStorybookInfo'; 5 | import { setStorybookInfo } from './storybookInfo'; 6 | 7 | vi.mock('../lib/getStorybookInfo'); 8 | vi.mock('../lib/getStorybookBaseDirectory'); 9 | 10 | const getStorybookInfo = vi.mocked(storybookInfo); 11 | const mockedGetStorybookBaseDirectory = vi.mocked(getStorybookBaseDirectory); 12 | 13 | describe('storybookInfo', () => { 14 | it('retrieves Storybook metadata and sets it on context', async () => { 15 | const storybook = { version: '1.0.0', viewLayer: 'react', addons: [] }; 16 | getStorybookInfo.mockResolvedValue(storybook); 17 | mockedGetStorybookBaseDirectory.mockReturnValue(''); 18 | 19 | const ctx = { packageJson: {}, git: { rootDir: process.cwd() } } as any; 20 | await setStorybookInfo(ctx); 21 | expect(ctx.storybook).toEqual({ 22 | ...storybook, 23 | baseDir: '', 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /node-src/tasks/storybookInfo.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node'; 2 | 3 | import { getStorybookBaseDirectory } from '../lib/getStorybookBaseDirectory'; 4 | import getStorybookInfo from '../lib/getStorybookInfo'; 5 | import { createTask, transitionTo } from '../lib/tasks'; 6 | import { Context } from '../types'; 7 | import { initial, pending, success } from '../ui/tasks/storybookInfo'; 8 | 9 | export const setStorybookInfo = async (ctx: Context) => { 10 | ctx.storybook = { 11 | ...((await getStorybookInfo(ctx)) as Context['storybook']), 12 | baseDir: getStorybookBaseDirectory(ctx), 13 | }; 14 | 15 | if (ctx.storybook) { 16 | if (ctx.storybook.version) { 17 | Sentry.setTag('storybookVersion', ctx.storybook.version); 18 | } 19 | if (ctx.storybook.viewLayer) { 20 | Sentry.setTag('storybookViewLayer', ctx.storybook.viewLayer); 21 | } 22 | Sentry.setContext('storybook', ctx.storybook); 23 | } 24 | }; 25 | 26 | /** 27 | * Sets up the Listr task for gathering Storybook information. 28 | * 29 | * @param ctx The context set when executing the CLI. 30 | * 31 | * @returns A Listr task. 32 | */ 33 | export default function main(ctx: Context) { 34 | return createTask({ 35 | name: 'storybookInfo', 36 | title: initial(ctx).title, 37 | skip: (ctx: Context) => ctx.skip, 38 | steps: [transitionTo(pending), setStorybookInfo, transitionTo(success, true)], 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /node-src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'yarn-or-npm' { 2 | import { SpawnOptions } from 'child_process'; 3 | import crossSpawn from 'cross-spawn'; 4 | 5 | export function spawn(args: string[], options?: SpawnOptions): ReturnType; 6 | 7 | export const hasYarn: () => boolean; 8 | } 9 | -------------------------------------------------------------------------------- /node-src/ui/components/activity.ts: -------------------------------------------------------------------------------- 1 | import { activityBar } from '../../lib/utils'; 2 | import { Context, Task } from '../../types'; 3 | 4 | const renderLoop = (ctx: Context, render: (frame: number) => void) => { 5 | const interval = ctx.options.interactive ? 100 : ctx.env.CHROMATIC_OUTPUT_INTERVAL; 6 | const maxFrames = ctx.env.CHROMATIC_TIMEOUT / interval; 7 | 8 | let timeout: NodeJS.Timeout; 9 | const tick = (frame = 0) => { 10 | render(frame); 11 | if (frame < maxFrames) { 12 | timeout = setTimeout(() => tick(frame + 1), interval); 13 | } 14 | }; 15 | 16 | tick(); 17 | return { 18 | end: () => clearTimeout(timeout), 19 | }; 20 | }; 21 | 22 | export const startActivity = async (ctx: Context, task: Task) => { 23 | if (ctx.options.interactive) return; 24 | ctx.activity = renderLoop(ctx, (n) => { 25 | task.output = activityBar(n); 26 | }); 27 | }; 28 | 29 | export const endActivity = (ctx: Context) => { 30 | if (ctx.activity) ctx.activity.end(); 31 | }; 32 | -------------------------------------------------------------------------------- /node-src/ui/components/icons.stories.ts: -------------------------------------------------------------------------------- 1 | import { arrowDown, arrowRight, chevronRight, error, info, success, warning } from './icons'; 2 | 3 | export default { 4 | title: 'CLI/Components/Icons', 5 | }; 6 | 7 | export const Info = () => info; 8 | export const Success = () => success; 9 | export const Warning = () => warning; 10 | export const Error = () => error; 11 | 12 | export const ArrowDown = () => arrowDown; 13 | export const ArrowRight = () => arrowRight; 14 | export const ChevronRight = () => chevronRight; 15 | -------------------------------------------------------------------------------- /node-src/ui/components/icons.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const isSupported = 4 | process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; 5 | 6 | export const cross = isSupported ? '✖' : '×'; 7 | export const check = isSupported ? '✔' : '√'; 8 | export const arrowDown = `↓`; 9 | export const arrowRight = `→`; 10 | export const chevronRight = `›`; 11 | export const spinner = `⠋`; 12 | 13 | export const info = chalk.blue(isSupported ? 'ℹ' : 'i'); 14 | export const success = chalk.green(check); 15 | export const warning = chalk.yellow(isSupported ? '⚠' : '‼'); 16 | export const error = chalk.red(cross); 17 | -------------------------------------------------------------------------------- /node-src/ui/components/link.stories.ts: -------------------------------------------------------------------------------- 1 | import link from './link'; 2 | 3 | export default { 4 | title: 'CLI/Components/Link', 5 | }; 6 | 7 | export const Default = () => link('https://www.chromatic.com'); 8 | -------------------------------------------------------------------------------- /node-src/ui/components/link.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export default (url: string) => chalk.cyan(url); 4 | -------------------------------------------------------------------------------- /node-src/ui/components/task.stories.ts: -------------------------------------------------------------------------------- 1 | import task from './task'; 2 | 3 | export default { 4 | title: 'CLI/Components/Task', 5 | }; 6 | 7 | const outputArray = ['Line one', 'Line two']; 8 | const outputString = ` 9 | Line one 10 | Line two 11 | `; 12 | 13 | export const Initial = () => 14 | task({ status: 'initial', title: 'Waiting for task to start', output: outputArray }); 15 | export const Pending = () => 16 | task({ status: 'pending', title: 'Task in progress', output: outputArray }); 17 | export const Skipped = () => 18 | task({ status: 'skipped', title: 'Task skipped', output: outputArray }); 19 | export const Success = () => 20 | task({ status: 'success', title: 'Successfully completed task', output: outputString }); 21 | export const Warning = () => 22 | task({ status: 'warning', title: 'Be aware this is a warning', output: outputString }); 23 | export const Info = () => task({ status: 'info', title: "Here's some info", output: outputString }); 24 | export const Error = () => 25 | task({ status: 'error', title: 'Something went wrong', output: outputString }); 26 | -------------------------------------------------------------------------------- /node-src/ui/components/task.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { 5 | arrowDown, 6 | arrowRight, 7 | chevronRight, 8 | error, 9 | info, 10 | spinner, 11 | success, 12 | warning, 13 | } from './icons'; 14 | 15 | const icons = { 16 | initial: chalk.gray(chevronRight), 17 | pending: chalk.yellow(spinner), 18 | skipped: chalk.magenta(arrowDown), 19 | success, 20 | warning, 21 | error, 22 | info, 23 | }; 24 | 25 | export default ({ 26 | status, 27 | title, 28 | output = [], 29 | }: { 30 | status: keyof typeof icons; 31 | title: string; 32 | output: string | string[]; 33 | }) => { 34 | const lines = Array.isArray(output) ? output : dedent(output).split('\n'); 35 | const icon = icons[status] ? `${icons[status]} ` : ''; 36 | return [ 37 | `${icon}${status === 'initial' ? title : chalk.bold(title)}`, 38 | ...lines.map((line) => chalk.dim(` ${arrowRight} ${line}`)), 39 | ].join('\n'); 40 | }; 41 | -------------------------------------------------------------------------------- /node-src/ui/html/metadata.html.stories.ts: -------------------------------------------------------------------------------- 1 | import metadataHtml from './metadata.html'; 2 | 3 | export default { 4 | title: 'HTML/Metadata index', 5 | includeStories: /^[A-Z]/, 6 | }; 7 | 8 | export const files = [ 9 | { 10 | contentLength: 833, 11 | localPath: 'build-storybook.log', 12 | targetPath: '.chromatic/build-storybook.log', 13 | }, 14 | { 15 | contentLength: 674, 16 | localPath: 'chromatic.log', 17 | targetPath: '.chromatic/chromatic.log', 18 | }, 19 | { 20 | contentLength: 3645, 21 | localPath: 'chromatic-diagnostics.json', 22 | targetPath: '.chromatic/chromatic-diagnostics.json', 23 | }, 24 | { 25 | contentLength: 423, 26 | localPath: 'main.ts', 27 | targetPath: '.chromatic/main.ts', 28 | }, 29 | { 30 | contentLength: 5635, 31 | localPath: 'preview.tsx', 32 | targetPath: '.chromatic/preview.tsx', 33 | }, 34 | { 35 | contentLength: 5635, 36 | localPath: 'preview-stats.json', 37 | targetPath: '.chromatic/preview-stats.json', 38 | }, 39 | ]; 40 | 41 | const announced: any = { announcedBuild: { number: 7805 } }; 42 | 43 | const build: any = { 44 | ...announced, 45 | build: { webUrl: 'https://www.chromatic.com/build?appId=5d67dc0374b2e300209c41e7&number=7805' }, 46 | }; 47 | 48 | const date = new Date('2023-10-12T12:05:23.706Z'); 49 | 50 | export const Default = () => metadataHtml(announced, files, date); 51 | 52 | export const WithBuildLink = () => metadataHtml(build, files, date); 53 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/brokenStorybook.stories.ts: -------------------------------------------------------------------------------- 1 | import brokenStorybook from './brokenStorybook'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | const failureReason = ` 8 | ReferenceError: foo is not defined 9 | at Module../.storybook/preview.js-generated-config-entry.js (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/main.72ad6d7a.iframe.bundle.js:1:2049) 10 | at __webpack_require__ (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/runtime~main.339b41cb.iframe.bundle.js:1:1301) 11 | at Object.0 (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/main.72ad6d7a.iframe.bundle.js:1:224364) 12 | at __webpack_require__ (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/runtime~main.339b41cb.iframe.bundle.js:1:1301) 13 | at checkDeferredModules (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/runtime~main.339b41cb.iframe.bundle.js:1:957) 14 | at Array.webpackJsonpCallback [as push] (https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/runtime~main.339b41cb.iframe.bundle.js:1:645) 15 | at https://61b0a4b8ebf0e344c2aa231c-nsoaxcirhi.capture.dev-chromatic.com/main.72ad6d7a.iframe.bundle.js:1:47 16 | `; 17 | 18 | const storybookUrl = 'https://61b0a4b8ebf0e344c2aa231c-wdooytetbw.dev-chromatic.com/'; 19 | 20 | export const BrokenStorybook = () => brokenStorybook({ failureReason, storybookUrl }); 21 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/brokenStorybook.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ failureReason, storybookUrl }) => 8 | `${dedent(chalk` 9 | ${error} {bold Failed to extract stories from your Storybook} 10 | This is usually a problem with your published Storybook, not with Chromatic. 11 | 12 | Build and open your Storybook locally and check the browser console for errors. 13 | Visit your published Storybook at ${link(storybookUrl)} 14 | The following error was encountered while running your Storybook: 15 | `)}\n\n${failureReason.trim()}`; 16 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildCanceled.stories.ts: -------------------------------------------------------------------------------- 1 | import buildCanceled from './buildCanceled'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const BuildCanceled = () => buildCanceled(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildCanceled.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} {bold Build canceled} 9 | The build was canceled before it completed. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildHasChanges.stories.ts: -------------------------------------------------------------------------------- 1 | import buildHasChanges from './buildHasChanges'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | const context = { 8 | build: { 9 | number: 42, 10 | changeCount: 2, 11 | accessibilityChangeCount: 1, 12 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 13 | app: { 14 | setupUrl: 'https://www.chromatic.com/setup?appId=59c59bd0183bd100364e1d57', 15 | }, 16 | }, 17 | exitCode: 1, 18 | isOnboarding: false, 19 | }; 20 | 21 | export const BuildHasChangesNotOnboarding = () => buildHasChanges(context); 22 | 23 | export const BuildHasChangesVisualOnly = () => 24 | buildHasChanges({ 25 | ...context, 26 | build: { ...context.build, accessibilityChangeCount: 0 }, 27 | }); 28 | 29 | export const BuildHasChangesAccessibilityOnly = () => 30 | buildHasChanges({ 31 | ...context, 32 | build: { ...context.build, changeCount: 0 }, 33 | }); 34 | 35 | export const BuildHasChangesIsOnboarding = () => 36 | buildHasChanges({ ...context, isOnboarding: true }); 37 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildHasChanges.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import pluralize from 'pluralize'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | import { error, info } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | export default ({ build, exitCode, isOnboarding }) => { 9 | const url = isOnboarding ? build.app.setupUrl : build.webUrl; 10 | 11 | const changes: any[] = []; 12 | if (build.changeCount > 0) { 13 | changes.push( 14 | chalk`${error} {bold Found ${pluralize('visual changes', build.changeCount, true)}}` 15 | ); 16 | } 17 | if (build.accessibilityChangeCount > 0) { 18 | changes.push( 19 | chalk`${error} {bold Found ${pluralize('accessibility changes', build.accessibilityChangeCount, true)}}` 20 | ); 21 | } 22 | 23 | return dedent(chalk` 24 | ${changes.join('\n')} 25 | 26 | Review the changes at ${link(url)} 27 | 28 | ${info} For CI/CD use cases, this command failed with exit code ${exitCode} 29 | Pass {bold --exit-zero-on-changes} to succeed this command regardless of changes. 30 | Pass {bold --auto-accept-changes} to succeed and automatically accept any changes. 31 | `); 32 | }; 33 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildHasErrors.stories.ts: -------------------------------------------------------------------------------- 1 | import buildHasErrors from './buildHasErrors'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const BuildHasErrors = () => 8 | buildHasErrors({ 9 | build: { 10 | errorCount: 2, 11 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 12 | }, 13 | exitCode: 1, 14 | }); 15 | export const BuildHasErrorsAndInteractionTestFailure = () => 16 | buildHasErrors({ 17 | build: { 18 | errorCount: 2, 19 | interactionTestFailuresCount: 1, 20 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 21 | }, 22 | exitCode: 1, 23 | }); 24 | 25 | export const BuildHasOnlyInteractionTestFailure = () => 26 | buildHasErrors({ 27 | build: { 28 | errorCount: 2, 29 | interactionTestFailuresCount: 2, 30 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 31 | }, 32 | exitCode: 1, 33 | }); 34 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/buildHasErrors.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import pluralize from 'pluralize'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | import { error as errorIcon, info as infoIcon } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | export default ({ build, exitCode }) => { 9 | const { errorCount, interactionTestFailuresCount, webUrl } = build; 10 | const hasInteractionTestFailures = interactionTestFailuresCount > 0; 11 | const hasOtherErrors = errorCount - interactionTestFailuresCount > 0; 12 | const failedTests = pluralize('failed test', interactionTestFailuresCount, true); 13 | 14 | let errorMessage; 15 | 16 | if (hasInteractionTestFailures && hasOtherErrors) { 17 | const errors = pluralize('build error', errorCount - interactionTestFailuresCount, true); 18 | errorMessage = `Encountered ${errors} and ${failedTests}`; 19 | } else if (hasInteractionTestFailures) { 20 | errorMessage = `Encountered ${failedTests}`; 21 | } else { 22 | const errors = pluralize('build error', errorCount, true); 23 | errorMessage = `Encountered ${errors}`; 24 | } 25 | 26 | return dedent(chalk` 27 | ${errorIcon} {bold ${errorMessage}}: failing with exit code ${exitCode} 28 | Pass {bold --allow-console-errors} to succeed this command regardless of runtime build errors. 29 | ${infoIcon} Review the errors at ${link(webUrl)} 30 | `); 31 | }; 32 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/dependentOption.stories.ts: -------------------------------------------------------------------------------- 1 | import dependentOption from './dependentOption'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const DependentOption = () => dependentOption('--untraced', '--only-changed'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/dependentOption.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (option, dependsOnOption) => 7 | dedent(chalk` 8 | ${error} Invalid {bold ${option}} 9 | This option can only be used in conjunction with {bold ${dependsOnOption}} 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/duplicatePatchBuild.stories.ts: -------------------------------------------------------------------------------- 1 | import duplicatePatchBuild from './duplicatePatchBuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const DuplicatePatchBuild = () => duplicatePatchBuild(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/duplicatePatchBuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid value to {bold --patch-build} 9 | The two branches cannot be identical. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/e2eBuildFailed.stories.ts: -------------------------------------------------------------------------------- 1 | import e2eBuildFailed from './e2eBuildFailed'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const E2EBuildFailed = () => 8 | e2eBuildFailed({ flag: 'playwright', errorMessage: 'Error Message' }); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/e2eBuildFailed.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default ({ flag, errorMessage }: { flag: string; errorMessage: string }) => { 7 | return dedent(chalk` 8 | ${error} Failed to run \`chromatic --${flag}\`: 9 | 10 | ${errorMessage} 11 | `); 12 | }; 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/fetchError.stories.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'node-fetch'; 2 | 3 | import fetchError from './fetchError'; 4 | 5 | export default { 6 | title: 'CLI/Messages/Errors', 7 | }; 8 | 9 | export const FetchError = () => 10 | fetchError( 11 | { title: 'Run a job' }, 12 | { 13 | error: { 14 | name: 'FetchError', 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore This seems to sometimes be required, sometimes disallowed. 17 | [Symbol.toStringTag]: 'FetchError', 18 | message: 19 | 'request to https://index.chromatic.com/graphql failed, reason: connect ECONNREFUSED', 20 | type: 'system', 21 | errno: 'ECONNREFUSED', 22 | code: 'ECONNREFUSED', 23 | }, 24 | } 25 | ); 26 | 27 | export const FetchError404 = () => 28 | fetchError( 29 | { title: 'Run a job' }, 30 | { statusCode: 404, response: { statusText: 'Not found' } as Response } 31 | ); 32 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/forksUnsupported.stories.ts: -------------------------------------------------------------------------------- 1 | import forksUnsupported from './forksUnsupported'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const ForksUnsupported = () => forksUnsupported(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/forksUnsupported.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${error} {bold Cross-fork PR builds unsupported in custom GitHub workflows} 10 | GitHub actions triggered by a fork do not report their repository owner, so cannot be properly linked to a pull request in Chromatic. 11 | Consider using the official Chromatic GitHub Action, or set CHROMATIC_BRANCH to include the forked repository owner (e.g. owner:branch). 12 | ${info} Read more at ${link('https://www.chromatic.com/docs/github-actions')} 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNoCommits.stories.ts: -------------------------------------------------------------------------------- 1 | import gitNoCommits from './gitNoCommits'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const GitNoCommits = () => 8 | gitNoCommits({ command: 'git log -n 1 --format="%H,%ct,%ce,%cn"' }); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNoCommits.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default ({ command }: { command: string }) => 7 | dedent(chalk` 8 | ${error} {bold Unable to execute command}: ${command} 9 | Chromatic requires your Git repository to have at least one commit. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNotInitialized.stories.ts: -------------------------------------------------------------------------------- 1 | import gitNotInitialized from './gitNotInitialized'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const GitNotInitialized = () => 8 | gitNotInitialized({ command: 'git --version' }).replaceAll('<', '<').replaceAll('>', '>'); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNotInitialized.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ command }: { command: string }) => 8 | dedent(chalk` 9 | ${error} {bold Unable to execute command}: ${command} 10 | Chromatic only works from inside a Git repository. 11 | 12 | You can initialize a new Git repository with \`git init\`. 13 | 14 | You will also need a single commit in order to run a build. To do that: 15 | 16 | - Add a file (or multiple files) with \`git add \` 17 | - Commit the file(s) with \`git commit --message=""\` 18 | 19 | Once you've done so, please run this build again. 20 | 21 | For more information on Git, feel free to check out the Pro Git book: ${link('https://git-scm.com/book/en/v2')} 22 | `); 23 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNotInstalled.stories.ts: -------------------------------------------------------------------------------- 1 | import gitNotInstalled from './gitNotInstalled'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const GitNotInstalled = () => gitNotInstalled({ command: 'git --version' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitNotInstalled.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default ({ command }: { command: string }) => 7 | dedent(chalk` 8 | ${error} {bold Unable to execute command}: ${command} 9 | Chromatic only works with Git installed. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitOneCommit.stories.ts: -------------------------------------------------------------------------------- 1 | import gitOneCommit from './gitOneCommit'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const GitOneCommit = () => gitOneCommit(); 8 | 9 | export const GitOneCommitAction = () => gitOneCommit(true); 10 | GitOneCommitAction.storyName = 'Git One Commit GitHub Action'; 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitOneCommit.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default (isGithubAction = false) => 8 | isGithubAction 9 | ? dedent(chalk` 10 | ${error} {bold Found only one commit} 11 | This typically means you have ran into one of the following scenarios: 12 | - You've checked out a shallow copy of the Git repository, which {bold actions/checkout@v2} does by default. 13 | In order for Chromatic to correctly determine baseline commits, we need access to the full Git history graph. 14 | With {bold actions/checkout@v2}, you can enable this by setting 'fetch-depth: 0'. 15 | ${info} Read more at ${link('https://www.chromatic.com/docs/github-actions')} 16 | - You've only made a single commit so far. 17 | Please make at least one additional commit in order for Chromatic to be able to detect what's changed. 18 | `) 19 | : dedent(chalk` 20 | ${error} {bold Found only one commit} 21 | This typically means you have ran into one of the following scenarios: 22 | - You've checked out a shallow copy of the Git repository, which some CI systems do by default. 23 | In order for Chromatic to correctly determine baseline commits, we need access to the full Git history graph. 24 | Refer to your CI provider's documentation for details. 25 | - You've only made a single commit so far. 26 | Please make at least one additional commit in order for Chromatic to be able to detect what's changed. 27 | `); 28 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitUserEmailNotFound.stories.ts: -------------------------------------------------------------------------------- 1 | import gitUserEmailNotFound from './gitUserEmailNotFound'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const GitUserEmailNotFound = () => gitUserEmailNotFound(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/gitUserEmailNotFound.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const localBuildsDocsLink = 8 | 'https://www.chromatic.com/docs/branching-and-baselines#what-are-local-builds'; 9 | 10 | export default () => 11 | `${dedent(chalk` 12 | ${error} {bold Failed to find the current git user's email} 13 | We were unable to find your git email so this local build 14 | will not belong to you and will not affect your future baselines. 15 | Read more: ${link(localBuildsDocsLink)} 16 | 17 | In order to associate your local changes with later CI builds, you 18 | need to configure git with the email address you'll commit with. 19 | You can do this with \`git config --global user.email YOUR_EMAIL\`. 20 | 21 | Once you've done so, please run this build again. 22 | `)}`; 23 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/graphqlError.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { GraphQLError } from '../../../io/graphqlClient'; 5 | import { error as icon } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | const lcfirst = (str: string) => `${str.charAt(0).toLowerCase()}${str.slice(1)}`; 9 | 10 | /** 11 | * Generate a failure message for a GraphQL error. 12 | * 13 | * @param context The context of the error message (which task were we running when the error occurred) 14 | * @param context.title Name of the task when the error occurred. 15 | * @param error The GraphQL error received. 16 | * @param error.message The error message from GraphQL. 17 | * @param error.extensions Additional details relating to the GraphQL error. 18 | * 19 | * @returns A message about a GraphQL error. 20 | */ 21 | export default function graphqlError( 22 | { title }: { title: string }, 23 | { message, extensions }: GraphQLError 24 | ) { 25 | const error = message 26 | ? chalk`\n{dim → ${extensions && extensions.code ? `${extensions.code}: ${message}` : message}}` 27 | : ''; 28 | return dedent(chalk` 29 | ${icon} {bold Failed to ${lcfirst(title)}} 30 | 31 | Error communicating with the Chromatic API. Check if your Chromatic client is up-to-date. 32 | Service status updates are provided at ${link('https://status.chromatic.com')} 33 | ${error} 34 | `); 35 | } 36 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/incompatibleOptions.stories.ts: -------------------------------------------------------------------------------- 1 | import incompatibleOptions from './incompatibleOptions'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const IncompatibleOptions = () => 8 | incompatibleOptions(['--junit-report', '--exit-once-uploaded']); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/incompatibleOptions.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (options: string[]) => 7 | dedent(chalk` 8 | ${error} Incompatible options: ${options.map((opt) => chalk.bold(opt)).join(', ')} 9 | These options cannot be used together. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidConfigurationFile.stories.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { invalidConfigurationFile } from './invalidConfigurationFile'; 4 | 5 | export default { 6 | title: 'CLI/Messages/Errors', 7 | }; 8 | 9 | let err; 10 | try { 11 | z.object({ 12 | a: z.string(), 13 | b: z.number(), 14 | }).parse({ a: 1, b: '1' }); 15 | } catch (error) { 16 | err = error; 17 | } 18 | 19 | export const InvalidConfigurationFile = () => invalidConfigurationFile('./my.config.json', err); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidConfigurationFile.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | import type { ZodError } from 'zod'; 4 | 5 | import { error } from '../../components/icons'; 6 | 7 | export const invalidConfigurationFile = (configFile: string, err: ZodError) => { 8 | const { formErrors, fieldErrors } = err.flatten(); 9 | 10 | return dedent(chalk` 11 | ${error} Configuration file {bold ${configFile}} was invalid, please check the allowed keys. 12 | ${ 13 | formErrors.length > 0 14 | ? `\n${formErrors.map((message) => chalk`- {bold ${message}}`).join('\n ')}\n\n` 15 | : '' 16 | } 17 | ${Object.entries(fieldErrors) 18 | .map(([field, message]) => chalk`- {bold ${field}}: ${message}`) 19 | .join('\n ')} 20 | `); 21 | }; 22 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidExitOnceUploaded.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidExitOnceUploaded from './invalidExitOnceUploaded'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidExitOnceUploaded = () => invalidExitOnceUploaded(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidExitOnceUploaded.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid {bold --exit-once-uploaded} 9 | This option is only supported when you use an uploaded build. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOnlyChanged.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidOnlyChanged from './invalidOnlyChanged'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidOnlyChanged = () => invalidOnlyChanged(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOnlyChanged.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid {bold --only-changed} 9 | This option is only supported when you use an uploaded build. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOnlyStoryNames.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidOnlyStoryNames from './invalidOnlyStoryNames'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidOnlyStoryNames = () => invalidOnlyStoryNames(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOnlyStoryNames.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid {bold --only-story-names} 9 | Value must be provided in the form {bold 'Path/To/MyStory'}. 10 | Globbing is supported, for example: 'Pages/**' 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOwnerName.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidOwnerName from './invalidOwnerName'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidOwnerName = () => invalidOwnerName('branchOwner', 'repoOwner'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidOwnerName.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (branchOwner: string, repoOwner: string) => 7 | dedent(chalk` 8 | ${error} Invalid value for {bold --branch-name} and/or {bold --repository-slug} 9 | The branch owner name prefix '${branchOwner}' does not match the repository owner '${repoOwner}'. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidPackageJson.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidPackageJson from './invalidPackageJson'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidPackageJson = () => invalidPackageJson('/path/to/package.json'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidPackageJson.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (packagePath: string) => 7 | dedent(chalk` 8 | ${error} {bold Invalid package.json} 9 | Found invalid package.json at {bold ${packagePath}} 10 | Make sure this is a valid Node.js package file, is readable, and contains a {bold "scripts"} block. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidPatchBuild.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidPatchBuild from './invalidPatchBuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidPatchBuild = () => invalidPatchBuild(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidPatchBuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const docsUrl = 'https://www.chromatic.com/docs/branching-and-baselines#patch-builds'; 8 | 9 | export default () => 10 | dedent(chalk` 11 | ${error} Invalid value for {bold --patch-build} 12 | This option expects two branch names like {bold headbranch...basebranch} 13 | ${info} Read more at ${link(docsUrl)} 14 | `); 15 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidProjectId.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidProjectId from './invalidProjectId'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidProjectId = () => invalidProjectId({ projectId: '5d67dc0374b2e300209c41e8' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidProjectId.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ projectId }: { projectId: string }) => 8 | dedent(chalk` 9 | ${error} Invalid project ID: ${projectId} 10 | You may not sufficient permissions to create builds on this project, or it may not exist. 11 | ${info} Read more at ${link('https://www.chromatic.com/docs/setup')} 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidProjectToken.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidProjectToken from './invalidProjectToken'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidProjectToken = () => invalidProjectToken({ projectToken: 'asdf123' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidProjectToken.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ projectToken }: { projectToken: string }) => 8 | dedent(chalk` 9 | ${error} Invalid {bold --project-token} '${projectToken}' 10 | You can find your project token on the Manage screen in your Chromatic project. 11 | Sign in to Chromatic at ${link('https://www.chromatic.com/start')} 12 | ${info} Read more at ${link('https://www.chromatic.com/docs/setup')} 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidReportPath.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidReportPath from './invalidReportPath'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidReportPath = () => invalidReportPath(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidReportPath.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid value for {bold --junit-report} 9 | If you pass a file path, make sure it ends with '.xml' 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidRepositorySlug.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidRepositorySlug from './invalidRepositorySlug'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidRepositorySlug = () => invalidRepositorySlug(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidRepositorySlug.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} Invalid value for {bold --repository-slug} 9 | The value must be in the format {bold ownerName/repositoryName} 10 | You can typically find this in the URL of your repository. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidSingularOptions.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidSingularOptions from './invalidSingularOptions'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidSingularOptions = () => 8 | invalidSingularOptions(['--build-script-name', '--storybook-build-dir']); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidSingularOptions.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (singularOptions: string[]) => 7 | dedent(chalk` 8 | ${error} You can only use one of {bold ${singularOptions.join(', ')}} 9 | `); 10 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidStorybookBaseDirectory.stories.ts: -------------------------------------------------------------------------------- 1 | import { invalidStorybookBaseDirectory } from './invalidStorybookBaseDirectory'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const InvalidStorybookBaseDirectory = () => invalidStorybookBaseDirectory(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/invalidStorybookBaseDirectory.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export const invalidStorybookBaseDirectory = () => 7 | dedent(chalk` 8 | ${error} TurboSnap disabled until base directory is set correctly 9 | The base directory allows TurboSnap to trace files. 10 | Set the {bold --storybook-base-dir} option as the relative path from the repository root to the Storybook project root. 11 | Run {bold @chromatic-com/turbosnap-helper} to get your base directory value. 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/maxFileCountExceeded.stories.ts: -------------------------------------------------------------------------------- 1 | import { maxFileCountExceeded } from './maxFileCountExceeded'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MaxFileCountExceeded = () => 8 | maxFileCountExceeded({ 9 | fileCount: 54_321, 10 | maxFileCount: 20_000, 11 | }); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/maxFileCountExceeded.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export const maxFileCountExceeded = ({ 7 | fileCount, 8 | maxFileCount, 9 | }: { 10 | fileCount: number; 11 | maxFileCount: number; 12 | }) => 13 | dedent(chalk` 14 | ${error} {bold Attempted to upload too many files} 15 | You're not allowed to upload more than ${maxFileCount} files per build. 16 | Your Storybook contains ${fileCount} files. This is a very high number. 17 | Do you have files in a static/public directory that shouldn't be there? 18 | Contact customer support if you need to increase this limit. 19 | `); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/maxFileSizeExceeded.stories.ts: -------------------------------------------------------------------------------- 1 | import { maxFileSizeExceeded } from './maxFileSizeExceeded'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MaxFileSizeExceeded = () => 8 | maxFileSizeExceeded({ filePaths: ['index.js', 'main.js'], maxFileSize: 12_345 }); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/maxFileSizeExceeded.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { filesize } from 'filesize'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | import { error } from '../../components/icons'; 6 | 7 | export const maxFileSizeExceeded = ({ 8 | filePaths, 9 | maxFileSize, 10 | }: { 11 | filePaths: string[]; 12 | maxFileSize: number; 13 | }) => 14 | dedent(chalk` 15 | ${error} {bold Attempted to exceed maximum file size} 16 | You're attempting to upload files that exceed the maximum file size of ${filesize(maxFileSize)}. 17 | Contact customer support if you need to increase this limit. 18 | - ${filePaths.map((path) => path).join('\n- ')} 19 | `); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/mergeBaseNotFound.stories.ts: -------------------------------------------------------------------------------- 1 | import mergeBaseNotFound from './mergeBaseNotFound'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MergeBaseNotFound = () => 8 | mergeBaseNotFound({ patchBaseRef: 'main', patchHeadRef: 'feature' }); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/mergeBaseNotFound.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const docsUrl = 8 | 'https://www.chromatic.com/docs/branching-and-baselines#how-the-merge-base-is-calculated'; 9 | 10 | export default ({ patchHeadRef, patchBaseRef }: any) => 11 | dedent(chalk` 12 | ${error} {bold Failed to retrieve the merge base} 13 | Are you sure the head branch is a descendant (i.e. fork) of the base branch? 14 | Try running this command yourself: {bold git merge-base ${patchHeadRef} ${patchBaseRef}} 15 | ${info} Read more at ${link(docsUrl)} 16 | `); 17 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingBuildScriptName.stories.ts: -------------------------------------------------------------------------------- 1 | import missingBuildScriptName from './missingBuildScriptName'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingBuildScriptName = () => missingBuildScriptName('invalid-build-script'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingBuildScriptName.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (buildScriptName: string) => 7 | dedent(chalk` 8 | ${error} {bold Build script not found} 9 | The CLI didn't find a script called {bold "${buildScriptName}"} in your {bold package.json}. 10 | Make sure you set the {bold --build-script-name} option to the value of the script name that builds your Storybook. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingConfigurationFile.stories.ts: -------------------------------------------------------------------------------- 1 | import { missingConfigurationFile } from './missingConfigurationFile'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingConfigurationFile = () => missingConfigurationFile('./my.config.json'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingConfigurationFile.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export const missingConfigurationFile = (configFile: string) => 7 | dedent(chalk` 8 | ${error} Configuration file {bold ${configFile}} could not be found. 9 | 10 | Check the {bold --config-file} flag of the CLI. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingDependency.stories.ts: -------------------------------------------------------------------------------- 1 | import missingDependency from './missingDependency'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingDependency = () => 8 | missingDependency({ dependencyName: '@chromatic-com/playwright', flag: 'playwright' }); 9 | 10 | export const MissingDependencyFromAction = () => 11 | missingDependency({ 12 | dependencyName: '@chromatic-com/playwright', 13 | flag: 'playwright', 14 | workingDir: '/opt/bin/chromatic', 15 | }); 16 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingDependency.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | 6 | export default ({ 7 | dependencyName, 8 | flag, 9 | workingDir, 10 | }: { 11 | dependencyName: string; 12 | flag: string; 13 | workingDir?: string; 14 | }) => { 15 | return dedent(chalk` 16 | ${error} Failed to import \`${dependencyName}\`, is it installed in \`package.json\`? 17 | 18 | ${info} To run \`chromatic --${flag}\` you must have \`${dependencyName}\` installed. 19 | ${ 20 | workingDir 21 | ? `\n${info} Chromatic looked in \`${workingDir}\`. If that's not the right directory, you might need to set the \`workingDir\` option to the action.` 22 | : '' 23 | } 24 | `); 25 | }; 26 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingGitHubInfo.stories.ts: -------------------------------------------------------------------------------- 1 | import missingGitHubInfo from './missingGitHubInfo'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingGitHubInfo = () => missingGitHubInfo({ GITHUB_EVENT_NAME: 'pull_request' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingGitHubInfo.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ GITHUB_EVENT_NAME }: { GITHUB_EVENT_NAME: string }) => 8 | dedent(chalk` 9 | ${error} {bold Missing GitHub environment variable} 10 | \`GITHUB_EVENT_NAME\` environment variable is set to '${GITHUB_EVENT_NAME}', but \`GITHUB_SHA\` and \`GITHUB_HEAD_REF\` are not both set. 11 | ${info} Read more at ${link('https://www.chromatic.com/docs/github-actions')} 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingProjectToken.stories.ts: -------------------------------------------------------------------------------- 1 | import missingProjectToken from './missingProjectToken'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingProjectToken = () => missingProjectToken(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingProjectToken.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${error} {bold Missing project token} 10 | 11 | Sign in to ${link('https://www.chromatic.com/start')} and create a new project, 12 | or find your project token on the Manage screen in an existing project. 13 | Set your project token as the {bold CHROMATIC_PROJECT_TOKEN} environment variable 14 | or pass the {bold --project-token} command line option. 15 | 16 | ${info} Read more at ${link('https://www.chromatic.com/docs/setup')} 17 | `); 18 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingStatsFile.stories.ts: -------------------------------------------------------------------------------- 1 | import missingStatsFile from './missingStatsFile'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingStatsFile = () => missingStatsFile({ legacy: false }); 8 | 9 | export const MissingStatsFileLegacy = () => missingStatsFile({ legacy: true }); 10 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingStatsFile.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default ({ legacy }: { legacy: boolean }) => 7 | dedent(chalk` 8 | ${error} {bold TurboSnap disabled due to missing stats file} 9 | Did not find {bold preview-stats.json} in your built Storybook. 10 | Make sure you pass {bold ${ 11 | legacy ? `--webpack-stats-json` : `--stats-json` 12 | }} when building your Storybook. 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingStories.stories.ts: -------------------------------------------------------------------------------- 1 | import missingStories from './missingStories'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingStories = () => 8 | missingStories({ 9 | options: { buildScriptName: 'build:storybook' }, 10 | buildLogFile: '/path/to/project/build-storybook.log', 11 | } as any); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingStories.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { Context } from '../../../types'; 5 | import { error } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | export default ({ options, buildLogFile }: Pick) => { 9 | const { buildScriptName } = options; 10 | return dedent(chalk` 11 | ${error} {bold Cannot run a build with no stories} 12 | 13 | Your statically built Storybook exposes no stories. This indicates a problem with your Storybook. Here's what to do: 14 | 15 | - Check the build log at {bold ${buildLogFile}} 16 | - Run {bold npm run ${buildScriptName}} or {bold yarn ${buildScriptName}} yourself and make sure it outputs a valid Storybook by opening the generated {bold index.html} in your browser. 17 | - Make sure you haven't accidently ignored all stories. See ${link( 18 | 'https://www.chromatic.com/docs/ignoring-elements#ignore-stories' 19 | )} for details. 20 | `); 21 | }; 22 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingTravisInfo.stories.ts: -------------------------------------------------------------------------------- 1 | import missingTravisInfo from './missingTravisInfo'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const MissingTravisInfo = () => missingTravisInfo({ TRAVIS_EVENT_TYPE: 'pull_request' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/missingTravisInfo.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ TRAVIS_EVENT_TYPE }: { TRAVIS_EVENT_TYPE: string }) => 8 | dedent(chalk` 9 | ${error} {bold Missing Travis environment variable} 10 | \`TRAVIS_EVENT_TYPE\` environment variable set to '${TRAVIS_EVENT_TYPE}', but 11 | \`TRAVIS_PULL_REQUEST_SHA\` and \`TRAVIS_PULL_REQUEST_BRANCH\` are not both set. 12 | ${info} Read more at ${link('https://www.chromatic.com/docs/ci#travis-ci')} 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noCSFGlobs.stories.ts: -------------------------------------------------------------------------------- 1 | import noCSFGlobs from './noCSFGlobs'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const NoCSFGlobs = () => 8 | noCSFGlobs({ 9 | statsPath: '/tmp/storybook-static/preview-stats.json', 10 | storybookDir: '.storybook', 11 | storybookBuildDir: undefined, 12 | entryFile: undefined, 13 | viewLayer: 'angular', 14 | }); 15 | 16 | export const NoCSFGlobsFoundEntry = () => 17 | noCSFGlobs({ 18 | statsPath: '/tmp/storybook-static/preview-stats.json', 19 | storybookDir: '.storybook', 20 | storybookBuildDir: undefined, 21 | entryFile: 'path/to/.storybook/generated-stories-entry.js', 22 | }); 23 | 24 | export const NoCSFGlobsFoundEntryPrebuilt = () => 25 | noCSFGlobs({ 26 | statsPath: '/tmp/storybook-static/preview-stats.json', 27 | storybookDir: '.storybook', 28 | storybookBuildDir: 'storybook-static', 29 | entryFile: 'path/to/.storybook/generated-stories-entry.js', 30 | }); 31 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noCSFGlobs.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error, info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ statsPath, storybookDir, storybookBuildDir, entryFile, viewLayer = 'react' }) => { 8 | if (entryFile) { 9 | const hint = storybookBuildDir 10 | ? chalk`Configure {bold --storybook-config-dir} with the value for {bold --config-dir} or {bold -c} from your build-storybook script.` 11 | : chalk`Configure {bold --build-script-name} to point at the {bold build-storybook} script which has {bold --config-dir} or {bold -c} set.`; 12 | return dedent(chalk` 13 | ${error} Did not find any CSF globs in {bold ${statsPath}} 14 | Found an entry file at {bold ${entryFile}} but expected it at {bold ${storybookDir}/generated-stories-entry.js}. 15 | ${hint} 16 | ${info} Read more at ${link('https://www.chromatic.com/docs/turbosnap')} 17 | `); 18 | } 19 | return dedent(chalk` 20 | ${error} Did not find any CSF globs in {bold ${statsPath}} 21 | Check your stories configuration in {bold ${storybookDir}/main.js} 22 | ${info} Read more at ${link(`https://storybook.js.org/docs/${viewLayer}/configure/overview`)} 23 | `); 24 | }; 25 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noPackageJson.stories.ts: -------------------------------------------------------------------------------- 1 | import noPackageJson from './noPackageJson'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const NoPackageJson = () => noPackageJson(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noPackageJson.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} {bold No package.json found} 9 | Chromatic only works from inside a JavaScript project. 10 | We expected to find a package.json somewhere up the directory tree. 11 | Are you sure you're running from your project directory? 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noViewLayerPackage.stories.ts: -------------------------------------------------------------------------------- 1 | import noViewLayerPackage from './noViewLayerPackage'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const NoViewLayerPackage = () => noViewLayerPackage('@storybook/vue'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/noViewLayerPackage.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default (packageName: string) => 7 | dedent(chalk` 8 | ${error} {bold Storybook package not installed} 9 | Could not find {bold ${packageName}} in {bold node_modules}. 10 | Most likely, you forgot to run {bold npm install} or {bold yarn} before running Chromatic. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/sentinelFileErrors.stories.ts: -------------------------------------------------------------------------------- 1 | import sentinelFileErrors from './sentinelFileErrors'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const SentinelFileErrors = () => sentinelFileErrors(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/sentinelFileErrors.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${error} Failed to finalize upload. Please check ${link('https://status.chromatic.com/')} or contact support. 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/taskError.stories.ts: -------------------------------------------------------------------------------- 1 | import taskError from './taskError'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const TaskError = () => taskError({ title: 'Run a job' }, new Error('Something went wrong')); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/taskError.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { error } from '../../components/icons'; 4 | 5 | /** 6 | * Generate a failure message for a task that errored. 7 | * 8 | * @param task The task object that received an error. 9 | * @param task.title The title of the errored task. 10 | * @param err The error message received from the task. 11 | * 12 | * @returns A message about a failed task. 13 | */ 14 | export default function taskError({ title }: { title: string }, err: Error) { 15 | return [chalk`${error} {bold Failed to ${lcfirst(title)}}`, err.message].join('\n'); 16 | } 17 | 18 | function lcfirst(str: string) { 19 | return `${str.charAt(0).toLowerCase()}${str.slice(1)}`; 20 | } 21 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/unparseableConfigurationFile.stories.ts: -------------------------------------------------------------------------------- 1 | import { unparseableConfigurationFile } from './unparseableConfigurationFile'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | let err; 8 | try { 9 | JSON.parse('foo'); 10 | } catch (error) { 11 | err = error; 12 | } 13 | 14 | export const UnparseableConfigurationFileJson = () => 15 | unparseableConfigurationFile('./my.config.json', err); 16 | 17 | export const UnparseableConfigurationFileJson5 = () => 18 | unparseableConfigurationFile('./my.config.json5', err); 19 | 20 | export const UnparseableConfigurationFileJsonc = () => 21 | unparseableConfigurationFile('./my.config.jsonc', err); 22 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/unparseableConfigurationFile.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export const unparseableConfigurationFile = (configFile: string, err: Error) => { 7 | const language = 8 | configFile.endsWith('.jsonc') || configFile.endsWith('.json5') ? 'JSON5' : 'JSON'; 9 | return dedent(chalk` 10 | ${error} Configuration file {bold ${configFile}} could not be parsed, is it valid ${language}? 11 | 12 | The error was: {bold ${err.message}} 13 | `); 14 | }; 15 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/uploadFailed.stories.ts: -------------------------------------------------------------------------------- 1 | import { uploadFailed } from './uploadFailed'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | const target = { 8 | contentLength: 12_345, 9 | localPath: 'local/path/to/file.js', 10 | targetPath: 'file.js', 11 | contentType: 'text/javascript', 12 | fileKey: 'file-key', 13 | filePath: 'file.js', 14 | formAction: 'https://bucket.s3.amazonaws.com/', 15 | formFields: { key: 'file-key', 'Content-Type': 'text/javascript' }, 16 | }; 17 | 18 | export const UploadFailed = () => uploadFailed({ target }); 19 | 20 | export const UploadFailedDebug = () => uploadFailed({ target }, true); 21 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/uploadFailed.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { FileDesc, TargetInfo } from '../../../types'; 5 | import { error as icon } from '../../components/icons'; 6 | 7 | /** 8 | * Generate a failure message for a file that failed to upload. 9 | * 10 | * @param file Information about the file that failed to upload. 11 | * @param file.target Path information for the file. 12 | * @param debug Enable debug output. 13 | * 14 | * @returns A message about a file that failed to upload. 15 | */ 16 | export function uploadFailed({ target }: { target: FileDesc & TargetInfo }, debug = false) { 17 | const diagnosis = 18 | encode(target.targetPath) === target.targetPath 19 | ? 'The file may have been modified during the upload process.' 20 | : 'It seems the file path may contain illegal characters.'; 21 | const message = dedent(chalk` 22 | ${icon} Failed to upload {bold ${target.localPath}} to {bold ${target.targetPath}} 23 | ${diagnosis} 24 | ${debug ? '' : chalk`Enable the {bold debug} option to get more information.`} 25 | `); 26 | return debug ? message + JSON.stringify(target, undefined, 2) : message; 27 | } 28 | 29 | function encode(path: string) { 30 | return path 31 | .split('/') 32 | .map((component) => encodeURIComponent(component)) 33 | .join('/'); 34 | } 35 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/workspaceNotClean.stories.ts: -------------------------------------------------------------------------------- 1 | import workspaceNotClean from './workspaceNotClean'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Errors', 5 | }; 6 | 7 | export const WorkspaceNotClean = () => workspaceNotClean(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/workspaceNotClean.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { error } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${error} {bold Workspace not clean} 9 | The git working directory must be clean before running a patch build. 10 | Use {bold git stash --include-untracked --keep-index} to stash changes before you continue. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/workspaceNotUpToDate.stories.ts: -------------------------------------------------------------------------------- 1 | import { dedent } from 'ts-dedent'; 2 | 3 | import workspaceNotUpToDate from './workspaceNotUpToDate'; 4 | 5 | export default { 6 | title: 'CLI/Messages/Errors', 7 | }; 8 | 9 | const statusMessage = dedent(` 10 | Your branch and 'origin/new-ui' have diverged, 11 | and have 1 and 1 different commits each, respectively. 12 | (use "git pull" to merge the remote branch into yours) 13 | `); 14 | 15 | export const WorkspaceNotUpToDate = () => workspaceNotUpToDate(statusMessage); 16 | -------------------------------------------------------------------------------- /node-src/ui/messages/errors/workspaceNotUpToDate.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { error } from '../../components/icons'; 4 | 5 | export default (statusMessage) => 6 | chalk`${error} {bold Workspace not up-to-date with remote}\n${statusMessage}`; 7 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/addedScript.stories.ts: -------------------------------------------------------------------------------- 1 | import addedScript from './addedScript'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const AddedScript = () => addedScript('chromatic'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/addedScript.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, success } from '../../components/icons'; 5 | 6 | export default (scriptName: string) => 7 | dedent(chalk` 8 | ${success} {bold Added script '${scriptName}' to package.json} 9 | You can now run it here or in CI with 'npm run ${scriptName}' or 'yarn ${scriptName}'. 10 | 11 | ${info} Your project token was added to the script via the {bold --project-token} flag. 12 | If you're running Chromatic via continuous integration, we recommend setting 13 | the {bold CHROMATIC_PROJECT_TOKEN} environment variable in your CI environment. 14 | You can then remove the {bold --project-token} from your package.json script. 15 | `); 16 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/buildPassedE2E.stories.ts: -------------------------------------------------------------------------------- 1 | import buildPassed from './buildPassed'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info/E2E', 5 | }; 6 | 7 | const ctx = { options: { playwright: true } } as any; 8 | 9 | export const BuildPassed = () => 10 | buildPassed({ 11 | ...ctx, 12 | build: { 13 | number: 42, 14 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 15 | changeCount: 0, 16 | }, 17 | } as any); 18 | 19 | export const BuildPassedWithChanges = () => 20 | buildPassed({ 21 | ...ctx, 22 | build: { 23 | number: 42, 24 | webUrl: 'https://www.chromatic.com/build?appId=59c59bd0183bd100364e1d57&number=42', 25 | changeCount: 2, 26 | }, 27 | } as any); 28 | 29 | export const FirstBuildPassed = () => 30 | buildPassed({ 31 | ...ctx, 32 | isOnboarding: true, 33 | build: { 34 | number: 1, 35 | testCount: 10, 36 | componentCount: 5, 37 | specCount: 8, 38 | actualCaptureCount: 20, 39 | app: { setupUrl: 'https://www.chromatic.com/setup?appId=59c59bd0183bd100364e1d57' }, 40 | }, 41 | } as any); 42 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/customGitHubAction.stories.ts: -------------------------------------------------------------------------------- 1 | import customGitHubAction from './customGitHubAction'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const CustomGitHubAction = () => customGitHubAction(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/customGitHubAction.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${info} {bold Use our GitHub Action} 10 | It appears you are using a GitHub Actions workflow, but are not using the official GitHub Action for Chromatic. 11 | Find it at ${link('https://github.com/marketplace/actions/publish-to-chromatic')} 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/forceRebuildHint.stories.ts: -------------------------------------------------------------------------------- 1 | import forceRebuildHint from './forceRebuildHint'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const ForceRebuildHint = () => forceRebuildHint(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/forceRebuildHint.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | import { skippedRebuild } from '../../tasks/gitInfo'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${info} {bold ${skippedRebuild().output}} 10 | A build for the same commit as the last build on the branch is considered a rebuild. 11 | If the last build is passed or accepted, the rebuild is skipped because it shouldn't change anything. 12 | You can override this using the {bold --force-rebuild} flag. 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/intro.stories.ts: -------------------------------------------------------------------------------- 1 | import intro from './intro'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | const pkg = { 8 | name: 'chromatic', 9 | version: '4.0.3', 10 | description: 'Visual Testing for Storybook', 11 | homepage: 'https://www.chromatic.com', 12 | docs: 'https://www.chromatic.com/docs/cli', 13 | bugs: { 14 | url: 'https://github.com/chromaui/chromatic-cli', 15 | email: 'support@chromatic.com', 16 | }, 17 | }; 18 | 19 | export const Intro = () => intro({ pkg }); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/intro.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { Context } from '../../../types'; 5 | 6 | export default ({ pkg }: Pick) => 7 | dedent(chalk` 8 | {bold Chromatic CLI v${pkg.version}} 9 | {dim ${pkg.docs}} 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/listingStories.stories.ts: -------------------------------------------------------------------------------- 1 | import listingStories from './listingStories'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | const snapshots = [ 8 | { spec: { name: 'MyStory', component: { name: 'Path/To/MyComponent' } } }, 9 | { spec: { name: 'AnotherStory', component: { name: 'Path/To/MyComponent' } } }, 10 | { spec: { name: 'SomeStory', component: { name: 'Path/To/AnotherComponent' } } }, 11 | ]; 12 | 13 | export const ListingStories = () => listingStories(snapshots); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/listingStories.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | 6 | interface Spec { 7 | name: string; 8 | component: { name: string }; 9 | } 10 | 11 | const snapshotRow = ({ spec }: { spec: Spec }) => 12 | chalk`{dim → }${spec.component.name}/${spec.name}`; 13 | 14 | export default (snapshots: { spec: Spec }[]) => 15 | dedent(chalk` 16 | {bold Listing available stories:} 17 | ${snapshots.map((snapshot) => snapshotRow(snapshot)).join('\n')} 18 | 19 | ${info} Use {bold --only-story-names} to run a build for a specific component or story. 20 | Globs are supported, for example: {bold --only-story-names "${ 21 | snapshots[0].spec.component.name 22 | }/**"} 23 | `); 24 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/notAddedScript.stories.ts: -------------------------------------------------------------------------------- 1 | import notAddedScript from './notAddedScript'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const NotAddedScript = () => 8 | notAddedScript('chromatic', 'chromatic --project-token=1234asd'); 9 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/notAddedScript.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | 6 | export default (scriptName: string, scriptCommand: string) => { 7 | const script = dedent` 8 | "scripts": { 9 | "${scriptName}": "${scriptCommand}" 10 | } 11 | `; 12 | return dedent(chalk` 13 | ${info} No problem. You can add it to your package.json yourself like so: 14 | {dim ${script}} 15 | `); 16 | }; 17 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/replacedBuild.stories.ts: -------------------------------------------------------------------------------- 1 | import replacedBuild from './replacedBuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | component: replacedBuild, 6 | }; 7 | 8 | export const ReplacedBuild = { 9 | args: { 10 | replacedBuild: { number: 4, commit: 'ae376da36bf2a5846e5543de97a8f0c7abce7dd9' }, 11 | replacementBuild: { number: 2, commit: 'f70da5a947035877c85eee5cb6588aaf4ef2c481' }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/replacedBuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | 6 | function commit(build) { 7 | return build.commit.slice(0, 7); 8 | } 9 | 10 | export default ({ replacedBuild, replacementBuild }) => 11 | dedent(chalk` 12 | ${info} {bold Missing commit detected} 13 | When detecting git changes for TurboSnap, we couldn't find the commit (${commit( 14 | replacedBuild 15 | )}) for the most recent build (#${replacedBuild.number}). 16 | To avoid re-snapshotting stories we know haven't changed, we copied from the most recent build (#${ 17 | replacementBuild.number 18 | }) that did have a commit (${commit(replacementBuild)}) instead. 19 | `); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/speedUpCI.stories.ts: -------------------------------------------------------------------------------- 1 | import speedUpCI from './speedUpCI'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const SpeedUpCI = () => speedUpCI('github'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/speedUpCI.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const providers = { 8 | github: 'GitHub', 9 | gitlab: 'GitLab', 10 | bitbucket: 'Bitbucket', 11 | }; 12 | 13 | export default (provider: string) => 14 | dedent(chalk` 15 | ${info} {bold Speed up Continuous Integration} 16 | Your project is linked to ${providers[provider]} so Chromatic will report results there. 17 | This means you can add the option \`with: exitOnceUploaded: true\` to your workflow to skip waiting for build results. 18 | Read more here: ${link('https://www.chromatic.com/docs/github-actions#available-options')} 19 | `); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/storybookPublished.stories.ts: -------------------------------------------------------------------------------- 1 | import storybookPublished from './storybookPublished'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | const ctx = { options: {} }; 8 | 9 | export const StorybookPublished = () => 10 | storybookPublished({ 11 | ...ctx, 12 | build: {}, 13 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 14 | } as any); 15 | 16 | export const StorybookPrepared = () => 17 | storybookPublished({ 18 | ...ctx, 19 | build: { 20 | actualCaptureCount: undefined, 21 | actualTestCount: undefined, 22 | testCount: undefined, 23 | changeCount: undefined, 24 | errorCount: undefined, 25 | componentCount: 5, 26 | specCount: 8, 27 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 28 | }, 29 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 30 | } as any); 31 | 32 | export const StorybookPreparedWithIncompleteBuild = () => 33 | storybookPublished({ 34 | ...ctx, 35 | build: { 36 | actualCaptureCount: undefined, 37 | actualTestCount: undefined, 38 | testCount: undefined, 39 | changeCount: undefined, 40 | errorCount: undefined, 41 | componentCount: undefined, 42 | specCount: undefined, 43 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 44 | }, 45 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 46 | } as any); 47 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/storybookPublished.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { isE2EBuild } from '../../../lib/e2eUtils'; 5 | import { Context } from '../../../types'; 6 | import { info, success } from '../../components/icons'; 7 | import link from '../../components/link'; 8 | import { stats } from '../../tasks/snapshot'; 9 | import { buildType, capitalize } from '../../tasks/utils'; 10 | 11 | export default (ctx: Context) => { 12 | if (!ctx.storybookUrl) { 13 | throw new Error('No Storybook URL provided'); 14 | } 15 | 16 | const result = [chalk`${success} {bold ${capitalize(buildType(ctx))} published}`]; 17 | 18 | // `ctx.build` is initialized and overwritten in many ways, which means that 19 | // this can be any kind of build without component and stories information, 20 | // like PASSED builds, for example 21 | if (ctx.build.componentCount && ctx.build.specCount) { 22 | const { components, stories, e2eTests } = stats({ build: ctx.build }); 23 | 24 | result.push( 25 | isE2EBuild(ctx.options) ? `We found ${e2eTests}.` : `We found ${components} with ${stories}.` 26 | ); 27 | } 28 | 29 | result.push(`${info} View your ${buildType(ctx)} at ${link(ctx.storybookUrl)}`); 30 | 31 | return dedent(chalk`${result.join('\n')}`); 32 | }; 33 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/storybookPublishedE2E.stories.ts: -------------------------------------------------------------------------------- 1 | import storybookPublished from './storybookPublished'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info/E2E', 5 | }; 6 | 7 | const ctx = { options: { playwright: true } } as any; 8 | 9 | export const StorybookPublished = () => 10 | storybookPublished({ 11 | ...ctx, 12 | build: {}, 13 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 14 | } as any); 15 | 16 | export const StorybookPrepared = () => 17 | storybookPublished({ 18 | ...ctx, 19 | build: { 20 | actualCaptureCount: undefined, 21 | actualTestCount: undefined, 22 | testCount: undefined, 23 | changeCount: undefined, 24 | errorCount: undefined, 25 | componentCount: 5, 26 | specCount: 8, 27 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 28 | }, 29 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 30 | } as any); 31 | 32 | export const StorybookPreparedWithIncompleteBuild = () => 33 | storybookPublished({ 34 | ...ctx, 35 | build: { 36 | actualCaptureCount: undefined, 37 | actualTestCount: undefined, 38 | testCount: undefined, 39 | changeCount: undefined, 40 | errorCount: undefined, 41 | componentCount: undefined, 42 | specCount: undefined, 43 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 44 | }, 45 | storybookUrl: 'https://5d67dc0374b2e300209c41e7-pfkaemtlit.chromatic.com/', 46 | } as any); 47 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/turboSnapEnabled.stories.ts: -------------------------------------------------------------------------------- 1 | import turboSnapEnabled from './turboSnapEnabled'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | args: { 6 | build: { actualCaptureCount: 12, inheritedCaptureCount: 42 }, 7 | options: {}, 8 | }, 9 | }; 10 | 11 | export const TurboSnapEnabled = (args: any) => turboSnapEnabled(args); 12 | 13 | export const TurboSnapEnabledInteractive = (args: any) => 14 | turboSnapEnabled({ ...args, options: { interactive: true } }); 15 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/turboSnapEnabled.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import pluralize from 'pluralize'; 3 | import { dedent } from 'ts-dedent'; 4 | 5 | import { Context } from '../../../types'; 6 | import { success } from '../../components/icons'; 7 | 8 | export default ({ 9 | build, 10 | options, 11 | skipSnapshots, 12 | }: Pick) => { 13 | const captures = pluralize('snapshot', build.actualCaptureCount, true); 14 | const skips = pluralize('snapshot', build.inheritedCaptureCount, true); 15 | return !options.interactive || skipSnapshots 16 | ? dedent(chalk` 17 | ${success} {bold TurboSnap enabled} 18 | Capturing ${captures} and skipping ${skips}. 19 | `) 20 | : dedent(chalk` 21 | ${success} {bold TurboSnap enabled} 22 | Captured ${captures} and skipped ${skips}. 23 | `); 24 | }; 25 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/uploadingMetadata.stories.ts: -------------------------------------------------------------------------------- 1 | import { files } from '../../html/metadata.html.stories'; 2 | import uploadingMetadata from './uploadingMetadata'; 3 | 4 | export default { 5 | title: 'CLI/Messages/Info', 6 | }; 7 | 8 | const directoryUrl = 'https://5d67dc0374b2e300209c41e7-dlmmxasauj.chromatic.com/.chromatic/'; 9 | 10 | export const UploadingMetadata = () => uploadingMetadata(directoryUrl, files); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/uploadingMetadata.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import pluralize from 'pluralize'; 3 | 4 | import { FileDesc } from '../../../types'; 5 | import { info } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | export default (directoryUrl: string, files: FileDesc[]) => { 9 | const count = pluralize('metadata file', files.length, true); 10 | const list = `- ${files.map((f) => f.targetPath.replace(/^\.chromatic\//, '')).join('\n- ')}`; 11 | return chalk`${info} Uploading {bold ${count}} to ${link(directoryUrl)}\n${list}`; 12 | }; 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/wroteReport.stories.ts: -------------------------------------------------------------------------------- 1 | import wroteReport from './wroteReport'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Info', 5 | }; 6 | 7 | export const WroteChromaticDiagnostics = () => 8 | wroteReport('./chromatic-diagnostics.json', 'Chromatic diagnostics'); 9 | 10 | export const WroteJUnitReport = () => wroteReport('./chromatic-build-123.xml', 'JUnit XML'); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/info/wroteReport.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { info } from '../../components/icons'; 4 | 5 | export default (filePath: string, label: string) => 6 | chalk`${info} Wrote ${label} report to {bold ${filePath}}`; 7 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/bailFile.stories.ts: -------------------------------------------------------------------------------- 1 | import bailFile from './bailFile'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const BailPackageFile = () => 8 | bailFile({ 9 | turboSnap: { bailReason: { changedPackageFiles: ['services/webapp/package.json'] } }, 10 | }); 11 | 12 | export const BailLockfile = () => 13 | bailFile({ 14 | turboSnap: { bailReason: { changedPackageFiles: ['services/webapp/yarn.lock'] } }, 15 | }); 16 | 17 | export const BailStaticFile = () => 18 | bailFile({ 19 | turboSnap: { bailReason: { changedStaticFiles: ['static/assets/fonts/percolate.woff'] } }, 20 | }); 21 | 22 | export const BailStorybookFile = () => 23 | bailFile({ 24 | turboSnap: { bailReason: { changedStorybookFiles: ['.storybook/preview-head.html'] } }, 25 | }); 26 | 27 | export const BailTwoFiles = () => 28 | bailFile({ 29 | turboSnap: { 30 | bailReason: { 31 | changedStorybookFiles: ['.storybook/preview-head.html', '.storybook/manager-head.html'], 32 | }, 33 | }, 34 | }); 35 | 36 | export const BailThreeFiles = () => 37 | bailFile({ 38 | turboSnap: { 39 | bailReason: { 40 | changedStorybookFiles: [ 41 | '.storybook/preview-head.html', 42 | '.storybook/manager-head.html', 43 | '.storybook/global-styles.css', 44 | ], 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/buildLimited.stories.ts: -------------------------------------------------------------------------------- 1 | import buildLimited from './buildLimited'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const BuildLimited = () => 8 | buildLimited({ 9 | billingUrl: 'https://www.chromatic.com/billing?accountId=5af25af03c9f2c4bdccc0fcb', 10 | }); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/buildLimited.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ billingUrl }: { billingUrl: string }) => 8 | dedent(chalk` 9 | ${warning} {bold Build limited} 10 | Visit ${link(billingUrl)} to verify your billing details. 11 | `); 12 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/deprecatedOption.stories.ts: -------------------------------------------------------------------------------- 1 | import deprecatedOption from './deprecatedOption'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const DeprecatedOption = () => deprecatedOption({ flag: 'preserveMissing' }); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/deprecatedOption.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { Flags } from '../../../types'; 5 | import { warning } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | const changelogUrl = 'https://github.com/chromaui/chromatic-cli/blob/main/CHANGELOG.md'; 9 | 10 | const snakify = (option: string) => `--${option.replaceAll(/[A-Z]/g, '-$&').toLowerCase()}`; 11 | 12 | export default ({ flag, replacement }: { flag: keyof Flags; replacement?: keyof Flags }) => 13 | dedent(chalk` 14 | ${warning} {bold Using deprecated option: ${snakify(flag)}} 15 | This option is ${ 16 | replacement ? chalk`superceded by {bold ${snakify(replacement)}}` : 'deprecated' 17 | } and may be removed in a future release. 18 | Refer to the changelog for more information: ${link(changelogUrl)} 19 | `); 20 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/deviatingOutputDirectory.stories.ts: -------------------------------------------------------------------------------- 1 | import deviatingOutputDirectory from './deviatingOutputDirectory'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | const withCustomScript = { scripts: { 'build:storybook': './run-storybook-build' } }; 8 | const withChainedScript = { scripts: { 'build:storybook': 'build-storybook && true' } }; 9 | const withNpmRunScript = { scripts: { 'build:storybook': 'npm run build-storybook' } }; 10 | 11 | const ctx = { 12 | sourceDir: '/var/folders/h3/ff9kk23958l99z2qbzfjdlxc0000gn/T/chromatic-20036LMP9FAlLEjpu', 13 | options: { buildScriptName: 'build:storybook' }, 14 | }; 15 | 16 | const outputDirectory = '/users/me/project/storybook-static'; 17 | 18 | export const DeviatingOutputDirectory = () => 19 | deviatingOutputDirectory({ ...ctx, packageJson: withCustomScript } as any, outputDirectory); 20 | 21 | export const DeviatingOutputDirectoryChained = () => 22 | deviatingOutputDirectory({ ...ctx, packageJson: withChainedScript } as any, outputDirectory); 23 | 24 | export const DeviatingOutputDirectoryNpmRun = () => 25 | deviatingOutputDirectory({ ...ctx, packageJson: withNpmRunScript } as any, outputDirectory); 26 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/externalsChanged.stories.ts: -------------------------------------------------------------------------------- 1 | import externalsChanged from './externalsChanged'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const ExternalChanged = () => externalsChanged(['./styles/main.scss']); 8 | 9 | export const ExternalsChanged = () => 10 | externalsChanged(['./styles/main.scss', './styles/font.woff']); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/externalsChanged.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const docsUrl = 'https://www.chromatic.com/docs/turbosnap#how-it-works'; 8 | 9 | export default (files: string[]) => { 10 | const count = files.length === 1 ? 'file' : `${files.length} files`; 11 | const names = files.map((f) => chalk.bold(f)); 12 | const listing = files.length === 1 ? names[0] : chalk`\n{dim →} ${names.join(chalk`\n{dim →} `)}`; 13 | return dedent(chalk` 14 | ${warning} {bold TurboSnap disabled due to matching --externals} 15 | Found ${count} with changes: ${listing} 16 | ${info} Read more at ${link(docsUrl)} 17 | `); 18 | }; 19 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/invalidChangedFiles.stories.ts: -------------------------------------------------------------------------------- 1 | import invalidChangedFiles from './invalidChangedFiles'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const InvalidChangedFiles = () => invalidChangedFiles(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/invalidChangedFiles.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const docsUrl = 'https://www.chromatic.com/docs/turbosnap#how-it-works'; 8 | 9 | export default () => 10 | dedent(chalk` 11 | ${warning} {bold TurboSnap disabled due to missing git history} 12 | Could not retrieve changed files since baseline commit(s). 13 | This typically happens after rebasing, force pushing, or when running against an ephemeral merge commit. 14 | ${info} Read more at ${link(docsUrl)} 15 | `); 16 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/isRebuild.stories.ts: -------------------------------------------------------------------------------- 1 | import isRebuild from './isRebuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const IsRebuild = () => isRebuild(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/isRebuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { warning } from '../../components/icons'; 5 | 6 | export default () => 7 | dedent(chalk` 8 | ${warning} {bold TurboSnap disabled due to rebuild} 9 | You appear to be rerunning an earlier build, because the baseline build has the same commit and branch name. 10 | Comparing against the same commit would yield zero changed files, so we would end up running a build with no snapshots. 11 | That's probably not what you want when rerunning a build, so we're just going to run a full build instead. 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/noAncestorBuild.stories.ts: -------------------------------------------------------------------------------- 1 | import noAncestorBuild from './noAncestorBuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | args: { 6 | announcedBuild: { number: 123 }, 7 | }, 8 | }; 9 | 10 | export const NoAncestorBuild = (args: any) => noAncestorBuild(args); 11 | 12 | export const NoAncestorBuildTurboSnap = (args: any) => noAncestorBuild({ ...args, turboSnap: {} }); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/noAncestorBuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { Context } from '../../../types'; 5 | import { info, warning } from '../../components/icons'; 6 | import link from '../../components/link'; 7 | 8 | const docsUrl = 9 | 'https://www.chromatic.com/docs/test#why-do-i-see-build-x-is-based-on-a-commit-without-ancestor-build'; 10 | 11 | export default ({ announcedBuild, turboSnap }: Pick) => 12 | turboSnap 13 | ? dedent(chalk` 14 | ${warning} {bold TurboSnap disabled due to missing ancestor build} 15 | An ancestor is required to determine which files have changed since the last Chromatic build. 16 | This usually happens when rebasing, force-pushing, squash-merging or running against an ephemeral merge commit. 17 | ${info} Read more at ${link(docsUrl)} 18 | `) 19 | : dedent(chalk` 20 | ${warning} {bold No ancestor build found} 21 | Build ${announcedBuild.number} is based on a commit without ancestor builds, which is unusual. 22 | This usually happens when rebasing, force-pushing, squash-merging or running against an ephemeral merge commit. 23 | ${info} Read more at ${link(docsUrl)} 24 | `); 25 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/noCommitDetails.stories.ts: -------------------------------------------------------------------------------- 1 | import noCommitDetails from './noCommitDetails'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const NoCommitDetails = () => 8 | noCommitDetails({ 9 | sha: '57745fe300dfee73c8c068154867c9366ad5ab99', 10 | }); 11 | 12 | export const NoCommitDetailsEnvironment = () => 13 | noCommitDetails({ 14 | sha: '57745fe300dfee73c8c068154867c9366ad5ab99', 15 | env: 'CHROMATIC_SHA', 16 | }); 17 | 18 | export const NoCommitDetailsBranch = () => 19 | noCommitDetails({ 20 | ref: 'feature/example', 21 | sha: '57745fe300dfee73c8c068154867c9366ad5ab99', 22 | env: 'GITHUB_HEAD_REF', 23 | }); 24 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/outdatedPackage.stories.ts: -------------------------------------------------------------------------------- 1 | import outdatedPackage from './outdatedPackage'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | const ctx = { pkg: { version: '3.0.1' } }; 8 | 9 | export const OutdatedPackage = () => outdatedPackage(ctx as any, '4.0.0'); 10 | export const OutdatedYarnPackage = () => outdatedPackage(ctx as any, '4.0.0', true); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/outdatedPackage.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { InitialContext } from '../../..'; 5 | import { warning } from '../../components/icons'; 6 | 7 | export default (ctx: InitialContext, latestVersion: string, hasYarn = false) => { 8 | const installScript = hasYarn 9 | ? `yarn upgrade chromatic --latest` 10 | : `npm install chromatic@latest --save-dev`; 11 | 12 | return dedent(chalk` 13 | ${warning} {bold Using outdated package} 14 | You are using an outdated version of the Chromatic CLI (v${ctx.pkg.version}). 15 | A new major version is available (v${latestVersion}). 16 | Run {bold ${installScript}} to upgrade. 17 | `); 18 | }; 19 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/paymentRequired.stories.ts: -------------------------------------------------------------------------------- 1 | import paymentRequired from './paymentRequired'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const PaymentRequired = () => 8 | paymentRequired({ 9 | billingUrl: 'https://www.chromatic.com/billing?accountId=5af25af03c9f2c4bdccc0fcb', 10 | }); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/paymentRequired.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ billingUrl }: { billingUrl: string }) => 8 | dedent(chalk` 9 | ${warning} {bold Payment required} 10 | This build is limited because your account has a payment past due. 11 | Visit ${link(billingUrl)} to update your billing details. 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/scriptNotFound.stories.ts: -------------------------------------------------------------------------------- 1 | import scriptNotFound from './scriptNotFound'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const ScriptNotFound = () => scriptNotFound('chromatic'); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/scriptNotFound.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { warning } from '../../components/icons'; 5 | 6 | export default (scriptName: string) => 7 | dedent(chalk` 8 | ${warning} {bold No '${scriptName}' script found in your package.json} 9 | Would you like me to add it for you? [y/N] 10 | `); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/snapshotQuotaReached.stories.ts: -------------------------------------------------------------------------------- 1 | import snapshotQuotaReached from './snapshotQuotaReached'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const SnapshotQuotaReached = () => 8 | snapshotQuotaReached({ 9 | billingUrl: 'https://www.chromatic.com/billing?accountId=5af25af03c9f2c4bdccc0fcb', 10 | }); 11 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/snapshotQuotaReached.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ billingUrl }: { billingUrl: string }) => 8 | dedent(chalk` 9 | ${warning} {bold Snapshot quota reached} 10 | This build is limited because your account is out of snapshots for the month. 11 | Visit ${link(billingUrl)} to upgrade your plan. 12 | `); 13 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/travisInternalBuild.stories.ts: -------------------------------------------------------------------------------- 1 | import travisInternalBuild from './travisInternalBuild'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const TravisInternalBuild = () => travisInternalBuild(); 8 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/travisInternalBuild.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default () => 8 | dedent(chalk` 9 | ${warning} {bold Running on a Travis PR build from an internal branch} 10 | It is recommended to run Chromatic on the push builds from Travis where possible. 11 | We advise turning on push builds and disabling Chromatic for internal PR builds. 12 | ${info} Read more at ${link('https://www.chromatic.com/docs/ci#travis-ci')} 13 | `); 14 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/turboSnapUnavailable.stories.ts: -------------------------------------------------------------------------------- 1 | import turboSnapUnavailable from './turboSnapUnavailable'; 2 | 3 | export default { 4 | title: 'CLI/Messages/Warnings', 5 | }; 6 | 7 | export const TurboSnapUnavailable = () => 8 | turboSnapUnavailable({ 9 | build: { 10 | app: { 11 | manageUrl: 'https://www.chromatic.com/manage?appId=59c59bd0183bd100364e1d57', 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/turboSnapUnavailable.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | export default ({ build }) => 8 | dedent(chalk` 9 | ${warning} {bold TurboSnap not available for your account} 10 | To ensure your project is fully setup and baselines are properly established, 11 | TurboSnap is not available until at least 10 builds are created from CI and one 12 | of those builds is accepted. 13 | 14 | ${info} Review your TurboSnap availability on the Manage screen: 15 | ${link(build.app.manageUrl)} 16 | `); 17 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/undefinedBranchOwner.stories.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'CLI/Messages/Warnings', 3 | }; 4 | 5 | export { default as UndefinedBranchOwner } from './undefinedBranchOwner'; 6 | -------------------------------------------------------------------------------- /node-src/ui/messages/warnings/undefinedBranchOwner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { dedent } from 'ts-dedent'; 3 | 4 | import { info, warning } from '../../components/icons'; 5 | import link from '../../components/link'; 6 | 7 | const docsUrl = 'https://www.chromatic.com/docs/faq/override-branch-name/'; 8 | 9 | export default () => { 10 | return dedent(chalk` 11 | ${warning} Removing unknown owner prefix from branch name. 12 | You may wish to set the branch directly to avoid incorrect values. 13 | ${info} Read more at ${link(docsUrl)} 14 | `); 15 | }; 16 | -------------------------------------------------------------------------------- /node-src/ui/tasks/auth.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { authenticated, authenticating, initial, invalidToken } from './auth'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Auth', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const environment = { CHROMATIC_INDEX_URL: 'https://index.chromatic.com' }; 10 | const options = { projectToken: '3cm6b49xnld' }; 11 | 12 | export const Initial = () => initial; 13 | export const Authenticating = () => authenticating({ env: environment } as any); 14 | export const Authenticated = () => authenticated({ env: environment, options } as any); 15 | export const InvalidToken = () => invalidToken({ env: environment, options } as any); 16 | -------------------------------------------------------------------------------- /node-src/ui/tasks/auth.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../types'; 2 | 3 | const mask = (secret: string) => '*'.repeat(secret.length - 4) + secret.slice(-4); 4 | 5 | const environment = (indexUrl: string) => { 6 | if (indexUrl.includes('dev')) return ' [dev]'; 7 | if (indexUrl.includes('staging')) return ' [staging]'; 8 | return ''; 9 | }; 10 | 11 | export const initial = { 12 | status: 'initial', 13 | title: 'Authenticate', 14 | }; 15 | 16 | export const authenticating = (ctx: Context) => ({ 17 | status: 'pending', 18 | title: `Authenticating with Chromatic${environment(ctx.env.CHROMATIC_INDEX_URL)}`, 19 | output: `Connecting to ${ctx.env.CHROMATIC_INDEX_URL}`, 20 | }); 21 | 22 | export const authenticated = (ctx: Context) => ({ 23 | status: 'success', 24 | title: `Authenticated with Chromatic${environment(ctx.env.CHROMATIC_INDEX_URL)}`, 25 | output: ctx.options.projectToken 26 | ? `Using project token '${mask(ctx.options.projectToken)}'` 27 | : `Using project ID '${ctx.options.projectId}' and user token`, 28 | }); 29 | 30 | export const invalidToken = (ctx: Context) => ({ 31 | status: 'error', 32 | title: `Failed to authenticate with Chromatic${environment(ctx.env.CHROMATIC_INDEX_URL)}`, 33 | output: `Invalid project token '${ctx.options.projectToken}'`, 34 | }); 35 | -------------------------------------------------------------------------------- /node-src/ui/tasks/build.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { failed, initial, pending, skipped, success } from './build'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Build', 6 | decorators: [(storyFunction) => task(storyFunction())], 7 | }; 8 | 9 | const ctx = { options: {} } as any; 10 | 11 | const buildCommand = 'yarn run build-storybook -o storybook-static'; 12 | 13 | export const Initial = () => initial(ctx); 14 | 15 | export const Building = () => pending({ ...ctx, buildCommand } as any); 16 | 17 | export const Built = () => 18 | success({ 19 | ...ctx, 20 | now: 0, 21 | startedAt: -32_100, 22 | buildLogFile: '/users/me/project/build-storybook.log', 23 | } as any); 24 | 25 | export const Skipped = () => 26 | skipped({ 27 | ...ctx, 28 | options: { ...ctx.options, storybookBuildDir: '/users/me/project/storybook-static' }, 29 | } as any); 30 | 31 | export const Failed = () => failed({ ...ctx, buildCommand } as any); 32 | -------------------------------------------------------------------------------- /node-src/ui/tasks/build.ts: -------------------------------------------------------------------------------- 1 | import { getDuration } from '../../lib/tasks'; 2 | import { Context } from '../../types'; 3 | import { buildType, capitalize } from './utils'; 4 | 5 | export const initial = (ctx: Context) => ({ 6 | status: 'initial', 7 | title: `Build ${buildType(ctx)}`, 8 | }); 9 | 10 | export const pending = (ctx: Context) => ({ 11 | status: 'pending', 12 | title: `Building your ${buildType(ctx)}`, 13 | output: `Running command: ${ctx.buildCommand}`, 14 | }); 15 | 16 | export const success = (ctx: Context) => ({ 17 | status: 'success', 18 | title: `${capitalize(buildType(ctx))} built in ${getDuration(ctx)}`, 19 | output: `View build log at ${ctx.buildLogFile}`, 20 | }); 21 | 22 | export const skipped = (ctx: Context) => ({ 23 | status: 'skipped', 24 | title: `Build ${buildType(ctx)} [skipped]`, 25 | output: `Using prebuilt ${buildType(ctx)} at ${ctx.options.storybookBuildDir}`, 26 | }); 27 | 28 | export const failed = (ctx: Context) => ({ 29 | status: 'error', 30 | title: `Building your ${buildType(ctx)}`, 31 | output: `Command failed: ${ctx.buildCommand}`, 32 | }); 33 | -------------------------------------------------------------------------------- /node-src/ui/tasks/buildE2E.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { failed, initial, pending, skipped, success } from './build'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Build/E2E', 6 | decorators: [(storyFunction) => task(storyFunction())], 7 | }; 8 | 9 | const ctx = { options: { playwright: true } } as any; 10 | 11 | const buildCommand = 'yarn build-archive-storybook'; 12 | 13 | export const Initial = () => initial(ctx); 14 | 15 | export const Building = () => pending({ ...ctx, buildCommand } as any); 16 | 17 | export const Built = () => 18 | success({ 19 | ...ctx, 20 | now: 0, 21 | startedAt: -32_100, 22 | buildLogFile: '/users/me/project/build-archive.log', 23 | } as any); 24 | 25 | export const Skipped = () => 26 | skipped({ 27 | ...ctx, 28 | options: { ...ctx.options, storybookBuildDir: '/users/me/project/archive-static' }, 29 | } as any); 30 | 31 | export const Failed = () => failed({ ...ctx, buildCommand } as any); 32 | -------------------------------------------------------------------------------- /node-src/ui/tasks/gitInfo.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { 3 | initial, 4 | pending, 5 | skipFailed, 6 | skippedForCommit, 7 | skippedRebuild, 8 | skippingBuild, 9 | success, 10 | } from './gitInfo'; 11 | 12 | export default { 13 | title: 'CLI/Tasks/GitInfo', 14 | decorators: [(storyFunction) => task(storyFunction())], 15 | }; 16 | 17 | const git = { commit: 'a32af7e265aa08e4a16d', branch: 'feat/new-ui', parentCommits: ['a', 'b'] }; 18 | const options = {}; 19 | 20 | export const Initial = () => initial; 21 | export const Pending = () => pending(); 22 | export const Success = () => success({ git, options } as any); 23 | export const FromFork = () => success({ git, options: { ownerName: 'chromaui' } } as any); 24 | export const NoBaselines = () => success({ git: { ...git, parentCommits: [] }, options } as any); 25 | export const TurboSnapDisabled = () => 26 | success({ git, options, turboSnap: { bailReason: {} } } as any); 27 | export const Skipping = () => skippingBuild({ git } as any); 28 | export const Skipped = () => skippedForCommit({ git } as any); 29 | export const SkippedRebuild = () => skippedRebuild(); 30 | export const SkipFailed = () => skipFailed(); 31 | -------------------------------------------------------------------------------- /node-src/ui/tasks/initialize.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { initial, pending, success } from './initialize'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Initialize', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const announcedBuild = { 10 | number: 42, 11 | }; 12 | 13 | export const Initial = () => initial; 14 | 15 | export const Pending = () => pending(); 16 | 17 | export const Success = () => success({ announcedBuild } as any); 18 | -------------------------------------------------------------------------------- /node-src/ui/tasks/initialize.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../types'; 2 | 3 | export const initial = { 4 | status: 'initial', 5 | title: 'Initialize build', 6 | }; 7 | 8 | export const pending = () => ({ 9 | status: 'pending', 10 | title: 'Initializing build', 11 | }); 12 | 13 | export const success = (ctx: Context) => ({ 14 | status: 'success', 15 | title: 'Initialized build', 16 | output: `Build ${ctx.announcedBuild.number} initialized`, 17 | }); 18 | -------------------------------------------------------------------------------- /node-src/ui/tasks/prepare.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { bailed, hashing, initial, invalid, success, traced, tracing, validating } from './prepare'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Prepare', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const ctx = { options: {} } as any; 10 | 11 | export const Initial = () => initial(ctx); 12 | 13 | export const Validating = () => validating(ctx); 14 | 15 | export const Invalid = () => 16 | invalid({ 17 | ...ctx, 18 | sourceDir: '/var/folders/h3/ff9kk23958l99z2qbzfjdlxc0000gn/T/chromatic-20036LMP9FAlLEjpu', 19 | buildLogFile: '/var/folders/h3/ff9kk23958l99z2qbzfjdlxc0000gn/T/build-storybook.log', 20 | } as any); 21 | 22 | export const Tracing = () => 23 | tracing({ ...ctx, git: { changedFiles: Array.from({ length: 3 }) } } as any); 24 | 25 | export const BailedPackageFile = () => 26 | bailed({ ...ctx, turboSnap: { bailReason: { changedPackageFiles: ['package.json'] } } } as any); 27 | 28 | export const BailedLockfile = () => 29 | bailed({ ...ctx, turboSnap: { bailReason: { changedPackageFiles: ['yarn.lock'] } } } as any); 30 | 31 | export const BailedSiblings = () => 32 | bailed({ 33 | ...ctx, 34 | turboSnap: { 35 | bailReason: { changedStorybookFiles: ['.storybook/preview.js', '.storybook/otherfile.js'] }, 36 | }, 37 | } as any); 38 | 39 | export const Traced = () => traced({ ...ctx, onlyStoryFiles: Array.from({ length: 5 }) } as any); 40 | 41 | export const Hashing = () => hashing(ctx); 42 | 43 | export const Success = () => success(ctx); 44 | -------------------------------------------------------------------------------- /node-src/ui/tasks/prepareE2E.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { bailed, hashing, invalid, success, traced, tracing, validating } from './prepare'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Prepare/E2E', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const ctx = { options: { playwright: true } } as any; 10 | 11 | export const Validating = () => validating(ctx); 12 | 13 | export const Invalid = () => 14 | invalid({ 15 | ...ctx, 16 | sourceDir: '/var/folders/h3/ff9kk23958l99z2qbzfjdlxc0000gn/T/chromatic-20036LMP9FAlLEjpu', 17 | buildLogFile: '/var/folders/h3/ff9kk23958l99z2qbzfjdlxc0000gn/T/build-storybook.log', 18 | } as any); 19 | 20 | export const Tracing = () => 21 | tracing({ ...ctx, git: { changedFiles: Array.from({ length: 3 }) } } as any); 22 | 23 | export const BailedPackageFile = () => 24 | bailed({ ...ctx, turboSnap: { bailReason: { changedPackageFiles: ['package.json'] } } } as any); 25 | 26 | export const BailedLockfile = () => 27 | bailed({ ...ctx, turboSnap: { bailReason: { changedPackageFiles: ['yarn.lock'] } } } as any); 28 | 29 | export const BailedSiblings = () => 30 | bailed({ 31 | ...ctx, 32 | turboSnap: { 33 | bailReason: { changedStorybookFiles: ['.storybook/preview.js', '.storybook/otherfile.js'] }, 34 | }, 35 | } as any); 36 | 37 | export const Traced = () => traced({ ...ctx, onlyStoryFiles: Array.from({ length: 5 }) } as any); 38 | 39 | export const Hashing = () => hashing(ctx); 40 | 41 | export const Success = () => 42 | success({ 43 | ...ctx, 44 | now: 0, 45 | startedAt: -54_321, 46 | } as any); 47 | -------------------------------------------------------------------------------- /node-src/ui/tasks/prepareWorkspace.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { 3 | checkoutMergeBase, 4 | initial, 5 | installingDependencies, 6 | lookupMergeBase, 7 | pending, 8 | success, 9 | } from './prepareWorkspace'; 10 | 11 | export default { 12 | title: 'CLI/Tasks/PrepareWorkspace', 13 | decorators: [(storyFunction: any) => task(storyFunction())], 14 | }; 15 | 16 | const options = { 17 | buildScriptName: 'build-storybook', 18 | patchBaseRef: 'main', 19 | patchHeadRef: 'feature', 20 | }; 21 | const mergeBase = '3f35708745837024bec510c0e5d8a3ac00ba6467'; 22 | 23 | export const Initial = () => initial; 24 | 25 | export const Pending = () => pending(); 26 | 27 | export const LookupMergeBase = () => lookupMergeBase({ options } as any); 28 | 29 | export const CheckoutMergeBase = () => checkoutMergeBase({ options, mergeBase } as any); 30 | 31 | export const InstallingDependencies = () => installingDependencies(); 32 | 33 | export const Success = () => success({ options, mergeBase } as any); 34 | -------------------------------------------------------------------------------- /node-src/ui/tasks/prepareWorkspace.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../types'; 2 | 3 | export const initial = { 4 | status: 'initial', 5 | title: 'Prepare workspace', 6 | }; 7 | 8 | export const pending = () => ({ 9 | status: 'pending', 10 | title: 'Preparing your workspace', 11 | output: `Ensuring your git workspace is clean and up-to-date`, 12 | }); 13 | 14 | export const lookupMergeBase = (ctx: Context) => ({ 15 | status: 'pending', 16 | title: 'Preparing your workspace', 17 | output: `Looking up the git merge base for '${ctx.options.patchHeadRef}' on '${ctx.options.patchBaseRef}'`, 18 | }); 19 | 20 | export const checkoutMergeBase = (ctx: Context) => ({ 21 | status: 'pending', 22 | title: 'Preparing your workspace', 23 | output: `Checking out merge base commit '${ctx.mergeBase?.slice(0, 7)}'`, 24 | }); 25 | 26 | export const installingDependencies = () => ({ 27 | status: 'pending', 28 | title: 'Preparing your workspace', 29 | output: 'Installing dependencies', 30 | }); 31 | 32 | export const success = (ctx: Context) => ({ 33 | status: 'success', 34 | title: `Prepared your workspace`, 35 | output: `Checked out commit '${ctx.mergeBase?.slice(0, 7)}' on '${ctx.options.patchBaseRef}'`, 36 | }); 37 | -------------------------------------------------------------------------------- /node-src/ui/tasks/report.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { initial, pending, success } from './report'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/Report', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | export const Initial = () => initial; 10 | 11 | export const Pending = () => pending(); 12 | 13 | export const Success = () => success({ reportPath: './chromatic-test-report.xml' } as any); 14 | -------------------------------------------------------------------------------- /node-src/ui/tasks/report.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '../../types'; 2 | 3 | export const initial = { 4 | status: 'initial', 5 | title: 'Generate build report', 6 | }; 7 | 8 | export const pending = () => ({ 9 | status: 'pending', 10 | title: 'Generating build report', 11 | output: `Collecting build information`, 12 | }); 13 | 14 | export const success = (ctx: Context) => ({ 15 | status: 'success', 16 | title: `Generated build report`, 17 | output: `View report at ${ctx.reportPath}`, 18 | }); 19 | -------------------------------------------------------------------------------- /node-src/ui/tasks/restoreWorkspace.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { initial, pending, success } from './restoreWorkspace'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/RestoreWorkspace', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | export const Initial = () => initial; 10 | export const Pending = () => pending(); 11 | export const Success = () => success(); 12 | -------------------------------------------------------------------------------- /node-src/ui/tasks/restoreWorkspace.ts: -------------------------------------------------------------------------------- 1 | export const initial = { 2 | status: 'initial', 3 | title: 'Restore workspace', 4 | }; 5 | 6 | export const pending = () => ({ 7 | status: 'pending', 8 | title: 'Restoring your workspace', 9 | output: `Discarding changes and restoring head location`, 10 | }); 11 | 12 | export const success = () => ({ 13 | status: 'success', 14 | title: `Restored your workspace`, 15 | }); 16 | -------------------------------------------------------------------------------- /node-src/ui/tasks/storybookInfo.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { initial, pending, success } from './storybookInfo'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/StorybookInfo', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const storybook = { 10 | version: '5.3.0', 11 | viewLayer: 'web-components', 12 | builder: { name: 'webpack4', packageVersion: '5.3.0' }, 13 | addons: [], 14 | }; 15 | const addons = [{ name: 'actions' }, { name: 'docs' }, { name: 'design-assets' }]; 16 | 17 | const ctx = { options: {} } as any; 18 | 19 | export const Initial = () => initial(ctx); 20 | 21 | export const Pending = () => pending(ctx); 22 | 23 | export const Success = () => success({ ...ctx, storybook } as any); 24 | 25 | export const WithAddons = () => success({ ...ctx, storybook: { ...storybook, addons } } as any); 26 | -------------------------------------------------------------------------------- /node-src/ui/tasks/storybookInfoE2E.stories.ts: -------------------------------------------------------------------------------- 1 | import task from '../components/task'; 2 | import { initial, pending, success } from './storybookInfo'; 3 | 4 | export default { 5 | title: 'CLI/Tasks/StorybookInfo/E2E', 6 | decorators: [(storyFunction: any) => task(storyFunction())], 7 | }; 8 | 9 | const storybook = { 10 | version: '5.3.0', 11 | viewLayer: 'web-components', 12 | builder: { name: 'webpack4', packageVersion: '5.3.0' }, 13 | addons: [], 14 | }; 15 | 16 | const ctx = { options: { playwright: true } } as any; 17 | 18 | export const Initial = () => initial(ctx); 19 | 20 | export const Pending = () => pending(ctx); 21 | 22 | export const SuccessPlaywright = () => success({ ...ctx, storybook } as any); 23 | 24 | export const SuccessCypress = () => 25 | success({ 26 | ...ctx, 27 | options: { ...ctx.options, playwright: false, cypress: true }, 28 | storybook, 29 | } as any); 30 | -------------------------------------------------------------------------------- /node-src/ui/tasks/utils.ts: -------------------------------------------------------------------------------- 1 | import { isE2EBuild } from '../../lib/e2eUtils'; 2 | import { Context } from '../../types'; 3 | 4 | export const buildType = (ctx: Context) => (isE2EBuild(ctx.options) ? 'test suite' : 'Storybook'); 5 | export const capitalize = (string: string) => string.charAt(0).toUpperCase() + string.slice(1); 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | tabWidth: 2, 4 | bracketSpacing: true, 5 | trailingComma: 'es5', 6 | singleQuote: true, 7 | arrowParens: 'always', 8 | }; 9 | -------------------------------------------------------------------------------- /scripts/releaseNext.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { $ } from 'execa'; 4 | 5 | import { main as publishAction } from './publishAction.mjs'; 6 | 7 | async function main() { 8 | const { stdout: status } = await $`git status --porcelain`; 9 | if (status) { 10 | console.error(`❗️ Working directory is not clean:\n${status}`); 11 | return; 12 | } 13 | 14 | await build(); 15 | await publishAction('next'); 16 | } 17 | 18 | async function build() { 19 | const { stdout: nextVersion } = await $`auto shipit --dry-run --quiet`; 20 | 21 | console.info(`📌 Temporarily bumping version to '${nextVersion}' for build step`); 22 | await $`npm --no-git-tag-version version ${nextVersion}`; 23 | 24 | console.info('📦 Building with new version'); 25 | await $({ 26 | stdio: 'inherit', 27 | env: { 28 | ...process.env, 29 | SENTRY_RELEASE: nextVersion, 30 | }, 31 | })`yarn build`; 32 | 33 | console.info('✅ Build with new version completed, ready for push to action-next!'); 34 | } 35 | 36 | main(); 37 | -------------------------------------------------------------------------------- /scripts/rename.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import pkgUp from 'pkg-up'; 4 | import { readFile, writeFile } from 'jsonfile'; 5 | 6 | const packageJson = { 7 | async read() { 8 | return pkgUp(__dirname).then((l) => readFile(l)); 9 | }, 10 | async write(json) { 11 | return pkgUp(__dirname).then((l) => writeFile(l, json, { spaces: 2 })); 12 | }, 13 | }; 14 | 15 | const rename = async (name) => { 16 | const initial = await packageJson.read(); 17 | 18 | const temp = { ...initial, name }; 19 | await packageJson.write(temp); 20 | }; 21 | 22 | rename(...process.argv.slice(2, 3)); 23 | -------------------------------------------------------------------------------- /scripts/run-via-node.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from 'process'; 4 | import { run } from '../dist/node.js'; 5 | 6 | run({ 7 | flags: { 8 | projectToken: process.env.CHROMATIC_PROJECT_TOKEN, 9 | exitZeroOnChanges: true, 10 | }, 11 | }).then( 12 | ({ code }) => { 13 | process.exit(code); 14 | }, 15 | (err) => { 16 | // eslint-disable-next-line no-console 17 | console.log(err); 18 | process.exit(1); 19 | } 20 | ); 21 | -------------------------------------------------------------------------------- /static/css/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: paleturquoise; 3 | } -------------------------------------------------------------------------------- /storybook-addon.d.ts: -------------------------------------------------------------------------------- 1 | // Use interface augmentation to add chromatic parameter to addParameter() types. 2 | declare module '@storybook/addons/dist/types' { 3 | interface Parameters { 4 | chromatic?: ChromaticParameters; 5 | } 6 | } 7 | 8 | export interface ChromaticParameters { 9 | /** 10 | * To set a viewport, specify one or more screen widths to the `chromatic.viewports` parameter. 11 | */ 12 | viewports?: number[]; 13 | 14 | /** 15 | * You can omit stories entirely from Chromatic testing using the disable story parameter. 16 | */ 17 | disable?: boolean; 18 | 19 | /** 20 | * Chromatic will pause CSS animations and reset them to their beginning state. 21 | * 22 | * Some animations are used to "animate in" visible elements. To specify that Chromatic should pause the 23 | * animation at the end, use the `pauseAnimationAtEnd` story parameter. 24 | */ 25 | pauseAnimationAtEnd?: boolean; 26 | 27 | /** 28 | * Use story-level delay to ensure a minimum amount of time (in milliseconds) has passed before Chromatic takes a 29 | * screenshot. 30 | */ 31 | delay?: number; 32 | 33 | /** 34 | * The diffThreshold parameter allows you to fine tune the threshold for visual change between snapshots before 35 | * they’re flagged by Chromatic. Sometimes you need assurance to the sub-pixel and other times you want to skip 36 | * visual noise generated by non-deterministic rendering such as anti-aliasing. 37 | * 38 | * 0 is the most accurate. 1 is the least accurate. 39 | * 40 | * @default 0.063 41 | */ 42 | diffThreshold?: number; 43 | } 44 | -------------------------------------------------------------------------------- /storybook-addon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = require('./dist/storybook-addon.js'); 3 | -------------------------------------------------------------------------------- /subdir/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../*.stories.js'], 3 | 4 | framework: { 5 | name: '@storybook/react-webpack5', 6 | options: {} 7 | }, 8 | 9 | docs: { 10 | autodocs: true 11 | }, 12 | 13 | typescript: { 14 | reactDocgen: 'react-docgen-typescript' 15 | }, 16 | 17 | addons: ['@storybook/addon-webpack5-compiler-swc'] 18 | }; 19 | -------------------------------------------------------------------------------- /subdir/One.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const One = ({ children }) =>

One {children}

; 4 | -------------------------------------------------------------------------------- /subdir/One.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { One } from './One'; 3 | 4 | export default { title: 'One' }; 5 | 6 | export const Default = () => ; 7 | -------------------------------------------------------------------------------- /subdir/README.md: -------------------------------------------------------------------------------- 1 | # Storybook in a subdirectory 2 | 3 | This exists purely to verify the behavior of --only-changed with --storybook-build-dir when pointed at a Storybook that was built in a subdirectory (such as this one). 4 | 5 | In this scenario, paths in `preview-stats.json` will use `subdir` as their root directory (`./`), which means they'll be misaligned with the git root directory. For example, git may report `./subdir/One.js` as changed, but since this file is reported as `./One.js` in the webpack stats, it will not find any dependent stories for that file. Passing `--storybook-root-dir subdir` will resolve that. 6 | -------------------------------------------------------------------------------- /subdir/Two.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { One } from './One'; 3 | 4 | export const Two = ({ children }) => Two {children}; 5 | -------------------------------------------------------------------------------- /subdir/Two.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Two } from './Two'; 3 | 4 | export default { title: 'Two' }; 5 | 6 | export const Default = () => ; 7 | -------------------------------------------------------------------------------- /subdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subdir", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "storybook dev", 6 | "build": "storybook build --stats-json --output-dir ../subdir-static", 7 | "postbuild": "node -r esm ../bin-src/trimStatsFile.js ../subdir-static/preview-stats.json" 8 | }, 9 | "dependencies": { 10 | "@storybook/react": "^8.3.4", 11 | "@storybook/react-webpack5": "^8.3.4", 12 | "react": "^18.3.1", 13 | "react-dom": "^18.3.1", 14 | "storybook": "^8.3.4" 15 | }, 16 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", 17 | "devDependencies": { 18 | "@storybook/addon-webpack5-compiler-swc": "^1.0.5" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test-stories/a.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | 4 | const style = { 5 | display: 'flex', 6 | alignItems: 'center', 7 | justifyContent: 'center', 8 | width: '50px', 9 | height: '50px', 10 | backgroundColor: 'darkkhaki', 11 | }; 12 | 13 | /** 14 | * A div used for test stories. 15 | * 16 | * @param param0 Additional properties for a
element. 17 | * @param param0.backgroundColor The desired background color for the div. 18 | * 19 | * @returns A stsyled div element. 20 | */ 21 | export default function A({ backgroundColor, ...props }) { 22 | let computedStyle = style; 23 | if (backgroundColor) { 24 | computedStyle = { ...style, backgroundColor }; 25 | } 26 | 27 | return
; 28 | } 29 | 30 | A.propTypes = { thing: PropTypes.func.isRequired }; 31 | -------------------------------------------------------------------------------- /test-stories/aWrap.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import A from './A'; 4 | 5 | const HOC = (Component) => { 6 | const Wrapped = (props) => { 7 | return ; 8 | }; 9 | 10 | Wrapped.displayName = `wrapped(${Component.displayName || Component.name})`; 11 | 12 | return Wrapped; 13 | }; 14 | 15 | export default HOC(HOC(HOC(A))); 16 | -------------------------------------------------------------------------------- /test-stories/b.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const style = { 4 | display: 'flex', 5 | alignItems: 'center', 6 | justifyContent: 'center', 7 | width: '20px', 8 | height: '20px', 9 | backgroundColor: 'blueviolet', 10 | }; 11 | 12 | /** 13 | * A span used for test stories. 14 | * 15 | * @param props Additional properties for a element. 16 | * 17 | * @returns A styled span element. 18 | */ 19 | export default function B(props) { 20 | return ; 21 | } 22 | -------------------------------------------------------------------------------- /test-stories/star.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /** 4 | * A star svg used for test stories. 5 | * 6 | * @returns A star svg. 7 | */ 8 | export default function Star() { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /test-stories/timing.stories-disabled.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | import { storiesOf } from '@storybook/react'; 4 | import React, { useState } from 'react'; 5 | 6 | // Some stories to test out timing code. Disabled by default 7 | // These stories are available at (e.g.) 8 | // http://vmdbnybkvx.staging-tunnel.chromaticqa.com/iframe.html?id=timing--5s 9 | 10 | // A component that guarantees the load event won't load for timeout seconds 11 | // Note that the img loading tends to take a litle longer so this is a minimum 12 | const WaitFor = ({ seconds }) => { 13 | const [count, setCount] = useState(seconds - 1); 14 | 15 | if (count > 0) { 16 | setTimeout(() => setCount(count - 1), 1000); 17 | } 18 | 19 | return ( 20 |
21 | {Array.from({ length: seconds - count }).map((_, index) => ( 22 | 27 | ))} 28 |
29 | ); 30 | }; 31 | 32 | // eslint-disable-next-line unicorn/prefer-module 33 | storiesOf('Timing', module) 34 | .add('5s', () => ) 35 | .add('40s', () => ) 36 | .add('2m', () => ); 37 | 38 | // Insert an invisible image into the body to ensure that the load event doesn't fire before 39 | // the story has been rendered. 40 | // For some reason this is not an issue in real Storybooks 41 | const img = document.createElement('img'); 42 | document.body.append(img); 43 | img.outerHTML = ``; 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules"], 3 | "extends": "@tsconfig/node16/tsconfig.json", 4 | "compilerOptions": { 5 | // Override module/moduleResolution since we're using tsup/esbuild 6 | "module": "es2022", 7 | "moduleResolution": "Bundler", 8 | "strict": true, 9 | "paths": { 10 | "@cli/*": ["./node-src/lib/*"] 11 | }, 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitAny": false, // Let this be a warning from ESLint until it becomes required 14 | "useUnknownInCatchVariables": false, // Treat caught errors as 'any' for now 15 | "resolveJsonModule": true, 16 | "types": ["webpack-env"], 17 | "lib": ["es5", "es6", "dom"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => [ 4 | { 5 | entry: { 6 | bin: 'bin-src/register.js', 7 | node: 'node-src/index.ts', 8 | }, 9 | splitting: true, 10 | minify: !options.watch, 11 | format: ['cjs'], 12 | dts: { 13 | entry: { node: 'node-src/index.ts' }, 14 | resolve: true, 15 | }, 16 | treeshake: true, 17 | sourcemap: true, 18 | clean: true, 19 | platform: 'node', 20 | target: 'node16', // Storybook still supports Node 16 21 | env: { 22 | SENTRY_ENVIRONMENT: process.env.CI ? 'production' : 'development', 23 | SENTRY_RELEASE: process.env.SENTRY_RELEASE || 'development', 24 | SENTRY_DIST: 'cli', 25 | }, 26 | }, 27 | { 28 | entry: ['action-src/register.js'], 29 | outDir: 'action', 30 | splitting: false, 31 | minify: !options.watch, 32 | format: ['cjs'], 33 | treeshake: true, 34 | sourcemap: true, 35 | clean: true, 36 | platform: 'node', 37 | target: 'node20', // Sync with `runs.using` in action.yml 38 | env: { 39 | SENTRY_ENVIRONMENT: process.env.CI ? 'production' : 'development', 40 | SENTRY_RELEASE: process.env.SENTRY_RELEASE || 'development', 41 | SENTRY_DIST: 'action', 42 | }, 43 | }, 44 | ]); 45 | -------------------------------------------------------------------------------- /vitest.config.mts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { configDefaults, coverageConfigDefaults, defineConfig, Plugin } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | exclude: [...configDefaults.exclude, '**/getParentCommits.test.ts'], 7 | clearMocks: true, // Clear all mocks between each test 8 | coverage: { 9 | provider: 'v8', 10 | exclude: [ 11 | 'vitest.no-threads.config.ts', 12 | 'scripts/**', 13 | '**/*.stories.{t,j}s', 14 | 'node-src/lib/testLogger.ts', 15 | ...coverageConfigDefaults.exclude, 16 | ], 17 | }, 18 | }, 19 | plugins: [tsconfigPaths() as Plugin], 20 | }); 21 | -------------------------------------------------------------------------------- /vitest.no-threads.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ['**/getParentCommits.test.ts'], 7 | poolOptions: { 8 | threads: { 9 | singleThread: true, 10 | }, 11 | }, 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------