diff --git a/.changeset/busy-icons-ask.md b/.changeset/busy-icons-ask.md new file mode 100644 index 00000000..a845151c --- /dev/null +++ b/.changeset/busy-icons-ask.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..cb2a2b6a --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Shared YouVersion Platform configuration +YVP_APP_KEY="" +YVP_API_HOST="api.youversion.com" + +# Optional Vite demo overrides +# Defaults to the demo's current origin. +# VITE_YVP_AUTH_REDIRECT_URL="http://localhost:5173" +# SDK UI language (BCP-47). Leave unset to follow the browser. +# VITE_YVP_LOCALE="es" +# Seeds the Reader version picker's Bible language. Distinct from locale. +# VITE_YVP_DEFAULT_LANGUAGE_ID="es" + +# Optional Storybook override +# Defaults to Storybook's current origin. +# STORYBOOK_AUTH_REDIRECT_URL="http://localhost:6006" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ababd296..dd3fd86c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,17 +44,15 @@ You'll need to obtain an app key from ### Set up environment variables -Create an .env.local file in the `./packages/core` package and update the app key variable. +Copy the root environment template and add your YouVersion app key. This one file configures the +core tests, Storybook, and the Vite example app. ```bash -cp ./packages/core/.env.example ./packages/core/.env.local +cp .env.example .env ``` -Create an .env.local file in the `./packages/ui` package and update the app key variable. - -```bash -cp ./packages/ui/.env.example ./packages/ui/.env.local -``` +Shell and CI variables take precedence over files. Existing package-local `.env.local` files remain +supported as optional harness-specific overrides; the root `.env` provides the default. > [!NOTE] > Our React hooks package does not require environment variables at this time. diff --git a/docs/cursor-cloud.md b/docs/cursor-cloud.md index fc12c02b..b54ab302 100644 --- a/docs/cursor-cloud.md +++ b/docs/cursor-cloud.md @@ -6,32 +6,35 @@ Standard install/lint/test/build/dev commands live in `CONTRIBUTING.md` and root ## Env files (gitignored) -Core unit tests use MSW but still throw if `YVP_API_HOST` is unset (`packages/core/src/__tests__/handlers.ts`). Copy the examples before `pnpm test`: +Copy the root environment template and add a real app key when live API access is needed: -- `packages/core/.env.example` → `packages/core/.env.local` (`YVP_API_HOST=api.youversion.com`; a placeholder `YVP_APP_KEY` is enough for mocked tests) -- `packages/ui/.env.example` → `packages/ui/.env.local` (Storybook) -- `examples/vite-react/.env.example` → `examples/vite-react/.env.local` (`VITE_YVP_APP_KEY` required for live Bible content) +```bash +cp .env.example .env +``` + +The root `.env` configures core integration tests, Storybook, and the Vite demo. Mocked tests and +builds do not require it. Existing package-local `.env.local` files remain supported as optional +harness-specific overrides. Shell and CI variables take precedence over files. -Get a real app key from https://platform.youversion.com. Without `VITE_YVP_APP_KEY`, the demo renders the SDK missing-app-key panel instead of the Bible reader. +Get a real app key from https://platform.youversion.com. Without `YVP_APP_KEY` or a +harness-specific app-key override, the demo and Storybook render the SDK missing-app-key panel, +and live core integration tests remain skipped. ## Running the demo -`pnpm dev:web` is stale (it still filters a removed `nextjs` package). Start the demo with: +Start the demo and its workspace dependency watchers with: ```bash -pnpm --filter vite-react dev --host 127.0.0.1 --port 5173 +pnpm dev:web ``` -Do not put an extra `--` before `--host`. `pnpm --filter vite-react dev -- --host 127.0.0.1` becomes `vite -- --host 127.0.0.1`; Vite then ignores `--host` and listens on `localhost` (often `::1` only), so `curl http://127.0.0.1:5173` fails. - -`pnpm --filter vite-react exec vite --host 127.0.0.1 --port 5173` is equivalent. - ## Live core client -After `pnpm build`, source `packages/core/.env.local` and pass those values into `ApiClient`. Test scripts load the file via `dotenv-cli`; the runtime client reads only the config object you give it. +After `pnpm build`, source the root `.env` and pass those values into `ApiClient`. Test scripts load +the root file via `dotenv-cli`; the runtime client reads only the config object you give it. ```bash -set -a && . packages/core/.env.local && set +a +set -a && . .env && set +a cd packages/core ``` diff --git a/examples/vite-react/.env.example b/examples/vite-react/.env.example deleted file mode 100644 index a35ea5ec..00000000 --- a/examples/vite-react/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -VITE_YVP_APP_KEY="" -VITE_YVP_API_HOST="api.youversion.com" -VITE_YVP_AUTH_REDIRECT_URL="http://localhost:5173" -# Optional. SDK UI language (BCP-47). Leave unset to follow the browser. -# VITE_YVP_LOCALE="es" -# Optional. Seeds the Reader version picker Bible language. Distinct from locale. -# VITE_YVP_DEFAULT_LANGUAGE_ID="es" diff --git a/examples/vite-react/README.md b/examples/vite-react/README.md index b8aa3b11..69bb5269 100644 --- a/examples/vite-react/README.md +++ b/examples/vite-react/README.md @@ -6,12 +6,14 @@ A demo app showcasing `@youversion/platform-react-ui` components. ## Setup +Run these commands from the repository root: + ```bash -cp .env.example .env.local -# Add your YouVersion App Key to .env.local +cp .env.example .env +# Add your YouVersion App Key to .env # Optional: VITE_YVP_LOCALE and VITE_YVP_DEFAULT_LANGUAGE_ID (e.g. es) pnpm install -pnpm dev +pnpm dev:web ``` ## React SDK Components Used diff --git a/examples/vite-react/src/vite-env.d.ts b/examples/vite-react/src/vite-env.d.ts index 41629bb8..69aec223 100644 --- a/examples/vite-react/src/vite-env.d.ts +++ b/examples/vite-react/src/vite-env.d.ts @@ -4,6 +4,7 @@ interface ImportMetaEnv { readonly VITE_YVP_APP_KEY?: string; readonly VITE_YVP_API_HOST?: string; readonly VITE_YVP_AUTH_REDIRECT_URL?: string; + readonly VITE_YVP_API_TIMEOUT_MS?: string; readonly VITE_YVP_LOCALE?: string; readonly VITE_YVP_DEFAULT_LANGUAGE_ID?: string; } diff --git a/examples/vite-react/vite.config.ts b/examples/vite-react/vite.config.ts index 2661af94..91c82448 100644 --- a/examples/vite-react/vite.config.ts +++ b/examples/vite-react/vite.config.ts @@ -1,14 +1,46 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { parseEnv } from 'node:util'; import path from 'path'; import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; -export default defineConfig({ - base: process.env.VITE_BASE_PATH ?? '/', - plugins: [react(), tailwindcss()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), +export default defineConfig(({ mode }) => { + const rootEnvPath = path.resolve(__dirname, '../..', '.env'); + const rootEnv = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, 'utf8')) : {}; + const packageEnv = loadEnv(mode, __dirname, 'VITE_'); + const exposedEnv = { + ...Object.fromEntries(Object.entries(rootEnv).filter(([name]) => name.startsWith('VITE_'))), + ...packageEnv, + // Explicit prefixed or generic process variables win, then legacy package-local + // values, then the root .env fallback. + VITE_YVP_APP_KEY: + process.env.VITE_YVP_APP_KEY ?? + process.env.YVP_APP_KEY ?? + packageEnv.VITE_YVP_APP_KEY ?? + rootEnv.VITE_YVP_APP_KEY ?? + rootEnv.YVP_APP_KEY, + VITE_YVP_API_HOST: + process.env.VITE_YVP_API_HOST ?? + process.env.YVP_API_HOST ?? + packageEnv.VITE_YVP_API_HOST ?? + rootEnv.VITE_YVP_API_HOST ?? + rootEnv.YVP_API_HOST, + }; + const definedEnv = Object.fromEntries( + Object.entries(exposedEnv) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([name, value]) => [`import.meta.env.${name}`, JSON.stringify(value)]), + ); + + return { + base: process.env.VITE_BASE_PATH ?? '/', + define: definedEnv, + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, }, - }, + }; }); diff --git a/package.json b/package.json index 82ef9e81..1fb7d39b 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "build": "turbo build", "clean": "rimraf packages/*/dist && rimraf packages/*/node_modules && rimraf ./node_modules", "dev": "turbo dev", - "dev:web": "pnpm turbo dev --filter @youversion/platform-react-ui --filter nextjs --filter core --concurrency=10", + "dev:web": "pnpm turbo dev --filter vite-react... --concurrency=10", "test:core": "pnpm --filter @youversion/platform-core test", "test:hooks": "pnpm --filter @youversion/platform-react-hooks test", "test:react": "pnpm --filter @youversion/platform-react-ui test", diff --git a/packages/core/.env.example b/packages/core/.env.example deleted file mode 100644 index 760f19c8..00000000 --- a/packages/core/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Your application ID from YouVersion Platform -YVP_APP_KEY="ADD_YOUR_APP_KEY_HERE" - -# API host - defaults to production (api.youversion.com) -YVP_API_HOST=api.youversion.com diff --git a/packages/core/package.json b/packages/core/package.json index f405ef77..ed3185aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -97,10 +97,10 @@ "build": "rm -rf dist && pnpm build:js && pnpm build:css && pnpm build:types", "prepublishOnly": "YVP_PUBLISH_BUILD=true pnpm build && node ../../scripts/check-sdk-version-stamp.mjs core", "typecheck": "tsc --noEmit", - "test": "dotenv -e .env.local -- vitest run", - "test:integration": "INTEGRATION_TESTS=true dotenv -e .env.local -- vitest run", - "test:watch": "dotenv -e .env.local -- vitest", - "test:coverage": "dotenv -e .env.local -- vitest run --coverage" + "test": "dotenv -e .env.local -e ../../.env -- vitest run", + "test:integration": "INTEGRATION_TESTS=true dotenv -e .env.local -e ../../.env -- vitest run", + "test:watch": "dotenv -e .env.local -e ../../.env -- vitest", + "test:coverage": "dotenv -e .env.local -e ../../.env -- vitest run --coverage" }, "devDependencies": { "@internal/tsconfig": "workspace:*", diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 35f1ab41..090e90ae 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -12,6 +12,9 @@ export default defineConfig({ }, test: { environment: 'node', + env: { + YVP_API_HOST: process.env.YVP_API_HOST || 'api.youversion.com', + }, setupFiles: ['./src/__tests__/polyfills.ts', './src/__tests__/setup.ts'], testTimeout: 10_000, coverage: { diff --git a/packages/ui/.env.example b/packages/ui/.env.example deleted file mode 100644 index 7d2ead39..00000000 --- a/packages/ui/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# If you supply an environment variable prefixed with STORYBOOK_, -# it will be available in import.meta.env when using the Vite builder. -STORYBOOK_YOUVERSION_APP_KEY="" - -# API host - defaults to production (api.youversion.com) -STORYBOOK_YOUVERSION_API_HOST=api.youversion.com - -STORYBOOK_AUTH_REDIRECT_URL=http://localhost:6006 diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index 9b71d199..a4df9a34 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -2,6 +2,8 @@ import type { StorybookConfig } from '@storybook/react-vite'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { readFileSync, existsSync } from 'fs'; +import { parseEnv } from 'util'; +import { loadEnv } from 'vite'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -43,8 +45,42 @@ const config: StorybookConfig = { }, staticDirs: ['../public'], // This is for Storybook mock service worker viteFinal: (config) => { + const rootEnvPath = resolve(__dirname, '../../..', '.env'); + const rootEnv = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, 'utf8')) : {}; + const packageEnv = loadEnv( + config.mode ?? 'development', + resolve(__dirname, '..'), + 'STORYBOOK_', + ); + const exposedEnv = { + ...Object.fromEntries( + Object.entries(rootEnv).filter(([name]) => name.startsWith('STORYBOOK_')), + ), + ...packageEnv, + // Explicit prefixed or generic process variables win, then legacy package-local + // values, then the root .env fallback. + STORYBOOK_YOUVERSION_APP_KEY: + process.env.STORYBOOK_YOUVERSION_APP_KEY ?? + process.env.YVP_APP_KEY ?? + packageEnv.STORYBOOK_YOUVERSION_APP_KEY ?? + rootEnv.STORYBOOK_YOUVERSION_APP_KEY ?? + rootEnv.YVP_APP_KEY, + STORYBOOK_YOUVERSION_API_HOST: + process.env.STORYBOOK_YOUVERSION_API_HOST ?? + process.env.YVP_API_HOST ?? + packageEnv.STORYBOOK_YOUVERSION_API_HOST ?? + rootEnv.STORYBOOK_YOUVERSION_API_HOST ?? + rootEnv.YVP_API_HOST, + }; + const definedEnv = Object.fromEntries( + Object.entries(exposedEnv) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([name, value]) => [`import.meta.env.${name}`, JSON.stringify(value)]), + ); + config.define = { ...config.define, + ...definedEnv, __YV_STYLES__: yvStyles, __YV_COMPONENT_STYLES__: yvComponentStyles, __YV_READER_STYLES__: yvReaderStyles, diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx index c589f615..8d43b3aa 100644 --- a/packages/ui/.storybook/preview.tsx +++ b/packages/ui/.storybook/preview.tsx @@ -93,16 +93,16 @@ const preview: Preview = { typeof context.parameters.locale === 'string' ? context.parameters.locale : context.globals.locale; - const requiredEnvVars = includeAuth - ? ['STORYBOOK_YOUVERSION_APP_KEY', 'STORYBOOK_AUTH_REDIRECT_URL'] - : ['STORYBOOK_YOUVERSION_APP_KEY']; + const requiredEnvVars = ['STORYBOOK_YOUVERSION_APP_KEY']; if (includeAuth) { return ( interface ImportMetaEnv { - readonly STORYBOOK_YOUVERSION_APP_KEY: string; - readonly STORYBOOK_AUTH_REDIRECT_URL: string; + readonly STORYBOOK_YOUVERSION_APP_KEY?: string; + readonly STORYBOOK_AUTH_REDIRECT_URL?: string; /** The YouVersion API host. @default api.youversion.com */ - readonly STORYBOOK_YOUVERSION_API_HOST: string; + readonly STORYBOOK_YOUVERSION_API_HOST?: string; } interface ImportMeta { diff --git a/packages/ui/src/test/StorybookEnvCheck.tsx b/packages/ui/src/test/StorybookEnvCheck.tsx index 83595fb0..674e8b47 100644 --- a/packages/ui/src/test/StorybookEnvCheck.tsx +++ b/packages/ui/src/test/StorybookEnvCheck.tsx @@ -7,12 +7,6 @@ function hasEnvVar(varName: string): boolean { if (varName === 'STORYBOOK_YOUVERSION_APP_KEY') { return Boolean(import.meta.env.STORYBOOK_YOUVERSION_APP_KEY); } - if (varName === 'STORYBOOK_AUTH_REDIRECT_URL') { - return Boolean(import.meta.env.STORYBOOK_AUTH_REDIRECT_URL); - } - if (varName === 'STORYBOOK_YOUVERSION_API_HOST') { - return Boolean(import.meta.env.STORYBOOK_YOUVERSION_API_HOST); - } return false; } @@ -42,8 +36,8 @@ export function StorybookEnvCheck({ ))}

