Skip to content

Commit 9a121fe

Browse files
authored
chore(repo): Introduce integration test for vite with sdk-node (clerk#1921)
* feat(repo): Add integration test for express + vite * feat(repo): Enable express tests in CICD * fix(repo): Copy integration tests to OS temp path This change will help us avoid accidentally using top-level node_modules/ from the current monorepo and will allow us to run tests isolated from the monorepo related dependencies. We had to use a folder outside the monorepo for the integration tests to avoid having the npm module resolution algorithm find unrelated dependencies. * fix(repo): Exit integration tests on application error exit code * fix(repo): Link local clerk packages when version is missing * fix(repo): Fix `npm run nuke` yalc cleanup
1 parent dd57030 commit 9a121fe

26 files changed

+322
-24
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ jobs:
132132

133133
strategy:
134134
matrix:
135-
test-name: [ 'generic', 'nextjs' ]
135+
test-name: [ 'generic', 'nextjs', 'express' ]
136136

137137
steps:
138138
- name: Checkout Repo

integration/constants.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/* eslint-disable turbo/no-undeclared-env-vars */
2+
import * as os from 'node:os';
23
import * as path from 'node:path';
34

45
export const constants = {
5-
TMP_DIR: path.join(process.cwd(), '.temp_integration'),
6-
APPS_STATE_FILE: path.join(process.cwd(), '.temp_integration', 'state.json'),
6+
TMP_DIR: path.join(os.tmpdir(), '.temp_integration'),
7+
APPS_STATE_FILE: path.join(os.tmpdir(), '.temp_integration', 'state.json'),
78
/**
89
* A URL to a running app that will be used to run the tests against.
910
* This is usually used when running the app has been started manually,

integration/models/application.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
6161
stderr: opts.detached ? fs.openSync(stderrFilePath, 'a') : undefined,
6262
log: opts.detached ? undefined : log,
6363
});
64-
// TODO @dimitris: Fail early if server exits
65-
// const shouldRetry = () => proc.exitCode !== 0 && proc.exitCode !== null;
66-
await waitForServer(serverUrl, { log, maxAttempts: Infinity });
64+
const shouldExit = () => !!proc.exitCode && proc.exitCode !== 0;
65+
await waitForServer(serverUrl, { log, maxAttempts: Infinity, shouldExit });
6766
log(`Server started at ${serverUrl}, pid: ${proc.pid}`);
6867
cleanupFns.push(() => awaitableTreekill(proc.pid, 'SIGKILL'));
6968
state.serverUrl = serverUrl;
@@ -85,7 +84,6 @@ export const application = (config: ApplicationConfig, appDirPath: string, appDi
8584
serve: async (opts: { port?: number; manualStart?: boolean } = {}) => {
8685
const port = opts.port || (await getPort());
8786
const serverUrl = `http://localhost:${port}`;
88-
const log = logger.child({ prefix: 'serve' }).info;
8987
// If this is ever used as a background process, we need to make sure
9088
// it's not using the log function. See the dev() method above
9189
const proc = run(scripts.serve, { cwd: appDirPath, env: { PORT: port.toString() } });

integration/models/applicationConfig.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as path from 'node:path';
22

3+
import { constants } from '../constants';
34
import { createLogger, fs } from '../scripts';
45
import { application } from './application.js';
56
import type { EnvironmentConfig } from './environment';
@@ -62,10 +63,9 @@ export const applicationConfig = () => {
6263
commit: async (opts?: { stableHash?: string }) => {
6364
const { stableHash } = opts || {};
6465
logger.info(`Creating project "${name}"`);
65-
const TMP_DIR = path.join(process.cwd(), '.temp_integration');
6666

6767
const appDirName = stableHash || `${name}__${Date.now()}__${hash()}`;
68-
const appDirPath = path.resolve(TMP_DIR, appDirName);
68+
const appDirPath = path.resolve(constants.TMP_DIR, appDirName);
6969

7070
// Copy template files
7171
for (const template of templates) {

integration/presets/express.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { constants } from '../constants';
2+
import { applicationConfig } from '../models/applicationConfig';
3+
import { templates } from '../templates';
4+
5+
const clerkNodeLocal = `file:${process.cwd()}/packages/sdk-node`;
6+
const vite = applicationConfig()
7+
.setName('express-vite')
8+
.useTemplate(templates['express-vite'])
9+
.setEnvFormatter('public', key => `VITE_${key}`)
10+
.addScript('setup', 'npm i --prefer-offline')
11+
.addScript('dev', 'npm run dev')
12+
.addScript('build', 'npm run build')
13+
.addScript('serve', 'npm run start')
14+
.addDependency('@clerk/clerk-sdk-node', constants.E2E_CLERK_VERSION || clerkNodeLocal);
15+
16+
export const express = {
17+
vite,
18+
} as const;

integration/presets/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { envs } from './envs';
2+
import { express } from './express';
23
import { createLongRunningApps } from './longRunningApps';
34
import { next } from './next';
45
import { react } from './react';
56
import { remix } from './remix';
67

78
export const appConfigs = {
89
envs,
10+
express,
911
longRunningApps: createLongRunningApps(),
1012
next,
1113
react,

integration/presets/longRunningApps.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { LongRunningApplication } from '../models/longRunningApplication';
22
import { longRunningApplication } from '../models/longRunningApplication';
33
import { envs } from './envs';
4+
import { express } from './express';
45
import { next } from './next';
56
import { react } from './react';
67
import { remix } from './remix';
@@ -12,6 +13,7 @@ import { remix } from './remix';
1213
*/
1314
export const createLongRunningApps = () => {
1415
const configs = [
16+
{ id: 'express.vite.withEmailCodes', config: express.vite, env: envs.withEmailCodes },
1517
{ id: 'react.vite.withEmailCodes', config: react.vite, env: envs.withEmailCodes },
1618
{ id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks },
1719
{ id: 'remix.node.withEmailCodes', config: remix.remixNode, env: envs.withEmailCodes },

integration/presets/next.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { constants } from '../constants';
22
import { applicationConfig } from '../models/applicationConfig.js';
33
import { templates } from '../templates/index.js';
44

5+
const clerkNextjsLocal = `file:${process.cwd()}/packages/nextjs`;
56
const appRouter = applicationConfig()
67
.setName('next-app-router')
78
.useTemplate(templates['next-app-router'])
@@ -11,7 +12,7 @@ const appRouter = applicationConfig()
1112
.addScript('build', 'npm run build')
1213
.addScript('serve', 'npm run start')
1314
.addDependency('next', constants.E2E_NEXTJS_VERSION)
14-
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION);
15+
.addDependency('@clerk/nextjs', constants.E2E_CLERK_VERSION || clerkNextjsLocal);
1516

1617
const appRouterTurbo = appRouter
1718
.clone()

integration/presets/react.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { constants } from '../constants';
22
import { applicationConfig } from '../models/applicationConfig';
33
import { templates } from '../templates';
44

5+
const clerkReactLocal = `file:${process.cwd()}/packages/react`;
6+
const clerkThemesLocal = `file:${process.cwd()}/packages/themes`;
7+
58
const cra = applicationConfig()
69
.setName('react-cra')
710
.useTemplate(templates['react-cra'])
@@ -10,8 +13,8 @@ const cra = applicationConfig()
1013
.addScript('dev', 'npm run start')
1114
.addScript('build', 'npm run build')
1215
.addScript('serve', 'npm run start')
13-
.addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION)
14-
.addDependency('@clerk/themes', constants.E2E_CLERK_VERSION);
16+
.addDependency('@clerk/clerk-react', constants.E2E_CLERK_VERSION || clerkReactLocal)
17+
.addDependency('@clerk/themes', constants.E2E_CLERK_VERSION || clerkThemesLocal);
1518

1619
const vite = cra
1720
.clone()

integration/presets/remix.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import { constants } from '../constants';
12
import { applicationConfig } from '../models/applicationConfig.js';
23
import { templates } from '../templates/index.js';
34

5+
const clerkRemixLocal = `file:${process.cwd()}/packages/remix`;
46
const remixNode = applicationConfig()
57
.setName('remix-node')
68
.useTemplate(templates['remix-node'])
79
.setEnvFormatter('public', key => `${key}`)
810
.addScript('setup', 'npm i --prefer-offline')
911
.addScript('dev', 'npm run dev')
10-
.addScript('build', 'npm run build');
11-
// .addScript('serve', 'npm run start');
12+
.addScript('build', 'npm run build')
13+
.addScript('serve', 'npm run start')
14+
.addDependency('@clerk/remix', constants.E2E_CLERK_VERSION || clerkRemixLocal);
1215

1316
export const remix = {
1417
remixNode,

integration/scripts/clerkJsServer.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os from 'node:os';
44
import path from 'node:path';
55

6+
import { constants } from '../constants';
67
import { stateFile } from '../models/stateFile';
78
import { awaitableTreekill, fs, waitForServer } from './index';
89
import { run } from './run';
@@ -40,9 +41,8 @@ const serveFromTempDir = async () => {
4041
const port = 18211;
4142
const serverUrl = `http://localhost:${port}`;
4243
const now = Date.now();
43-
const TMP_DIR = path.join(process.cwd(), '.temp_integration');
44-
const stdoutFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.log`);
45-
const stderrFilePath = path.resolve(TMP_DIR, `clerkJsHttpServer.${now}.err.log`);
44+
const stdoutFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.log`);
45+
const stderrFilePath = path.resolve(constants.TMP_DIR, `clerkJsHttpServer.${now}.err.log`);
4646
const clerkJsTempDir = getClerkJsTempDir();
4747
const proc = run(`node_modules/.bin/http-server ${clerkJsTempDir} -d --gzip --cors -a localhost`, {
4848
cwd: process.cwd(),

integration/scripts/waitForServer.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
type WaitForServerArgsType = {
2+
log;
3+
delayInMs?: number;
4+
maxAttempts?: number;
5+
shouldExit?: () => boolean;
6+
};
7+
18
// Poll a url until it returns a 200 status code
2-
export const waitForServer = async (url: string, opts: { delayInMs?: number; maxAttempts?: number; log }) => {
3-
const { delayInMs = 1000, maxAttempts = 20, log } = opts || {};
9+
export const waitForServer = async (url: string, opts: WaitForServerArgsType) => {
10+
const { log, delayInMs = 1000, maxAttempts = 20, shouldExit = () => false } = opts;
411
let attempts = 0;
512
while (attempts < maxAttempts) {
13+
if (shouldExit()) {
14+
throw new Error(`Polling ${url} failed after ${maxAttempts} attempts (due to forced exit)`);
15+
}
16+
617
try {
718
log(`Polling ${url}...`);
819
const res = await fetch(url);
@@ -15,5 +26,6 @@ export const waitForServer = async (url: string, opts: { delayInMs?: number; max
1526
attempts++;
1627
await new Promise(resolve => setTimeout(resolve, delayInMs));
1728
}
29+
1830
throw new Error(`Polling ${url} failed after ${maxAttempts} attempts`);
1931
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "express-vite",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "vite build",
7+
"dev": "PORT=$PORT ts-node src/server/main.ts",
8+
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
9+
"preview": "vite preview --port $PORT --no-open",
10+
"start": "PORT=$PORT ts-node src/server/main.ts"
11+
},
12+
"dependencies": {
13+
"dotenv": "^16.3.1",
14+
"ejs": "^3.1.6",
15+
"express": "^4.18.2",
16+
"ts-node": "^10.9.1",
17+
"typescript": "^4.9.3",
18+
"vite-express": "^0.11.0"
19+
},
20+
"devDependencies": {
21+
"@types/express": "^4.17.15",
22+
"@types/node": "^18.11.18",
23+
"nodemon": "^2.0.20",
24+
"vite": "^4.0.4"
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Should be at the top of the file - used to load clerk secret key
2+
import * as dotenv from 'dotenv';
3+
dotenv.config();
4+
5+
import { clerkClient } from '@clerk/clerk-sdk-node';
6+
import express from 'express';
7+
import ViteExpress from 'vite-express';
8+
9+
const app = express();
10+
11+
app.set('view engine', 'ejs');
12+
app.set('views', 'src/views');
13+
14+
app.get('/api/protected', [clerkClient.expressRequireAuth() as any], (_req: any, res: any) => {
15+
res.send('Protected API response').end();
16+
});
17+
18+
app.get('/sign-in', (_req: any, res: any) => {
19+
return res.render('sign-in.ejs', {
20+
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
21+
signInUrl: process.env.CLERK_SIGN_IN_URL,
22+
});
23+
});
24+
25+
app.get('/', (_req: any, res: any) => {
26+
return res.render('index.ejs', {
27+
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
28+
signInUrl: process.env.CLERK_SIGN_IN_URL,
29+
});
30+
});
31+
32+
app.get('/sign-up', (_req: any, res: any) => {
33+
return res.render('sign-up.ejs', {
34+
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
35+
signUpUrl: process.env.CLERK_SIGN_UP_URL,
36+
});
37+
});
38+
39+
app.get('/protected', (_req: any, res: any) => {
40+
return res.render('protected.ejs', {
41+
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
42+
signInUrl: process.env.CLERK_SIGN_IN_URL,
43+
signUpUrl: process.env.CLERK_SIGN_UP_URL,
44+
});
45+
});
46+
47+
// Handle authentication error, otherwise application will crash
48+
// @ts-ignore
49+
app.use((err, req, res, next) => {
50+
if (err) {
51+
console.error(err);
52+
res.status(401).end();
53+
return;
54+
}
55+
56+
return next();
57+
});
58+
59+
const port = parseInt(process.env.PORT as string) || 3002;
60+
ViteExpress.listen(app, port, () => console.log(`Server is listening on port ${port}...`));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<script
6+
data-clerk-publishable-key="<%= publishableKey %>"
7+
onLoad="startClerk()"
8+
crossorigin="anonymous"
9+
async=""
10+
src="https://clerk.clerk.com/npm/@clerk/clerk-js@4/dist/clerk.browser.js"
11+
></script>
12+
</head>
13+
<body>
14+
<div id="app"></div>
15+
<div id="user-state"></div>
16+
17+
<script>
18+
window.startClerk = async () => {
19+
await Clerk.load({ signInUrl: '<%= signInUrl %>' });
20+
const appEl = document.querySelector('#app');
21+
const controlStateEl = document.querySelector('#user-state');
22+
23+
if (Clerk.user) {
24+
Clerk.mountUserButton(appEl);
25+
controlStateEl.innerHTML = 'SignedIn';
26+
} else {
27+
controlStateEl.innerHTML = 'SignedOut';
28+
}
29+
};
30+
</script>
31+
</body>
32+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<script
6+
data-clerk-publishable-key="<%= publishableKey %>"
7+
onLoad="startClerk()"
8+
crossorigin="anonymous"
9+
async=""
10+
src="https://clerk.clerk.com/npm/@clerk/clerk-js@4/dist/clerk.browser.js"
11+
></script>
12+
</head>
13+
<body>
14+
<div id="app"></div>
15+
<div id="user-state"></div>
16+
17+
<script>
18+
window.startClerk = async () => {
19+
await Clerk.load({ signInUrl: '<%= signInUrl %>' });
20+
21+
if (Clerk.user) {
22+
const apiResponse = await fetch('/api/protected').then(res => res.text());
23+
24+
const div = document.createElement('div');
25+
div.setAttribute('data-test-id', 'protected-api-response');
26+
div.innerText = apiResponse;
27+
document.body.appendChild(div);
28+
}
29+
};
30+
</script>
31+
</body>
32+
</html>

0 commit comments

Comments
 (0)