diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 18c1d2dae..bf754b071 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -37,11 +37,11 @@ jobs: working-directory: client run: npm run generate - - name: get server dependencies - run: npm ci --only=production + - name: get server dependencies and compile server + run: npm ci && npm run build:server && npm prune --omit=dev - name: build binary - run: pkg -t node20-linux-x64 -o audiobookshelf . + run: pkg -t node20-linux-x64 -o audiobookshelf dist-server/index.js - name: run audiobookshelf run: | diff --git a/.gitignore b/.gitignore index 12ebec1c2..9ca683854 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /client/.nuxt/ /client/dist/ /dist/ +/dist-server/ /deploy/ /coverage/ /.nyc_output/ diff --git a/Dockerfile b/Dockerfile index 816bdd3c3..8bff3201b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN apk add --no-cache --update \ WORKDIR /server COPY index.js package* /server +COPY /scripts /server/scripts COPY /server /server/server RUN case "$TARGETPLATFORM" in \ @@ -38,7 +39,7 @@ RUN case "$TARGETPLATFORM" in \ unzip /tmp/library.zip -d $NUSQLITE3_DIR && \ rm /tmp/library.zip -RUN npm ci --only=production +RUN npm ci && npm run build:server && npm prune --omit=dev ### STAGE 2: Create minimal runtime image ### FROM node:20-alpine @@ -70,4 +71,4 @@ ENV NUSQLITE3_DIR=${NUSQLITE3_DIR} ENV NUSQLITE3_PATH=${NUSQLITE3_PATH} ENTRYPOINT ["tini", "--"] -CMD ["node", "index.js"] +CMD ["node", "dist-server/index.js"] diff --git a/build/linuxpackager b/build/linuxpackager index 52a9beba9..d3fbb8508 100755 --- a/build/linuxpackager +++ b/build/linuxpackager @@ -17,6 +17,7 @@ echo "--------------------" cd client rm -rf node_modules npm ci --unsafe-perm=true --allow-root +npm run build:server npm run generate cd .. @@ -48,7 +49,7 @@ Description: $DESCRIPTION" echo "$controlfile" > dist/debian/DEBIAN/control; # Package debian -pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . +pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf dist-server/index.js fakeroot dpkg-deb -Zxz --build dist/debian diff --git a/package-lock.json b/package-lock.json index 22d966447..abc221fa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,11 +34,13 @@ "audiobookshelf": "index.js" }, "devDependencies": { + "@types/node": "^20.17.0", "chai": "^4.3.10", "mocha": "^10.2.0", "nodemon": "^2.0.20", "nyc": "^15.1.0", - "sinon": "^17.0.1" + "sinon": "^17.0.1", + "typescript": "^5.7.3" } }, "node_modules/@ampproject/remapping": { @@ -697,9 +699,13 @@ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } }, "node_modules/@types/validator": { "version": "13.7.17", @@ -5263,6 +5269,20 @@ "is-typedarray": "^1.0.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -5280,6 +5300,12 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/package.json b/package.json index bbdb473e9..ef6516292 100644 --- a/package.json +++ b/package.json @@ -3,33 +3,34 @@ "version": "2.36.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", - "main": "index.js", + "main": "dist-server/index.js", "scripts": { - "dev": "nodemon --watch server index.js -- --dev", - "start": "node index.js", - "start-dev": "node index.js --prod-with-dev-env", + "build:server": "node scripts/buildServer.js", + "dev": "nodemon --watch server --watch index.js --ext js,ts --exec \"npm run build:server && node dist-server/index.js\" -- --dev", + "start": "node dist-server/index.js", + "start-dev": "node dist-server/index.js --prod-with-dev-env", "client": "cd client && npm ci && npm run generate", - "prod": "npm run client && npm ci && node index.js", - "build-win": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip .", - "build-win-no-compress": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf .", + "prod": "npm run client && npm ci && npm run build:server && npm run start", + "build-win": "npm run client && npm run build:server && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip dist-server/index.js", + "build-win-no-compress": "npm run client && npm run build:server && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf dist-server/index.js", "build-linux": "build/linuxpackager", "docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf", "docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local", "docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local", "deploy-linux": "node deploy/linux", - "test": "mocha", - "coverage": "nyc mocha" + "test": "npm run build:server && mocha dist-server/test", + "coverage": "npm run build:server && nyc mocha dist-server/test" }, - "bin": "index.js", + "bin": "dist-server/index.js", "pkg": { "assets": [ "client/dist/**/*", "node_modules/sqlite3/lib/binding/**/*.node", - "server/migrations/*.js" + "dist-server/server/migrations/*.js" ], "scripts": [ - "index.js", - "server/**/*.js" + "dist-server/index.js", + "dist-server/server/**/*.js" ] }, "mocha": { @@ -60,11 +61,13 @@ "xml2js": "^0.5.0" }, "devDependencies": { + "@types/node": "^20.17.0", "chai": "^4.3.10", "mocha": "^10.2.0", "nodemon": "^2.0.20", "nyc": "^15.1.0", - "sinon": "^17.0.1" + "sinon": "^17.0.1", + "typescript": "^5.7.3" }, "allowScripts": { "sqlite3@5.1.7": true diff --git a/readme.md b/readme.md index 16a346c55..00ae8ea1f 100644 --- a/readme.md +++ b/readme.md @@ -183,7 +183,7 @@ cd .. After setting up your development environment, either using the dev container or using your own custom environment, the following commands will help you run the server and client. -To run the server, you can use the command `npm run dev`. This will use the client that was built when you ran `npm run generate` in the client directory or when you started the dev container. If you make changes to the server, you will need to restart the server. If you make changes to the client, you will need to run the command `(cd client; npm run generate)` and then restart the server. By default the client runs at `localhost:3333`, though the port can be configured in `dev.js`. +To run the server, you can use the command `npm run dev`. This will compile the server and use the client that was built when you ran `npm run generate` in the client directory or when you started the dev container. Server changes are compiled and restarted automatically. If you make changes to the client, you will need to run the command `(cd client; npm run generate)` and then restart the server. By default the client runs at `localhost:3333`, though the port can be configured in `dev.js`. You can also build a version of the client that supports live reloading. To do this, start the server, then run the command `(cd client; npm run dev)`. This will run a separate instance of the client at `localhost:3000` that will be automatically updated as you make changes to the client. diff --git a/scripts/buildServer.js b/scripts/buildServer.js new file mode 100644 index 000000000..a8213c4a3 --- /dev/null +++ b/scripts/buildServer.js @@ -0,0 +1,46 @@ +const fs = require('fs') +const path = require('path') +const { spawnSync } = require('child_process') + +const projectRoot = path.resolve(__dirname, '..') +const outputDirectory = path.join(projectRoot, 'dist-server') + +fs.rmSync(outputDirectory, { recursive: true, force: true }) +fs.mkdirSync(outputDirectory, { recursive: true }) +fs.copyFileSync(path.join(projectRoot, 'package.json'), path.join(outputDirectory, 'package.json')) +fs.copyFileSync(path.join(projectRoot, 'index.js'), path.join(outputDirectory, 'index.js')) + +const devConfigPath = path.join(projectRoot, 'dev.js') +if (fs.existsSync(devConfigPath)) { + fs.copyFileSync(devConfigPath, path.join(outputDirectory, 'dev.js')) +} + +fs.cpSync(path.join(projectRoot, 'server'), path.join(outputDirectory, 'server'), { + recursive: true, + force: true +}) + +const testDirectory = path.join(projectRoot, 'test') +if (fs.existsSync(testDirectory)) { + fs.cpSync(testDirectory, path.join(outputDirectory, 'test'), { + recursive: true, + force: true + }) +} + +function hasTypeScriptSource(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name) + if (entry.isDirectory() && hasTypeScriptSource(entryPath)) return true + if (entry.isFile() && entry.name.endsWith('.ts')) return true + } + return false +} + +if (hasTypeScriptSource(path.join(projectRoot, 'server'))) { + const result = spawnSync(process.execPath, [require.resolve('typescript/bin/tsc'), '--project', 'tsconfig.server.json'], { + cwd: projectRoot, + stdio: 'inherit' + }) + process.exit(result.status || 0) +} diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 000000000..212a26240 --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "rootDir": ".", + "outDir": "dist-server", + "strict": true, + "skipLibCheck": true, + "noEmitOnError": true + }, + "include": [ + "server/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist-server", + "client" + ] +}