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