Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/verify-svelte-browser-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'seamless-cli': minor
---

`seamless verify` now drives a SvelteKit app through the same browser specs as the React template. The `svelte` and `svelte-dev` layers run the reference app in `verify/svelte-app` on `@seamless-auth/svelte` (SvelteKit 3 as a single-page app, as a production build and on `vite dev` with `--dev`), or a Svelte web template once the registry has one. `SEAMLESS_SVELTE_DIR` points the run at another Svelte app, and `--local` installs the locally built `@seamless-auth/client` and `@seamless-auth/svelte`.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,16 @@ their own Dockerfile's `runtime` target and driven by specs written for their ow
Mobile templates are announced and skipped: the harness has no
simulator to drive, so a native app is checked by running it against a `--keep-up` stack.

Angular and Vue run the same browser specs as React. `@seamless-auth/angular` and
`@seamless-auth/vue` keep the React screens' accessible names, so the `angular` and `vue`
Playwright projects point at the React specs and drive an app of that framework instead: a web
template whose `template.json` sets `verify.project` (or whose registry `framework` is) `angular`
or `vue`, or, until one exists, the reference app in [verify/angular-app](verify/angular-app) or
[verify/vue-app](verify/vue-app). `SEAMLESS_ANGULAR_DIR` and `SEAMLESS_VUE_DIR` point the run at
another app. With `--local`, the client and each binding are packed into `verify/angular-vendor`
and `verify/vue-vendor` and installed over the published ones.
Angular, Vue and Svelte run the same browser specs as React. `@seamless-auth/angular`,
`@seamless-auth/vue` and `@seamless-auth/svelte` keep the React screens' accessible names, so the
`angular`, `vue` and `svelte` Playwright projects point at the React specs and drive an app of that
framework instead: a web template whose `template.json` sets `verify.project` (or whose registry
`framework` is) `angular`, `vue` or `svelte`, or, until one exists, the reference app in
[verify/angular-app](verify/angular-app), [verify/vue-app](verify/vue-app) or
[verify/svelte-app](verify/svelte-app) (SvelteKit 3, as a single-page app).
`SEAMLESS_ANGULAR_DIR`, `SEAMLESS_VUE_DIR` and `SEAMLESS_SVELTE_DIR` point the run at another app.
With `--local`, the client and each binding are packed into that app's vendor directory and
installed over the published ones.

`--adapter-url` holds a server adapter this repository does not build (Go, Rust, Python)
to the same contract as the Express and Fastify adapters. It starts only Postgres and the
Expand All @@ -431,7 +433,7 @@ the reference app must serve, and how it reaches the API, is in
[verify/CONFORMANCE.md](verify/CONFORMANCE.md).

