diff --git a/.gitignore b/.gitignore index 35394555..e9d1b52a 100755 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ lerna-debug.log* # Build outputs dist/ +dist-server/ dist-ssr/ build/ out/ @@ -138,4 +139,4 @@ tasks/ !src/i18n/locales/de/tasks.json # Git worktrees -.worktrees/ \ No newline at end of file +.worktrees/ diff --git a/eslint.config.js b/eslint.config.js index 5645f2e8..9176e2c6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,9 @@ import tseslint from "typescript-eslint"; import react from "eslint-plugin-react"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; -import importX from "eslint-plugin-import-x"; +import { createNodeResolver, importX } from "eslint-plugin-import-x"; +import { createTypeScriptImportResolver } from "eslint-import-resolver-typescript"; +import boundaries from "eslint-plugin-boundaries"; import tailwindcss from "eslint-plugin-tailwindcss"; import unusedImports from "eslint-plugin-unused-imports"; import globals from "globals"; @@ -82,7 +84,7 @@ export default tseslint.config( "sibling", "index", ], - "newlines-between": "never", + "newlines-between": "always", }, ], @@ -98,5 +100,117 @@ export default tseslint.config( "no-control-regex": "off", "no-useless-escape": "off", }, + }, + { + files: ["server/**/*.{js,ts}"], // apply this block only to backend source files + ignores: ["server/**/*.d.ts"], // skip generated declaration files in backend linting + plugins: { + boundaries, // enforce backend architecture boundaries (module-to-module contracts) + "import-x": importX, // keep import hygiene rules (duplicates, unresolved paths, etc.) + "unused-imports": unusedImports, // remove dead imports/variables from backend files + }, + languageOptions: { + parser: tseslint.parser, // parse both JS and TS syntax in backend files + parserOptions: { + ecmaVersion: "latest", // support modern ECMAScript syntax in backend code + sourceType: "module", // treat backend files as ESM modules + }, + globals: { + ...globals.node, // expose Node.js globals such as process, Buffer, and __dirname equivalents + }, + }, + settings: { + "boundaries/include": ["server/**/*.{js,ts}"], // only analyze dependency boundaries inside backend files + "import/resolver": { + // boundaries resolves imports through eslint-module-utils, which reads the classic + // import/resolver setting instead of import-x/resolver-next. + typescript: { + project: ["server/tsconfig.json"], // resolve backend aliases using the canonical backend tsconfig + alwaysTryTypes: true, // keep normal TS package/type resolution working alongside aliases + }, + node: { + extensions: [".mjs", ".cjs", ".js", ".json", ".node", ".ts", ".tsx"], // preserve Node-style fallback resolution for plain files + }, + }, + "import-x/resolver-next": [ + // ESLint's import plugin does not read tsconfig path aliases on its own. + // This resolver teaches import-x how to understand the backend-only "@/*" + // mapping defined in server/tsconfig.json, which fixes false no-unresolved errors in editors. + createTypeScriptImportResolver({ + project: ["server/tsconfig.json"], // point the resolver at the canonical backend tsconfig instead of the frontend one + alwaysTryTypes: true, // keep standard TypeScript package resolution working while backend aliases are enabled + }), + // Keep Node-style resolution available for normal package imports and plain relative JS files. + // The TypeScript resolver handles aliases, while the Node resolver preserves the expected fallback behavior. + createNodeResolver({ + extensions: [".mjs", ".cjs", ".js", ".json", ".node", ".ts", ".tsx"], + }), + ], + "boundaries/elements": [ + { + type: "backend-module", // logical element name used by boundaries rules below + pattern: "server/modules/*", // each direct folder in server/modules is treated as one module boundary + mode: "folder", // classify dependencies at folder-module level (not per individual file) + capture: ["moduleName"], // capture the module folder name for messages/debugging/template use + }, + ], + }, + rules: { + // --- Unused imports/vars (backend) --- + "unused-imports/no-unused-imports": "warn", // warn when imports are not used so they can be cleaned up + "unused-imports/no-unused-vars": "off", // keep backend signal focused on dead imports instead of local unused variables + + // --- Import hygiene (backend) --- + "import-x/no-duplicates": "warn", // prevent duplicate import lines from the same module + "import-x/order": [ + "warn", // keep backend import grouping/order consistent with the frontend config + { + groups: [ + "builtin", // Node built-ins such as fs, path, and url come first + "external", // third-party packages come after built-ins + "internal", // aliased internal imports such as @/... come next + "parent", // ../ imports come after aliased internal imports + "sibling", // ./foo imports come after parent imports + "index", // bare ./ imports stay last + ], + "newlines-between": "always", // require a blank line between import groups in backend files too + }, + ], + "import-x/no-unresolved": "error", // fail when an import path cannot be resolved + "import-x/no-useless-path-segments": "warn", // prefer cleaner paths (remove redundant ./ and ../ segments) + "import-x/no-absolute-path": "error", // disallow absolute filesystem imports in backend files + + // --- General safety/style (backend) --- + eqeqeq: ["warn", "always", { null: "ignore" }], // avoid accidental coercion while still allowing x == null checks + + // --- Architecture boundaries (backend modules) --- + "boundaries/dependencies": [ + "error", // treat architecture violations as lint errors + { + default: "allow", // allow normal imports unless a rule below explicitly disallows them + checkInternals: false, // do not apply these cross-module rules to imports inside the same module + rules: [ + { + to: { type: "backend-module" }, // when importing anything that belongs to another backend module + disallow: { to: { internalPath: "**" } }, // block all direct/deep imports into module internals by default + message: + "Cross-module imports must go through that module's barrel file (server/modules//index.ts or index.js).", // explicit error message for architecture violations + }, + { + to: { type: "backend-module" }, // same target scope as the disallow rule above + allow: { + to: { + internalPath: [ + "index", // allow extensionless barrel imports resolved as module root index + "index.{js,mjs,cjs,ts,tsx}", // allow explicit index.* barrel file imports + ], + }, + }, // re-allow only public module entry points (barrel files) + }, + ], + }, + ], + "boundaries/no-unknown": "error", // fail fast if boundaries cannot classify a dependency, which prevents silent rule bypasses + }, } ); diff --git a/package-lock.json b/package-lock.json index 1e80b8bd..ce4258f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,8 @@ "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", "eslint": "^9.39.3", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -98,6 +100,8 @@ "release-it": "^19.0.5", "sharp": "^0.34.2", "tailwindcss": "^3.4.0", + "tsc-alias": "^1.8.16", + "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.0.4" @@ -443,6 +447,23 @@ "node": ">=6.9.0" } }, + "node_modules/@boundaries/elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@boundaries/elements/-/elements-2.0.1.tgz", + "integrity": "sha512-sAWO3D8PFP6pBXdxxW93SQi/KQqqhE2AAHo3AgWfdtJXwO6bfK6/wUN81XnOZk0qRC6vHzUEKhjwVD9dtDWvxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "is-core-module": "2.16.1", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.18.6", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", @@ -4815,6 +4836,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6673,6 +6704,19 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -7217,6 +7261,112 @@ } } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-boundaries": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-boundaries/-/eslint-plugin-boundaries-6.0.2.tgz", + "integrity": "sha512-wSHgiYeMEbziP91lH0UQ9oslgF2djG1x+LV9z/qO19ggMKZaCB8pKIGePHAY91eLF4EAgpsxQk8MRSFGRPfPzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@boundaries/elements": "2.0.1", + "chalk": "4.1.2", + "eslint-import-resolver-node": "0.3.9", + "eslint-module-utils": "2.12.1", + "handlebars": "4.7.9", + "micromatch": "4.0.8" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-plugin-import-x": { "version": "4.16.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.16.1.tgz", @@ -8519,6 +8669,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8554,9 +8725,9 @@ } }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9446,6 +9617,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -12026,6 +12220,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -12989,6 +13197,16 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -13051,6 +13269,19 @@ "pathe": "^2.0.3" } }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13412,6 +13643,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15173,6 +15414,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -16479,12 +16730,599 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "license": "Apache-2.0" }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tsc-alias/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tsc-alias/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/tsc-alias/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tsc-alias/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index c3fc7358..599e4952 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,18 @@ "url": "https://github.com/siteboon/claudecodeui/issues" }, "scripts": { - "dev": "concurrently --kill-others \"npm run server\" \"npm run client\"", - "server": "node server/index.js", + "dev": "concurrently --kill-others \"npm run server:dev\" \"npm run client\"", + "server": "node dist-server/server/index.js", + "server:dev": "tsx --tsconfig server/tsconfig.json server/index.js", + "server:dev-watch": "tsx watch --tsconfig server/tsconfig.json server/index.js", "client": "vite", - "build": "vite build", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build", + "build:server": "node scripts/build-server.mjs", "preview": "vite preview", - "typecheck": "tsc --noEmit -p tsconfig.json", - "lint": "eslint src/", - "lint:fix": "eslint src/ --fix", + "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p server/tsconfig.json", + "lint": "eslint src/ server/", + "lint:fix": "eslint src/ server/ --fix", "start": "npm run build && npm run server", "release": "./release.sh", "prepublishOnly": "npm run build", @@ -129,6 +133,8 @@ "autoprefixer": "^10.4.16", "concurrently": "^8.2.2", "eslint": "^9.39.3", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-boundaries": "^6.0.2", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", @@ -143,6 +149,8 @@ "release-it": "^19.0.5", "sharp": "^0.34.2", "tailwindcss": "^3.4.0", + "tsc-alias": "^1.8.16", + "tsx": "^4.21.0", "typescript": "^5.9.3", "typescript-eslint": "^8.56.1", "vite": "^7.0.4" diff --git a/scripts/build-server.mjs b/scripts/build-server.mjs new file mode 100644 index 00000000..5a736be4 --- /dev/null +++ b/scripts/build-server.mjs @@ -0,0 +1,61 @@ +import fs from 'node:fs'; +import fsPromises from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); +const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '..'); +const OUTPUT_DIR = path.join(PROJECT_ROOT, 'dist-server'); +const SERVER_TSCONFIG_PATH = 'server/tsconfig.json'; + +function getPackageBinaryPath(packageName) { + const packageJsonPath = require.resolve(`${packageName}/package.json`); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const binField = packageJson.bin; + const binaryRelativePath = + typeof binField === 'string' + ? binField + : binField?.[packageName] ?? Object.values(binField ?? {})[0]; + + if (!binaryRelativePath) { + throw new Error(`Could not find a runnable binary for ${packageName}.`); + } + + return path.resolve(path.dirname(packageJsonPath), binaryRelativePath); +} + +function runPackageBinary(packageName, args) { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [getPackageBinaryPath(packageName), ...args], { + cwd: PROJECT_ROOT, + stdio: 'inherit', + env: process.env, + }); + + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + + reject(new Error(`${packageName} exited with code ${code}.`)); + }); + }); +} + +async function main() { + // Clean first so removed server files do not linger in dist-server and shadow newer source changes. + await fsPromises.rm(OUTPUT_DIR, { recursive: true, force: true }); + + await runPackageBinary('typescript', ['-p', SERVER_TSCONFIG_PATH]); + await runPackageBinary('tsc-alias', ['-p', SERVER_TSCONFIG_PATH]); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/server/cli.js b/server/cli.js index 39461673..2f909d5d 100755 --- a/server/cli.js +++ b/server/cli.js @@ -15,11 +15,12 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// The CLI is compiled into dist-server/server, but it still needs to read the top-level +// package.json and .env file. Resolving the app root once keeps those lookups stable. +const APP_ROOT = findAppRoot(__dirname); // ANSI color codes for terminal output const colors = { @@ -49,13 +50,16 @@ const c = { }; // Load package.json for version info -const packageJsonPath = path.join(__dirname, '../package.json'); +const packageJsonPath = path.join(APP_ROOT, 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +// Match the runtime fallback in load-env.js so "cloudcli status" reports the same default +// database location that the backend will actually use when no DATABASE_PATH is configured. +const DEFAULT_DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); // Load environment variables from .env file if it exists function loadEnvFile() { try { - const envPath = path.join(__dirname, '../.env'); + const envPath = path.join(APP_ROOT, '.env'); const envFile = fs.readFileSync(envPath, 'utf8'); envFile.split('\n').forEach(line => { const trimmedLine = line.trim(); @@ -74,12 +78,12 @@ function loadEnvFile() { // Get the database path (same logic as db.js) function getDatabasePath() { loadEnvFile(); - return process.env.DATABASE_PATH || path.join(__dirname, 'database', 'auth.db'); + return process.env.DATABASE_PATH || DEFAULT_DATABASE_PATH; } // Get the installation directory function getInstallDir() { - return path.join(__dirname, '..'); + return APP_ROOT; } // Show status command @@ -123,7 +127,7 @@ function showStatus() { console.log(` Status: ${projectsExists ? c.ok('[OK] Exists') : c.warn('[WARN] Not found')}`); // Config file location - const envFilePath = path.join(__dirname, '../.env'); + const envFilePath = path.join(APP_ROOT, '.env'); const envExists = fs.existsSync(envFilePath); console.log(`\n${c.info('[INFO]')} Configuration File:`); console.log(` ${c.dim(envFilePath)}`); diff --git a/server/database/db.js b/server/database/db.js index 1127594b..f43c894a 100644 --- a/server/database/db.js +++ b/server/database/db.js @@ -2,8 +2,7 @@ import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js'; import { APP_CONFIG_TABLE_SQL, USER_NOTIFICATION_PREFERENCES_TABLE_SQL, @@ -14,8 +13,10 @@ import { DATABASE_SCHEMA_SQL } from './schema.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// The compiled backend lives under dist-server/server/database, but the install root we log +// should still point at the project/app root. Resolving it here avoids build-layout drift. +const APP_ROOT = findAppRoot(__dirname); // ANSI color codes for terminal output const colors = { @@ -73,7 +74,7 @@ const db = new Database(DB_PATH); db.exec(APP_CONFIG_TABLE_SQL); // Show app installation path prominently -const appInstallPath = path.join(__dirname, '../..'); +const appInstallPath = APP_ROOT; console.log(''); console.log(c.dim('═'.repeat(60))); console.log(`${c.info('[INFO]')} App Installation: ${c.bright(appInstallPath)}`); diff --git a/server/index.js b/server/index.js index d295fae4..0b80b1f1 100755 --- a/server/index.js +++ b/server/index.js @@ -3,13 +3,13 @@ import './load-env.js'; import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm'; +const __dirname = getModuleDir(import.meta.url); +// The server source runs from /server, while the compiled output runs from /dist-server/server. +// Resolving the app root once keeps every repo-level lookup below aligned across both layouts. +const APP_ROOT = findAppRoot(__dirname); +const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm'; // ANSI color codes for terminal output const colors = { @@ -405,11 +405,11 @@ app.use('/api/sessions', authenticateToken, messagesRoutes); app.use('/api/agent', agentRoutes); // Serve public files (like api-docs.html) -app.use(express.static(path.join(__dirname, '../public'))); +app.use(express.static(path.join(APP_ROOT, 'public'))); // Static files served after API routes // Add cache control: HTML files should not be cached, but assets can be cached -app.use(express.static(path.join(__dirname, '../dist'), { +app.use(express.static(path.join(APP_ROOT, 'dist'), { setHeaders: (res, filePath) => { if (filePath.endsWith('.html')) { // Prevent HTML caching to avoid service worker issues after builds @@ -431,7 +431,7 @@ app.use(express.static(path.join(__dirname, '../dist'), { app.post('/api/system/update', authenticateToken, async (req, res) => { try { // Get the project root directory (parent of server directory) - const projectRoot = path.join(__dirname, '..'); + const projectRoot = APP_ROOT; console.log('Starting system update from directory:', projectRoot); @@ -2266,7 +2266,7 @@ app.get('*', (req, res) => { // Only serve index.html for HTML routes, not for static assets // Static assets should already be handled by express.static middleware above - const indexPath = path.join(__dirname, '../dist/index.html'); + const indexPath = path.join(APP_ROOT, 'dist', 'index.html'); // Check if dist/index.html exists (production build available) if (fs.existsSync(indexPath)) { @@ -2381,7 +2381,7 @@ async function startServer() { configureWebPush(); // Check if running in production mode (dist folder exists) - const distIndexPath = path.join(__dirname, '../dist/index.html'); + const distIndexPath = path.join(APP_ROOT, 'dist', 'index.html'); const isProduction = fs.existsSync(distIndexPath); // Log Claude implementation mode @@ -2395,7 +2395,7 @@ async function startServer() { console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`); server.listen(SERVER_PORT, HOST, async () => { - const appInstallPath = path.join(__dirname, '..'); + const appInstallPath = APP_ROOT; console.log(''); console.log(c.dim('═'.repeat(63))); diff --git a/server/load-env.js b/server/load-env.js index ad9ccbb7..2c889534 100644 --- a/server/load-env.js +++ b/server/load-env.js @@ -2,14 +2,15 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import { findAppRoot, getModuleDir } from './utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// Resolve the repo/app root via the nearest /server folder so this file keeps finding the +// same top-level .env file from both /server/load-env.js and /dist-server/server/load-env.js. +const APP_ROOT = findAppRoot(__dirname); try { - const envPath = path.join(__dirname, '../.env'); + const envPath = path.join(APP_ROOT, '.env'); const envFile = fs.readFileSync(envPath, 'utf8'); envFile.split('\n').forEach(line => { const trimmedLine = line.trim(); @@ -24,6 +25,10 @@ try { console.log('No .env file found or error reading it:', e.message); } +// Keep the default database in a stable user-level location so rebuilding dist-server +// never changes where the backend stores auth.db when DATABASE_PATH is not set explicitly. +const DEFAULT_DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); + if (!process.env.DATABASE_PATH) { - process.env.DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db'); + process.env.DATABASE_PATH = DEFAULT_DATABASE_PATH; } diff --git a/server/routes/commands.js b/server/routes/commands.js index 388a8f76..4ce3c4c0 100644 --- a/server/routes/commands.js +++ b/server/routes/commands.js @@ -1,13 +1,15 @@ import express from 'express'; import { promises as fs } from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; import os from 'os'; import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js'; import { parseFrontmatter } from '../utils/frontmatter.js'; +import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __dirname = getModuleDir(import.meta.url); +// This route reads the top-level package.json for the status command, so it needs the real +// app root even after compilation moves the route file under dist-server/server/routes. +const APP_ROOT = findAppRoot(__dirname); const router = express.Router(); @@ -291,7 +293,7 @@ Custom commands can be created in: '/status': async (args, context) => { // Read version from package.json - const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json'); + const packageJsonPath = path.join(APP_ROOT, 'package.json'); let version = 'unknown'; let packageName = 'claude-code-ui'; diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..59a363ed --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "baseUrl": ".", + "paths": { + // In the backend config, "@" maps to the /server directory itself. + "@/*": ["*"] + }, + // The backend is still mostly JavaScript today, so allowJs lets us add a real + // TypeScript build without forcing a large rename before the tooling is usable. + "allowJs": true, + // Keep the migration incremental: existing JS keeps building, while any new TS files + // still go through the normal TypeScript pipeline and strict checks. + "checkJs": false, + "strict": true, + "noEmitOnError": true, + // The backend build emits both /server and /shared into dist-server, so rootDir must + // stay one level above this file even though the config itself now lives in /server. + "rootDir": "..", + "outDir": "../dist-server", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["./**/*.js", "./**/*.ts", "../shared/**/*.js", "../shared/**/*.ts"], + "exclude": ["../dist", "../dist-server", "../node_modules", "../src"] +} diff --git a/server/utils/runtime-paths.js b/server/utils/runtime-paths.js new file mode 100644 index 00000000..92137187 --- /dev/null +++ b/server/utils/runtime-paths.js @@ -0,0 +1,37 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +export function getModuleDir(importMetaUrl) { + return path.dirname(fileURLToPath(importMetaUrl)); +} + +export function findServerRoot(startDir) { + // Source files live under /server, while compiled files live under /dist-server/server. + // Walking up to the nearest "server" folder gives every backend module one stable anchor + // that works in both layouts instead of relying on fragile "../.." assumptions. + let currentDir = startDir; + + while (path.basename(currentDir) !== 'server') { + const parentDir = path.dirname(currentDir); + + if (parentDir === currentDir) { + throw new Error(`Could not resolve the backend server root from "${startDir}".`); + } + + currentDir = parentDir; + } + + return currentDir; +} + +export function findAppRoot(startDir) { + const serverRoot = findServerRoot(startDir); + const parentOfServerRoot = path.dirname(serverRoot); + + // Source files live at /server, while compiled files live at /dist-server/server. + // When the nearest server folder sits inside dist-server we need to hop one extra level up + // so repo-level files still resolve from the real app root instead of the build directory. + return path.basename(parentOfServerRoot) === 'dist-server' + ? path.dirname(parentOfServerRoot) + : parentOfServerRoot; +} diff --git a/tsconfig.json b/tsconfig.json index ec67cc2b..94c553dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,9 @@ "module": "ESNext", "baseUrl": ".", "paths": { + // The frontend keeps "@" mapped to /src. + // The backend gets its own "@" mapping in server/tsconfig.json so both sides can use + // the same alias name without sharing one compiler configuration. "@/*": ["src/*"] }, "skipLibCheck": true, @@ -19,7 +22,7 @@ "allowJs": true, // "checkJs": true, "types": ["vite/client"], - "ignoreDeprecations": "6.0" + "ignoreDeprecations": "5.0" }, "include": ["src", "shared", "vite.config.js"] } diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 00000000..e7117bc8 --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,3 @@ +{ + "extends": "./server/tsconfig.json" +}