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/bin ./bin
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/package.json server/package-lock.json ./
COPY server/start*.sh ./

View File

@ -2,8 +2,7 @@ import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { json } from 'body-parser';
import cookieParser from 'cookie-parser';
import { existsSync } from 'node:fs';
import sirv from 'sirv';
import type { Handler, NextFunction, Request, Response } from 'express';
import { ApiModule } from 'src/app.module';
import { excludePaths, serverVersion } from 'src/constants';
import { ImmichEnvironment } from 'src/enum';
@ -42,24 +41,21 @@ async function bootstrap() {
useSwagger(app, { write: isDev });
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
// provides serving of precompressed assets and caching of immutable assets
app.use(
sirv(resourcePaths.web.root, {
etag: true,
gzip: true,
brotli: true,
extensions: [],
setHeaders: (res, pathname) => {
if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) {
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
}
},
}),
);
const svelteHandler = `${resourcePaths.web.root}/handler.js`;
try {
const { handler } = (await import(svelteHandler)) as { handler: Handler };
app.use((req: Request, res: Response, next: NextFunction) => {
if (req.url.startsWith('/api') || excludePaths.some((path) => req.url.startsWith(path))) {
return next();
}
return handler(req, res, next);
});
} catch {
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));
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" run build
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283/}"
until wget --spider --quiet "${UPSTREAM}/api/server/config"; do
UPSTREAM="${IMMICH_SERVER_URL:-http://immich-server:2283}"
until wget --spider --quiet "${UPSTREAM}/api/server/ping"; do
echo 'waiting for api server...'
sleep 1
done

132
web/package-lock.json generated
View File

@ -42,9 +42,9 @@
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.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/kit": "^2.15.2",
"@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.6",
@ -70,7 +70,7 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^5.14.0",
"svelte": "^5.25.3",
"svelte": "^5.28.1",
"svelte-check": "^4.1.5",
"tailwindcss": "^3.4.17",
"tslib": "^2.6.2",
@ -1756,6 +1756,89 @@
"dev": true,
"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": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
@ -2074,14 +2157,20 @@
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
"integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"dev": true,
"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": {
"@sveltejs/kit": "^2.0.0"
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/enhanced-img": {
@ -2433,6 +2522,13 @@
"@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": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
@ -3531,6 +3627,13 @@
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -5443,6 +5546,13 @@
"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": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -8178,9 +8288,9 @@
}
},
"node_modules/svelte": {
"version": "5.27.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.3.tgz",
"integrity": "sha512-MK16NUEFwAunCkdJpIIJ6hvKElx0zFlKMqQd7NAIugMfrL0YeOH8VEn5pg9g2Q6RLj2JrGJL6c0zaAwmXx/nHQ==",
"version": "5.28.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.28.2.tgz",
"integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",

View File

@ -58,9 +58,9 @@
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.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/kit": "^2.15.2",
"@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.6",
@ -86,7 +86,7 @@
"prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3",
"rollup-plugin-visualizer": "^5.14.0",
"svelte": "^5.25.3",
"svelte": "^5.28.1",
"svelte-check": "^4.1.5",
"tailwindcss": "^3.4.17",
"tslib": "^2.6.2",

View File

@ -10,18 +10,30 @@ function getNumber(string: string | null, fallback: number) {
}
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 = {
LAYOUT: {
WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false),
WASM: getBoolean(getItem('LAYOUT.WASM'), false),
},
TIMELINE: {
INTERSECTION_EXPAND_TOP: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500),
INTERSECTION_EXPAND_BOTTOM: getNumber(localStorage.getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500),
INTERSECTION_EXPAND_TOP: getNumber(getItem('TIMELINE_INTERSECTION_EXPAND_TOP'), 500),
INTERSECTION_EXPAND_BOTTOM: getNumber(getItem('TIMELINE_INTERSECTION_EXPAND_BOTTOM'), 500),
},
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: {
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 { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { tick } from 'svelte';
import { browser } from '$app/environment';
interface Props {
data: PageData;
@ -57,41 +58,45 @@
<title>{title}</title>
<meta name="description" content={description} />
</svelte:head>
{#if passwordRequired}
<header>
<ControlAppBar showBackButton={false}>
{#snippet leading()}
<ImmichLogoSmallLink />
{/snippet}
{#if browser}
{#if passwordRequired}
<header>
<ControlAppBar showBackButton={false}>
{#snippet leading()}
<ImmichLogoSmallLink />
{/snippet}
{#snippet trailing()}
<ThemeButton />
{/snippet}
</ControlAppBar>
</header>
<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"
>
<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="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
{$t('sharing_enter_password')}
</div>
<div class="mt-4">
<form class="flex gap-x-2" novalidate {onsubmit}>
<PasswordField autocomplete="off" bind:password placeholder="Password" />
<Button type="submit">{$t('submit')}</Button>
</form>
{#snippet trailing()}
<ThemeButton />
{/snippet}
</ControlAppBar>
</header>
<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"
>
<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="mt-4 text-lg text-immich-primary dark:text-immich-dark-primary">
{$t('sharing_enter_password')}
</div>
<div class="mt-4">
<form class="flex gap-x-2" novalidate {onsubmit}>
<PasswordField autocomplete="off" bind:password placeholder="Password" />
<Button type="submit">{$t('submit')}</Button>
</form>
</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>
</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>
{/if}
{/if}

View File

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

View File

@ -22,6 +22,7 @@
import { t } from 'svelte-i18n';
import { run } from 'svelte/legacy';
import '../app.css';
import { browser } from '$app/environment';
interface Props {
children?: Snippet;
@ -44,6 +45,10 @@
theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
}
if (!browser) {
return;
}
if (theme.value === Theme.LIGHT) {
document.documentElement.classList.remove('dark');
} 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 dotenv from 'dotenv';
@ -14,10 +14,7 @@ const config = {
},
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
fallback: 'index.html',
precompress: true,
}),
adapter: adapter(),
alias: {
$lib: 'src/lib',
'$lib/*': 'src/lib/*',