From 5d731ee191fa6bfe39ba690e19164ba08f6488e6 Mon Sep 17 00:00:00 2001 From: Rizky Date: Sun, 15 Dec 2024 05:21:32 +0700 Subject: [PATCH] init --- .gitignore | 178 ++++++++++++++++++++++++++++++++++++ .vscode/settings.json | 18 ++++ bun.lockb | Bin 0 -> 11194 bytes internal/db/ensure.ts | 53 +++++++++++ internal/deploy/download.ts | 21 +++++ internal/deploy/ensure.ts | 27 ++++++ internal/deploy/load.ts | 45 +++++++++ internal/server/ensure.ts | 33 +++++++ internal/server/server.ts | 16 ++++ internal/server/start.ts | 11 +++ internal/supervisor.ts | 27 ++++++ internal/utils/color.ts | 25 +++++ internal/utils/config.ts | 39 ++++++++ internal/utils/download.ts | 50 ++++++++++ internal/utils/fs.ts | 88 ++++++++++++++++++ internal/utils/global.ts | 59 ++++++++++++ internal/utils/log.ts | 9 ++ internal/utils/spawn.ts | 82 +++++++++++++++++ package.json | 23 +++++ tsconfig.json | 32 +++++++ 20 files changed, 836 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100755 bun.lockb create mode 100644 internal/db/ensure.ts create mode 100644 internal/deploy/download.ts create mode 100644 internal/deploy/ensure.ts create mode 100644 internal/deploy/load.ts create mode 100644 internal/server/ensure.ts create mode 100644 internal/server/server.ts create mode 100644 internal/server/start.ts create mode 100644 internal/supervisor.ts create mode 100644 internal/utils/color.ts create mode 100644 internal/utils/config.ts create mode 100644 internal/utils/download.ts create mode 100644 internal/utils/fs.ts create mode 100644 internal/utils/global.ts create mode 100644 internal/utils/log.ts create mode 100644 internal/utils/spawn.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..700a722 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +site +site/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..511ec13 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,18 @@ +{ + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + ".codesandbox": true, + ".devcontainer": true, + ".vscode": true, + "node_modules": true, + ".gitignore": true, + "bun.lockb": true, + "port.json": true + }, + "hide-files.files": [] +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..614b7c00a50ffbf2fe4d431bc751dae12a8eca33 GIT binary patch literal 11194 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 { + if (!config.current?.db?.url) { + dbLog("Warning: db.url is empty. Please set it in site.json"); + } else { + const db = config.current?.db; + if (db.orm === "prisma") { + const cwd = fs.path(`site:app/db`); + + const url = new URL(db.url); + const host = `[${c.blue}${url.hostname}${c.esc}]`; + const db_type = `[${c.red}${url.protocol.slice(0, -1).toUpperCase()}${ + c.esc + }]`; + if (!fs.exists("site:app/db")) { + dbLog(`Preparing PrismaDB ${db_type} on ${host} ${url.pathname}`); + await removeAsync(cwd); + await dirAsync(cwd); + + await $`bun init .`.cwd(cwd).quiet(); + await $`bun add prisma`.cwd(cwd).quiet(); + await $`bun prisma init`.cwd(cwd).quiet(); + + dbLog(`PrismaDB created at ${cwd}`); + fs.write(`site:app/db/.env`, `DATABASE_URL=${db.url}`); + + await $`bun prisma db pull`.cwd(cwd).quiet(); + dbLog(`PrismaDB instrospected (db pull)`); + + await $`bun prisma generate`.cwd(cwd).quiet(); + dbLog(`PrismaDB ready`); + + await fs.write( + `site:app/db/index.ts`, + `\ +import { PrismaClient } from "@prisma/client/extension"; +export const db = new PrismaClient(); +` + ); + } else { + await $`bun prisma generate`.cwd(cwd).quiet(); + dbLog(`PrismaDB Ready: ${db_type} on ${host} ${url.pathname}`); + } + } + } +}; diff --git a/internal/deploy/download.ts b/internal/deploy/download.ts new file mode 100644 index 0000000..2977f50 --- /dev/null +++ b/internal/deploy/download.ts @@ -0,0 +1,21 @@ +import { config } from "utils/config"; +import { downloadFile } from "utils/download"; +import { fs } from "utils/fs"; +import { siteLog } from "utils/log"; + +export const downloadDeployedSite = async (site_id: string) => { + let base_url = "https://prasi.avolut.com"; + const ts = Date.now(); + siteLog("Downloading site deploy: "); + await downloadFile( + `${base_url}/prod-zip/${site_id}?ts=${ts}&msgpack=1`, + fs.path(`site:deploy/history/${ts}.gz`), + (rec, total) => { + if (rec % 10 === 0) process.stdout.write("."); + } + ); + config.set("deploy.current", ts); + config.set("deploy.history", [...(config.current?.deploy.history || []), ts]); + await fs.copy(`site:deploy/history/${ts}.gz`, `site:deploy/current/${ts}.gz`); + return ts; +}; diff --git a/internal/deploy/ensure.ts b/internal/deploy/ensure.ts new file mode 100644 index 0000000..196921d --- /dev/null +++ b/internal/deploy/ensure.ts @@ -0,0 +1,27 @@ +import { config } from "utils/config"; +import { fs } from "utils/fs"; +import { downloadDeployedSite } from "./download"; + +export const ensureDeployExists = async (site_id: string) => { + let download_deploy = false; + const ts = config.current?.deploy.current; + if (!ts) { + download_deploy = true; + } else if (!fs.exists(`site:deploy/current/${ts}.gz`)) { + if (fs.exists(`site:deploy/history/${ts}.gz`)) { + await fs.copy( + `site:deploy/history/${ts}.gz`, + `site:deploy/current/${ts}.gz` + ); + } else { + download_deploy = true; + } + } + + if (download_deploy) { + const ts = await downloadDeployedSite(site_id); + process.stdout.write("\n"); + return ts; + } + return ts as number; +}; diff --git a/internal/deploy/load.ts b/internal/deploy/load.ts new file mode 100644 index 0000000..95be840 --- /dev/null +++ b/internal/deploy/load.ts @@ -0,0 +1,45 @@ +import { gunzipSync } from "bun"; +import { removeAsync } from "fs-jetpack"; +import get from "lodash.get"; +import { decode } from "msgpackr"; +import { config } from "utils/config"; +import { fs } from "utils/fs"; +import { g } from "utils/global"; + +export const loadCurrentDeploy = async (ts: number) => { + if (fs.exists(`site:deploy/current/${ts}.gz`)) { + await removeAsync(fs.path(`site:deploy/current/files`)); + + const content = decode( + gunzipSync( + new Uint8Array( + await Bun.file(fs.path(`site:deploy/current/${ts}.gz`)).arrayBuffer() + ) + ) + ); + + g.site = { + layouts: content.layouts, + pages: content.pages, + comps: content.comps, + db: config.current?.db, + info: content.site, + }; + + for (const key of ["public", "code.server", "code.site", "code.core"]) { + const files = get(content, key); + if (files) { + for (const [path, raw_content] of Object.entries(files)) { + const prefix = key.split(".").pop() || ""; + await fs.write( + `site:deploy/current/files/${prefix ? `${prefix}/` : ""}${path}`, + raw_content, + { + mode: "raw", + } + ); + } + } + } + } +}; diff --git a/internal/server/ensure.ts b/internal/server/ensure.ts new file mode 100644 index 0000000..a078fc2 --- /dev/null +++ b/internal/server/ensure.ts @@ -0,0 +1,33 @@ +import { $ } from "bun"; +import { fs } from "utils/fs"; + +export const ensureServerReady = async (is_dev: boolean) => { + if (!fs.exists("site:app/package.json")) { + await fs.write(`site:app/package.json`, { + name: "prasi-app", + workspaces: ["db"], + dependencies: { db: "workspace:*" }, + }); + } + await $`bun i`.cwd(fs.path("site:app")).quiet().nothrow(); + + if (is_dev) { + const rebuild = async () => { + try { + await $`bun build --watch --target bun --entry ${fs.path( + "internal:server/server.ts" + )} --outdir ${fs.path("site:app")} --sourcemap=linked`.quiet(); + } catch (e) { + rebuild(); + } + }; + rebuild(); + } else { + await Bun.build({ + target: "bun", + entrypoints: [fs.path(`internal:server/server.ts`)], + outdir: fs.path("site:app"), + sourcemap: "linked", + }); + } +}; diff --git a/internal/server/server.ts b/internal/server/server.ts new file mode 100644 index 0000000..d468652 --- /dev/null +++ b/internal/server/server.ts @@ -0,0 +1,16 @@ +import { c } from "utils/color"; +import { config } from "utils/config"; +import { g, startup } from "utils/global"; +import { siteLog } from "utils/log"; +import { loadCurrentDeploy } from "../deploy/load"; + +startup("site", async () => { + await config.init("site:site.json"); + const ts = config.current?.deploy.current; + if (ts) { + await loadCurrentDeploy(ts); + siteLog( + `Site Loaded [${c.green}${g.site.pages.length} pages${c.esc}] [${c.blue}${g.site.comps.length} components${c.esc}]` + ); + } +}); diff --git a/internal/server/start.ts b/internal/server/start.ts new file mode 100644 index 0000000..1081ca0 --- /dev/null +++ b/internal/server/start.ts @@ -0,0 +1,11 @@ +import { fs } from "utils/fs"; +import { g } from "utils/global"; +import { spawn } from "utils/spawn"; + +export const startServer = (is_dev: boolean) => { + g.server = spawn({ + cmd: is_dev ? "bun run --watch server.js" : "bun run server.js", + cwd: fs.path("site:app"), + mode: "passthrough", + }); +}; diff --git a/internal/supervisor.ts b/internal/supervisor.ts new file mode 100644 index 0000000..31babe3 --- /dev/null +++ b/internal/supervisor.ts @@ -0,0 +1,27 @@ +import { c } from "utils/color"; +import { config } from "utils/config"; +import { fs } from "utils/fs"; +import { startup } from "utils/global"; +import { siteLog } from "utils/log"; +import { ensureDBReady } from "./db/ensure"; +import { ensureDeployExists } from "./deploy/ensure"; +import { ensureServerReady } from "./server/ensure"; +import { startServer } from "./server/start"; + +const is_dev = process.argv.includes("--dev"); +startup("supervisor", async () => { + console.log(`${c.green}Prasi Server:${c.esc} ${fs.path("site:")}`); + await config.init("site:site.json"); + + const site_id = config.get("site_id") as string; + if (!site_id) { + siteLog("No Site Loaded"); + } else { + siteLog(`Site ID: ${site_id}`); + await ensureDeployExists(site_id); + await ensureServerReady(is_dev); + await ensureDBReady(); + + startServer(is_dev); + } +}); diff --git a/internal/utils/color.ts b/internal/utils/color.ts new file mode 100644 index 0000000..4f573e5 --- /dev/null +++ b/internal/utils/color.ts @@ -0,0 +1,25 @@ +export const c = { + esc: "\x1b[0m", + black: "\x1b[30m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + blackbg: "\x1b[40m", + redbg: "\x1b[41m", + greenbg: "\x1b[42m", + yellowbg: "\x1b[43m", + bluebg: "\x1b[44m", + magentabg: "\x1b[45m", + cyanbg: "\x1b[46m", + whitebg: "\x1b[47m", + bold: "\x1b[1m", + dim: "\x1b[2m", + italic: "\x1b[3m", + underline: "\x1b[4m", + rgb: (r: number, g: number, b: number) => `\x1b[38;2;${r};${g};${b}m`, + rgbbg: (r: number, g: number, b: number) => `\x1b[48;2;${r};${g};${b}m`, +} diff --git a/internal/utils/config.ts b/internal/utils/config.ts new file mode 100644 index 0000000..3e91933 --- /dev/null +++ b/internal/utils/config.ts @@ -0,0 +1,39 @@ +import { dirAsync } from "fs-jetpack"; +import { fs } from "./fs"; +import get from "lodash.get"; +import set from "lodash.set"; + +export const config = { + async init(path: string) { + if (!fs.exists(path)) { + await fs.write(path, default_config); + } + + const result = await fs.read(path, "json"); + this.current = result as typeof default_config; + this.file_path = path; + return result as typeof default_config; + }, + get(path: string) { + return get(this.current, path); + }, + async set(path: string, value: any) { + set(this.current as any, path, value); + await fs.write(this.file_path, this.current); + }, + file_path: "", + current: null as null | typeof default_config, +}; + +const default_config = { + site_id: "", + port: 0, + upload_path: "upload", + db: { orm: "prisma" as "prisma" | "prasi", url: "" }, + deploy: { + current: 0, + history: [], + }, +}; + +export type SiteConfig = typeof default_config; diff --git a/internal/utils/download.ts b/internal/utils/download.ts new file mode 100644 index 0000000..356c689 --- /dev/null +++ b/internal/utils/download.ts @@ -0,0 +1,50 @@ +import { dirAsync } from "fs-jetpack"; +import { dirname } from "path"; + +export const downloadFile = async ( + url: string, + filePath: string, + progress?: (rec: number, total: number) => void +) => { + try { + const _url = new URL(url); + if (_url.hostname === "localhost") { + _url.hostname = "127.0.0.1"; + } + const res = await fetch(_url as any); + if (res.body) { + await dirAsync(dirname(filePath)); + const file = Bun.file(filePath); + + const writer = file.writer(); + const reader = res.body.getReader(); + + // Step 3: read the data + let receivedLength = 0; // received that many bytes at the moment + let chunks = []; // array of received binary chunks (comprises the body) + while (true) { + const { done, value } = await reader.read(); + + if (done) { + writer.end(); + break; + } + + chunks.push(value); + writer.write(value); + receivedLength += value.length; + + if (progress) { + progress( + receivedLength, + parseInt(res.headers.get("content-length") || "0") + ); + } + } + } + return true; + } catch (e) { + console.log(e); + return false; + } +}; diff --git a/internal/utils/fs.ts b/internal/utils/fs.ts new file mode 100644 index 0000000..92d534c --- /dev/null +++ b/internal/utils/fs.ts @@ -0,0 +1,88 @@ +import { mkdirSync, statSync } from "fs"; +import { copyAsync } from "fs-jetpack"; +import { g } from "./global"; +import { dirname, join } from "path"; +const internal = Symbol("internal"); + +export const fs = { + exists(path: string) { + try { + const s = statSync(this.path(path)); + return s.isDirectory() || s.isFile(); + } catch (e) {} + return false; + }, + path(path: string) { + const all_prefix = this[internal].prefix as Record; + const prefix_key = Object.keys(all_prefix).find((e) => path.startsWith(e)); + const prefix_path = all_prefix[prefix_key!]; + + if (prefix_key && prefix_path) { + return `${prefix_path}/${path.substring(prefix_key.length + 1)}`; + } + return path; + }, + async copy(from: string, to: string) { + const from_dir = this.path(from); + const to_path = this.path(to); + const is_dir = statSync(from_dir).isDirectory(); + if (is_dir && !this.exists(to)) { + mkdirSync(to_path, { recursive: true }); + } else { + const to_dir = dirname(to_path); + if (!fs.exists(to_dir)) { + mkdirSync(to_dir, { recursive: true }); + } + } + + return await copyAsync(from_dir, to_path, { overwrite: true }); + }, + + async modify(arg: { + path: string; + save: (content: any) => string | object | Promise; + as?: "json" | "string"; + }) { + const as = arg.as || arg.path.endsWith(".json") ? "json" : "string"; + const content = await this.read(arg.path, as); + const result = await arg.save(content); + return await this.write(arg.path, result); + }, + async read(path: string, as?: "json" | "string") { + const file = Bun.file(this.path(path)); + if (as === "json") { + return await file.json(); + } + + return await file.text(); + }, + + async write( + path: string, + data: any, + opt?: { + mode?: "json" | "raw"; + } + ) { + const file = Bun.file(this.path(path)); + if (typeof data === "object" && opt?.mode !== "raw") { + return await Bun.write(file, JSON.stringify(data, null, 2), { + createPath: true, + }); + } + + return await Bun.write(file, data, { + createPath: true, + }); + }, + init() { + this[internal].prefix.site = join(g.dir.root, "site"); + this[internal].prefix.internal = join(process.cwd(), "internal"); + }, + [internal]: { + prefix: { + site: "", + internal: "", + }, + }, +}; diff --git a/internal/utils/global.ts b/internal/utils/global.ts new file mode 100644 index 0000000..5f84f2a --- /dev/null +++ b/internal/utils/global.ts @@ -0,0 +1,59 @@ +import { join, resolve } from "path"; +import { fs } from "./fs"; +import type { SiteConfig } from "./config"; +import type { spawn } from "./spawn"; + +if (!(globalThis as any).prasi) { + (globalThis as any).prasi = {}; +} + +export const g = (globalThis as any).prasi as unknown as { + dir: { root: string }; + server: ReturnType; + site: { + db?: SiteConfig["db"]; + layouts: { + id: string; + name: string; + url: string; + content_tree: any; + is_default_layout: boolean; + }[]; + pages: { + id: string; + name: string; + url: string; + content_tree: any; + }[]; + comps: { + id: string; + content_tree: any; + }[]; + info: { + id: string; + name: string; + config?: { + api_url: string; + }; + responsive: string; + domain: string; + }; + }; +}; + +export const startup = (mode: "supervisor" | "site", fn: () => void) => { + g.dir = { root: "" }; + if (mode === "supervisor") { + const argv = process.argv.filter((e) => e !== "--dev"); + if (argv.length > 2) { + g.dir.root = resolve(argv[2]); + } else { + g.dir.root = process.cwd(); + } + } else { + g.dir.root = join(process.cwd(), "..", ".."); + } + fs.init(); + + fn(); +}; diff --git a/internal/utils/log.ts b/internal/utils/log.ts new file mode 100644 index 0000000..0338c44 --- /dev/null +++ b/internal/utils/log.ts @@ -0,0 +1,9 @@ +import { c } from "./color"; + +export const siteLog = (msg: string) => { + console.log(`${c.magenta}[SITE]${c.esc} ${msg}`); +}; + +export const dbLog = (msg: string) => { + console.log(`${c.cyan}[ DB ]${c.esc} ${msg}`); +}; diff --git a/internal/utils/spawn.ts b/internal/utils/spawn.ts new file mode 100644 index 0000000..605909e --- /dev/null +++ b/internal/utils/spawn.ts @@ -0,0 +1,82 @@ +import { spawn as bunSpawn, type Subprocess } from "bun"; +import { Readable } from "node:stream"; + +export const spawn = ( + arg: { + cmd: string; + cwd?: string; + log?: false | { max_lines: number }; + ipc?(message: any, subprocess: Subprocess): void; + } & ( + | { + onMessage: (arg: { + from: "stdout" | "stderr"; + text: string; + raw: string; + }) => void; + mode?: "pipe"; + } + | { + mode?: "passthrough"; + } + ) +) => { + const log = { + lines: 0, + text: [] as string[], + }; + + async function processStream( + stream: AsyncIterable, + from: "stderr" | "stdout" + ) { + for await (const x of stream) { + const buf = x as Buffer; + const raw = buf.toString("utf-8"); + const text = raw + .trim() + .replace( + /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, + "" + ); + + if (arg.log) { + log.lines += 1; + log.text.push(text); + if (log.lines > arg.log.max_lines) { + log.text.shift(); + } + } + + const on_msg = (arg as any).onMessage; + if (arg.mode !== "passthrough" && on_msg) { + on_msg({ from: from, text, raw }); + } + } + } + + const is_piped = arg.mode === "pipe" || !arg.mode; + + const proc = bunSpawn({ + cmd: arg.cmd.split(" "), + cwd: arg.cwd, + env: { ...process.env, FORCE_COLOR: "1" }, + ...(is_piped + ? { stderr: "pipe", stdout: "pipe" } + : { stderr: "inherit", stdout: "inherit" }), + ...(arg.ipc ? { ipc: arg.ipc } : undefined), + }); + + if (is_piped) { + const stdout = Readable.fromWeb(proc.stdout as any); + const stderr = Readable.fromWeb(proc.stderr as any); + processStream(stdout, "stdout"); + processStream(stderr, "stderr"); + } + + return { + process: proc, + exited: proc.exited, + log, + }; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..4e8df9a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "prasi-srv", + "module": "index.ts", + "type": "module", + "scripts": { + "dev": "bun run --watch internal/supervisor.ts --dev" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@types/lodash.get": "^4.4.9", + "@types/lodash.set": "^4.3.9", + "dayjs": "^1.11.13", + "fs-jetpack": "^5.1.0", + "lodash.get": "^4.4.2", + "lodash.set": "^4.3.2", + "msgpackr": "^1.11.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..28e7e0f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,32 @@ +{ + "exclude": ["site/*"], + "compilerOptions": { + "paths": { + "utils/*": ["./internal/utils/*"] + }, + + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}