- Copy .env.example to .env.local in packages/ui/{' '} - and fill in the required values. + Copy the repository's root .env.example to .env and add your + app key.

diff --git a/turbo.json b/turbo.json index e66f6ca2..78580030 100644 --- a/turbo.json +++ b/turbo.json @@ -1,7 +1,7 @@ { "$schema": "https://turbo.build/schema.json", "ui": "tui", - "globalDependencies": ["**/.env*local", "tsconfig.json"], + "globalDependencies": [".env", "**/.env*local", "tsconfig.json"], "tasks": { "build": { "dependsOn": ["^build"], @@ -9,21 +9,38 @@ "env": [ "NODE_ENV", "YVP_PUBLISH_BUILD", + "YVP_APP_KEY", + "YVP_API_HOST", "VITE_BASE_PATH", "VITE_YVP_APP_KEY", "VITE_YVP_API_HOST", - "VITE_YVP_AUTH_REDIRECT_URL" + "VITE_YVP_AUTH_REDIRECT_URL", + "VITE_YVP_API_TIMEOUT_MS", + "VITE_YVP_DEFAULT_LANGUAGE_ID", + "VITE_YVP_LOCALE" ], "cache": true }, "dev": { "dependsOn": ["^build"], + "env": [ + "YVP_APP_KEY", + "YVP_API_HOST", + "VITE_BASE_PATH", + "VITE_YVP_APP_KEY", + "VITE_YVP_API_HOST", + "VITE_YVP_AUTH_REDIRECT_URL", + "VITE_YVP_API_TIMEOUT_MS", + "VITE_YVP_DEFAULT_LANGUAGE_ID", + "VITE_YVP_LOCALE" + ], "cache": false, "persistent": true }, "test": { "dependsOn": ["^build"], + "env": ["INTEGRATION_TESTS", "YVP_APP_KEY", "YVP_API_HOST"], "cache": true }, "test:watch": {