From b3a1efe5d9ec6dff23772018bebce0028c80f999 Mon Sep 17 00:00:00 2001 From: Lucas Garron Date: Sun, 5 May 2024 14:51:01 -0700 Subject: [PATCH] Initial version. --- .gitignore | 2 + Makefile | 11 ++ README.md | 27 ++++ biome.json | 13 ++ bun.lockb | Bin 0 -> 3137 bytes .../test/deploy/index.html | 25 ++++ package.json | 14 +++ src/main.ts | 117 ++++++++++++++++++ tsconfig.json | 27 ++++ 9 files changed, 236 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 biome.json create mode 100755 bun.lockb create mode 100644 dist/web/experiments.cubing.net/test/deploy/index.html create mode 100644 package.json create mode 100644 src/main.ts create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..9091e8caa5cfcb78530c48365522003b2832c022 GIT binary patch literal 3137 zcmY#Z)GsYA(of3F(@)JSQ%EY!;{sycoc!eMw9K4T-L(9o+{6;yG6OCq1_p+Z*}1Bk z$&G4>ZrQ7NzxAc2tt~j+z#vrg@WMmG)?lx^jod&*K)?>67&y@A1}HxprU1<6V_;~I z0J6A%bU2Xa1ky{P^ei9^G_*m6k%55+NCVY^)y*=x;23zl`@tzT&Ar|RjbEzYq@V2& zdabnh^o+{+S(h>=>PPjeZMC)J%(prADsa~B`Kk>%+O4TcKw~HZpilq-B_K8hx(8$) z41>f80hs;-ph7Do{cMCZLgiul*8%w^K>eT)W(8tk*bxO_`X2#RgX|Xp(x6C_BT6>| z15CdL(9@1U{UE!gfEYv*17P|~f%ATqG1UP zMgxV&)c247AILyz0-$+}3=ngXjb@qZzIV-L#Rh|^ohN2)V|e^TS@qVtcYfMSsBwE zSxr^=mFp%-bjoZj56L=Ec#}nL+rhm%Cr>;s)bhtFb!Fj^71un33g)+}ba_OUZFl&* z?PgT^l_r-EP^N*o7Zlzwz_KqzDDe2LX_K@r%4fZM^5RzLo0ktemfbJdR&>|szjxRq zWe)y-JHG`kwhUbuzErVeOG&X+x{T_qXHjmc z{B?gbf2_L8A8@DJk2UwA+W$K&M%}He#Wcj?qKv#tLJo**Z3*C4)93FEQ`x)cTD5!>?r}tIbzzRD2dN)$go| z{@#`uk(I15`W-c!CMkY@xZhhe?N!whBy*ATE6d(P7C}=In$Kn^in1P*74v93wLLan zeM>`F7_ZX%3Cosmiuu1_DO1tkd3NG%ZATR?jMgZhtiJZw=D~ywhBp}74+6~{C;$t0 z7)_2l2GIHBICr4*Y=D+UN1!y9O>t4OUS?i#Nn%cp9<0XI3n@xXwNo%MP$kAP2O%<+3R?HnIci!(jp_zCh&zC@+KJ9b|?D zRBs58X28l>kU1bTU}k~xTL;t(J77f)0$euHMrN_47C>K_!QEtt>MK~i4y&gH7~@R! z4D<{b7+@^}SWVBtl#^JJT3o`w0BbA2>V76hLp?)1GoX#IRs*aRz{FT*WT0ngsAs$s zC<|&|z}g8w84EpAJwpR5dbB`}2Lf2z1J;%R>M_+bGSf3*=z#k01l*^{QCgguTb5d+ z=UP#cT9lWV6I@c1nU`*-V2B6=3wR)u6eol2I`CU$Y0;DkBA^BtNR0p9Vw-oeg}X|M zi&Kk$ro~=XXbej8{*eicL{NLE#lrLqV+G&8A5a&pgtowdX#g1BhPZ;8!6CU6Xhphy zN@_t)eq~8zL27YwQD#Aj1DL4~Yy)PLloS+O>FXDzre_wH6jkcw735|W>*W`v>mwBD z>w=W(g2LTUuOz(+!~vUtM>kvzR5MsT*a$O_5m3YP@>5cw%<^J9roq(!O)D?fg+wOE z9Kw1)RseN@C|zjsFaYT+%}dEl&Lrp$WMx2Okh!|x^kE7P5pYC8A`g#~5HSEW3c&#y PWCBc{Ku3X7#~>sC3?)E7 literal 0 HcmV?d00001 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 + } +}