Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dev-env scripts #1045

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ To add a new package, simply run `pnpm turbo gen init` in the monorepo root. Thi

The generator sets up the `package.json`, `tsconfig.json` and a `index.ts`, as well as configures all the necessary configurations for tooling around your package such as formatting, linting and typechecking. When the package is created, you're ready to go build out the package.

### 4. Configuring Next-Auth to work with Expo
### 4. Ensuring Next-Auth works with Expo locally

In order for the CSRF protection to work when developing locally, you will need to set the AUTH_URL to the same IP address your expo dev server is listening on. This address is displayed in your Expo CLI when starting the dev server.
Though everything should work out of the box, there's some auto-infering being done that may not work in all cases. Take a look at the [auth package README.md](./packages/auth/README.md) if you encounter any issue.

## FAQ

Expand Down
10 changes: 6 additions & 4 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
"main": "expo-router/entry",
"scripts": {
"clean": "git clean -xdf .expo .turbo node_modules",
"dev": "expo start",
"dev:android": "expo start --android",
"dev:ios": "expo start --ios",
"dev": "pnpm with-dev-env expo start",
"dev:android": "pnpm with-dev-env expo start --android",
"dev:ios": "pnpm with-dev-env expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"with-dev-env": "dotenv -e ../../.env -e ../../.env.local --",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@bacons/text-decoder": "^0.0.0",
Expand Down
56 changes: 37 additions & 19 deletions apps/expo/src/utils/base-url.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
import Constants from "expo-constants";

const NAME = "EXPO_PUBLIC_API_BASE_URL";

/**
* Extend this function when going to production by
* setting the baseUrl to your production API URL.
*/
export const getBaseUrl = () => {
/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];
export const getBaseUrl =
process.env.NODE_ENV === "production"
? () => {
if (!process.env[NAME]) {
throw new Error(
`Failed to get API base url from \`${NAME}\` env var, which is required to be set manually in production.`,
);
}
return process.env[NAME];
}
: () => {
/**
* If the environment variable is set, use it.
*/
if (process.env[NAME]) {
return process.env[NAME];
}

/**
* Gets the IP address of your host-machine. If it cannot automatically find it,
* you'll have to manually set it. NOTE: Port 3000 should work for most but confirm
* you don't have anything else running on it, or you'd have to change it.
*
* **NOTE**: This is only for development. In production, you'll want to set the
* baseUrl to your production API URL.
*/
const debuggerHost = Constants.expoConfig?.hostUri;
const localhost = debuggerHost?.split(":")[0];

if (!localhost) {
// return "https://turbo.t3.gg";
throw new Error(
"Failed to get localhost. Please point to your production server.",
);
}
return `http://${localhost}:3000`;
};
if (!localhost) {
throw new Error(
`Failed to get local API base url. Please point to your local server using \`${NAME}\` env var.`,
);
}
return `http://${localhost}:3000`;
};
6 changes: 5 additions & 1 deletion apps/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
"scripts": {
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"next:dev": "next dev --hostname $HOSTNAME",
"dev": "pnpm with-dev-env pnpm next:dev",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
"write-dev-env": "pnpm with-env node ./scripts/write-dev-env.js",
"with-dev-env": "dotenv -e ../../.env -e ../../.env.local --",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
Expand All @@ -34,6 +37,7 @@
"devDependencies": {
"@acme/eslint-config": "workspace:*",
"@acme/prettier-config": "workspace:*",
"@acme/scripts": "workspace:*",
"@acme/tailwind-config": "workspace:*",
"@acme/tsconfig": "workspace:*",
"@types/node": "^20.12.9",
Expand Down
16 changes: 16 additions & 0 deletions apps/nextjs/scripts/write-dev-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @ts-check
import * as api from "@acme/api/scripts/dev-env.js";
import * as auth from "@acme/auth/scripts/dev-env.js";
import { addDevEnvToFile, makeDevEnv } from "@acme/scripts/dev-env.js";

const filePath = "../../.env.local";
const devEnv = await makeDevEnv([api.getDevEnv, auth.getDevEnv]);

await addDevEnvToFile({
filePath,
devEnv,
source: "/apps/nextjs/scripts/write-dev-env.js",
}).catch((err) => {
console.error(`Failed to write dev env variables to ${filePath}`, err);
process.exit(1);
});
5 changes: 3 additions & 2 deletions apps/nextjs/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ export const GET = async (
cookies().delete(EXPO_COOKIE_NAME);

const authResponse = await DEFAULT_GET(req);

const setCookie = authResponse.headers
.getSetCookie()
.find((cookie) => cookie.startsWith("authjs.session-token"));
.find((cookie) => AUTH_COOKIE_PATTERN.test(cookie));
const match = setCookie?.match(AUTH_COOKIE_PATTERN)?.[1];

if (!match)
throw new Error(
"Unable to find session cookie: " +
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lint:ws": "pnpm dlx sherif@latest",
"postinstall": "pnpm lint:ws",
"typecheck": "turbo typecheck",
"ui-add": "pnpm -F ui ui-add"
"ui-add": "pnpm -F ui ui-add",
"write-dev-env": "pnpm turbo run write-dev-env"
},
"devDependencies": {
"@acme/prettier-config": "workspace:*",
Expand Down
9 changes: 9 additions & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# packages/api

## FAQ

### What's going on with `EXPO_PUBLIC_API_BASE_URL`?

The `EXPO_PUBLIC_API_BASE_URL` is needed to resolve Next.js' exposed API from Expo. This variable needs to be specified when building the app in production, and it should point the production deployment of the Next.js.

The `pnpm dev` command will try to infer the URL automatically, but it may not always work and you may get a different IP address and a different port. If that happens, you can always set it manually in your `.env` file or by setting it before the command, such as `EXPO_PUBLIC_API_BASE_URL=http://x.x.x.x:x pnpm dev`.
4 changes: 3 additions & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
".": {
"types": "./dist/index.d.ts",
"default": "./src/index.ts"
}
},
"./scripts/dev-env.js": "./scripts/dev-env.js"
},
"license": "MIT",
"scripts": {
Expand All @@ -21,6 +22,7 @@
"dependencies": {
"@acme/auth": "workspace:*",
"@acme/db": "workspace:*",
"@acme/scripts": "workspace:*",
"@acme/validators": "workspace:*",
"@trpc/server": "11.0.0-rc.364",
"superjson": "2.2.1",
Expand Down
34 changes: 34 additions & 0 deletions packages/api/scripts/dev-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-check
import path from "path";

import {
EnvVars as AuthEnvVars,
baseUrlFallbackFn,
hostnameFallbackFn,
portFallbackFn,
} from "@acme/auth/scripts/dev-env.js";
import { makeGetDevEnv } from "@acme/scripts/dev-env.js";

export const EnvVars = Object.freeze({
HOSTNAME: AuthEnvVars.HOSTNAME,
PORT: AuthEnvVars.PORT,
EXPO_PUBLIC_API_BASE_URL: "EXPO_PUBLIC_API_BASE_URL",
});

/** @typedef {keyof typeof EnvVars} EnvVarsKeys */
/** @typedef {import('@acme/scripts/dev-env.js').FallbackFn<EnvVarsKeys>} AuthFallbackFn */

export const getDevEnv = makeGetDevEnv(
[
[EnvVars.HOSTNAME, hostnameFallbackFn],
[EnvVars.PORT, portFallbackFn],
[EnvVars.EXPO_PUBLIC_API_BASE_URL, baseUrlFallbackFn],
],
{
source: `/packages/api/scripts/dev-env.js`,
readme: path.relative(
process.cwd(),
path.resolve(import.meta.dirname, "../README.md"),
),
},
);
2 changes: 1 addition & 1 deletion packages/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"outDir": "dist",
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["src"],
"include": ["src", "scripts/*.js"],
"exclude": ["node_modules"]
}
9 changes: 9 additions & 0 deletions packages/auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# packages/auth

## FAQ

### What's going on with `AUTH_URL` when running `pnpm dev`?

In order for certain auth features to work when developing locally (CSRF protection and redirects), the `AUTH_URL` needs to point to the NextJS app but through the IP address your expo dev server is listening on (the address is displayed in your Expo CLI when starting the dev server).

The `pnpm dev` command will try to infer the URL automatically, but it may not always work and you may get a different IP address and a different port. If that happens, you can always set it manually in your `.env` file or by setting it before the command, such as `AUTH_URL=http://x.x.x.x:x pnpm dev`.
5 changes: 4 additions & 1 deletion packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"react-server": "./src/index.rsc.ts",
"default": "./src/index.ts"
},
"./env": "./env.ts"
"./env": "./env.ts",
"./scripts/dev-env.js": "./scripts/dev-env.js"
},
"license": "MIT",
"scripts": {
Expand All @@ -19,6 +20,7 @@
},
"dependencies": {
"@acme/db": "workspace:*",
"@acme/scripts": "workspace:*",
"@auth/drizzle-adapter": "^1.1.0",
"@t3-oss/env-nextjs": "^0.10.1",
"next": "^14.2.3",
Expand All @@ -30,6 +32,7 @@
"devDependencies": {
"@acme/eslint-config": "workspace:*",
"@acme/prettier-config": "workspace:*",
"@acme/scripts": "workspace:*",
"@acme/tsconfig": "workspace:*",
"eslint": "^9.2.0",
"prettier": "^3.2.5",
Expand Down
89 changes: 89 additions & 0 deletions packages/auth/scripts/dev-env.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// @ts-check
import { networkInterfaces } from "os";
import path from "path";

import { makeGetDevEnv } from "@acme/scripts/dev-env.js";
import { getUnusedPort, sanititisePort } from "@acme/scripts/http.js";

export const EnvVars = Object.freeze({
HOSTNAME: "HOSTNAME",
PORT: "PORT",
AUTH_URL: "AUTH_URL",
EXPO_PUBLIC_API_BASE_URL: "EXPO_PUBLIC_API_BASE_URL",
});

/** @typedef {keyof typeof EnvVars} EnvVarsKeys */
/** @typedef {import('@acme/scripts/dev-env.js').FallbackFn<EnvVarsKeys>} AuthFallbackFn */

export const getDevEnv = makeGetDevEnv(
[
[EnvVars.HOSTNAME, hostnameFallbackFn],
[EnvVars.PORT, portFallbackFn],
[EnvVars.AUTH_URL, baseUrlFallbackFn],
[EnvVars.EXPO_PUBLIC_API_BASE_URL, baseUrlFallbackFn],
],
{
source: `/packages/auth/scripts/dev-env.js`,
readme: path.relative(
process.cwd(),
path.resolve(import.meta.dirname, "../README.md"),
),
},
);

export const NEXTJS_INITIAL_PORT =
// eslint-disable-next-line no-restricted-properties
sanititisePort(process.env.PORT) ??
// TODO: read port from command?
null ??
3000;

// this is not going to work in Expo, but using it anyway as default
export const NEXTJS_INITIAL_HOSTNAME = "localhost";

export const NEXTJS_INITIAL_PROTOCOL = "http";

/** @type {AuthFallbackFn} */
export function hostnameFallbackFn() {
const interfaces = networkInterfaces();
/** @type {string[]} */
const addresses = [];
for (const name in interfaces) {
const candidates = interfaces[name] ?? [];
for (const candidate of candidates) {
if (candidate.family === "IPv4" && !candidate.internal) {
addresses.push(candidate.address);
}
}
}
const [address = NEXTJS_INITIAL_HOSTNAME] = addresses.sort(
(a, b) => getIPv4Priority(a) - getIPv4Priority(b),
);
return address;
}

/** @type {AuthFallbackFn} */
export function portFallbackFn() {
return getUnusedPort(NEXTJS_INITIAL_PORT).then(String);
}

/** @type {AuthFallbackFn} */
export function baseUrlFallbackFn() {
const protocol = NEXTJS_INITIAL_PROTOCOL;

return `${protocol}://$${EnvVars.HOSTNAME}:$${EnvVars.PORT}`;
}

/** @param {string} ipv4 */
export function getIPv4Priority(ipv4) {
switch (true) {
case ipv4.startsWith("127."):
return 1000;
case ipv4.startsWith("192.168.1."):
return -100;
case ipv4.startsWith("192.168."):
return -1;
default:
return 0;
}
}
2 changes: 1 addition & 1 deletion packages/auth/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["src", "*.ts"],
"include": ["src", "*.ts", "scripts/*.js"],
"exclude": ["node_modules"]
}
10 changes: 10 additions & 0 deletions packages/scripts/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import baseConfig, { restrictEnvAccess } from "@acme/eslint-config/base";

/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
...restrictEnvAccess,
];
Loading