diff --git a/config/env.ts b/config/env.ts index 643b9060d9..a8a62bf2d9 100644 --- a/config/env.ts +++ b/config/env.ts @@ -1,3 +1,5 @@ +// TODO(serhalp): Extract most of this module to https://github.com/unjs/std-env. + import Git from 'simple-git' import * as process from 'node:process' @@ -27,19 +29,63 @@ export const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF /** * Environment variable `CONTEXT` provided by Netlify. + * `dev`, `production`, `deploy-preview`, `branch-deploy`, `preview-server`, or a branch name * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#build-metadata} * * Environment variable `VERCEL_ENV` provided by Vercel. + * `production`, `preview`, or `development` * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV} * - * Whether triggered by PR, `deploy-preview` or `dev`. + * Whether this is some sort of preview environment. */ export const isPreview = isPR || - process.env.CONTEXT === 'deploy-preview' || - process.env.CONTEXT === 'dev' || + (process.env.CONTEXT && process.env.CONTEXT !== 'production') || process.env.VERCEL_ENV === 'preview' || process.env.VERCEL_ENV === 'development' +export const isProduction = + process.env.CONTEXT === 'production' || process.env.VERCEL_ENV === 'production' + +/** + * Environment variable `URL` provided by Netlify. + * This is always the current deploy URL, regardless of env. + * @see {@link https://docs.netlify.com/build/functions/environment-variables/#functions} + * + * Environment variable `VERCEL_URL` provided by Vercel. + * This is always the current deploy URL, regardless of env. + * NOTE: Not a valid URL, as the protocol is omitted. + * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_URL} + * + * Preview URL for the current deployment, only available in preview environments. + */ +export const getPreviewUrl = () => + isPreview + ? process.env.URL + ? process.env.URL + : process.env.NUXT_ENV_VERCEL_URL + ? `https://${process.env.NUXT_ENV_VERCEL_URL}` + : undefined + : undefined + +/** + * Environment variable `URL` provided by Netlify. + * This is always the current deploy URL, regardless of env. + * @see {@link https://docs.netlify.com/build/functions/environment-variables/#functions} + * + * Environment variable `VERCEL_PROJECT_PRODUCTION_URL` provided by Vercel. + * NOTE: Not a valid URL, as the protocol is omitted. + * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_PROJECT_PRODUCTION_URL} + * + * Production URL for the current deployment, only available in production environments. + */ +export const getProductionUrl = () => + isProduction + ? process.env.URL + ? process.env.URL + : process.env.NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL + ? `https://${process.env.NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL}` + : undefined + : undefined const git = Git() export async function getGitInfo() { @@ -92,5 +138,14 @@ export async function getEnv(isDevelopment: boolean) { : branch === 'main' ? 'canary' : 'release' - return { commit, shortCommit, branch, env } as const + const previewUrl = getPreviewUrl() + const productionUrl = getProductionUrl() + return { + commit, + shortCommit, + branch, + env, + previewUrl, + productionUrl, + } as const } diff --git a/modules/oauth.ts b/modules/oauth.ts index e8ecbaf79b..51f08a7837 100644 --- a/modules/oauth.ts +++ b/modules/oauth.ts @@ -3,26 +3,17 @@ import process from 'node:process' import { join } from 'node:path' import { appendFileSync, existsSync, readFileSync } from 'node:fs' import { randomUUID } from 'node:crypto' +import { getEnv } from '../config/env.ts' export default defineNuxtModule({ meta: { name: 'oauth', }, - setup() { + async setup() { const nuxt = useNuxt() - const env = process.env.NUXT_ENV_VERCEL_ENV - const previewUrl = process.env.NUXT_ENV_VERCEL_URL - const prodUrl = process.env.NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL - - let clientUri: string - if (env === 'preview' && previewUrl) { - clientUri = `https://${previewUrl}` - } else if (env === 'production' && prodUrl) { - clientUri = `https://${prodUrl}` - } else { - clientUri = 'http://127.0.0.1:3000' - } + const { previewUrl, productionUrl } = await getEnv(nuxt.options.dev) + const clientUri = productionUrl || previewUrl || 'http://127.0.0.1:3000' // bake it into a virtual file addServerTemplate({ diff --git a/test/unit/config/env.spec.ts b/test/unit/config/env.spec.ts new file mode 100644 index 0000000000..2d81ad4d77 --- /dev/null +++ b/test/unit/config/env.spec.ts @@ -0,0 +1,173 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const ALL_ENV_VARS = [ + 'CONTEXT', + 'VERCEL_ENV', + 'URL', + 'NUXT_ENV_VERCEL_URL', + 'NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL', +] + +describe('getPreviewUrl', () => { + beforeEach(() => { + // Reset consts evaluated at module init time + vi.resetModules() + }) + + beforeEach(() => { + for (const envVar of ALL_ENV_VARS) { + vi.stubEnv(envVar, undefined) + } + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('returns `undefined` if no known preview env is detected', async () => { + const { getPreviewUrl } = await import('../../../config/env') + + expect(getPreviewUrl()).toBeUndefined() + }) + + it.each([ + ['Netlify production', { CONTEXT: 'production', URL: 'https://prod.example.com' }], + ['Vercel production', { VERCEL_ENV: 'production', NUXT_ENV_VERCEL_URL: 'prod.example.com' }], + ])('%s environment returns `undefined`', async (_name, envVars) => { + for (const [key, value] of Object.entries(envVars)) { + vi.stubEnv(key, value) + } + const { getPreviewUrl } = await import('../../../config/env') + + expect(getPreviewUrl()).toBeUndefined() + }) + + it.each([ + ['Netlify dev', { CONTEXT: 'dev', URL: 'https://dev.example.com' }, 'https://dev.example.com'], + [ + 'Netlify deploy-preview', + { + CONTEXT: 'deploy-preview', + URL: 'https://preview.example.com', + }, + 'https://preview.example.com', + ], + [ + 'Netlify branch-deploy', + { CONTEXT: 'branch-deploy', URL: 'https://beta.example.com' }, + 'https://beta.example.com', + ], + [ + 'Netlify preview-server', + { + CONTEXT: 'preview-server', + URL: 'https://my-feat--preview.example.com', + }, + 'https://my-feat--preview.example.com', + ], + [ + 'Vercel development', + { VERCEL_ENV: 'development', NUXT_ENV_VERCEL_URL: 'dev.example.com' }, + 'https://dev.example.com', + ], + [ + 'Vercel preview', + { VERCEL_ENV: 'preview', NUXT_ENV_VERCEL_URL: 'preview.example.com' }, + 'https://preview.example.com', + ], + ])('%s environment returns preview URL', async (_name, envVars, expectedUrl) => { + for (const [key, value] of Object.entries(envVars)) { + vi.stubEnv(key, value) + } + + const { getPreviewUrl } = await import('../../../config/env') + + expect(getPreviewUrl()).toBe(expectedUrl) + }) +}) + +describe('getProductionUrl', () => { + beforeEach(() => { + // Reset consts evaluated at module init time + vi.resetModules() + }) + + beforeEach(() => { + for (const envVar of ALL_ENV_VARS) { + vi.stubEnv(envVar, undefined) + } + }) + + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('returns `undefined` if no known production env is detected', async () => { + const { getProductionUrl } = await import('../../../config/env') + + expect(getProductionUrl()).toBeUndefined() + }) + + it.each([ + ['Netlify dev', { CONTEXT: 'dev', URL: 'https://dev.example.com' }], + [ + 'Netlify deploy-preview', + { + CONTEXT: 'deploy-preview', + URL: 'https://preview.example.com', + }, + ], + ['Netlify branch-deploy', { CONTEXT: 'branch-deploy', URL: 'https://beta.example.com' }], + [ + 'Netlify preview-server', + { + CONTEXT: 'preview-server', + URL: 'https://my-feat--preview.example.com', + }, + ], + [ + 'Vercel development', + { + VERCEL_ENV: 'development', + NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL: 'dev.example.com', + }, + ], + [ + 'Vercel preview', + { + VERCEL_ENV: 'preview', + NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL: 'preview.example.com', + }, + ], + ])('%s environment returns `undefined`', async (_name, envVars) => { + for (const [key, value] of Object.entries(envVars)) { + vi.stubEnv(key, value) + } + const { getProductionUrl } = await import('../../../config/env') + + expect(getProductionUrl()).toBeUndefined() + }) + + it.each([ + [ + 'Netlify production', + { CONTEXT: 'production', URL: 'https://prod.example.com' }, + 'https://prod.example.com', + ], + [ + 'Vercel production', + { + VERCEL_ENV: 'production', + NUXT_ENV_VERCEL_PROJECT_PRODUCTION_URL: 'prod.example.com', + }, + 'https://prod.example.com', + ], + ])('%s environment returns production URL', async (_name, envVars, expectedUrl) => { + for (const [key, value] of Object.entries(envVars)) { + vi.stubEnv(key, value) + } + const { getProductionUrl } = await import('../../../config/env') + + expect(getProductionUrl()).toBe(expectedUrl) + }) +}) diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts deleted file mode 100644 index 0180f83a27..0000000000 --- a/test/unit/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from 'vitest' - -describe('work', () => { - it('should work', () => { - expect(true).toBe(true) - }) -})