feat: use sveltekit served by nest.js

This commit is contained in:
Daniel Dietzler 2025-04-21 20:53:10 +02:00
parent e822e3eca9
commit 38376c5a08
No known key found for this signature in database
GPG Key ID: A1C0B97CD8E18DFF
10 changed files with 207 additions and 79 deletions

View File

@ -53,6 +53,7 @@ COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin COPY --from=prod /usr/src/app/bin ./bin
COPY --from=web /usr/src/app/build /build/www COPY --from=web /usr/src/app/build /build/www
COPY --from=web /usr/src/app/node_modules /build/node_modules
COPY server/resources resources COPY server/resources resources
COPY server/package.json server/package-lock.json ./ COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./ COPY server/start*.sh ./

View File

@ -2,8 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express'; import { NestExpressApplication } from '@nestjs/platform-express';
import { json } from 'body-parser'; import { json } from 'body-parser';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import { existsSync } from 'node:fs'; import type { Handler, NextFunction, Request, Response } from 'express';
import sirv from 'sirv';
import { ApiModule } from 'src/app.module'; import { ApiModule } from 'src/app.module';
import { excludePaths, serverVersion } from 'src/constants'; import { excludePaths, serverVersion } from 'src/constants';
import { ImmichEnvironment } from 'src/enum'; import { ImmichEnvironment } from 'src/enum';
@ -42,24 +41,21 @@ async function bootstrap() {
useSwagger(app, { write: isDev }); useSwagger(app, { write: isDev });
app.setGlobalPrefix('api', { exclude: excludePaths }); app.setGlobalPrefix('api', { exclude: excludePaths });
if (existsSync(resourcePaths.web.root)) {
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46 const svelteHandler = `${resourcePaths.web.root}/handler.js`;
// provides serving of precompressed assets and caching of immutable assets try {
app.use( const { handler } = (await import(svelteHandler)) as { handler: Handler };
sirv(resourcePaths.web.root, { app.use((req: Request, res: Response, next: NextFunction) => {
etag: true, if (req.url.startsWith('/api') || excludePaths.some((path) => req.url.startsWith(path))) {
gzip: true, return next();
brotli: true, }
extensions: [], return handler(req, res, next);
setHeaders: (res, pathname) => { });
if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) { } catch {
res.setHeader('cache-control', 'public,max-age=31536000,immutable'); logger.log('Not serving SvelteKit.');
}
},
}),
);
} }
app.use(app.get(ApiService).ssr(excludePaths));
// app.use(app.get(ApiService).ssr(excludePaths));
const server = await (host ? app.listen(port, host) : app.listen(port)); const server = await (host ? app.listen(port, host) : app.listen(port));
server.requestTimeout = 24 * 60 * 60 * 1000; server.requestTimeout = 24 * 60 * 60 * 1000;

View File

@ -5,8 +5,8 @@ TYPESCRIPT_SDK=/usr/src/open-api/typescript-sdk
npm --prefix "$TYPESCRIPT_SDK" install npm --prefix "$TYPESCRIPT_SDK" install
npm --prefix "$TYPESCRIPT_SDK" run build npm --prefix "$TYPESCRIPT_SDK" run build
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}" UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283}"
until wget --spider --quiet "${UPSTREAM}/api/server/config"; do until wget --spider --quiet "${UPSTREAM}/api/server/ping"; do
echo 'waiting for api server...' echo 'waiting for api server...'
sleep 1 sleep 1
done done

132
web/package-lock.json generated
View File

