diff --git a/.changeset/spotty-buckets-reflect.md b/.changeset/spotty-buckets-reflect.md new file mode 100644 index 00000000..674623db --- /dev/null +++ b/.changeset/spotty-buckets-reflect.md @@ -0,0 +1,5 @@ +--- +"@webstone/cli": minor +--- + +Add a `webstone web configure deployment` CLI command. diff --git a/.gitpod.yml b/.gitpod.yml index 8170a823..e1b2d57b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -17,10 +17,10 @@ tasks: command: | cd webstone-dev-app while [ ! -f ../webstone/packages/cli/dist/bin.js ]; do sleep 1; done - pnpm link ../webstone/packages/cli + pnpm add -w -D ../webstone/packages/cli export HMR_HOST=`gp url 3000` pnpm ws dev gitpod web-patch-svelte-config-js - pnpm dev + pnpm ws dev openMode: split-right ports: diff --git a/packages/cli/bin b/packages/cli/bin index e3a73877..1e39c3fd 100755 --- a/packages/cli/bin +++ b/packages/cli/bin @@ -1,7 +1,7 @@ #!/usr/bin/env node if (process.argv.includes("--tests")) { require('ts-node').register({ project: `${__dirname}/tsconfig.json` }) - require(`${__dirname}/src/bin`).run(process.argv) + require(`${__dirname}/src/bin`).run() } else { - require(`${__dirname}/dist/bin`).run(process.argv); + require(`${__dirname}/dist/bin`).run(); } diff --git a/packages/cli/package.json b/packages/cli/package.json index e82f154f..242f72c7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf ./dist", "compile": "tsc -p .", "copy-templates": "if [ -e ./src/templates ]; then cp -a ./src/templates ./dist/; fi", - "dev": "pnpm copy-templates && pnpm dev:watch-src & pnpm dev:watch-templates", + "dev": "pnpm clean && pnpm copy-templates && pnpm dev:watch-src & pnpm dev:watch-templates", "dev:watch-src": "tsc -w", "dev:watch-templates": "npm-watch copy-templates", "format": "prettier --write **/*.{js,ts,tsx,json}", diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 82a075ba..35edfe10 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -1,6 +1,6 @@ const { build } = require("gluegun"); -async function run(argv) { +async function run() { const cli = build() .brand("webstone") .src(__dirname) @@ -21,7 +21,7 @@ async function run(argv) { // "template", ]) .create(); - const toolbox = await cli.run(argv); + const toolbox = await cli.run(process.argv); // Return to use it in tests return toolbox; diff --git a/packages/cli/src/commands/web/configure/deployment.ts b/packages/cli/src/commands/web/configure/deployment.ts new file mode 100644 index 00000000..de7ea927 --- /dev/null +++ b/packages/cli/src/commands/web/configure/deployment.ts @@ -0,0 +1,80 @@ +import type { GluegunCommand, GluegunToolbox } from "gluegun"; +import type { WebToolbox } from "../../../extensions/web/types"; + +interface WebstoneToolbox extends GluegunToolbox, WebToolbox {} + +const command: GluegunCommand = { + // @ts-ignore: WebstoneToolbox extends GluegunToolbox, ignore TS error. + run: async (toolbox: WebstoneToolbox) => { + const { print, prompt, web } = toolbox; + + const availableAdapters = web.configure.deployment.availableAdapters; + const adapterPromptResult = await prompt.ask({ + type: "select", + name: "adapter", + message: `Where would you like to deploy the "web" service to?`, + choices: availableAdapters.map( + (adapter) => `${adapter.name} (${adapter.npmPackage})` + ), + }); + let adapterIdentifier = ""; + if (adapterPromptResult && adapterPromptResult.adapter) + adapterIdentifier = adapterPromptResult.adapter; + + if (adapterIdentifier === "") { + print.error("Please choose an adapter."); + return; + } + + const chosenAdapter = availableAdapters.find((availableAdapter) => + adapterIdentifier.includes(availableAdapter.npmPackage) + ); + if (!chosenAdapter) { + print.error(`The chosen adapter ${adapterIdentifier} is not available.`); + return; + } + + const nextStepsInstructions: string[] = []; + if (web.configure.deployment.isAnyAdapterInstalled()) { + const installedAdapterNpmPackage = web.configure.deployment.getInstalledAdapterPackageName(); + const installedAdapter = availableAdapters.find( + (adapter) => adapter.npmPackage === installedAdapterNpmPackage + ); + + if (installedAdapterNpmPackage === chosenAdapter.npmPackage) { + print.info(`Adapter ${chosenAdapter.name} is already installed.`); + return; + } + + if (!installedAdapter) { + print.error( + `Adapter ${installedAdapterNpmPackage} should be installed, but isn't... This is an unexpected error, please manually review the "services/web/package.json file."` + ); + return; + } + const isReplaceInstalledAdapter = await prompt.confirm( + `The ${installedAdapter.name} adapter is already installed. Would you like to replace it with ${chosenAdapter.name}?` + ); + if (isReplaceInstalledAdapter) { + await web.configure.deployment.removeAdapter(installedAdapter); + nextStepsInstructions.push( + `- to completely remove the ${installedAdapter.name} adapter: ${installedAdapter.nextStepsDocsLink}` + ); + } else { + return; + } + } + + await web.configure.deployment.installAdapter(chosenAdapter); + nextStepsInstructions.push( + `- to finalize the configuration of the newly installed ${chosenAdapter.name} adapter, undo the changes at: ${chosenAdapter.nextStepsDocsLink}` + ); + print.highlight( + `\nPlease perform the following next steps by reading the docs:\n${nextStepsInstructions.join( + "\n" + )}` + ); + }, +}; + +module.exports = command; diff --git a/packages/cli/src/extensions/pnpm.ts b/packages/cli/src/extensions/pnpm.ts new file mode 100644 index 00000000..f630e51f --- /dev/null +++ b/packages/cli/src/extensions/pnpm.ts @@ -0,0 +1,11 @@ +import type { GluegunToolbox } from "gluegun"; + +import add from "../toolbox/pnpm/add"; +import remove from "../toolbox/pnpm/remove"; + +export default (toolbox: GluegunToolbox) => { + toolbox.pnpm = { + add, + remove, + }; +}; diff --git a/packages/cli/src/extensions/web.ts b/packages/cli/src/extensions/web.ts new file mode 100644 index 00000000..26cf7ac2 --- /dev/null +++ b/packages/cli/src/extensions/web.ts @@ -0,0 +1,17 @@ +import type { GluegunToolbox } from "gluegun"; +import type { WebToolbox } from "./web/types"; + +import webConfigureDeployment from "./web/configure/deployment"; + +export default (toolbox: GluegunToolbox) => { + const webToolbox: WebToolbox = { + web: { + configure: { + deployment: webConfigureDeployment, + }, + }, + }; + toolbox.web = { + ...webToolbox.web, + }; +}; diff --git a/packages/cli/src/extensions/web/configure/deployment.ts b/packages/cli/src/extensions/web/configure/deployment.ts new file mode 100644 index 00000000..c824e090 --- /dev/null +++ b/packages/cli/src/extensions/web/configure/deployment.ts @@ -0,0 +1,13 @@ +import { availableAdapters } from "../../../toolbox/web/configure/deployment/adapters"; +import getInstalledAdapterPackageName from "../../../toolbox/web/configure/deployment/get-installed-adapter-package-name"; +import installAdapter from "../../../toolbox/web/configure/deployment/install-adapter"; +import isAnyAdapterInstalled from "../../../toolbox/web/configure/deployment/is-any-adapter-installed"; +import removeAdapter from "../../../toolbox/web/configure/deployment/remove-adapter"; + +export default { + availableAdapters, + getInstalledAdapterPackageName, + installAdapter, + isAnyAdapterInstalled, + removeAdapter, +}; diff --git a/packages/cli/src/extensions/web/types.d.ts b/packages/cli/src/extensions/web/types.d.ts new file mode 100644 index 00000000..9d5f708a --- /dev/null +++ b/packages/cli/src/extensions/web/types.d.ts @@ -0,0 +1,15 @@ +import type { Adapter } from "../../toolbox/web/configure/deployment/types"; + +export interface WebToolbox { + web: { + configure: { + deployment: { + availableAdapters: Adapter[]; + getInstalledAdapterPackageName: () => string; + installAdapter: (adapter: Adapter) => Promise; + isAnyAdapterInstalled: () => boolean; + removeAdapter: (adapter: Adapter) => Promise; + }; + }; + }; +} diff --git a/packages/cli/src/toolbox/pnpm/add.ts b/packages/cli/src/toolbox/pnpm/add.ts new file mode 100644 index 00000000..dc911ea6 --- /dev/null +++ b/packages/cli/src/toolbox/pnpm/add.ts @@ -0,0 +1,11 @@ +import type { PnpmOptions } from "./types"; + +import { system } from "gluegun"; + +export default async (packageName: string, options?: PnpmOptions) => { + const dev = options?.dev ? "-D " : ""; + const stdout = await system.run(`pnpm add ${dev}${packageName}`, { + cwd: options?.dir || ".", + }); + return { success: true, stdout }; +}; diff --git a/packages/cli/src/toolbox/pnpm/remove.ts b/packages/cli/src/toolbox/pnpm/remove.ts new file mode 100644 index 00000000..19ef9f76 --- /dev/null +++ b/packages/cli/src/toolbox/pnpm/remove.ts @@ -0,0 +1,11 @@ +import type { PnpmOptions } from "./types"; + +import { system } from "gluegun"; + +export default async (packageName: string, options?: PnpmOptions) => { + const dev = options?.dev ? "-D " : ""; + const stdout = await system.run(`pnpm remove ${dev}${packageName}`, { + cwd: options?.dir || "", + }); + return { success: true, stdout }; +}; diff --git a/packages/cli/src/toolbox/pnpm/types.d.ts b/packages/cli/src/toolbox/pnpm/types.d.ts new file mode 100644 index 00000000..9dba9dcc --- /dev/null +++ b/packages/cli/src/toolbox/pnpm/types.d.ts @@ -0,0 +1,4 @@ +export type PnpmOptions = { + dev: boolean; + dir: string; +}; diff --git a/packages/cli/src/toolbox/web/configure/deployment/adapters.ts b/packages/cli/src/toolbox/web/configure/deployment/adapters.ts new file mode 100644 index 00000000..fdce4258 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/adapters.ts @@ -0,0 +1,50 @@ +import { Adapter } from "./types"; + +export const availableAdapters: Adapter[] = [ + { + // Enable & test when https://github.com/architect/architect/issues/1236 is resolved. + // identifier: "adapter-begin", + // name: "Begin / Architect", + // npmPackage: "@architect/sveltekit-adapter", + // nextStepsDocsLink: "https://github.com/architect/sveltekit-adapter" + // }, { + identifier: "adapter-cloudflare-workers", + name: "Cloudflare Workers", + npmPackage: "@sveltejs/adapter-cloudflare-workers", + npmPackageVersion: "@next", + nextStepsDocsLink: + "https://github.com/sveltejs/kit/tree/master/packages/adapter-cloudflare-workers", + }, + { + identifier: "adapter-netlify", + name: "Netlify", + npmPackage: "@sveltejs/adapter-netlify", + npmPackageVersion: "@next", + nextStepsDocsLink: + "https://github.com/sveltejs/kit/tree/master/packages/adapter-netlify", + }, + { + identifier: "adapter-node", + name: "Node.js", + npmPackage: "@sveltejs/adapter-node", + npmPackageVersion: "@next", + nextStepsDocsLink: + "https://github.com/sveltejs/kit/tree/master/packages/adapter-node", + }, + { + identifier: "adapter-static", + name: "Static", + npmPackage: "@sveltejs/adapter-static", + npmPackageVersion: "@next", + nextStepsDocsLink: + "https://github.com/sveltejs/kit/tree/master/packages/adapter-static", + }, + { + identifier: "adapter-vercel", + name: "Vercel", + npmPackage: "@sveltejs/adapter-vercel", + npmPackageVersion: "@next", + nextStepsDocsLink: + "https://github.com/sveltejs/kit/tree/master/packages/adapter-vercel", + }, +]; diff --git a/packages/cli/src/toolbox/web/configure/deployment/get-installed-adapter-package-name.ts b/packages/cli/src/toolbox/web/configure/deployment/get-installed-adapter-package-name.ts new file mode 100644 index 00000000..206390e3 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/get-installed-adapter-package-name.ts @@ -0,0 +1,14 @@ +import { filesystem } from "gluegun"; +import { availableAdapters } from "./adapters"; + +export default () => { + const webPackageJson = filesystem.read("./services/web/package.json", "json"); + const availableAdapterNpmPackages = availableAdapters.map( + (adapter) => adapter.npmPackage + ); + return ( + Object.keys(webPackageJson.devDependencies).find((devDependency) => + availableAdapterNpmPackages.includes(devDependency) + ) || "" + ); +}; diff --git a/packages/cli/src/toolbox/web/configure/deployment/install-adapter.ts b/packages/cli/src/toolbox/web/configure/deployment/install-adapter.ts new file mode 100644 index 00000000..3368b839 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/install-adapter.ts @@ -0,0 +1,17 @@ +import type { Adapter } from "./types"; + +import { print } from "gluegun"; +import add from "../../../pnpm/add"; + +export default async (adapter: Adapter) => { + const spinner = print.spin( + `Adding adapter package "${adapter.npmPackage}${ + adapter.npmPackageVersion || "" + }"...` + ); + await add(`${adapter.npmPackage}${adapter.npmPackageVersion || ""}`, { + dev: true, + dir: "./services/web", + }); + spinner.succeed(`Adapter added: ${adapter.npmPackage}`); +}; diff --git a/packages/cli/src/toolbox/web/configure/deployment/is-any-adapter-installed.ts b/packages/cli/src/toolbox/web/configure/deployment/is-any-adapter-installed.ts new file mode 100644 index 00000000..6fbb0851 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/is-any-adapter-installed.ts @@ -0,0 +1,3 @@ +import getInstalledAdapterPackageName from "./get-installed-adapter-package-name"; + +export default () => !!getInstalledAdapterPackageName(); diff --git a/packages/cli/src/toolbox/web/configure/deployment/remove-adapter.ts b/packages/cli/src/toolbox/web/configure/deployment/remove-adapter.ts new file mode 100644 index 00000000..729cf570 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/remove-adapter.ts @@ -0,0 +1,15 @@ +import type { Adapter } from "./types"; + +import { print } from "gluegun"; +import remove from "../../../pnpm/remove"; + +export default async (adapter: Adapter) => { + const spinner = print.spin( + `Removing adapter package "${adapter.npmPackage}"...` + ); + await remove(adapter.npmPackage, { + dev: true, + dir: "./services/web", + }); + spinner.succeed(`Adapter removed: ${adapter.npmPackage}`); +}; diff --git a/packages/cli/src/toolbox/web/configure/deployment/types.d.ts b/packages/cli/src/toolbox/web/configure/deployment/types.d.ts new file mode 100644 index 00000000..d2c1b187 --- /dev/null +++ b/packages/cli/src/toolbox/web/configure/deployment/types.d.ts @@ -0,0 +1,13 @@ +export type Adapter = { + identifier: + | "adapter-begin" + | "adapter-cloudflare-workers" + | "adapter-netlify" + | "adapter-node" + | "adapter-static" + | "adapter-vercel"; + name: string; + npmPackage: string; + npmPackageVersion?: string; + nextStepsDocsLink: string; +}; diff --git a/packages/cli/src/types.ts b/packages/cli/src/types.ts deleted file mode 100644 index 07a21bbf..00000000 --- a/packages/cli/src/types.ts +++ /dev/null @@ -1 +0,0 @@ -// export types diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index b6a20dc2..26719b0d 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -12,13 +12,13 @@ ], "module": "commonjs", "moduleResolution": "node", - "noImplicitAny": false, + "noImplicitAny": true, "noImplicitThis": true, "noUnusedLocals": true, "sourceMap": false, "inlineSourceMap": true, "outDir": "dist", - "strict": false, + "strict": true, "target": "es5", "declaration": true, "declarationDir": "dist/types"