`--dev` runs every browser template a second time on its development server (`vite`,
`ng serve`, or `next dev`). Production React runs each effect once; Strict Mode in development runs it
`ng serve`, `vite dev` for SvelteKit, or `next dev`). Production React runs each effect once; Strict Mode in development runs it
twice, so an effect that is not idempotent (a single-use magic link verified twice, say)
passes the production pass and fails this one, the way it fails on a developer's first
`npm run dev`. The conformance workflow passes `--dev` on every pull request.
Expand Down
32 changes: 28 additions & 4 deletions src/commands/verify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const ALL_PROFILES = [
"--profile",
"vue-dev",
"--profile",
"svelte",
"--profile",
"svelte-dev",
"--profile",
"nextjs",
"--profile",
"nextjs-dev",
Expand Down Expand Up @@ -126,7 +130,7 @@ describe("runVerify — published (default) mode", () => {
await runVerify([]);

// Stale tarballs are removed from every vendor dir; non-tgz files are left.
expect(fs.rmSync).toHaveBeenCalledTimes(5);
expect(fs.rmSync).toHaveBeenCalledTimes(6);

const tails = dockerTails();
expect(tails).toContainEqual([...ALL_PROFILES, "down", "-v"]); // initial clean
Expand Down Expand Up @@ -166,6 +170,8 @@ describe("runVerify — published (default) mode", () => {
expect(npmTests).toContainEqual(["test", "--", "--project", "angular"]);
expect(tails).toContainEqual(["--profile", "vue", "up", "-d", "--build", "vue"]);
expect(npmTests).toContainEqual(["test", "--", "--project", "vue"]);
expect(tails).toContainEqual(["--profile", "svelte", "up", "-d", "--build", "svelte"]);
expect(npmTests).toContainEqual(["test", "--", "--project", "svelte"]);

// A successful run does not exit non-zero.
expect(exitSpy).not.toHaveBeenCalled();
Expand Down Expand Up @@ -329,6 +335,8 @@ describe("runVerify — flag parsing", () => {
"angular-dev",
"vue",
"vue-dev",
"svelte",
"svelte-dev",
"nextjs",
"nextjs-dev",
]);
Expand All @@ -338,6 +346,7 @@ describe("runVerify — flag parsing", () => {
expect(out).toContain("Web (dev) · web-basic");
expect(out).toContain("Web (dev) · angular-reference");
expect(out).toContain("Web (dev) · vue-reference");
expect(out).toContain("Web (dev) · svelte-reference");
expect(out).toContain("Full-stack (dev) · nextjs");
});

Expand Down Expand Up @@ -498,9 +507,9 @@ describe("runVerify — local mode", () => {
});

it("packs the react SDK as a single package when the checkout has no workspaces", async () => {
// A checkout that predates the Angular and Vue packages has neither.
// A checkout that predates the Angular, Vue and Svelte packages has none of them.
vi.mocked(fs.existsSync).mockImplementation(
(p: never) => !/packages\/(angular\/dist|vue)/.test(String(p)),
(p: never) => !/packages\/(angular\/dist|vue|svelte)/.test(String(p)),
);

await runVerify(["--local"]);
Expand Down Expand Up @@ -544,9 +553,24 @@ describe("runVerify — local mode", () => {
]);
});

it("packs the Svelte package and its client core for the Svelte app", async () => {
await runVerify(["--local"]);

const packs = callsFor("npm").filter((a) => a[0] === "pack");
expect(packs).toContainEqual([
"pack",
"-w",
"@seamless-auth/client",
"-w",
"@seamless-auth/svelte",
"--pack-destination",
expect.stringContaining("svelte-vendor"),
]);
});

it("packs the client core alongside react when the checkout is a workspace", async () => {
vi.mocked(fs.existsSync).mockImplementation(
(p: never) => !/packages\/(angular\/dist|vue)/.test(String(p)),
(p: never) => !/packages\/(angular\/dist|vue|svelte)/.test(String(p)),
);
vi.mocked(fs.readFileSync).mockImplementation((p: never) => {
const s = String(p);
Expand Down
32 changes: 25 additions & 7 deletions src/commands/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ interface BrowserRuntime {
| "SEAMLESS_REACT_DIR"
| "SEAMLESS_ANGULAR_DIR"
| "SEAMLESS_VUE_DIR"
| "SEAMLESS_SVELTE_DIR"
| "SEAMLESS_FULLSTACK_DIR";
}

Expand Down Expand Up @@ -123,12 +124,20 @@ const VUE_RUNTIME: BrowserRuntime = {
dirEnv: "SEAMLESS_VUE_DIR",
};

const SVELTE_RUNTIME: BrowserRuntime = {
project: "svelte",
service: "svelte",
devService: "svelte-dev",
dirEnv: "SEAMLESS_SVELTE_DIR",
};

// Web templates pick their runtime by verify.project, falling back to the
// framework, and then to React, which is what every web template was before.
const WEB_RUNTIMES: Record<string, BrowserRuntime> = {
react: WEB_RUNTIME,
angular: ANGULAR_RUNTIME,
vue: VUE_RUNTIME,
svelte: SVELTE_RUNTIME,
};

// The reference apps in this repo, one per binding without a web template yet
Expand All @@ -138,6 +147,7 @@ const WEB_RUNTIMES: Record<string, BrowserRuntime> = {
const REFERENCE_APPS: Array<{ id: string; runtime: BrowserRuntime; dir: string }> = [
{ id: "angular-reference", runtime: ANGULAR_RUNTIME, dir: path.join(VERIFY_DIR, "angular-app") },
{ id: "vue-reference", runtime: VUE_RUNTIME, dir: path.join(VERIFY_DIR, "vue-app") },
{ id: "svelte-reference", runtime: SVELTE_RUNTIME, dir: path.join(VERIFY_DIR, "svelte-app") },
];

// Full-stack templates render their own screens, so each framework has its own
Expand Down Expand Up @@ -292,6 +302,7 @@ const FASTIFY_VENDOR_DIR = path.join(VERIFY_DIR, "adapter-fastify-app", "vendor"
const REACT_VENDOR_DIR = path.join(VERIFY_DIR, "react-vendor");
const ANGULAR_VENDOR_DIR = path.join(VERIFY_DIR, "angular-vendor");
const VUE_VENDOR_DIR = path.join(VERIFY_DIR, "vue-vendor");
const SVELTE_VENDOR_DIR = path.join(VERIFY_DIR, "svelte-vendor");

// The React client SDK (@seamless-auth/react). Defaults to a sibling checkout;
// override with SEAMLESS_REACT_SDK_DIR. Only needed for --local browser runs.
Expand Down Expand Up @@ -329,6 +340,7 @@ function cleanVendor(): void {
REACT_VENDOR_DIR,
ANGULAR_VENDOR_DIR,
VUE_VENDOR_DIR,
SVELTE_VENDOR_DIR,
]) {
for (const f of fs.readdirSync(dir)) {
if (f.endsWith(".tgz")) fs.rmSync(path.join(dir, f));
Expand All @@ -352,7 +364,9 @@ function reactSdkWorkspaces(sdkDir: string): string[] {
// over the published version (--local browser runs).
async function packLocalReactSdk(env: NodeJS.ProcessEnv): Promise<void> {
const sdkDir = resolveReactSdkDir();
console.log(kleur.cyan("→ Building & packing the local client SDKs (react, angular, vue)…"));
console.log(
kleur.cyan("→ Building & packing the local client SDKs (react, angular, vue, svelte)…"),
);
await runCommand("npm", ["run", "build"], sdkDir, env);
const workspaceArgs = reactSdkWorkspaces(sdkDir).flatMap((pkg) => ["-w", pkg]);
await runCommand(
Expand Down Expand Up @@ -380,18 +394,22 @@ async function packLocalReactSdk(env: NodeJS.ProcessEnv): Promise<void> {
);
}

// @seamless-auth/vue publishes its own directory, like react.
if (fs.existsSync(path.join(sdkDir, "packages", "vue", "package.json"))) {
// @seamless-auth/vue and /svelte publish their own directories, like react.
for (const [binding, vendorDir] of [
["vue", VUE_VENDOR_DIR],
["svelte", SVELTE_VENDOR_DIR],
] as const) {
if (!fs.existsSync(path.join(sdkDir, "packages", binding, "package.json"))) continue;
await runCommand(
"npm",
[
"pack",
"-w",
"@seamless-auth/client",
"-w",
"@seamless-auth/vue",
`@seamless-auth/${binding}`,
"--pack-destination",
VUE_VENDOR_DIR,
vendorDir,
],
sdkDir,
env,
Expand Down Expand Up @@ -517,7 +535,7 @@ function collectPackageVersions(
? path.join(sdkDir, "packages", "react", "package.json")
: path.join(sdkDir, "package.json");
push("@seamless-auth/react", readPkgVersion(reactPkg));
for (const binding of ["angular", "vue"]) {
for (const binding of ["angular", "vue", "svelte"]) {
const pkg = path.join(sdkDir, "packages", binding, "package.json");
if (fs.existsSync(pkg)) push(`@seamless-auth/${binding}`, readPkgVersion(pkg));
}
Expand Down Expand Up @@ -546,7 +564,7 @@ function collectPackageVersions(
for (const pin of reactPins) push("@seamless-auth/react", pin);
}

// A full-stack, Angular, or Vue template builds from its own lockfile in both
// A full-stack, Angular, Vue, or Svelte template builds from its own lockfile in both
// modes, so its SDK is always the version it pins. A reference app pins
// nothing: it installs the published package unless --local packed one.
for (const tmpl of webTemplates.filter((t) => t.runtime !== WEB_RUNTIME)) {
Expand Down
14 changes: 14 additions & 0 deletions verify/docker-compose.cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,17 @@ services:
- type=gha,scope=verify-vue-dev-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}
cache_to:
- type=gha,mode=max,scope=verify-vue-dev-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}

svelte:
build:
cache_from:
- type=gha,scope=verify-svelte-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}
cache_to:
- type=gha,mode=max,scope=verify-svelte-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}

svelte-dev:
build:
cache_from:
- type=gha,scope=verify-svelte-dev-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}
cache_to:
- type=gha,mode=max,scope=verify-svelte-dev-${SEAMLESS_VERIFY_TEMPLATE_ID:-default}
45 changes: 45 additions & 0 deletions verify/docker-compose.verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
# angular-dev profiles); defaults to ./angular-app
# SEAMLESS_VUE_DIR absolute path to the Vue app to serve (vue and vue-dev
# profiles); defaults to ./vue-app
# SEAMLESS_SVELTE_DIR absolute path to the Svelte app to serve (svelte and
# svelte-dev profiles); defaults to ./svelte-app
# API_SERVICE_TOKEN shared secret between the API and both adapters
# JWKS_KID key id the adapters expect (must match the API's active kid)
# OWNER_EMAIL tenant owner; this email gets the admin role at signup
Expand Down Expand Up @@ -339,6 +341,49 @@ services:
timeout: 3s
retries: 40

# A SvelteKit app on @seamless-auth/svelte, driven by the same browser specs as
# the React template. Built like the Vue app, with the client and Svelte
# tarballs a --local run packs into ./svelte-vendor.
svelte:
profiles: ['svelte']
build:
context: ${SEAMLESS_SVELTE_DIR:-./svelte-app}
additional_contexts:
sdk: ./svelte-vendor
target: runtime
ports:
- '5173:80'
environment:
API_URL: http://localhost:3000
depends_on:
adapter:
condition: service_healthy
healthcheck:
test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost/health']
interval: 3s
timeout: 3s
retries: 20

# The same app on `vite dev` (the Dockerfile's `dev` target). It reads the API
# origin from static/config.js, which already points at localhost:3000.
svelte-dev:
profiles: ['svelte-dev']
build:
context: ${SEAMLESS_SVELTE_DIR:-./svelte-app}
additional_contexts:
sdk: ./svelte-vendor
target: dev
ports:
- '5173:5173'
depends_on:
adapter:
condition: service_healthy
healthcheck:
test: ['CMD', 'node', '-e', "fetch('http://localhost:5173/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 3s
timeout: 3s
retries: 40

# A full-stack template (the Next.js starter) built with its own Dockerfile's
# `runtime` target: the standalone production server, on port 80 with /health.
# It serves /auth itself, so it talks to the auth API directly and needs no
Expand Down
2 changes: 2 additions & 0 deletions verify/harness/lib/matrixReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const LAYERS = [
'angular-dev',
'vue',
'vue-dev',
'svelte',
'svelte-dev',
'nextjs',
'nextjs-dev',
] as const;
Expand Down
4 changes: 4 additions & 0 deletions verify/harness/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ export default defineConfig<AdapterOptions>({
// on `vite`.
{ name: 'vue', testDir: './react', use: { ...browser, baseURL: REACT_URL } },
{ name: 'vue-dev', testDir: './react', use: { ...browser, baseURL: REACT_URL } },
// And against a SvelteKit app on @seamless-auth/svelte; its dev pass runs on
// `vite dev`.
{ name: 'svelte', testDir: './react', use: { ...browser, baseURL: REACT_URL } },
{ name: 'svelte-dev', testDir: './react', use: { ...browser, baseURL: REACT_URL } },
// The Next.js full-stack starter renders its own screens, so it has its own
// specs; its dev pass runs them on `next dev`.
{ name: 'nextjs', testDir: './nextjs', use: { ...browser, baseURL: REACT_URL } },
Expand Down
3 changes: 3 additions & 0 deletions verify/svelte-app/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
build
.svelte-kit
3 changes: 3 additions & 0 deletions verify/svelte-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
build/
.svelte-kit/
30 changes: 30 additions & 0 deletions verify/svelte-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# The verify harness builds this image with an `sdk` build context: verify/svelte-vendor,
# which holds local @seamless-auth/client and @seamless-auth/svelte tarballs on a --local
# run and nothing otherwise. Without them, the published @seamless-auth/svelte is used.
FROM node:24-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY --from=sdk . /tmp/sdk/
ARG SEAMLESS_SVELTE_VERSION=latest
RUN if ls /tmp/sdk/*.tgz >/dev/null 2>&1; then \
npm install --no-save /tmp/sdk/*.tgz; \
else \
npm install --no-save "@seamless-auth/svelte@${SEAMLESS_SVELTE_VERSION}"; \
fi
COPY . .

FROM deps AS dev
EXPOSE 5173
CMD ["npm", "run", "dev"]

FROM deps AS builder
RUN npm run build

FROM nginx:alpine AS runtime
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/build /usr/share/nginx/html
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 80
CMD ["/entrypoint.sh"]
6 changes: 6 additions & 0 deletions verify/svelte-app/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh
set -e
cat <<CONFIG > /usr/share/nginx/html/config.js
window.__SEAMLESS_CONFIG__ = { API_URL: "${API_URL}" };
CONFIG
exec nginx -g "daemon off;"
17 changes: 17 additions & 0 deletions verify/svelte-app/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
server {
listen 80;
root /usr/share/nginx/html;

location = /health {
access_log off;
return 200 "ok";
}

location = /config.js {
add_header Cache-Control "no-store";
}

location / {
try_files $uri /index.html;
}
}
Loading
Loading