Skip to content

Commit

Permalink
feat: persist project config in deno.json
Browse files Browse the repository at this point in the history
  • Loading branch information
arnauorriols committed Nov 11, 2023
1 parent 420376e commit c3ffffd
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 3 deletions.
13 changes: 13 additions & 0 deletions deployctl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import upgradeSubcommand from "./src/subcommands/upgrade.ts";
import logsSubcommand from "./src/subcommands/logs.ts";
import { MINIMUM_DENO_VERSION, VERSION } from "./src/version.ts";
import { fetchReleases, getConfigPaths } from "./src/utils/info.ts";
import configFile from "./src/config_file.ts";
import { wait } from "./src/utils/spinner.ts";

const help = `deployctl ${VERSION}
Command line tool for Deno Deploy.
Expand All @@ -33,6 +35,17 @@ if (!semverGreaterThanOrEquals(Deno.version.deno, MINIMUM_DENO_VERSION)) {
}

const args = parseArgs(Deno.args);
const config = await configFile.read(
args.config ?? configFile.cwdOrAncestors(),
);
if (config === null && args.config !== undefined && !args["save-config"]) {
error(`Could not find or read the config file '${args.config}'`);
}
if (config !== null) {
wait("").info(`Using config file '${config.path()}'`);
config.useAsDefaultFor(args);
args.config = config.path();
}

if (Deno.isatty(Deno.stdin.rid)) {
let latestVersion;
Expand Down
3 changes: 3 additions & 0 deletions src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function parseArgs(args: string[]) {
"static",
"version",
"dry-run",
"save-config"
],
string: [
"project",
Expand All @@ -29,11 +30,13 @@ export function parseArgs(args: string[]) {
"levels",
"regions",
"limit",
"config",
],
collect: ["grep"],
default: {
static: true,
limit: "100",
config: Deno.env.get("DEPLOYCTL_CONFIG_FILE"),
},
});
return parsed;
Expand Down
172 changes: 172 additions & 0 deletions src/config_file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.

import { dirname } from "https://deno.land/[email protected]/path/win32.ts";
import { join } from "../deps.ts";
import { error } from "./error.ts";
import { wait } from "./utils/spinner.ts";

const DEFAULT_FILENAME = "deno.json";

/** Arguments persisted in the deno.json config file */
interface ConfigArgs {
project?: string;
}

class ConfigFile {
#path: string;
#content: { deploy?: ConfigArgs };

constructor(path: string, content: { deploy?: ConfigArgs }) {
this.#path = path;
this.#content = content;
}

/**
* Create a new `ConfigFile` using an object that _at least_ contains the `ConfigArgs`.
*
* Ignores any property in `args` not meant to be persisted.
*/
static create(path: string, args: ConfigArgs) {
const config = new ConfigFile(path, { deploy: {} });
// Use override to clean-up args
config.override(args);
return config;
}

/**
* Override the `ConfigArgs` of this ConfigFile.
*
* Ignores any property in `args` not meant to be persisted.
*/
override(args: ConfigArgs) {
if (this.#content.deploy === undefined) {
this.#content.deploy = {};
}
// Update only specific properties as args might contain properties we don't want in the config file
this.#content.deploy.project = args.project;
}

/**
* For every arg in `ConfigArgs`, if the `args` argument object does not contain
* the arg, fill it with the value in this `ConfigFile`, if any.
*/
useAsDefaultFor(args: ConfigArgs) {
if (args.project === undefined && this.#content.deploy?.project) {
args.project = this.#content.deploy?.project;
}
}

/** Check if the `ConfigArgs` in this `ConfigFile` match `args`
*
* Ignores any property in `args` not meant to be persisted.
*/
eq(args: ConfigArgs) {
const thisContent = (this.#content.deploy ?? {}) as {
[x: string]: unknown;
};
const otherConfig = ConfigFile.create(this.#path, args);
for (const [key, otherValue] of Object.entries(otherConfig.args())) {
if (thisContent[key] !== otherValue) {
return false;
}
}
return true;
}

/** Return whether the `ConfigFile` has the `deploy` namespace */
hasDeployConfig() {
return this.#content.deploy !== undefined;
}

stringify() {
return JSON.stringify(this.#content, null, 2);
}

path() {
return this.#path;
}

args() {
return (this.#content.deploy ?? {});
}
}

export default {
/** Read a `ConfigFile` from disk */
async read(
path: string | Iterable<string>,
): Promise<ConfigFile | null> {
const paths = typeof path === "string" ? [path] : path;
for (const filepath of paths) {
let content;
try {
content = await Deno.readTextFile(filepath);
} catch {
// File not found, try next
continue;
}
try {
const parsedContent = JSON.parse(content);
return new ConfigFile(filepath, parsedContent);
} catch (e) {
error(e);
}
}
// config file not found
return null;
},

/**
* Write `ConfigArgs` to the config file.
*
* @param path {string | null} path where to write the config file. If the file already exists and
* `override` is `true`, its content will be merged with the `args`
* argument. If null, will default to `DEFAULT_FILENAME`.
* @param args {ConfigArgs} args to be upserted into the config file.
* @param overwrite {boolean} control whether an existing config file should be overwritten.
*/
maybeWrite: async function (
path: string | null,
args: ConfigArgs,
overwrite: boolean,
): Promise<void> {
const pathOrDefault = path ?? DEFAULT_FILENAME;
const existingConfig = await this.read(pathOrDefault);
let config;
if (existingConfig && existingConfig.hasDeployConfig() && !overwrite) {
if (!existingConfig.eq(args)) {
wait("").info(
`Some of the config used differ from the config found in '${existingConfig.path()}'. Use --save-config to overwrite it.`,
);
}
return;
} else if (existingConfig) {
existingConfig.override(args);
config = existingConfig;
} else {
config = ConfigFile.create(pathOrDefault, args);
}
await Deno.writeTextFile(
config.path(),
(config satisfies ConfigFile).stringify(),
);
wait("").succeed(
`${
existingConfig ? "Updated" : "Created"
} config file '${config.path()}'.`,
);
},

cwdOrAncestors: function* () {
let wd = Deno.cwd();
while (wd) {
yield join(wd, DEFAULT_FILENAME);
const newWd = dirname(wd);
if (newWd === wd) {
return;
} else {
wd = newWd;
}
}
},
};
19 changes: 16 additions & 3 deletions src/subcommands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { fromFileUrl, normalize, Spinner } from "../../deps.ts";
import { wait } from "../utils/spinner.ts";
import configFile from "../config_file.ts";
import { error } from "../error.ts";
import { API, APIError } from "../utils/api.ts";
import { ManifestEntry } from "../utils/api_types.ts";
Expand Down Expand Up @@ -49,6 +50,8 @@ export interface Args {
project: string | null;
importMap: string | null;
dryRun: boolean;
config: string | null;
saveConfig: boolean;
}

// deno-lint-ignore no-explicit-any
Expand All @@ -63,6 +66,8 @@ export default async function (rawArgs: Record<string, any>): Promise<void> {
exclude: rawArgs.exclude?.split(","),
include: rawArgs.include?.split(","),
dryRun: !!rawArgs["dry-run"],
config: rawArgs.config ? String(rawArgs.config) : null,
saveConfig: !!rawArgs["save-config"],
};
const entrypoint: string | null = typeof rawArgs._[0] === "string"
? rawArgs._[0]
Expand Down Expand Up @@ -98,6 +103,8 @@ export default async function (rawArgs: Record<string, any>): Promise<void> {
include: args.include?.map((pattern) => normalize(pattern)),
exclude: args.exclude?.map((pattern) => normalize(pattern)),
dryRun: args.dryRun,
config: args.config,
saveConfig: args.saveConfig,
};

await deploy(opts);
Expand All @@ -113,6 +120,8 @@ interface DeployOpts {
token: string | null;
project: string;
dryRun: boolean;
config: string | null;
saveConfig: boolean;
}

async function deploy(opts: DeployOpts): Promise<void> {
Expand All @@ -127,16 +136,19 @@ async function deploy(opts: DeployOpts): Promise<void> {
const project = await api.getProject(opts.project);
if (project === null) {
projectSpinner.fail("Project not found.");
Deno.exit(1);
return Deno.exit(1);
}
// opts.project is persisted in deno.json. We want to store the project id even if user provided
// project name to facilitate project renaming.
opts.project = project.id;

const deploymentsListing = await api.getDeployments(project!.id);
const deploymentsListing = await api.getDeployments(project.id);
if (deploymentsListing === null) {
projectSpinner.fail("Project deployments details not found.");
Deno.exit(1);
}
const [projectDeployments, _pagination] = deploymentsListing!;
projectSpinner.succeed(`Project: ${project!.name}`);
projectSpinner.succeed(`Project: ${project.name}`);

if (projectDeployments.length === 0) {
wait("").start().info(
Expand Down Expand Up @@ -252,6 +264,7 @@ async function deploy(opts: DeployOpts): Promise<void> {
case "success": {
const deploymentKind = opts.prod ? "Production" : "Preview";
deploySpinner!.succeed(`${deploymentKind} deployment complete.`);
await configFile.maybeWrite(opts.config, opts, opts.saveConfig)
console.log("\nView at:");
for (const { domain } of event.domainMappings) {
console.log(` - https://${domain}`);
Expand Down

0 comments on commit c3ffffd

Please sign in to comment.