diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..856961ac46 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,83 @@ +language: en-GB + +reviews: + profile: chill + # Keep the high-level summary enabled (default), but place it in the + # walkthrough comment instead of relying on PR description updates. + high_level_summary_in_walkthrough: true + review_status: true + review_details: false + changed_files_summary: true + sequence_diagrams: true + estimate_code_review_effort: false + assess_linked_issues: true + related_issues: true + related_prs: true + suggested_labels: false + suggested_reviewers: true + in_progress_fortune: false + poem: false + + slop_detection: + enabled: true + label: '007' + + auto_review: + auto_pause_after_reviewed_commits: 5 + labels: + - '!release' + ignore_title_keywords: + - 'WIP' + - '[skip-review]' + - 'chore(release)' + ignore_usernames: + - 'renovate[bot]' + - 'dependabot[bot]' + - 'github-actions[bot]' + + # Built-in PR metadata/content checks. Modes are: off, warning, error. + pre_merge_checks: + docstrings: + mode: off + title: + mode: error + description: + mode: warning + issue_assessment: + mode: warning + + tools: + gitleaks: + enabled: true + osvScanner: + enabled: true + actionlint: + enabled: true + yamllint: + enabled: true + shellcheck: + enabled: true + dotenvLint: + enabled: true + + # Disable tools redundant with our own CI + eslint: + enabled: false + biome: + enabled: false + oxc: + enabled: false + markdownlint: + enabled: false + languagetool: + enabled: false + github-checks: + enabled: false + + # Security-related, but not a good fit for this repo + checkov: + enabled: false + trivy: + enabled: false + opengrep: + enabled: false diff --git a/.github/workflows/dependency-diff-comment.yml b/.github/workflows/dependency-diff-comment.yml new file mode 100644 index 0000000000..bc25964635 --- /dev/null +++ b/.github/workflows/dependency-diff-comment.yml @@ -0,0 +1,31 @@ +name: dependency-diff-comment + +on: + workflow_run: + workflows: ['dependency-diff'] + types: + - completed + +permissions: + pull-requests: write + actions: read + +jobs: + dependency-diff-comment: + name: 💬 Dependency diff comment + runs-on: ubuntu-slim + if: github.event.workflow_run.conclusion == 'success' + + steps: + - name: 📥 Download artifact + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: e18e-diff-result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: 💬 Post comment + uses: e18e/action-dependency-diff@f825d5b5c5ce0a42dc46c47ec20de24460affcd8 # v1.5.0 + with: + mode: comment-from-artifact + artifact-path: e18e-diff-result.json diff --git a/.github/workflows/dependency-diff.yml b/.github/workflows/dependency-diff.yml new file mode 100644 index 0000000000..e445a7b981 --- /dev/null +++ b/.github/workflows/dependency-diff.yml @@ -0,0 +1,47 @@ +name: dependency-diff + +on: + pull_request: + branches: + - main + - release + paths: + - package.json + - pnpm-lock.yaml + - pnpm-workspace.yaml + - docs/package.json + - cli/package.json + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-diff: + name: 🔎 Dependency diff + runs-on: ubuntu-slim + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: 🔎 Compare dependencies + id: analyze + uses: e18e/action-dependency-diff@f825d5b5c5ce0a42dc46c47ec20de24460affcd8 # v1.5.0 + with: + mode: artifact + detect-replacements: 'true' + duplicate-threshold: '4' + dependency-threshold: '15' + size-threshold: '200000' + + - name: 📤 Upload artifact + if: steps.analyze.outputs.artifact-path + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: e18e-diff-result + path: ${{ steps.analyze.outputs.artifact-path }} diff --git a/.github/workflows/remove-needs-review-on-review.yml b/.github/workflows/remove-needs-review-on-review.yml deleted file mode 100644 index 4c8d2d29ab..0000000000 --- a/.github/workflows/remove-needs-review-on-review.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: remove-needs-review-on-review - -on: - pull_request_review: - types: - - submitted - -jobs: - remove-needs-review: - name: 🏷️ Remove needs review label - if: github.repository == 'npmx-dev/npmx.dev' - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: 🏷️ Remove needs review label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - script: | - const pr = context.payload.pull_request; - const review = context.payload.review; - const LABEL = 'needs review'; - const reviewer = review?.user?.login; - const author = pr.user?.login; - const reviewerType = review?.user?.type; - - if (!reviewer) { - console.log('No reviewer login found in payload, skipping.'); - return; - } - - if (reviewerType === 'Bot') { - console.log(`Skipping bot review from @${reviewer} on PR #${pr.number}.`); - return; - } - - if (reviewer === author) { - console.log(`Skipping self-review from @${reviewer} on PR #${pr.number}.`); - return; - } - - if (!pr.labels.some(({ name }) => name === LABEL)) { - console.log(`PR #${pr.number} does not have the "${LABEL}" label.`); - return; - } - - const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: reviewer, - }); - - console.log(`Reviewer @${reviewer} permission is "${permission.permission}".`); - - if (!['admin', 'maintain', 'write'].includes(permission.permission)) { - console.log(`Reviewer @${reviewer} is not a maintainer, skipping.`); - return; - } - - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - name: LABEL, - }); - - console.log(`Removed "${LABEL}" from PR #${pr.number}.`); diff --git a/.github/workflows/welcome-close.yml b/.github/workflows/welcome-close.yml index 606f691bc6..0de27332b6 100644 --- a/.github/workflows/welcome-close.yml +++ b/.github/workflows/welcome-close.yml @@ -39,7 +39,7 @@ jobs: const body = [ `Thanks for your first contribution, @${author}! ${emoji}`, '', - `We'd love to welcome you to the npmx community. Come and say hi on [Discord](https://chat.npmx.dev)! And once you've joined, visit [npmx.wamellow.com](https://npmx.wamellow.com/) to claim the **contributor** role.`, + `We'd love to welcome you to the npmx community. Come and say hi on [Discord](https://build.npmx.dev)! And once you've joined, visit [npmx.wamellow.com](https://npmx.wamellow.com/) to claim the **contributor** role.`, ].join('\n'); await github.rest.issues.createComment({ diff --git a/.storybook/main.ts b/.storybook/main.ts index 31dc739ca8..8e14495eb5 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,6 @@ import type { StorybookConfig } from '@storybook-vue/nuxt' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' const config = { stories: [ @@ -21,29 +23,95 @@ const config = { async viteFinal(newConfig) { newConfig.plugins ??= [] + // Fix: nuxt:components:imports-alias relies on internal Nuxt state that is + // cleaned up after nuxt.close() in @storybook-vue/nuxt's loadNuxtViteConfig. + // When that state is gone, `import X from '#components'` is left unresolved + // and Vite 8 falls through to package-subpath resolution, which fails with + // "Missing '#components' specifier in 'nuxt' package". + // This plugin intercepts #components first and serves a virtual module built + // from the components.d.ts written during the same Nuxt boot. + // Resolve the Nuxt build dir from Vite's alias map, which can be either a + // plain-object (Record) or Vite's resolved array form + // (readonly Alias[] where find is string | RegExp). We must handle both + // without casting to Record, which would be unsound for the + // array form. + const aliases = newConfig.resolve?.alias + const buildDir = (() => { + if (!aliases) return undefined + if (Array.isArray(aliases)) { + const entry = aliases.find(a => a.find === '#build') + return typeof entry?.replacement === 'string' ? entry.replacement : undefined + } + const value = (aliases as Record)['#build'] + return typeof value === 'string' ? value : undefined + })() + newConfig.plugins.unshift({ + name: 'storybook-nuxt-components', + enforce: 'pre', + resolveId(id) { + if (id === '#components') return '\0virtual:#components' + return null + }, + load(id) { + if (id !== '\0virtual:#components') return + if (!buildDir) { + throw new Error('[storybook-nuxt-components] Could not resolve the `#build` alias.') + } + const dtsPath = resolve(buildDir, 'components.d.ts') + // Wire the generated declaration file into Vite's file-watch graph so + // that the virtual module is invalidated when Nuxt regenerates it. + this.addWatchFile(dtsPath) + const dts = readFileSync(dtsPath, 'utf-8') + const lines: string[] = [] + // Match only the direct `typeof import("…").default` form. + // Lazy/island wrappers (LazyComponent, IslandComponent) are + // excluded intentionally — Storybook only needs the concrete type. + // The format has been stable across all Nuxt 3 releases. + const re = + /^export const (\w+): typeof import\("([^"]+)"\)(?:\.default|\[['"]default['"]\])\s*;?$/gm + let match: RegExpExecArray | null + while ((match = re.exec(dts)) !== null) { + const [, name, rel] = match + if (!name || !rel) continue + const abs = resolve(buildDir, rel).replaceAll('\\', '/') + lines.push(`export { default as ${name} } from ${JSON.stringify(abs)}`) + } + if (lines.length === 0) { + throw new Error( + `[storybook-nuxt-components] No component exports were found in ${dtsPath}.`, + ) + } + return lines.join('\n') + }, + }) + // Bridge compatibility between Storybook v10 core and v9 @storybook-vue/nuxt // v10 expects module federation globals that v9 doesn't provide newConfig.plugins.push({ name: 'storybook-v10-compat', transformIndexHtml: { order: 'pre', - handler(html) { - const script = ` -` - return html.replace(/ - - - - diff --git a/app/components/Compare/FacetScatterChart.vue b/app/components/Compare/FacetScatterChart.vue new file mode 100644 index 0000000000..a0de43bb25 --- /dev/null +++ b/app/components/Compare/FacetScatterChart.vue @@ -0,0 +1,530 @@ + + + + + diff --git a/app/components/Diff/SidebarPanel.vue b/app/components/Diff/SidebarPanel.vue index 385a779d20..7cc37e7bb4 100644 --- a/app/components/Diff/SidebarPanel.vue +++ b/app/components/Diff/SidebarPanel.vue @@ -1,5 +1,6 @@