Skip to content

Commit af0b974

Browse files
committed
fix: swap discord links based on env
1 parent 7d898b7 commit af0b974

10 files changed

Lines changed: 226 additions & 16 deletions

File tree

app/components/AppFooter.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NPMX_DOCS_SITE } from '#shared/utils/constants'
44
const route = useRoute()
55
const isHome = computed(() => route.name === 'index')
66
7+
const discord = useDiscordLink()
78
const modalRef = useTemplateRef('modalRef')
89
const showModal = () => modalRef.value?.showModal?.()
910
const closeModal = () => modalRef.value?.close?.()
@@ -124,8 +125,8 @@ const closeModal = () => modalRef.value?.close?.()
124125
<LinkBase to="https://social.npmx.dev">
125126
{{ $t('footer.social') }}
126127
</LinkBase>
127-
<LinkBase to="https://chat.npmx.dev">
128-
{{ $t('footer.chat') }}
128+
<LinkBase :to="discord.url">
129+
{{ discord.label }}
129130
</LinkBase>
130131
</div>
131132
</div>

app/components/AppHeader.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isEditableElement } from '~/utils/input'
55
import { NPMX_DOCS_SITE } from '#shared/utils/constants'
66
77
const keyboardShortcuts = useKeyboardShortcuts()
8+
const discord = useDiscordLink()
89
910
withDefaults(
1011
defineProps<{
@@ -122,8 +123,8 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
122123
},
123124
{
124125
name: 'Chat',
125-
label: $t('footer.chat'),
126-
href: 'https://chat.npmx.dev',
126+
label: discord.value.label,
127+
href: discord.value.url,
127128
target: '_blank',
128129
type: 'link',
129130
external: true,

app/components/CallToAction.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
const discord = useDiscordLink()
23
const socialLinks = computed(() => [
34
{
45
id: 'github',
@@ -10,11 +11,11 @@ const socialLinks = computed(() => [
1011
},
1112
{
1213
id: 'discord',
13-
href: 'https://chat.npmx.dev',
14+
href: discord.value.url,
1415
icon: 'i-lucide:message-circle',
15-
titleKey: $t('about.get_involved.community.title'),
16-
descriptionKey: $t('about.get_involved.community.description'),
17-
ctaKey: $t('about.get_involved.community.cta'),
16+
titleKey: discord.value.title,
17+
descriptionKey: discord.value.description,
18+
ctaKey: discord.value.cta,
1819
},
1920
{
2021
id: 'bluesky',

app/composables/useDiscordLink.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { DISCORD_BUILDERS_URL, DISCORD_COMMUNITY_URL } from '#shared/utils/constants'
2+
3+
/**
4+
* Returns the Discord invite URL and translated labels based on the current build environment.
5+
*
6+
* - `release` (npmx.dev) links to the **community** Discord.
7+
* - All other environments (`canary`, `preview`, `dev`) link to the **builders** Discord.
8+
*/
9+
export function useDiscordLink() {
10+
const { env } = useAppConfig().buildInfo
11+
const { t } = useI18n()
12+
13+
return computed(() =>
14+
env !== 'release'
15+
? {
16+
url: DISCORD_BUILDERS_URL,
17+
label: t('footer.builders_chat'),
18+
title: t('about.get_involved.builders.title'),
19+
description: t('about.get_involved.builders.description'),
20+
cta: t('about.get_involved.builders.cta'),
21+
}
22+
: {
23+
url: DISCORD_COMMUNITY_URL,
24+
label: t('footer.chat'),
25+
title: t('about.get_involved.community.title'),
26+
description: t('about.get_involved.community.description'),
27+
cta: t('about.get_involved.community.cta'),
28+
},
29+
)
30+
}

config/env.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,22 @@ export const prNumber = process.env.REVIEW_ID || process.env.VERCEL_GIT_PULL_REQ
3838
*/
3939
export const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF
4040

41+
/**
42+
* Environment variable `VERCEL_ENV` provided by Vercel.
43+
* `production`, `preview`, `development`, or a custom environment name (e.g. `canary`).
44+
* @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV}
45+
*
46+
* Whether this is the canary custom Vercel environment (main.npmx.dev).
47+
*/
48+
export const isCanary = process.env.VERCEL_ENV === 'canary'
49+
4150
/**
4251
* Environment variable `CONTEXT` provided by Netlify.
4352
* `dev`, `production`, `deploy-preview`, `branch-deploy`, `preview-server`, or a branch name
4453
* @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#build-metadata}
4554
*
4655
* Environment variable `VERCEL_ENV` provided by Vercel.
47-
* `production`, `preview`, or `development`
56+
* `production`, `preview`, `development`, or a custom environment name (e.g. `canary`).
4857
* @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV}
4958
*
5059
* Whether this is some sort of preview environment.
@@ -142,13 +151,7 @@ export async function getFileLastUpdated(path: string) {
142151

143152
export async function getEnv(isDevelopment: boolean) {
144153
const { commit, shortCommit, branch } = await getGitInfo()
145-
const env = isDevelopment
146-
? 'dev'
147-
: isPreview
148-
? 'preview'
149-
: branch === 'main'
150-
? 'canary'
151-
: 'release'
154+
const env = isDevelopment ? 'dev' : isCanary ? 'canary' : isPreview ? 'preview' : 'release'
152155
const previewUrl = getPreviewUrl()
153156
const productionUrl = getProductionUrl()
154157
return {

i18n/locales/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"source": "source",
1919
"social": "social",
2020
"chat": "chat",
21+
"builders_chat": "builders",
2122
"keyboard_shortcuts": "keyboard shortcuts"
2223
},
2324
"shortcuts": {
@@ -958,6 +959,11 @@
958959
"description": "Chat, ask questions, and share ideas.",
959960
"cta": "Join Discord"
960961
},
962+
"builders": {
963+
"title": "Help build npmx",
964+
"description": "Join the builders shaping the future of npmx.",
965+
"cta": "Join Builders Discord"
966+
},
961967
"follow": {
962968
"title": "Stay updated",
963969
"description": "Find out the latest on npmx.",

i18n/schema.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
"chat": {
5959
"type": "string"
6060
},
61+
"builders_chat": {
62+
"type": "string"
63+
},
6164
"keyboard_shortcuts": {
6265
"type": "string"
6366
}
@@ -2878,6 +2881,21 @@
28782881
},
28792882
"additionalProperties": false
28802883
},
2884+
"builders": {
2885+
"type": "object",
2886+
"properties": {
2887+
"title": {
2888+
"type": "string"
2889+
},
2890+
"description": {
2891+
"type": "string"
2892+
},
2893+
"cta": {
2894+
"type": "string"
2895+
}
2896+
},
2897+
"additionalProperties": false
2898+
},
28812899
"follow": {
28822900
"type": "object",
28832901
"properties": {

shared/utils/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export const PACKAGE_SUBJECT_REF = (packageName: string) =>
5252
export const LIKES_SCOPE = `repo:${dev.npmx.feed.like.$nsid}`
5353
export const PROFILE_SCOPE = `repo:${dev.npmx.actor.profile.$nsid}`
5454

55+
// Discord
56+
export const DISCORD_COMMUNITY_URL = 'https://chat.npmx.dev'
57+
export const DISCORD_BUILDERS_URL = 'https://build.npmx.dev'
58+
5559
// Theming
5660
export const ACCENT_COLORS = {
5761
light: {

test/nuxt/components/BuildEnvironment.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ describe('BuildEnvironment', () => {
3131
expect(tagLink.exists()).toBe(false)
3232
})
3333

34+
it('renders canary environment correctly', async () => {
35+
const buildInfo: BuildInfo = {
36+
env: 'canary',
37+
version: '1.2.3',
38+
time: 1234567890,
39+
commit: 'abcdef',
40+
shortCommit: 'abc',
41+
branch: 'main',
42+
privacyPolicyDate: new Date().toISOString(),
43+
prNumber: null,
44+
}
45+
const component = await mountSuspended(BuildEnvironment, {
46+
props: {
47+
buildInfo,
48+
},
49+
})
50+
51+
// In canary mode, it shows env name, not version link
52+
const envSpan = component.find('span.tracking-wider')
53+
expect(envSpan.exists()).toBe(true)
54+
expect(envSpan.text()).toBe('canary')
55+
const commitLink = component.find(`a[href$="/commit/${buildInfo.commit}"]`)
56+
expect(commitLink.exists()).toBe(true)
57+
const tagLink = component.find(`a[href$="/tag/v${buildInfo.version}"]`)
58+
expect(tagLink.exists()).toBe(false)
59+
})
60+
3461
it('renders release environment correctly', async () => {
3562
const buildInfo: BuildInfo = {
3663
env: 'release',

test/unit/config/env.spec.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,126 @@ const ALL_ENV_VARS = [
66
'URL',
77
'NUXT_ENV_VERCEL_URL',
88
'NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL',
9+
'PULL_REQUEST',
10+
'VERCEL_GIT_PULL_REQUEST_ID',
11+
'BRANCH',
12+
'VERCEL_GIT_COMMIT_REF',
913
]
1014

15+
describe('isCanary', () => {
16+
beforeEach(() => {
17+
vi.resetModules()
18+
})
19+
20+
beforeEach(() => {
21+
for (const envVar of ALL_ENV_VARS) {
22+
vi.stubEnv(envVar, undefined)
23+
}
24+
})
25+
26+
afterEach(() => {
27+
vi.unstubAllEnvs()
28+
})
29+
30+
it('returns true when VERCEL_ENV is "canary"', async () => {
31+
vi.stubEnv('VERCEL_ENV', 'canary')
32+
const { isCanary } = await import('../../../config/env')
33+
34+
expect(isCanary).toBe(true)
35+
})
36+
37+
it.each([
38+
['production', 'production'],
39+
['preview', 'preview'],
40+
['development', 'development'],
41+
['unset', undefined],
42+
])('returns false when VERCEL_ENV is %s', async (_label, value) => {
43+
if (value !== undefined) vi.stubEnv('VERCEL_ENV', value)
44+
const { isCanary } = await import('../../../config/env')
45+
46+
expect(isCanary).toBe(false)
47+
})
48+
})
49+
50+
describe('getEnv', () => {
51+
beforeEach(() => {
52+
vi.resetModules()
53+
})
54+
55+
beforeEach(() => {
56+
for (const envVar of ALL_ENV_VARS) {
57+
vi.stubEnv(envVar, undefined)
58+
}
59+
})
60+
61+
afterEach(() => {
62+
vi.unstubAllEnvs()
63+
})
64+
65+
it('returns "dev" in development mode', async () => {
66+
const { getEnv } = await import('../../../config/env')
67+
const result = await getEnv(true)
68+
69+
expect(result.env).toBe('dev')
70+
})
71+
72+
it('returns "canary" when VERCEL_ENV is "canary"', async () => {
73+
vi.stubEnv('VERCEL_ENV', 'canary')
74+
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
75+
const { getEnv } = await import('../../../config/env')
76+
const result = await getEnv(false)
77+
78+
expect(result.env).toBe('canary')
79+
})
80+
81+
it('returns "preview" for Vercel preview deploys', async () => {
82+
vi.stubEnv('VERCEL_ENV', 'preview')
83+
vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '123')
84+
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
85+
const { getEnv } = await import('../../../config/env')
86+
const result = await getEnv(false)
87+
88+
expect(result.env).toBe('preview')
89+
})
90+
91+
it('returns "preview" for PR deploys from main branch', async () => {
92+
vi.stubEnv('VERCEL_ENV', 'preview')
93+
vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '456')
94+
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
95+
const { getEnv } = await import('../../../config/env')
96+
const result = await getEnv(false)
97+
98+
expect(result.env).toBe('preview')
99+
})
100+
101+
it('returns "release" for Vercel production deploys', async () => {
102+
vi.stubEnv('VERCEL_ENV', 'production')
103+
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'v1.0.0')
104+
const { getEnv } = await import('../../../config/env')
105+
const result = await getEnv(false)
106+
107+
expect(result.env).toBe('release')
108+
})
109+
110+
it('prioritises "canary" over "preview" when VERCEL_ENV is "canary" and PR is open', async () => {
111+
vi.stubEnv('VERCEL_ENV', 'canary')
112+
vi.stubEnv('VERCEL_GIT_PULL_REQUEST_ID', '789')
113+
vi.stubEnv('VERCEL_GIT_COMMIT_REF', 'main')
114+
const { getEnv } = await import('../../../config/env')
115+
const result = await getEnv(false)
116+
117+
expect(result.env).toBe('canary')
118+
})
119+
120+
it('prioritises "dev" over "canary" in development mode', async () => {
121+
vi.stubEnv('VERCEL_ENV', 'canary')
122+
const { getEnv } = await import('../../../config/env')
123+
const result = await getEnv(true)
124+
125+
expect(result.env).toBe('dev')
126+
})
127+
})
128+
11129
describe('getPreviewUrl', () => {
12130
beforeEach(() => {
13131
// Reset consts evaluated at module init time
@@ -33,6 +151,7 @@ describe('getPreviewUrl', () => {
33151
it.each([
34152
['Netlify production', { CONTEXT: 'production', URL: 'https://prod.example.com' }],
35153
['Vercel production', { VERCEL_ENV: 'production', NUXT_ENV_VERCEL_URL: 'prod.example.com' }],
154+
['Vercel canary', { VERCEL_ENV: 'canary', NUXT_ENV_VERCEL_URL: 'main.example.com' }],
36155
])('%s environment returns `undefined`', async (_name, envVars) => {
37156
for (const [key, value] of Object.entries(envVars)) {
38157
vi.stubEnv(key, value)

0 commit comments

Comments
 (0)