Init backend, frontend and DB schema
This commit is contained in:
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = tab
|
||||||
|
insert_final_newline = true
|
||||||
|
tab_width = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 2
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
storage
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
9
package.json
Normal file
9
package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "music-repo",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/backend/package.json
Normal file
16
packages/backend/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@effect/schema": "catalog:",
|
||||||
|
"effect": "catalog:",
|
||||||
|
"fast-check": "catalog:",
|
||||||
|
"ulid": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
packages/backend/src/app.ts
Normal file
29
packages/backend/src/app.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Effect } from "effect";
|
||||||
|
import { Request } from "./services/request";
|
||||||
|
import { Storage } from "./services/storage";
|
||||||
|
|
||||||
|
const match = (method: string, ...pattern: readonly string[]) => Effect.gen(function* () {
|
||||||
|
|
||||||
|
const req = yield* Request;
|
||||||
|
|
||||||
|
return req.method === method
|
||||||
|
&& req.path.length === pattern.length
|
||||||
|
&& pattern.every((x, i) => x === "*" || x === req.path[i]);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const app = Effect.gen(function* () {
|
||||||
|
|
||||||
|
const req = yield* Request;
|
||||||
|
const storage = yield* Storage;
|
||||||
|
|
||||||
|
if (yield* match("GET", "ping")) {
|
||||||
|
return new Response("pong", {
|
||||||
|
headers: {
|
||||||
|
"Content-Length": "4",
|
||||||
|
"Content-Type": "text/plain;charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 404 });
|
||||||
|
});
|
||||||
67
packages/backend/src/index.ts
Normal file
67
packages/backend/src/index.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { AccessLog, RequestId } from "common";
|
||||||
|
import { DateTime, Effect, Exit, Option as O, pipe } from "effect";
|
||||||
|
import { ulid } from "ulid";
|
||||||
|
import { app } from "./app";
|
||||||
|
import { Request, RequestInterface } from "./services/request";
|
||||||
|
import { StorageFilesystem } from "./services/storage";
|
||||||
|
|
||||||
|
const storage = StorageFilesystem("storage");
|
||||||
|
|
||||||
|
const server = Bun.serve({
|
||||||
|
fetch: (request, server) => {
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const ip = pipe(
|
||||||
|
server.requestIP(request),
|
||||||
|
O.fromNullable,
|
||||||
|
O.map((a) => a.address),
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = Object.freeze(Object.fromEntries(url.searchParams.entries()));
|
||||||
|
|
||||||
|
const requestId = RequestId.make(ulid());
|
||||||
|
const timestamp = Effect.runSync(DateTime.now);
|
||||||
|
|
||||||
|
const accessLog = AccessLog.make({
|
||||||
|
timestamp,
|
||||||
|
requestId,
|
||||||
|
method: request.method,
|
||||||
|
pathname: url.pathname,
|
||||||
|
query,
|
||||||
|
ip: O.getOrNull(ip),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(JSON.stringify(accessLog));
|
||||||
|
|
||||||
|
const requestInterface = Object.freeze<RequestInterface>({
|
||||||
|
requestId,
|
||||||
|
method: request.method,
|
||||||
|
path: Object.freeze(url.pathname.slice(1).split("/")),
|
||||||
|
query,
|
||||||
|
headers: Object.freeze(Object.fromEntries(request.headers.entries())),
|
||||||
|
ip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fiber = pipe(
|
||||||
|
app,
|
||||||
|
Effect.provideService(Request, requestInterface),
|
||||||
|
Effect.provide(storage),
|
||||||
|
Effect.runFork,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fiber.addObserver(Exit.match({
|
||||||
|
onSuccess: resolve,
|
||||||
|
onFailure: reject,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
websocket: {
|
||||||
|
message: () => { },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
server.stop();
|
||||||
|
});
|
||||||
13
packages/backend/src/services/request.ts
Normal file
13
packages/backend/src/services/request.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { RequestId } from "common";
|
||||||
|
import { Context, Option as O } from "effect";
|
||||||
|
|
||||||
|
export interface RequestInterface {
|
||||||
|
readonly requestId: RequestId;
|
||||||
|
readonly method: string;
|
||||||
|
readonly path: readonly string[];
|
||||||
|
readonly query: { readonly [_: string]: string };
|
||||||
|
readonly headers: { readonly [_: string]: string };
|
||||||
|
readonly ip: O.Option<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Request extends Context.Tag("Request")<Request, RequestInterface>() { }
|
||||||
107
packages/backend/src/services/storage.ts
Normal file
107
packages/backend/src/services/storage.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { Sha256 } from "common";
|
||||||
|
import { Context, Data, Effect, Layer } from "effect";
|
||||||
|
import { NoSuchElementException } from "effect/Cause";
|
||||||
|
import { constant, flow } from "effect/Function";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export class StorageError extends Data.TaggedError("StorageError")<{ cause: unknown }> { }
|
||||||
|
|
||||||
|
export interface StorageInterface {
|
||||||
|
/**
|
||||||
|
* @param sha256 SHA-256 of blob to read
|
||||||
|
* @returns Blob, if exists
|
||||||
|
*/
|
||||||
|
readonly read: (sha256: Sha256) => Effect.Effect<Uint8Array, NoSuchElementException | StorageError>;
|
||||||
|
/**
|
||||||
|
* @param data Blob to write
|
||||||
|
* @param sha256 Precomputed SHA-256, trusted to be accurate if provided
|
||||||
|
* @returns Precomputed SHA-256, if provided, internally computed SHA-256
|
||||||
|
* otherwise
|
||||||
|
*/
|
||||||
|
readonly write: (data: Uint8Array, sha256?: Sha256) => Effect.Effect<Sha256, StorageError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Storage extends Context.Tag("Storage")<Storage, StorageInterface>() { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage implementation that uses the filesystem.
|
||||||
|
*/
|
||||||
|
export const StorageFilesystem = (baseDir: string = ".") => {
|
||||||
|
const sha256toFilePath = (sha256: Sha256): string => {
|
||||||
|
const sha256hex = Buffer.from(sha256).toString("hex");
|
||||||
|
return path.join(
|
||||||
|
baseDir,
|
||||||
|
sha256hex.slice(0, 2),
|
||||||
|
sha256hex.slice(2, 4),
|
||||||
|
sha256hex.slice(4),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Layer.succeed(Storage, Object.freeze<StorageInterface>({
|
||||||
|
read: (sha256) => {
|
||||||
|
const filePath = sha256toFilePath(sha256);
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const arrayBuffer = yield* Effect.tryPromise({
|
||||||
|
try: () => Bun.file(filePath).arrayBuffer(),
|
||||||
|
catch: (error) => new StorageError({ cause: error }),
|
||||||
|
});
|
||||||
|
const array = new Uint8Array(arrayBuffer);
|
||||||
|
return array;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
write: (data, sha256) => Effect.gen(function* () {
|
||||||
|
if (sha256 === undefined) {
|
||||||
|
sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
|
||||||
|
Bun.SHA256.hash(data, sha256);
|
||||||
|
}
|
||||||
|
const filePath = sha256toFilePath(sha256);
|
||||||
|
yield* Effect.tryPromise({
|
||||||
|
try: () => Bun.write(filePath, data),
|
||||||
|
catch: (error) => new StorageError({ cause: error }),
|
||||||
|
});
|
||||||
|
return sha256;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const sha256ToBase64 = (sha256: Sha256): string => {
|
||||||
|
return Buffer.from(sha256).toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage that keeps all data in memory.
|
||||||
|
*/
|
||||||
|
export const StorageMemory = Layer.sync(Storage, () => {
|
||||||
|
const map = new Map<string, Uint8Array>();
|
||||||
|
|
||||||
|
return Object.freeze<StorageInterface>({
|
||||||
|
read: flow(
|
||||||
|
sha256ToBase64,
|
||||||
|
_ => map.get(_),
|
||||||
|
Effect.fromNullable,
|
||||||
|
),
|
||||||
|
write: (data, sha256) => Effect.sync(() => {
|
||||||
|
if (sha256 === undefined) {
|
||||||
|
sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
|
||||||
|
Bun.SHA256.hash(data, sha256);
|
||||||
|
}
|
||||||
|
map.set(sha256ToBase64(sha256), data);
|
||||||
|
return sha256;
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage that discards any writes silently and always fails to read.
|
||||||
|
*/
|
||||||
|
export const StorageEmpty = Layer.succeed(Storage, Object.freeze<StorageInterface>({
|
||||||
|
read: constant(Effect.fail(new NoSuchElementException())),
|
||||||
|
write: (data, sha256) => Effect.sync(() => {
|
||||||
|
if (sha256 === undefined) {
|
||||||
|
sha256 = Sha256.make(new Uint8Array(Bun.SHA256.byteLength));
|
||||||
|
Bun.SHA256.hash(data, sha256);
|
||||||
|
}
|
||||||
|
return sha256;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
9
packages/backend/tsconfig.json
Normal file
9
packages/backend/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["bun"],
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{ "path": "../common" },
|
||||||
|
]
|
||||||
|
}
|
||||||
14
packages/common/package.json
Normal file
14
packages/common/package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "common",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "catalog:"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@effect/schema": "catalog:",
|
||||||
|
"effect": "catalog:",
|
||||||
|
"fast-check": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
86
packages/common/src/index.ts
Normal file
86
packages/common/src/index.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { Schema as S } from "@effect/schema";
|
||||||
|
import { Brand as B, pipe } from "effect";
|
||||||
|
|
||||||
|
export const AttachmentId = pipe(S.ULID, S.brand("AttachmentId"));
|
||||||
|
export const PieceId = pipe(S.ULID, S.brand("PieceId"));
|
||||||
|
export const RequestId = pipe(S.ULID, S.brand("RequestId"));
|
||||||
|
export const UserId = pipe(S.ULID, S.brand("UserId"));
|
||||||
|
|
||||||
|
export type AttachmentId = typeof AttachmentId.Type;
|
||||||
|
export type PieceId = typeof PieceId.Type;
|
||||||
|
export type RequestId = typeof RequestId.Type;
|
||||||
|
export type UserId = typeof UserId.Type;
|
||||||
|
|
||||||
|
export type Sha256 = B.Branded<Uint8Array, "Sha256">;
|
||||||
|
export const Sha256 = pipe(
|
||||||
|
S.Uint8ArrayFromSelf,
|
||||||
|
S.fromBrand(B.refined<Sha256>(
|
||||||
|
(array) => array.byteLength === 32,
|
||||||
|
() => B.error(`Expected Uint8Array to be 32 bytes long`),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
export class BooleanFromNumber extends S.transform(
|
||||||
|
S.Number,
|
||||||
|
S.Boolean,
|
||||||
|
{
|
||||||
|
strict: true,
|
||||||
|
decode: (a) => a !== 0,
|
||||||
|
encode: (i) => i ? 1 : 0,
|
||||||
|
},
|
||||||
|
).annotations({ identifier: "BooleanFromNumber" }) { }
|
||||||
|
|
||||||
|
export const SystemInformation = S.Struct({
|
||||||
|
createdBy: UserId,
|
||||||
|
createdAt: S.DateTimeUtc,
|
||||||
|
modifiedBy: UserId,
|
||||||
|
modifiedAt: S.DateTimeUtc,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SystemInformation = typeof SystemInformation.Type;
|
||||||
|
|
||||||
|
export const AccessLog = S.Struct({
|
||||||
|
timestamp: S.DateTimeUtc,
|
||||||
|
requestId: RequestId,
|
||||||
|
method: S.NonEmptyString,
|
||||||
|
pathname: S.NonEmptyString,
|
||||||
|
query: S.parseJson(S.Record({
|
||||||
|
key: S.String,
|
||||||
|
value: S.String,
|
||||||
|
})),
|
||||||
|
ip: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Attachment = pipe(
|
||||||
|
S.Struct({
|
||||||
|
attachmentId: AttachmentId,
|
||||||
|
pieceId: PieceId,
|
||||||
|
sha256: Sha256,
|
||||||
|
filename: S.NonEmptyString,
|
||||||
|
mediaType: S.NonEmptyString,
|
||||||
|
}),
|
||||||
|
S.extend(SystemInformation),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Piece = pipe(
|
||||||
|
S.Struct({
|
||||||
|
pieceId: PieceId,
|
||||||
|
name: S.NonEmptyString,
|
||||||
|
composer: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
lyricist: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
arranger: S.Union(S.NonEmptyString, S.Null),
|
||||||
|
}),
|
||||||
|
S.extend(SystemInformation),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const User = S.Struct({
|
||||||
|
userId: UserId,
|
||||||
|
username: S.NonEmptyString,
|
||||||
|
password: S.NonEmptyString,
|
||||||
|
admin: BooleanFromNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AccessLog = typeof AccessLog.Type;
|
||||||
|
export type Attachment = typeof Attachment.Type;
|
||||||
|
export type Piece = typeof Piece.Type;
|
||||||
|
export type User = typeof User.Type;
|
||||||
3
packages/common/tsconfig.json
Normal file
3
packages/common/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
}
|
||||||
10
packages/frontend/index.html
Normal file
10
packages/frontend/index.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Repozytorium muzyczne</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<script type="module" src="src/index.tsx"></script>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
||||||
18
packages/frontend/package.json
Normal file
18
packages/frontend/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@vanilla-extract/css": "catalog:",
|
||||||
|
"@vanilla-extract/vite-plugin": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
"vite": "catalog:"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@effect/schema": "catalog:",
|
||||||
|
"effect": "catalog:",
|
||||||
|
"fast-check": "catalog:",
|
||||||
|
"preact": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
packages/frontend/src/index.css.ts
Normal file
20
packages/frontend/src/index.css.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { globalStyle } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
globalStyle("html, body", {
|
||||||
|
margin: 0,
|
||||||
|
padding: 0,
|
||||||
|
|
||||||
|
backgroundColor: "white",
|
||||||
|
color: "black",
|
||||||
|
|
||||||
|
fontFamily: "system-ui, sans-serif",
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "normal",
|
||||||
|
|
||||||
|
"@media": {
|
||||||
|
"(prefers-color-scheme: dark)": {
|
||||||
|
backgroundColor: "black",
|
||||||
|
color: "white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
5
packages/frontend/src/index.tsx
Normal file
5
packages/frontend/src/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { render } from "preact";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
render(<p>Hello World</p>, document.body);
|
||||||
63
packages/frontend/src/store.ts
Normal file
63
packages/frontend/src/store.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { identity } from "effect";
|
||||||
|
import { useLayoutEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
export type Update<T> = T | ((prev: T) => T);
|
||||||
|
export type Updater<T> = (action: Update<T>) => void;
|
||||||
|
|
||||||
|
export interface Store {
|
||||||
|
readonly count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let store: Store = Object.freeze<Store>({
|
||||||
|
count: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- STORE IMPLEMENTATION ----------------------------------------------------
|
||||||
|
|
||||||
|
class Listener<T> {
|
||||||
|
|
||||||
|
private lastState: T;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly selector: Selector<T>,
|
||||||
|
readonly setState: (state: T) => void,
|
||||||
|
) {
|
||||||
|
this.lastState = selector(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
react() {
|
||||||
|
const state = this.selector(store);
|
||||||
|
if (!Object.is(this.lastState, state)) {
|
||||||
|
this.lastState = state;
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = new Set<Listener<any>>();
|
||||||
|
|
||||||
|
export type Selector<T> = (store: Store) => T;
|
||||||
|
|
||||||
|
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), replace?: false): void;
|
||||||
|
function set(action: Update<Store>, replace: true): void;
|
||||||
|
function set(action: Partial<Store> | ((store: Store) => Partial<Store>), replace: boolean = false): void {
|
||||||
|
const nextPartial = typeof action === "function" ? action(store) : action;
|
||||||
|
if (Object.is(store, nextPartial)) return;
|
||||||
|
store = Object.freeze(replace ? nextPartial as Store : Object.assign({}, store, nextPartial));
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener.react();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStore<T = Store>(selector: Selector<T> = identity as Selector<T>): T {
|
||||||
|
|
||||||
|
const [state, setState] = useState(() => selector(store));
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const listener = new Listener(selector, setState);
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => listeners.delete(listener);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
3
packages/frontend/tsconfig.json
Normal file
3
packages/frontend/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
}
|
||||||
6
packages/frontend/vite.config.js
Normal file
6
packages/frontend/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
|
||||||
|
|
||||||
|
/** @type {import("vite").UserConfig} */
|
||||||
|
export default {
|
||||||
|
plugins: [vanillaExtractPlugin()],
|
||||||
|
};
|
||||||
1364
pnpm-lock.yaml
generated
Normal file
1364
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
pnpm-workspace.yaml
Normal file
13
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
|
|
||||||
|
catalog:
|
||||||
|
'@effect/schema': '^0.70.1'
|
||||||
|
'@vanilla-extract/css': '^1.15.3'
|
||||||
|
'@vanilla-extract/vite-plugin': '^4.0.13'
|
||||||
|
effect: '^3.6.0'
|
||||||
|
fast-check: '^3.20.0'
|
||||||
|
preact: '^10.23.1'
|
||||||
|
typescript: '^5.6.0-beta'
|
||||||
|
ulid: '^2.3.0'
|
||||||
|
vite: '^5.3.5'
|
||||||
34
tsconfig.base.json
Normal file
34
tsconfig.base.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
|
||||||
|
"rootDir": "${configDir}/src",
|
||||||
|
"outDir": "${configDir}/dist",
|
||||||
|
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
|
||||||
|
"types": [],
|
||||||
|
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
"paths": {
|
||||||
|
"common": ["./packages/common/src/index.ts"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": ["${configDir}/src"],
|
||||||
|
}
|
||||||
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "packages/backend" },
|
||||||
|
{ "path": "packages/common" },
|
||||||
|
{ "path": "packages/frontend" },
|
||||||
|
],
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user