Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/project/project-profile.ts
6446 views
1
/*
2
* profile.ts
3
*
4
* Copyright (C) 2022 Posit Software, PBC
5
*/
6
7
import { error } from "../deno_ral/log.ts";
8
import { basename, join } from "../deno_ral/path.ts";
9
10
import { ProjectConfig } from "../project/types.ts";
11
import * as ld from "../core/lodash.ts";
12
import { readAndValidateYamlFromFile } from "../core/schema/validated-yaml.ts";
13
import { mergeProjectMetadata } from "../config/metadata.ts";
14
import { safeExistsSync } from "../core/path.ts";
15
import { Schema } from "../core/lib/yaml-schema/types.ts";
16
import {
17
activeProfiles,
18
kQuartoProfile,
19
readProfile,
20
} from "../quarto-core/profile.ts";
21
import { dotenvQuartoProfile } from "../quarto-core/dotenv.ts";
22
import { Metadata } from "../config/types.ts";
23
24
const kQuartoProfileConfig = "profile";
25
26
type QuartoProfileConfig = {
27
default?: string | string[] | undefined;
28
group?: string[] | Array<string[]> | undefined;
29
};
30
31
// cache original QUARTO_PROFILE env var
32
let baseQuartoProfile: string | undefined;
33
34
export async function initializeProfileConfig(
35
dir: string,
36
config: ProjectConfig,
37
schema: Schema,
38
) {
39
// read the original env var once
40
const firstRun = baseQuartoProfile === undefined;
41
if (firstRun) {
42
baseQuartoProfile = Deno.env.get(kQuartoProfile) || "";
43
}
44
45
// read the config then delete it
46
const profileConfig = ld.isObject(config[kQuartoProfileConfig])
47
? config[kQuartoProfileConfig] as QuartoProfileConfig
48
: undefined;
49
delete config[kQuartoProfileConfig];
50
51
// if there is no profile defined see if the user has provided a default
52
// either with an external environment variable, a dotenv file, or within
53
// a _quarto.yml.local (external definition takes precedence)
54
let quartoProfile = baseQuartoProfile || await dotenvQuartoProfile(dir) ||
55
await localConfigQuartoProfile(dir, schema) || "";
56
57
// none-specified so read from the current profile file
58
if (!quartoProfile) {
59
if (Array.isArray(profileConfig?.default)) {
60
quartoProfile = profileConfig!.default
61
.map((value) => String(value)).join(",");
62
} else if (typeof (profileConfig?.default) === "string") {
63
quartoProfile = profileConfig.default;
64
}
65
}
66
67
// read any profile defined (could be from base env or from the default)
68
const active = readProfile(quartoProfile);
69
if (active.length === 0) {
70
// do some smart detection of connect if there are no profiles defined
71
if (Deno.env.get("RSTUDIO_PRODUCT") === "CONNECT") {
72
active.push("connect");
73
}
74
}
75
76
// read profile groups -- ensure that at least one member of each group is in the profile
77
const groups = readProfileGroups(profileConfig);
78
for (const group of groups) {
79
if (!group.some((name) => active!.includes(name))) {
80
active.push(group[0]);
81
}
82
}
83
84
// if this isn't the first run and the active profile has changed then
85
// notify any listeners of this
86
const updatedQuartoProfile = active.join(",");
87
if (!firstRun) {
88
if (Deno.env.get(kQuartoProfile) !== updatedQuartoProfile) {
89
fireActiveProfileChanged(updatedQuartoProfile);
90
}
91
}
92
93
// set the environment variable for those that want to read it directly
94
Deno.env.set(kQuartoProfile, active.join(","));
95
96
return await mergeProfiles(
97
dir,
98
config,
99
schema,
100
);
101
}
102
103
// broadcast changes
104
const listeners = new Array<(profile: string) => void>();
105
function fireActiveProfileChanged(profile: string) {
106
listeners.forEach((listener) => listener(profile));
107
}
108
export function onActiveProfileChanged(
109
listener: (profile: string) => void,
110
) {
111
listeners.push(listener);
112
}
113
114
async function localConfigQuartoProfile(dir: string, schema: Schema) {
115
const localConfigPath = localProjectConfigFile(dir);
116
if (localConfigPath) {
117
const yaml = await readAndValidateYamlFromFile(
118
localConfigPath,
119
schema,
120
`Validation of configuration profile file ${
121
basename(localConfigPath)
122
} failed.`,
123
"{}",
124
) as Metadata;
125
const profile = yaml[kQuartoProfileConfig] as
126
| QuartoProfileConfig
127
| undefined;
128
if (Array.isArray(profile?.default)) {
129
return profile?.default.join(",");
130
} else if (typeof (profile?.default) === "string") {
131
return profile?.default;
132
} else {
133
return undefined;
134
}
135
} else {
136
return undefined;
137
}
138
}
139
140
async function mergeProfiles(
141
dir: string,
142
config: ProjectConfig,
143
schema: Schema,
144
) {
145
// config files to return
146
const files: string[] = [];
147
148
// function to merge a profile
149
const mergeProfile = async (profilePath: string) => {
150
try {
151
const yaml = await readAndValidateYamlFromFile(
152
profilePath,
153
schema,
154
`Validation of configuration profile file ${
155
basename(profilePath)
156
} failed.`,
157
"{}",
158
);
159
config = mergeProjectMetadata(config, yaml);
160
files.push(profilePath);
161
} catch (e) {
162
error(
163
"\nError reading configuration profile file from " +
164
basename(profilePath) +
165
"\n",
166
);
167
throw e;
168
}
169
};
170
171
// merge all active profiles (reverse order so first gets priority)
172
for (const profileName of activeProfiles().reverse()) {
173
const profilePath = [".yml", ".yaml"].map((
174
ext,
175
) => join(dir, `_quarto-${profileName}${ext}`)).find(safeExistsSync);
176
if (profilePath) {
177
await mergeProfile(profilePath);
178
}
179
}
180
// merge local config
181
const localConfigPath = localProjectConfigFile(dir);
182
if (localConfigPath) {
183
await mergeProfile(localConfigPath);
184
}
185
186
return { config, files };
187
}
188
189
function localProjectConfigFile(dir: string) {
190
return [".yml", ".yaml"].map((ext) => join(dir, `_quarto${ext}.local`))
191
.find(safeExistsSync);
192
}
193
194
function readProfileGroups(
195
profileConfig?: QuartoProfileConfig,
196
): Array<string[]> {
197
// read all the groups
198
const groups: Array<string[]> = [];
199
const configGroup = profileConfig?.group as unknown;
200
if (Array.isArray(configGroup)) {
201
// array of strings is a single group
202
if (configGroup.every((value) => typeof value === "string")) {
203
groups.push(configGroup);
204
} else if (configGroup.every(Array.isArray)) {
205
groups.push(...configGroup);
206
}
207
}
208
return groups;
209
}
210
211