Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/project/project-create.ts
12924 views
1
/*
2
* project-create.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import * as ld from "../core/lodash.ts";
8
import {
9
ensureDirSync,
10
ensureUserWritable,
11
existsSync,
12
} from "../deno_ral/fs.ts";
13
import { basename, dirname, join } from "../deno_ral/path.ts";
14
import { info } from "../deno_ral/log.ts";
15
16
import { jupyterKernelspec } from "../core/jupyter/kernels.ts";
17
import {
18
jupyterCreateCondaenv,
19
jupyterCreateVenv,
20
} from "../core/jupyter/venv.ts";
21
22
import { projectType } from "./types/project-types.ts";
23
import { renderEjs } from "../core/ejs.ts";
24
25
import { executionEngine } from "../execute/engine.ts";
26
import { ExecutionEngineDiscovery } from "../execute/types.ts";
27
28
import { projectConfigFile } from "./project-shared.ts";
29
import { ensureGitignore } from "./project-gitignore.ts";
30
import { kWebsite } from "./types/website/website-constants.ts";
31
import { copyTo } from "../core/copy.ts";
32
import { normalizePath } from "../core/path.ts";
33
34
export interface ProjectCreateOptions {
35
dir: string;
36
type: string;
37
title: string;
38
scaffold: boolean;
39
engine: string;
40
kernel?: string;
41
editor?: string;
42
venv?: boolean;
43
condaenv?: boolean;
44
envPackages?: string[];
45
template?: string;
46
quiet?: boolean;
47
}
48
49
export async function projectCreate(options: ProjectCreateOptions) {
50
// read and validate options
51
options = await readOptions(options);
52
// computed options
53
const engine = executionEngine(options.engine);
54
if (!engine) {
55
throw Error(`Invalid execution engine: ${options.engine}`);
56
}
57
58
// ensure that the directory exists
59
ensureDirSync(options.dir);
60
61
options.dir = normalizePath(options.dir);
62
if (!options.quiet) {
63
info(`Creating project at `, { newline: false });
64
info(`${options.dir}`, { bold: true, newline: false });
65
info(":");
66
}
67
68
// 'website' used to be 'site'
69
if (options.type === "site") {
70
options.type = kWebsite;
71
}
72
73
// call create on the project type
74
const projType = projectType(options.type);
75
const projCreate = projType.create(options.title, options.template);
76
77
// create the initial project config
78
const quartoConfig = renderEjs(projCreate.configTemplate, {
79
title: options.title,
80
editor: options.editor,
81
ext: engine.defaultExt,
82
}, false);
83
Deno.writeTextFileSync(join(options.dir, "_quarto.yml"), quartoConfig);
84
if (!options.quiet) {
85
info(
86
"- Created _quarto.yml",
87
{ indent: 2 },
88
);
89
}
90
if (
91
await ensureGitignore(options.dir, !!options.venv || !!options.condaenv) &&
92
!options.quiet
93
) {
94
info(
95
"- Created .gitignore",
96
{ indent: 2 },
97
);
98
}
99
100
// create scaffold files if we aren't creating a project within the
101
// current working directory (which presumably already has files)
102
if (options.scaffold && projCreate.scaffold) {
103
for (
104
const scaffold of projCreate.scaffold(
105
options.engine,
106
options.kernel,
107
options.envPackages,
108
)
109
) {
110
const md = projectMarkdownFile(
111
options.dir,
112
scaffold.name,
113
scaffold.content,
114
engine,
115
options.kernel,
116
scaffold.title,
117
scaffold.noEngineContent,
118
scaffold.yaml,
119
scaffold.subdirectory,
120
scaffold.supporting,
121
);
122
if (md && !options.quiet) {
123
info("- Created " + md, { indent: 2 });
124
}
125
}
126
}
127
128
// copy supporting files
129
if (projCreate.supporting) {
130
for (const supporting of projCreate.supporting) {
131
let src;
132
let dest;
133
let displayName;
134
if (typeof supporting === "string") {
135
src = join(projCreate.resourceDir, supporting);
136
dest = join(options.dir, supporting);
137
displayName = supporting;
138
} else {
139
src = join(projCreate.resourceDir, supporting.from);
140
dest = join(options.dir, supporting.to);
141
displayName = supporting.to;
142
}
143
if (!existsSync(dest)) {
144
ensureDirSync(dirname(dest));
145
copyTo(src, dest);
146
ensureUserWritable(dest);
147
if (!options.quiet) {
148
info("- Created " + displayName, { indent: 2 });
149
}
150
}
151
}
152
}
153
154
// create venv if requested
155
if (options.venv) {
156
await jupyterCreateVenv(options.dir, options.envPackages);
157
} else if (options.condaenv) {
158
await jupyterCreateCondaenv(options.dir, options.envPackages);
159
}
160
}
161
162
// validate and potentialy provide some defaults
163
async function readOptions(options: ProjectCreateOptions) {
164
options = ld.cloneDeep(options);
165
166
// validate/complete engine if it's jupyter
167
if (options.engine === "jupyter") {
168
const kernel = options.kernel || "python3";
169
const kernelspec = await jupyterKernelspec(kernel);
170
if (!kernelspec) {
171
throw new Error(
172
`Specified jupyter kernel ('${kernel}') not found.`,
173
);
174
}
175
} else {
176
// error to create a venv outside of jupyter
177
if (options.venv) {
178
throw new Error("You can only use --with-venv with the jupyter engine");
179
}
180
}
181
182
// provide default title
183
options.title = options.title || basename(options.dir);
184
185
// error if the quarto config file already exists
186
if (projectConfigFile(options.dir)) {
187
throw new Error(
188
`The directory '${options.dir}' already contains a quarto project`,
189
);
190
}
191
192
return options;
193
}
194
195
function projectMarkdownFile(
196
dir: string,
197
name: string,
198
content: string,
199
engine: ExecutionEngineDiscovery,
200
kernel?: string,
201
title?: string,
202
noEngineContent?: boolean,
203
yaml?: string,
204
subdirectory?: string,
205
supporting?: string[],
206
): string | undefined {
207
// yaml/title
208
const lines: string[] = ["---"];
209
if (title) {
210
lines.push(`title: "${title}"`);
211
}
212
213
if (yaml) {
214
lines.push(yaml);
215
}
216
217
// write jupyter kernel if necessary
218
if (!noEngineContent) {
219
lines.push(...engine.defaultYaml(kernel));
220
}
221
222
// end yaml
223
lines.push("---", "");
224
225
// if there are only 3 lines then there was no title or jupyter entry, clear them
226
if (lines.length === 3) {
227
lines.splice(0, lines.length);
228
}
229
230
// content
231
lines.push(content);
232
233
// see if the engine has defautl content
234
if (!noEngineContent) {
235
const engineContent = engine.defaultContent(kernel);
236
if (engineContent.length > 0) {
237
lines.push("");
238
lines.push(...engineContent);
239
}
240
}
241
242
// write file and return it's name
243
name = name + engine.defaultExt;
244
245
const ensureSubDir = (dir: string, name: string, subdirectory?: string) => {
246
if (subdirectory) {
247
const newDir = join(dir, subdirectory);
248
ensureDirSync(newDir);
249
return join(newDir, name);
250
} else {
251
return join(dir, name);
252
}
253
};
254
255
const path = ensureSubDir(dir, name, subdirectory);
256
if (!existsSync(path)) {
257
Deno.writeTextFileSync(path, lines.join("\n") + "\n");
258
259
// Write supporting files
260
supporting?.forEach((from) => {
261
const name = basename(from);
262
const target = join(dirname(path), name);
263
copyTo(from, target);
264
ensureUserWritable(target);
265
});
266
267
return subdirectory ? join(subdirectory, name) : name;
268
} else {
269
return undefined;
270
}
271
}
272
273