diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d502512 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/package-lock.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..77696e8 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: lint +lint: + npx @biomejs/biome check ./src + +.PHONY: format +format: + npx @biomejs/biome check ./src + +.PHONY: publish +publish: + npm publish diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c73c76 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# @cubing/deploy + +Dreamhost-compatible deploys using [`bun`](https://bun.sh/) and `rsync`. + +1. Install: + +```shell +bun add --development @cubing/deploy +# or +npm install --save-dev @cubing/deploy +``` + +2. Add URLs to `package.json`: + +```json +{ + "@cubing/deploy": { + "https://experiments.cubing.net/test/deploy": {} + } +} +``` + +3. Run: + +```shell +bun x @cubing/deploy +``` diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c139865 --- /dev/null +++ b/biome.json @@ -0,0 +1,13 @@ +{ + "formatter": { + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} + diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..9091e8c Binary files /dev/null and b/bun.lockb differ diff --git a/dist/web/experiments.cubing.net/test/deploy/index.html b/dist/web/experiments.cubing.net/test/deploy/index.html new file mode 100644 index 0000000..80898c0 --- /dev/null +++ b/dist/web/experiments.cubing.net/test/deploy/index.html @@ -0,0 +1,25 @@ + + + + + Example + + + + + + Example! + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b34998 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "@cubing/deploy", + "version": "0.1.0", + "module": "src/main.ts", + "type": "module", + "devDependencies": { + "@biomejs/biome": "^1.7.2", + "@types/bun": "latest", + "typescript": "^5.4.5" + }, + "@cubing/deploy": { + "https://experiments.cubing.net/test/deploy": {} + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..4cbbd2a --- /dev/null +++ b/src/main.ts @@ -0,0 +1,117 @@ +import assert from "node:assert"; +import { exit } from "node:process"; +import { parseArgs } from "node:util"; +import { argv } from "bun"; + +function printHelpAndExit(): void { + console.log( + `Usage: bun x @cubing/deploy + +Options: + + --dry-run + --create-folder-on-server + +Requires \`bun\` and \`rsync\` to be installed. Reads paths from a field in \`package.json\` in the current folder: + +{ + "@cubing/deploy": { + "https://experiments.cubing.net/test/deploy": {} + } +} +`, + ); + exit(1); +} + +const { values } = parseArgs({ + args: argv.slice(2), + options: { + "dry-run": { + type: "boolean", + }, + "create-folder-on-server": { + type: "boolean", + }, + }, + strict: true, +}); + +function barebonesShellEscape(s: string): string { + return s.replaceAll('"', '\\"'); +} + +function printCommand(c: string[]): void { + console.log(c.map((s) => `"${barebonesShellEscape(s)}"`).join(" ")); +} + +// TODO: reuse connections based on domain or host IP. +async function deployURL(urlString: string): Promise { + if (urlString.at(-1) !== "/") { + // biome-ignore lint/style/noParameterAssign: Safety check + urlString = `${urlString}/`; // Only sync folder contents. + } + const url = new URL(urlString); // TODO: avoid URL encoding special chars + + const localDistPath = `./dist/web/${url.hostname}${url.pathname}`; + + const rsyncCommand = ["rsync", "-avz"]; + // rsyncCommand.push("--mkpath"); // TODO: requires `rsync` v3.2.3 (https://stackoverflow.com/a/65435579) but Dreamhost is stuck on 3.1.3. 😖 + rsyncCommand.push("--exclude", ".DS_Store"); + rsyncCommand.push("--exclude", ".git"); // TODO: we probably don't need this? + rsyncCommand.push(localDistPath); + + let login_host = url.hostname; + if (url.username) { + login_host = `${url.username}@${url.hostname}`; + } + + const serverFolder = url.hostname + url.pathname; + + const rsyncTarget = `${login_host}:~/${serverFolder}`; + rsyncCommand.push(rsyncTarget); + + const sshCommand = [ + "ssh", + login_host, + `mkdir -p "${barebonesShellEscape(serverFolder)}"`, + ]; + + console.log("--------"); + console.log(`Deploying from: ${localDistPath}`); + console.log(`Deploying to: ${rsyncTarget}`); + if (values["dry-run"]) { + if (values["create-folder-on-server"]) { + console.write("[--dry-run] "); + printCommand(sshCommand); + } + console.write("[--dry-run] "); + printCommand(rsyncCommand); + } else { + if (values["create-folder-on-server"]) { + assert((await Bun.spawn(sshCommand).exited) === 0); + } + assert((await Bun.spawn(rsyncCommand).exited) === 0); + console.log(` +Successfully deployed: + + ${url} +`); + } +} + +const packageJSON = await Bun.file("package.json").json(); +const cubingDeployArgs = packageJSON["@cubing/deploy"]; +if (!cubingDeployArgs) { + console.log("No `@cubing/deploy` entry was found in `package.json`"); + printHelpAndExit(); +} +const urlStrings = Object.keys(cubingDeployArgs); + +if (urlStrings.length === 0) { + printHelpAndExit(); +} + +for (const urlString of urlStrings) { + await deployURL(urlString); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // 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 + } +}