diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index a2a42daf2b..c7bafdad6e 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -25,6 +25,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + fetch-depth: 0 - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 with: diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index 55bab726b7..109fcc57ac 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -16,6 +16,7 @@ permissions: jobs: chromatic: name: 📚 Chromatic + if: github.repository == 'npmx-dev/npmx.dev' runs-on: ubuntu-24.04-arm steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63df86eecf..044ddd22f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,26 +77,27 @@ jobs: - name: 🧪 Unit tests run: vp test --project unit --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - - name: ⬆︎ Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - disable_search: true - files: test-report.junit.xml - flags: unit - report_type: test_results - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - - name: ⬆︎ Upload coverage reports to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - disable_search: true - files: coverage/clover.xml - flags: unit - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - parallel: + - name: ⬆︎ Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + files: test-report.junit.xml + flags: unit + report_type: test_results + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: ⬆︎ Upload coverage reports to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + files: coverage/clover.xml + flags: unit + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} test: name: 🧪 Component tests @@ -119,26 +120,27 @@ jobs: - name: 🧪 Component tests run: vp test --project nuxt --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - - name: ⬆︎ Upload test results to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - disable_search: true - files: test-report.junit.xml - flags: component - report_type: test_results - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - - name: ⬆︎ Upload coverage reports to Codecov - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - disable_search: true - files: coverage/clover.xml - flags: component - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - parallel: + - name: ⬆︎ Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + files: test-report.junit.xml + flags: component + report_type: test_results + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: ⬆︎ Upload coverage reports to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + files: coverage/clover.xml + flags: component + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} browser: name: 🖥️ Browser tests @@ -234,7 +236,7 @@ jobs: i18n: name: 🌐 i18n validation - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-slim steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/deploy-canary.yml b/.github/workflows/deploy-canary.yml index 50e4132738..b34d98ce11 100644 --- a/.github/workflows/deploy-canary.yml +++ b/.github/workflows/deploy-canary.yml @@ -15,12 +15,12 @@ jobs: deploy-canary: if: github.repository == 'npmx-dev/npmx.dev' name: 🚀 Deploy to canary (main.npmx.dev) - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-slim steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - fetch-depth: 2 + fetch-depth: 0 - uses: voidzero-dev/setup-vp@313600b80b104eadebb9111787d37a2e83e014ca # v1.17.0 with: diff --git a/.github/workflows/lunaria.yml b/.github/workflows/lunaria.yml index 65dc1f1dc9..ae667c60f2 100644 --- a/.github/workflows/lunaria.yml +++ b/.github/workflows/lunaria.yml @@ -15,6 +15,7 @@ permissions: {} jobs: lunaria-overview: name: 🌝 Generate Lunaria Overview + if: github.repository == 'npmx-dev/npmx.dev' runs-on: ubuntu-24.04-arm permissions: contents: read @@ -36,4 +37,4 @@ jobs: sfw: true - name: Generate Lunaria Overview - uses: lunariajs/action@4911ad0736d1e3b20af4cb70f5079aea2327ed8e # astro-docs + uses: lunariajs/action@e5ab09c3c8353fa80786d8c2149873ba3978a3a7 # v0.2.0 diff --git a/.github/workflows/mirror-tangled.yml b/.github/workflows/mirror-tangled.yml index cfabd9ea83..39d5b5a0b5 100644 --- a/.github/workflows/mirror-tangled.yml +++ b/.github/workflows/mirror-tangled.yml @@ -18,7 +18,7 @@ jobs: mirror: name: 🕸️ Mirror to Tangled if: ${{ github.repository == 'npmx-dev/npmx.dev' }} - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-slim steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index be5fb70fe6..6a00fc3ec5 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -77,7 +77,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FROM_REF: ${{ steps.version.outputs.from }} - run: node scripts/release-notes.ts "$FROM_REF" > /tmp/release-notes.md + TO_REF: ${{ steps.version.outputs.next }} + run: node scripts/release-notes.ts "$FROM_REF" "$TO_REF" > /tmp/release-notes.md - name: 🚀 Create GitHub Release if: steps.check.outputs.skip == 'false' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index a02e7eea3e..0000000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Stale Issues and PRs - -on: - schedule: - # Run daily at 2 AM UTC - - cron: '0 2 * * *' - workflow_dispatch: # Allow manual trigger - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: true - -permissions: {} - -jobs: - stale-bugs: - name: 🧹 Mark stale bug issues - runs-on: ubuntu-latest - permissions: - issues: write # mark and close stale bug issues - steps: - - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 - with: - days-before-issue-stale: 30 - days-before-issue-close: 7 - days-before-pr-stale: -1 - days-before-pr-close: -1 - remove-stale-when-updated: true - only-issue-types: 'bug' - stale-issue-label: 'stale' - close-issue-label: 'stale-to-close' - operations-per-run: 500 - - stale-prs: - name: 🧹 Mark stale pull requests - runs-on: ubuntu-latest - permissions: - pull-requests: write # mark and close stale pull requests - steps: - - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 - with: - days-before-issue-stale: -1 - days-before-issue-close: -1 - days-before-pr-stale: 30 - days-before-pr-close: 7 - remove-stale-when-updated: true - stale-pr-label: 'stale' - close-pr-label: 'stale-to-close' - operations-per-run: 500 diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 600359efcb..8a706714bf 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -4,7 +4,7 @@ rules: stale-action-refs: ignore: # lunariajs/action has no tag refs; keep the branch commit hash-pinned. - - lunaria.yml:39 + - lunaria.yml:40 dangerous-triggers: ignore: - enforce-release-source.yml diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 0000000000..5aaa6977a5 --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,34 @@ +# AI Contribution Policy + +This project **welcomes the thoughtful use of AI tools** when contributing, yet asks all contributors to follow two core principles. + +## Never let an LLM speak for you + +When maintainers read a comment, an issue, or a pull request from you, they want to know they are hearing your words. Grammar and spelling do not matter. What matters is real connection. + +- All comments, issues, and pull request descriptions should be written in your own voice +- We value clear, human communication over perfect grammar or spelling +- Avoid copy-pasting AI-generated summaries that don't reflect your own understanding + +AI-generated summaries tend to be long-winded, dense, and often inaccurate. Simplicity is an art. The goal is not to sound impressive, but to communicate clearly. + +## Never let an LLM think for you + +Go ahead and use AI to explore the codebase, or to write the function or test you need. But the final step before contributing should always be understanding what it has written. + +- Feel free to use AI tools to generate code or explore ideas +- Only submit contributions you fully understand and can explain +- Contributions should reflect your own reasoning and problem-solving + +Use AI to point you in the right direction, but always take personal responsibility. Do not quote an LLM in an issue or pull request. Maintainers want to know what you think. + +Our aim is ensuring quality and maintaining the joy of collaborating and communicating with real people. + +## Summary + +- AI may be used to generate code or explore ideas +- Comments, issues, and pull request descriptions must be written in your own voice +- Contributions must be fully understood and explainable by the contributor +- Contribution guidelines must still be followed + +Read more about this and other AI contribution policies on [ai-policy.dev](https://ai-policy.dev/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eb942b099..6f60c45b03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1249,15 +1249,7 @@ The configuration for the pre-commit hook is defined in the [`staged` block of v ## Using AI -You're welcome to use AI tools to help you contribute. But there are two important ground rules: - -### 1. Never let an LLM speak for you - -When you write a comment, issue, or PR description, use your own words. Grammar and spelling don't matter – real connection does. AI-generated summaries tend to be long-winded, dense, and often inaccurate. Simplicity is an art. The goal is not to sound impressive, but to communicate clearly. - -### 2. Never let an LLM think for you - -Feel free to use AI to write code, tests, or point you in the right direction. But always understand what it's written before contributing it. Take personal responsibility for your contributions. Don't say "ChatGPT says..." – tell us what _you_ think. +Please read our full [AI Contribution Policy](./AI_POLICY.md) before contributing. For more context, see [Using AI in open source](https://roe.dev/blog/using-ai-in-open-source). diff --git a/app/assets/logos/oss-partners/index.ts b/app/assets/logos/oss-partners/index.ts index 2b0412e59c..e7e3dd439b 100644 --- a/app/assets/logos/oss-partners/index.ts +++ b/app/assets/logos/oss-partners/index.ts @@ -49,7 +49,7 @@ export const OSS_PARTNERS = [ url: 'https://opensourcepledge.com/', }, { - name: 'Void Zero', + name: 'VoidZero', items: [ { name: 'Vite', diff --git a/app/assets/logos/sponsors/index.ts b/app/assets/logos/sponsors/index.ts index de52c48609..c872c03f07 100644 --- a/app/assets/logos/sponsors/index.ts +++ b/app/assets/logos/sponsors/index.ts @@ -45,7 +45,7 @@ export const SPONSORS = { ], silver: [ { - name: 'Void Zero', + name: 'VoidZero', logo: { dark: LogoVoidZero, light: LogoVoidZeroLight, diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index 4580671cd3..67590100ee 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -79,6 +79,10 @@ const footerSections = computed>(( name: t('shortcuts.compare'), href: '/compare', }, + { + name: t('footer.tools'), + href: '/tools', + }, { name: t('shortcuts.settings'), href: '/settings', diff --git a/app/components/Chart/SplitSparkline.vue b/app/components/Chart/SplitSparkline.vue index 7b9fee5c6f..9cb6761efd 100644 --- a/app/components/Chart/SplitSparkline.vue +++ b/app/components/Chart/SplitSparkline.vue @@ -9,6 +9,8 @@ import { useColors } from '~/composables/useColors' import type { VueUiXyDatasetItem } from 'vue-data-ui/vue-ui-xy' import { getPalette, lightenColor } from 'vue-data-ui/utils' import { CHART_PATTERN_CONFIG } from '~/utils/charts' +import { isMissingDownloadValue } from '#shared/utils/trends-chart' +import type { ChartTimeGranularity } from '~/types/chart' import('vue-data-ui/style.css') @@ -27,6 +29,9 @@ const props = defineProps<{ day: string } showLastDatapointEstimation: boolean + nullifyZeroValues?: boolean + largePackageSeries?: boolean[] + granularity: ChartTimeGranularity }>() const { locale } = useI18n() @@ -54,36 +59,113 @@ const { colors } = useColors(rootEl) const isDarkMode = computed(() => resolvedMode.value === 'dark') +const selectedIndex = ref() +const isInteracting = ref(false) + +function isLargeSeries(seriesIndex: number): boolean { + return props.largePackageSeries?.[seriesIndex] === true +} + +function getLastValidValue( + series: Array, + beforeIndex: number, +): number | null { + for (let index = beforeIndex - 1; index >= 0; index -= 1) { + const value = series[index] + if (isMissingDownloadValue(value)) continue + const numericValue = Number(value) + if (Number.isFinite(numericValue)) return numericValue + } + return null +} + +function formatDataLabel(seriesIndex: number, value: number): string { + const series = props.dataset?.[seriesIndex]?.series ?? [] + const lastIndex = Math.min(series.length, props.dates.length) - 1 + + // Idle state: the sparkline label shows the final datapoint: if the value is null, for a large package, show the last recorded valid value + if (!isInteracting.value || typeof selectedIndex.value !== 'number') { + const rawLastValue = series[lastIndex] + + if (isLargeSeries(seriesIndex) && isMissingDownloadValue(rawLastValue)) { + const lastValidValue = getLastValidValue(series, lastIndex) + if (lastValidValue !== null) { + return `${numberFormatter.value.format(lastValidValue)}*` + } + } + + return numberFormatter.value.format(value) + } + + // Hover | kbd interaction: show the actual value at the hovered index + const rawValue = series[selectedIndex.value] + if (isLargeSeries(seriesIndex) && isMissingDownloadValue(rawValue)) { + return $t('package.trends.no_data_short') + } + + return numberFormatter.value.format(value) +} + const datasets = computed(() => { - return (props.dataset ?? []).map(unit => { - return props.dates.map((period, i) => { + return (props.dataset ?? []).map((unit, seriesIndex) => { + const lastIndex = Math.min(unit.series.length, props.dates.length) - 1 + const rawLastValue = unit.series[lastIndex] + const hasNoDataTail = + isLargeSeries(seriesIndex) && lastIndex >= 0 && isMissingDownloadValue(rawLastValue) + const lastValidValue = hasNoDataTail ? getLastValidValue(unit.series, lastIndex) : null + + return props.dates.map((period, index) => { + const rawValue = unit.series[index] + + // Large package with a 0 or null last value : keep the final x index, but render it at the same y value as the previous valid datapoint + if (hasNoDataTail && index === lastIndex && lastValidValue !== null) { + return { period, value: lastValidValue } + } + return { period, - value: unit.series[i] ?? 0, + value: + rawValue === 0 + ? props.nullifyZeroValues && hasNoDataTail + ? index === props.dates.length - 1 + ? 0 + : null + : 0 + : rawValue || (index === props.dates.length - 1 ? 0 : null), } }) }) }) -const selectedIndex = ref(null) - function hoverIndex({ index }: { index: number | undefined | null }) { if (typeof index === 'number') { selectedIndex.value = index } } +function startInteraction() { + isInteracting.value = true +} + function resetHover() { - selectedIndex.value = null + isInteracting.value = false + selectedIndex.value = undefined step.value += 1 // required to reset all chart instances } const configs = computed(() => { return (props.dataset || []).map((unit, i) => { - const lastIndex = unit.series.length - 1 - const dashIndices = props.showLastDatapointEstimation - ? Array.from(new Set([...(unit.dashIndices ?? []), lastIndex])) - : unit.dashIndices + const lastIndex = Math.min(unit.series.length, props.dates.length) - 1 + const hasNoDataTail = + isLargeSeries(i) && lastIndex >= 0 && isMissingDownloadValue(unit.series[lastIndex]) + + const dashIndices = Array.from( + new Set([ + ...(unit.dashIndices ?? []), + ...(props.showLastDatapointEstimation && lastIndex >= 0 ? [lastIndex] : []), + ...(hasNoDataTail ? [lastIndex] : []), + ]), + ) // Ensure we loop through available palette colours when the series count is higher than the available palette const fallbackColor = palette[i] ?? palette[i % palette.length] ?? palette[0]! @@ -93,6 +175,7 @@ const configs = computed(() => { : (lightenColor(seriesColor, 0.5) ?? seriesColor) // palette uses hex colours return { + useCursorPointer: true, a11y: { translations: { keyboardNavigation: $t( @@ -138,9 +221,7 @@ const configs = computed(() => { fontSize: 24, bold: false, color: colors.value.fg, - formatter: ({ value }) => { - return numberFormatter.value.format(value) - }, + formatter: ({ value }) => formatDataLabel(i, value), datetimeFormatter: { enable: true, locale: locale.value, @@ -150,8 +231,13 @@ const configs = computed(() => { }, line: { color: seriesColor, + strokeWidth: 1, dashIndices, dashArray: 3, + cutNullValues: false, + nullDashes: { + show: true, + }, }, plot: { radius: 6, @@ -162,27 +248,43 @@ const configs = computed(() => { color: colors.value.fgSubtle, bold: false, }, - verticalIndicator: { strokeDasharray: 0, color: colors.value.fgSubtle, }, padding: { left: 0, - right: 0, + right: 24, top: 0, bottom: 0, }, + // Drag to zoom + zoom: { + show: true, + resetButton: { + title: $t('package.trends.sparkline_zoom_reset.title'), + ariaLabel: $t('package.trends.sparkline_zoom_reset.aria_label'), + }, + }, }, } }) }) + +const zoomState = ref(null) +

