Skip to content

Commit c3ffffd

Browse files
committed
feat: persist project config in deno.json
1 parent 420376e commit c3ffffd

File tree

4 files changed

+204
-3
lines changed

4 files changed

+204
-3
lines changed

deployctl.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import upgradeSubcommand from "./src/subcommands/upgrade.ts";
1010
import logsSubcommand from "./src/subcommands/logs.ts";
1111
import { MINIMUM_DENO_VERSION, VERSION } from "./src/version.ts";
1212
import { fetchReleases, getConfigPaths } from "./src/utils/info.ts";
13+
import configFile from "./src/config_file.ts";
14+
import { wait } from "./src/utils/spinner.ts";
1315

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

3537
const args = parseArgs(Deno.args);
38+
const config = await configFile.read(
39+
args.config ?? configFile.cwdOrAncestors(),
40+
);
41+
if (config === null && args.config !== undefined && !args["save-config"]) {
42+
error(`Could not find or read the config file '${args.config}'`);
43+
}
44+
if (config !== null) {
45+
wait("").info(`Using config file '${config.path()}'`);
46+
config.useAsDefaultFor(args);
47+
args.config = config.path();
48+
}
3649

3750
if (Deno.isatty(Deno.stdin.rid)) {
3851
let latestVersion;

src/args.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function parseArgs(args: string[]) {
1515
"static",
1616
"version",
1717
"dry-run",
18+
"save-config"
1819
],
1920
string: [
2021
"project",
@@ -29,11 +30,13 @@ export function parseArgs(args: string[]) {
2930
"levels",
3031
"regions",
3132
"limit",
33+
"config",
3234
],
3335
collect: ["grep"],
3436
default: {
3537
static: true,
3638
limit: "100",
39+
config: Deno.env.get("DEPLOYCTL_CONFIG_FILE"),
3740
},
3841
});
3942
return parsed;