@ -42,9 +42,9 @@
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.0", "@faker-js/faker": "^9.3.0",
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/enhanced-img": "^0.4.4", "@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.15.2", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.6", "@testing-library/svelte": "^5.2.6",
@ -70,7 +70,7 @@
"prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^5.14.0",
"svelte": "^5.25.3", "svelte": "^5.28.1",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tslib": "^2.6.2", "tslib": "^2.6.2",
@ -1756,6 +1756,89 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
"integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs/node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": { "node_modules/@rollup/pluginutils": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
@ -2074,14 +2157,20 @@
"acorn": "^8.9.0" "acorn": "^8.9.0"
} }
}, },
"node_modules/@sveltejs/adapter-static": { "node_modules/@sveltejs/adapter-node": {
"version": "3.0.8", "version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==", "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": { "peerDependencies": {
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.4.0"
} }
}, },
"node_modules/@sveltejs/enhanced-img": { "node_modules/@sveltejs/enhanced-img": {
@ -2433,6 +2522,13 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/supercluster": { "node_modules/@types/supercluster": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
@ -3531,6 +3627,13 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -5443,6 +5546,13 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true,
"license": "MIT"
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -8178,9 +8288,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.27.3", "version": "5.28.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.3.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz",
"integrity": "sha512-MK16NUEFwAunCkdJpIIJ6hvKElx0zFlKMqQd7NAIugMfrL0YeOH8VEn5pg9g2Q6RLj2JrGJL6c0zaAwmXx/nHQ==", "integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.3.0", "@ampproject/remapping": "^2.3.0",

View File

@ -58,9 +58,9 @@
"@eslint/js": "^9.18.0", "@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.0", "@faker-js/faker": "^9.3.0",
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.8", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/enhanced-img": "^0.4.4", "@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.15.2", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.6", "@testing-library/svelte": "^5.2.6",
@ -86,7 +86,7 @@
"prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^5.14.0", "rollup-plugin-visualizer": "^5.14.0",
"svelte": "^5.25.3", "svelte": "^5.28.1",
"svelte-check": "^4.1.5", "svelte-check": "^4.1.5",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"tslib": "^2.6.2", "tslib": "^2.6.2",

View File

@ -10,18 +10,30 @@ function getNumber(string: string | null, fallback: number) {
} }
return Number.parseInt(string); return Number.parseInt(string);
} }
const getItem = (key: string) => {
if (!globalThis.localStorage) {
const error = new Error('test');
Error.stackTraceLimit = Infinity;
console.log('local storage is not available', error.stack);
return null;
}
return globalThis.localStorage.getItem(key);
};
export const TUNABLES = { export const TUNABLES = {
LAYOUT: { LAYOUT: {
WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false), WASM: getBoolean(getItem('LAYOUT.WASM'), false),
}, },
TIMELINE: { TIMELINE: {
INTERSECTION_EXPAND_TOP: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500), INTERSECTION_EXPAND_TOP: getNumber(getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500),
INTERSECTION_EXPAND_BOTTOM: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500), INTERSECTION_EXPAND_BOTTOM: getNumber(getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500),
}, },
ASSET_GRID: { ASSET_GRID: {
NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(localStorage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false), NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false),
}, },
IMAGE_THUMBNAIL: { IMAGE_THUMBNAIL: {
THUMBHASH_FADE_DURATION: getNumber(localStorage.getItem('THUMBHASH_FADE_DURATION'), 150), THUMBHASH_FADE_DURATION: getNumber(getItem('THUMBHASH_FADE_DURATION'), 150),
}, },
}; };

View File

@ -15,6 +15,7 @@
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { tick } from 'svelte'; import { tick } from 'svelte';
import { browser } from '$app/environment';
interface Props { interface Props {
data: PageData; data: PageData;
@ -57,41 +58,45 @@
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
</svelte:head> </svelte:head>
{#if passwordRequired} {#if browser}
<header> {#if passwordRequired}
<ControlAppBar showBackButton={false}> <header>
{#snippet leading()} <ControlAppBar showBackButton={false}>
<ImmichLogoSmallLink /> {#snippet leading()}
{/snippet} <ImmichLogoSmallLink />
{/snippet}
{#snippet trailing()} {#snippet trailing()}
<ThemeButton /> <ThemeButton />
{/snippet} {/snippet}
</ControlAppBar> </ControlAppBar>
</header> </header>
<main <main
class="relative h-dvh overflow-hidden bg-immich-bg px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40" class="relative h-dvh overflow-hidden bg-immich-bg px-6 max-md:pt-[var(--navbar-height-md)] pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
> >
<div class="flex flex-col items-center justify-center mt-20"> <div class="flex flex-col items-center justify-center mt-20">
<div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">{$t('password_required')}</div> <div class="text-2xl font-bold text-immich-primary dark:text-immich-dark-primary">
<div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary"> {$t('password_required')}
{$t('sharing_enter_password')} </div>
</div> <div class="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
<div class="mt-4"> {$t('sharing_enter_password')}
<form class="flex gap-x-2" novalidate {onsubmit}> </div>
<PasswordField autocomplete="off" bind:password placeholder="Password" /> <div class="mt-4">
<Button type="submit">{$t('submit')}</Button> <form class="flex gap-x-2" novalidate {onsubmit}>
</form> <PasswordField autocomplete="off" bind:password placeholder="Password" />
<Button type="submit">{$t('submit')}</Button>
</form>
</div>
</div> </div>
</main>
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
<AlbumViewer {sharedLink} />
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
<div class="immich-scrollbar">
<IndividualSharedViewer {sharedLink} {isOwned} />
</div> </div>
</main> {/if}
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
<AlbumViewer {sharedLink} />
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
<div class="immich-scrollbar">
<IndividualSharedViewer {sharedLink} {isOwned} />
</div>
{/if} {/if}

View File

@ -5,6 +5,8 @@ import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getMySharedLink, isHttpError } from '@immich/sdk'; import { getMySharedLink, isHttpError } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const ssr = true;
export const load = (async ({ params }) => { export const load = (async ({ params }) => {
const { key } = params; const { key } = params;
await authenticate({ public: true }); await authenticate({ public: true });

View File

@ -22,6 +22,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy'; import { run } from 'svelte/legacy';
import '../app.css'; import '../app.css';
import { browser } from '$app/environment';
interface Props { interface Props {
children?: Snippet; children?: Snippet;
@ -44,6 +45,10 @@
theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT; theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
} }
if (!browser) {
return;
}
if (theme.value === Theme.LIGHT) { if (theme.value === Theme.LIGHT) {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
} else { } else {

View File

@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-static'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
@ -14,10 +14,7 @@ const config = {
}, },
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter({ adapter: adapter(),
fallback: 'index.html',
precompress: true,
}),
alias: { alias: {
$lib: 'src/lib', $lib: 'src/lib',
'$lib/*': 'src/lib/*', '$lib/*': 'src/lib/*',