+ {{ $t('package.trends.last_recorded_value') }} +

diff --git a/app/components/Compare/LineChart.vue b/app/components/Compare/LineChart.vue index deeda8ed92..5f440ee4e2 100644 --- a/app/components/Compare/LineChart.vue +++ b/app/components/Compare/LineChart.vue @@ -8,6 +8,6 @@ const { packages } = defineProps<{ diff --git a/app/components/DepsStats/DependencyList.vue b/app/components/DepsStats/DependencyList.vue new file mode 100644 index 0000000000..eb6251721a --- /dev/null +++ b/app/components/DepsStats/DependencyList.vue @@ -0,0 +1,284 @@ + + + diff --git a/app/components/DepsStats/DependencyStats.vue b/app/components/DepsStats/DependencyStats.vue new file mode 100644 index 0000000000..afa3491437 --- /dev/null +++ b/app/components/DepsStats/DependencyStats.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/components/DepsStats/DependencyStatsPanel.vue b/app/components/DepsStats/DependencyStatsPanel.vue new file mode 100644 index 0000000000..3b7291a0ed --- /dev/null +++ b/app/components/DepsStats/DependencyStatsPanel.vue @@ -0,0 +1,179 @@ + + + diff --git a/app/components/DepsStats/PackageJsonUpload.vue b/app/components/DepsStats/PackageJsonUpload.vue new file mode 100644 index 0000000000..47738b079b --- /dev/null +++ b/app/components/DepsStats/PackageJsonUpload.vue @@ -0,0 +1,94 @@ + + + diff --git a/app/components/Package/SelectionCheckbox.vue b/app/components/Package/SelectionCheckbox.vue index dddebc4672..2c1718a8c2 100644 --- a/app/components/Package/SelectionCheckbox.vue +++ b/app/components/Package/SelectionCheckbox.vue @@ -15,7 +15,7 @@ const disabledText = t('package.card.select_maximum', MAX_PACKAGE_SELECTION)