Compare commits
77 Commits
Author | SHA1 | Date | |
---|---|---|---|
37d6cd848d
|
|||
1aff6554b5
|
|||
a6359d9516
|
|||
6ab52c1830
|
|||
cfb5564830
|
|||
48948a180a
|
|||
1557d2cca9
|
|||
019d1d2e62
|
|||
1b4bb1d2e2
|
|||
824852fe7a
|
|||
4e2b1b74a4
|
|||
0aa7a18f05
|
|||
10d6be5f82
|
|||
6615e2788a
|
|||
1f846bd5fe | |||
da7b056095 | |||
4b8ecc65c3
|
|||
3696bcade2
|
|||
dea401fec0
|
|||
66494c6760
|
|||
05320916b2
|
|||
e6857f5b5d
|
|||
5dba5471d1
|
|||
f1ff9f1c38
|
|||
7386a29ec5
|
|||
88b158b7b4
|
|||
a2e78e2e36
|
|||
b2bab79562
|
|||
0b8bddb1f6
|
|||
cbd03de6a3
|
|||
0842c107e6 | |||
a59f8688d0 | |||
91bba73796 | |||
85e1874188 | |||
6ab6560009 | |||
16bbb29f32 | |||
c0c35b8e0f | |||
626982fe3e | |||
79e079f115 | |||
cd933b77cb | |||
ef4ddf872f | |||
ca3178e42a | |||
338d3ccdbb | |||
83eb10aadd | |||
2858c8c375 | |||
0c09a354b8 | |||
52ada9d040 | |||
6cf7361905 | |||
335f341839 | |||
f4d5ccd4c3 | |||
abd6a8a3be | |||
990e0e131f | |||
d6c24702d6 | |||
13467c1c8f | |||
3b338ea854 | |||
7a85f1aae3 | |||
1e90cf7fc5 | |||
1b36f81b85 | |||
fba4548df4 | |||
8ca2e03e6b | |||
ac6c82f2d8 | |||
35dc5db5b9 | |||
186d6423d2 | |||
9b7cd22d31 | |||
4e7c757e71 | |||
c7ce50b334 | |||
518041ce4e | |||
2ca056059d | |||
66e2cce28d | |||
deb3382023 | |||
667eaabd58 | |||
13c958db03 | |||
36a297837d | |||
07fcf86fef | |||
ce25ef7ec1 | |||
84e8d3b2bd | |||
9c8459c3e1
|
24
.gitea/workflows/lints.yml
Normal file
24
.gitea/workflows/lints.yml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
shellcheck:
|
||||||
|
name: Misc Linters
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: https://gitea.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Lint shellscripts
|
||||||
|
uses: ludeeus/action-shellcheck@master
|
||||||
|
|
||||||
|
- name: Lint Dockerfile
|
||||||
|
uses: hadolint/hadolint-action@v3.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: ./Containerfile
|
@ -6,6 +6,9 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
lints:
|
||||||
|
uses: ./.gitea/workflows/lints.yml
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
uses: ./.gitea/workflows/node.yml
|
uses: ./.gitea/workflows/node.yml
|
||||||
|
|
||||||
@ -21,7 +24,7 @@ jobs:
|
|||||||
- name: Checkout sources
|
- name: Checkout sources
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: https://git.vbrandl.net
|
registry: https://git.vbrandl.net
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "es5",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:21-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
@ -7,9 +7,12 @@ RUN npm ci
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM node:21-alpine
|
FROM node:22-alpine
|
||||||
USER node:node
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
USER node:node
|
||||||
|
COPY ./container/entrypoint.sh /entrypoint.sh
|
||||||
COPY package.json .
|
COPY package.json .
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY --from=builder /app/build ./build
|
COPY --from=builder /app/build ./build
|
||||||
CMD ["node", "./build/index.js"]
|
ENTRYPOINT ["sh", "/entrypoint.sh"]
|
||||||
|
12
container/entrypoint.sh
Executable file
12
container/entrypoint.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export STORAGE_PATH="${STORAGE_PATH:-./uploads}"
|
||||||
|
|
||||||
|
entrypoint() {
|
||||||
|
mkdir -p "${STORAGE_PATH}"
|
||||||
|
node ./build/index.js
|
||||||
|
}
|
||||||
|
|
||||||
|
entrypoint
|
@ -15,19 +15,19 @@ export default [
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
...globals.browser,
|
...globals.browser,
|
||||||
...globals.node
|
...globals.node,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.svelte'],
|
files: ['**/*.svelte'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: ts.parser
|
parser: ts.parser,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
ignores: ['build/', '.svelte-kit/', 'dist/'],
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
6
flake.lock
generated
6
flake.lock
generated
@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1720957393,
|
"lastModified": 1723637854,
|
||||||
"narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=",
|
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "693bc46d169f5af9c992095736e82c3488bf7dbb",
|
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
595
package-lock.json
generated
595
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fotochallenge",
|
"name": "fotochallenge",
|
||||||
"version": "0.0.1",
|
"version": "0.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@ -17,8 +17,10 @@
|
|||||||
"@sveltejs/adapter-node": "^5.2.0",
|
"@sveltejs/adapter-node": "^5.2.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
|
"@types/bunyan": "^1.8.11",
|
||||||
"@types/eslint": "^9.0.0",
|
"@types/eslint": "^9.0.0",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"bulma": "^1.0.1",
|
"bulma": "^1.0.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
@ -39,5 +41,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">20"
|
"node": ">20"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bunyan": "^1.8.15",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
|
||||||
extends: ['local>renovate-bot/renovate-config']
|
extends: ['local>renovate-bot/renovate-config'],
|
||||||
}
|
}
|
||||||
|
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
@ -9,3 +9,9 @@ declare global {
|
|||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Gabi und Hannes Fotochallenge</title>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body data-sveltekit-preload-data="hover">
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
45
src/hooks.server.ts
Normal file
45
src/hooks.server.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { log, timedExecution } from '$lib';
|
||||||
|
import { validate, v7 as uuidv7 } from 'uuid';
|
||||||
|
|
||||||
|
const requestIdHeader = 'x-request-id';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
|
export async function handle({ event, resolve }) {
|
||||||
|
// use incomming requestId, if it is a valid uuid, else generate one
|
||||||
|
const reqIdFromRequest = event.request.headers.get(requestIdHeader);
|
||||||
|
const { requestId, fromRequest } =
|
||||||
|
reqIdFromRequest && validate(reqIdFromRequest)
|
||||||
|
? { requestId: reqIdFromRequest, fromRequest: true }
|
||||||
|
: { requestId: uuidv7(), fromRequest: false };
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
requestId,
|
||||||
|
route: event.route.id,
|
||||||
|
method: event.request.method,
|
||||||
|
userAgent: event.request.headers.get('user-agent'),
|
||||||
|
clientIP: event.getClientAddress(),
|
||||||
|
};
|
||||||
|
if (fromRequest) {
|
||||||
|
log.trace(context, 'using incomming request-id');
|
||||||
|
}
|
||||||
|
|
||||||
|
// make requestId available to handlers
|
||||||
|
event.locals.requestId = requestId;
|
||||||
|
|
||||||
|
const { executionTime, result: response } = await timedExecution(async () => {
|
||||||
|
return await resolve(event);
|
||||||
|
});
|
||||||
|
response.headers.set(requestIdHeader, requestId);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
{
|
||||||
|
executionTime: `${executionTime}ms`,
|
||||||
|
status: response.status,
|
||||||
|
size: response.headers.get('content-length'),
|
||||||
|
...context,
|
||||||
|
},
|
||||||
|
'finished request'
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
@ -2,7 +2,31 @@ import safePath from '$lib';
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
describe('safe path', () => {
|
describe('safe path', () => {
|
||||||
it('removes non alphanum from string', () => {
|
it('reject names with ../', () => {
|
||||||
expect(safePath('../../!=-.,/abc123')).toBe('abc123');
|
expect(safePath('./uplodas', '../foobar')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept names with ./', () => {
|
||||||
|
expect(safePath('./uplodas', './foobar')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject names with /', () => {
|
||||||
|
expect(safePath('./uplodas', 'foo/bar')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept happy path', () => {
|
||||||
|
expect(safePath('./uplodas', 'foobar')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept names starting with `..`', () => {
|
||||||
|
expect(safePath('./uplodas', '..foobar')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept names ending with `..`', () => {
|
||||||
|
expect(safePath('./uplodas', 'foobar..')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accept names starting and ending with `..`', () => {
|
||||||
|
expect(safePath('./uplodas', '..foobar..')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,42 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
|
import path from 'path';
|
||||||
|
import bunyan from 'bunyan';
|
||||||
|
import type { MaybePromise } from '@sveltejs/kit';
|
||||||
|
|
||||||
const safePath = (input: string) => input.replace(/\W/g, '');
|
export const log = bunyan.createLogger({
|
||||||
export const storagePath: string = './uploads';
|
name: 'fotochallenge',
|
||||||
|
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||||
|
src: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function safePath(basePath: string, name: string): boolean {
|
||||||
|
const fullPath = `${basePath}/${name}`;
|
||||||
|
const relative = path.relative(basePath, fullPath);
|
||||||
|
return (
|
||||||
|
!!relative &&
|
||||||
|
// does move out of `basePath`
|
||||||
|
!relative.startsWith(`..${path.sep}`) &&
|
||||||
|
// exactly one layer deep, e.g. no `./uplodas/foo/bar`
|
||||||
|
!relative.includes(path.sep) &&
|
||||||
|
// result is not an absolute path
|
||||||
|
!path.isAbsolute(relative)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPath: string = './uploads';
|
||||||
|
if (!('STORAGE_PATH' in process.env)) {
|
||||||
|
log.warn(`'STORAGE_PATH' environment variable is not set. Defaulting to ${defaultPath}`);
|
||||||
|
}
|
||||||
|
export const storagePath: string = process.env.STORAGE_PATH ?? defaultPath;
|
||||||
|
|
||||||
export default safePath;
|
export default safePath;
|
||||||
|
|
||||||
|
export async function timedExecution<T>(
|
||||||
|
fn: () => MaybePromise<T>
|
||||||
|
): Promise<{ executionTime: number; result: T }> {
|
||||||
|
const start = process.hrtime();
|
||||||
|
const result = await fn();
|
||||||
|
const end = process.hrtime(start);
|
||||||
|
const executionTime = (end[0] * 1e6 + end[1]) / 1e6;
|
||||||
|
return { executionTime, result };
|
||||||
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
||||||
import { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
import type { RequestEvent } from './$types';
|
import type { RequestEvent } from './$types';
|
||||||
import safePath, { storagePath } from '$lib';
|
import safePath, { storagePath, log } from '$lib';
|
||||||
import { hash } from 'crypto';
|
import { hash } from 'crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
@ -12,29 +12,44 @@ const mkdirIfNotExists = (path: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request }: RequestEvent) => {
|
default: async ({ request, locals }: RequestEvent) => {
|
||||||
|
let context: { [key: string]: string | string[] } = { requestId: locals.requestId };
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
|
||||||
const formFiles = data.getAll('files');
|
const formFiles = data.getAll('files');
|
||||||
if (!formFiles) {
|
if (!formFiles) {
|
||||||
|
log.debug(context, 'missing files');
|
||||||
return fail(400, { field: 'files', files: formFiles, missing: true });
|
return fail(400, { field: 'files', files: formFiles, missing: true });
|
||||||
} else if (!(formFiles as File[])) {
|
} else if (!(formFiles as File[])) {
|
||||||
|
log.debug(context, 'invalid files');
|
||||||
return fail(400, { field: 'files', files: formFiles, incorrect: true });
|
return fail(400, { field: 'files', files: formFiles, incorrect: true });
|
||||||
}
|
}
|
||||||
const files = formFiles as File[];
|
const files = formFiles as File[];
|
||||||
console.log(files);
|
const fileNames = files.map((file) => file.name);
|
||||||
|
context = { fileNames, ...context };
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
|
log.debug(context, 'empty files');
|
||||||
return fail(400, { field: 'files', files: formFiles, empty: true });
|
return fail(400, { field: 'files', files: formFiles, empty: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const formName = data.get('name');
|
const formName = data.get('name');
|
||||||
if (!formName) {
|
if (!formName) {
|
||||||
|
log.debug(context, 'missing name');
|
||||||
return fail(400, { field: 'name', name: formName, missing: true });
|
return fail(400, { field: 'name', name: formName, missing: true });
|
||||||
} else if (!(formName as string)) {
|
} else if (!(formName as string)) {
|
||||||
|
log.debug(context, 'invalid name');
|
||||||
return fail(400, { field: 'name', name: formName, incorrect: true });
|
return fail(400, { field: 'name', name: formName, incorrect: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = safePath(formName as string);
|
const name = formName as string;
|
||||||
|
context = { name, ...context };
|
||||||
|
|
||||||
|
if (!safePath(storagePath, name)) {
|
||||||
|
log.warn(context, 'Supplied name would cause dir traversal. Rejecting...');
|
||||||
|
return fail(400, { field: 'name', name: name, incorrect: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(context, 'Uploading files');
|
||||||
|
|
||||||
files.forEach(async (file) => {
|
files.forEach(async (file) => {
|
||||||
const outPath = `${storagePath}/${name}`;
|
const outPath = `${storagePath}/${name}`;
|
||||||
@ -42,17 +57,19 @@ export const actions = {
|
|||||||
const ext = path.extname(file.name);
|
const ext = path.extname(file.name);
|
||||||
|
|
||||||
mkdirIfNotExists(outPath);
|
mkdirIfNotExists(outPath);
|
||||||
const filename = hash('sha1', content);
|
const filename = `${hash('sha1', content)}${ext}`;
|
||||||
const fullPath = `${outPath}/${filename}${ext}`;
|
const fullPath = `${outPath}/${filename}`;
|
||||||
|
context = { file: fullPath, ...context };
|
||||||
if (existsSync(fullPath)) {
|
if (existsSync(fullPath)) {
|
||||||
console.warn(`${fullPath} has already been uploaded. Skipping...`);
|
log.debug(context, 'File has already been uploaded. Skipping...');
|
||||||
} else {
|
} else {
|
||||||
|
log.debug(context, 'saving file');
|
||||||
writeFileSync(fullPath, Buffer.from(await file.arrayBuffer()), { flag: 'a+' });
|
writeFileSync(fullPath, Buffer.from(await file.arrayBuffer()), { flag: 'a+' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true
|
success: true,
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
const siPrefixes = new Map([
|
const siPrefixes = new Map([
|
||||||
[1_000_000, 'M'],
|
[1_000_000, 'M'],
|
||||||
[1_000, 'k']
|
[1_000, 'k'],
|
||||||
]);
|
]);
|
||||||
const fileSize = (files: FileList) => {
|
const fileSize = (files: FileList) => {
|
||||||
const size = Array.from(files)
|
const size = Array.from(files)
|
||||||
|
@ -11,8 +11,8 @@ const config = {
|
|||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter()
|
adapter: adapter(),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
@ -5,14 +5,14 @@ export default defineConfig({
|
|||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||||
},
|
},
|
||||||
|
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
additionalData: '@use "src/variables.scss" as *;'
|
additionalData: '@use "src/variables.scss" as *;',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user