src/config_file.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
// Copyright 2021 Deno Land Inc. All rights reserved. MIT license.
2+
3+
import { dirname } from "https://deno.land/[email protected]/path/win32.ts";
4+
import { join } from "../deps.ts";
5+
import { error } from "./error.ts";
6+
import { wait } from "./utils/spinner.ts";
7+
8+
const DEFAULT_FILENAME = "deno.json";
9+
10+
/** Arguments persisted in the deno.json config file */
11+
interface ConfigArgs {
12+
project?: string;
13+
}
14+
15+
class ConfigFile {
16+
#path: string;
17+
#content: { deploy?: ConfigArgs };
18+
19+
constructor(path: string, content: { deploy?: ConfigArgs }) {
20+
this.#path = path;
21+
this.#content = content;
22+
}
23+
24+
/**
25+
* Create a new `ConfigFile` using an object that _at least_ contains the `ConfigArgs`.
26+
*
27+
* Ignores any property in `args` not meant to be persisted.
28+
*/
29+
static create(path: string, args: ConfigArgs) {
30+
const config = new ConfigFile(path, { deploy: {} });
31+
// Use override to clean-up args
32+
config.override(args);
33+
return config;
34+
}
35+
36+
/**
37+
* Override the `ConfigArgs` of this ConfigFile.
38+
*
39+
* Ignores any property in `args` not meant to be persisted.
40+
*/
41+
override(args: ConfigArgs) {
42+
if (this.#content.deploy === undefined) {
43+
this.#content.deploy = {};
44+
}
45+
// Update only specific properties as args might contain properties we don't want in the config file
46+
this.#content.deploy.project = args.project;
47+
}
48+
49+
/**
50+
* For every arg in `ConfigArgs`, if the `args` argument object does not contain
51+
* the arg, fill it with the value in this `ConfigFile`, if any.
52+
*/
53+
useAsDefaultFor(args: ConfigArgs) {
54+
if (args.project === undefined && this.#content.deploy?.project) {
55+
args.project = this.#content.deploy?.project;
56+
}
57+
}
58+
59+
/** Check if the `ConfigArgs` in this `ConfigFile` match `args`
60+
*
61+
* Ignores any property in `args` not meant to be persisted.
62+
*/
63+
eq(args: ConfigArgs) {
64+
const thisContent = (this.#content.deploy ?? {}) as {
65+
[x: string]: unknown;
66+
};
67+
const otherConfig = ConfigFile.create(this.#path, args);
68+
for (const [key, otherValue] of Object.entries(otherConfig.args())) {
69+
if (thisContent[key] !== otherValue) {
70+
return false;
71+
}
72+
}
73+
return true;
74+
}
75+
76+
/** Return whether the `ConfigFile` has the `deploy` namespace */
77+
hasDeployConfig() {
78+
return this.#content.deploy !== undefined;
79+
}
80+
81+
stringify() {
82+
return JSON.stringify(this.#content, null, 2);
83+
}
84+
85+
path() {
86+
return this.#path;
87+
}
88+
89+
args() {
90+
return (this.#content.deploy ?? {});
91+
}
92+
}
93+
94+
export default {
95+
/** Read a `ConfigFile` from disk */
96+
async read(
97+
path: string | Iterable<string>,
98+
): Promise<ConfigFile | null> {
99+
const paths = typeof path === "string" ? [path] : path;
100+
for (const filepath of paths) {
101+
let content;
102+
try {
103+
content = await Deno.readTextFile(filepath);
104+
} catch {
105+
// File not found, try next
106+
continue;
107+
}
108+
try {
109+
const parsedContent = JSON.parse(content);
110+
return new ConfigFile(filepath, parsedContent);
111+
} catch (e) {
112+
error(e);
113+
}
114+
}
115+
// config file not found
116+
return null;
117+
},
118+
119+
/**
120+
* Write `ConfigArgs` to the config file.
121+
*
122+
* @param path {string | null} path where to write the config file. If the file already exists and
123+
* `override` is `true`, its content will be merged with the `args`
124+
* argument. If null, will default to `DEFAULT_FILENAME`.
125+
* @param args {ConfigArgs} args to be upserted into the config file.
126+
* @param overwrite {boolean} control whether an existing config file should be overwritten.
127+
*/
128+
maybeWrite: async function (
129+
path: string | null,
130+
args: ConfigArgs,
131+
overwrite: boolean,
132+
): Promise<void> {
133+
const pathOrDefault = path ?? DEFAULT_FILENAME;
134+
const existingConfig = await this.read(pathOrDefault);
135+
let config;
136+
if (existingConfig && existingConfig.hasDeployConfig() && !overwrite) {
137+
if (!existingConfig.eq(args)) {
138+
wait("").info(
139+
`Some of the config used differ from the config found in '${existingConfig.path()}'. Use --save-config to overwrite it.`,
140+
);
141+
}
142+
return;
143+
} else if (existingConfig) {
144+
existingConfig.override(args);
145+
config = existingConfig;
146+
} else {
147+
config = ConfigFile.create(pathOrDefault, args);
148+
}
149+
await Deno.writeTextFile(
150+
config.path(),
151+
(config satisfies ConfigFile).stringify(),
152+
);
153+
wait("").succeed(
154+
`${
155+
existingConfig ? "Updated" : "Created"
156+
} config file '${config.path()}'.`,
157+
);
158+
},
159+
160+
cwdOrAncestors: function* () {
161+
let wd = Deno.cwd();
162+
while (wd) {
163+
yield join(wd, DEFAULT_FILENAME);
164+
const newWd = dirname(wd);
165+
if (newWd === wd) {
166+
return;
167+
} else {
168+
wd = newWd;
169+
}
170+
}
171+
},
172+
};

src/subcommands/deploy.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { fromFileUrl, normalize, Spinner } from "../../deps.ts";
44
import { wait } from "../utils/spinner.ts";
5+
import configFile from "../config_file.ts";
56
import { error } from "../error.ts";
67
import { API, APIError } from "../utils/api.ts";
78
import { ManifestEntry } from "../utils/api_types.ts";
@@ -49,6 +50,8 @@ export interface Args {
4950
project: string | null;
5051
importMap: string | null;
5152
dryRun: boolean;
53+
config: string | null;
54+
saveConfig: boolean;
5255
}
5356

5457
// deno-lint-ignore no-explicit-any
@@ -63,6 +66,8 @@ export default async function (rawArgs: Record<string, any>): Promise<void> {
6366
exclude: rawArgs.exclude?.split(","),
6467
include: rawArgs.include?.split(","),
6568
dryRun: !!rawArgs["dry-run"],
69+
config: rawArgs.config ? String(rawArgs.config) : null,
70+
saveConfig: !!rawArgs["save-config"],
6671
};
6772
const entrypoint: string | null = typeof rawArgs._[0] === "string"
6873
? rawArgs._[0]
@@ -98,6 +103,8 @@ export default async function (rawArgs: Record<string, any>): Promise<void> {
98103
include: args.include?.map((pattern) => normalize(pattern)),
99104
exclude: args.exclude?.map((pattern) => normalize(pattern)),
100105
dryRun: args.dryRun,
106+
config: args.config,
107+
saveConfig: args.saveConfig,
101108
};
102109

103110
await deploy(opts);
@@ -113,6 +120,8 @@ interface DeployOpts {
113120
token: string | null;
114121
project: string;
115122
dryRun: boolean;
123+
config: string | null;
124+
saveConfig: boolean;
116125
}
117126

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

133-
const deploymentsListing = await api.getDeployments(project!.id);
145+
const deploymentsListing = await api.getDeployments(project.id);
134146
if (deploymentsListing === null) {
135147
projectSpinner.fail("Project deployments details not found.");
136148
Deno.exit(1);
137149
}
138150
const [projectDeployments, _pagination] = deploymentsListing!;
139-
projectSpinner.succeed(`Project: ${project!.name}`);
151+
projectSpinner.succeed(`Project: ${project.name}`);
140152

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

0 commit comments

Comments
 (0)