Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/create/artifacts/project.ts
3587 views
1
/*
2
* project.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import {
8
ArtifactCreator,
9
CreateContext,
10
CreateDirective,
11
} from "../cmd-types.ts";
12
13
import { capitalizeTitle } from "../../../core/text.ts";
14
import { kMarkdownEngine } from "../../../execute/types.ts";
15
import { projectCreate } from "../../../project/project-create.ts";
16
import {
17
parseProjectType,
18
projectType,
19
projectTypeAliases,
20
projectTypes,
21
} from "../../../project/types/project-types.ts";
22
23
import { Input, Select } from "cliffy/prompt/mod.ts";
24
import { join } from "../../../deno_ral/path.ts";
25
26
// ensures project types are registered
27
import "../../../project/types/register.ts";
28
import { warning } from "../../../deno_ral/log.ts";
29
30
const kProjectTypes = projectTypes();
31
const kProjectTypeAliases = projectTypeAliases();
32
const kProjectTypesAndAliases = [
33
...kProjectTypes,
34
...kProjectTypeAliases,
35
];
36
37
const kType = "type";
38
const kSubdirectory = "subdirectory";
39
40
const kBlogTypeAlias = "blog";
41
const kConfluenceAlias = "confluence";
42
43
const kTypeProj = "project";
44
45
const kProjectCreateTypes = [
46
...kProjectTypes,
47
kBlogTypeAlias,
48
kConfluenceAlias,
49
];
50
const kProjectTypeOrder = [
51
"default",
52
"website",
53
kBlogTypeAlias,
54
"manuscript",
55
"book",
56
kConfluenceAlias,
57
];
58
59
export const projectArtifactCreator: ArtifactCreator = {
60
displayName: "Project",
61
type: kTypeProj,
62
resolveOptions,
63
finalizeOptions,
64
nextPrompt,
65
createArtifact,
66
};
67
68
function resolveOptions(args: string[]): Record<string, unknown> {
69
// The first argument is the type (website, default, etc...)
70
// The second argument is the directory
71
const typeRaw = args.length > 0 ? args[0] : undefined;
72
const directoryRaw = args.length > 1 ? args[1] : undefined;
73
const titleRaw = args.length > 2 ? args[2] : undefined;
74
75
const options: Record<string, unknown> = {};
76
if (typeRaw) {
77
if (kProjectCreateTypes.includes(typeRaw)) {
78
// This is a recognized type
79
options[kType] = typeRaw;
80
}
81
}
82
// Populate a directory, if provided
83
if (directoryRaw) {
84
options[kSubdirectory] = directoryRaw;
85
}
86
87
if (titleRaw) {
88
options.name = titleRaw;
89
}
90
91
return options;
92
}
93
94
// We specially handle website and blog
95
// (website means a website with the default template,
96
// blog means a website with the blog template)
97
function resolveTemplate(type: string) {
98
if (type === "website") {
99
return {
100
type,
101
template: "default",
102
};
103
} else if (type === kBlogTypeAlias) {
104
return {
105
type: "website",
106
template: "blog",
107
};
108
} else if (type === "confluence") {
109
return {
110
type: "default",
111
template: kConfluenceAlias,
112
};
113
} else {
114
return {
115
type,
116
};
117
}
118
}
119
120
function finalizeOptions(createContext: CreateContext) {
121
const typeStr = createContext.options[kType] as string || "default";
122
// Resolve the type and template
123
const resolved = resolveTemplate(typeStr);
124
const subdirectory = createContext.options[kSubdirectory] as string;
125
if (!subdirectory) {
126
throw new Error(
127
"A directory is required for project creation with \`quarto create project\`",
128
);
129
}
130
const directory = join(createContext.cwd, subdirectory);
131
let name = createContext.options.name;
132
if (!name) {
133
name = defaultName(subdirectory, typeStr);
134
warning(
135
`No 'title' for project provided in \`quarto create project\`. Using '${name}' as default.`,
136
);
137
}
138
const template = resolved.template
139
? `${resolved.type}:${resolved.template}`
140
: resolved.type;
141
142
return {
143
displayType: "project",
144
name,
145
directory,
146
template,
147
} as CreateDirective;
148
}
149
150
function nextPrompt(
151
createOptions: CreateContext,
152
) {
153
// First ensure that there is a type
154
if (!createOptions.options[kType]) {
155
const orderedTypes = kProjectCreateTypes.sort((t1, t2) => {
156
if (t1 === t2) {
157
return 0;
158
} else if (kProjectTypeOrder.indexOf(t1) === -1) {
159
return 1;
160
} else {
161
return kProjectTypeOrder.indexOf(t1) - kProjectTypeOrder.indexOf(t2);
162
}
163
});
164
165
return {
166
name: kType,
167
message: "Type",
168
type: Select,
169
options: orderedTypes.map((t) => {
170
return {
171
name: t,
172
value: t,
173
};
174
}),
175
};
176
}
177
178
// Collect a name
179
if (!createOptions.options[kSubdirectory]) {
180
return {
181
name: kSubdirectory,
182
message: "Directory",
183
type: Input,
184
};
185
}
186
187
if (!createOptions.options.name) {
188
return {
189
name: "name",
190
message: "Title",
191
type: Input,
192
default: defaultName(
193
createOptions.options[kSubdirectory] as string,
194
createOptions.options[kType] as string,
195
),
196
};
197
}
198
}
199
200
async function createArtifact(
201
createDirective: CreateDirective,
202
quiet?: boolean,
203
) {
204
const dir = createDirective.directory;
205
const projectTitle = createDirective.name;
206
const directiveType = createDirective.template;
207
208
// Parse the project type and template
209
const { type, template } = parseProjectType(directiveType);
210
211
// Validate the type
212
if (kProjectTypesAndAliases.indexOf(type) === -1) {
213
throw new Error(
214
`Project type must be one of ${
215
kProjectTypes.join(", ")
216
}, but got "${type}".`,
217
);
218
}
219
220
// Validate the template
221
const projType = projectType(type);
222
if (template && !projType.templates?.includes(template)) {
223
if (projType.templates) {
224
throw new Error(
225
`Project template must be one of ${
226
projType.templates.join(", ")
227
}, but got "${template}".`,
228
);
229
} else {
230
throw new Error(
231
`The project type ${type} does not support any templates.`,
232
);
233
}
234
}
235
236
await projectCreate({
237
dir,
238
type: type,
239
title: projectTitle,
240
scaffold: true,
241
engine: kMarkdownEngine,
242
template: template,
243
quiet,
244
});
245
246
return {
247
path: dir,
248
openfiles: type !== "default"
249
? ["index.qmd", "_quarto.yml"]
250
: ["_quarto.yml"],
251
};
252
}
253
254
// choose a default name if none provided in the createContext
255
function defaultName(subdirectory: string, type: string) {
256
return subdirectory !== "." ? subdirectory : type;
257
}
258
259