Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/inspect/inspect.ts
6446 views
1
/*
2
* inspect.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { existsSync } from "../deno_ral/fs.ts";
8
import { dirname, join, relative } from "../deno_ral/path.ts";
9
10
import * as ld from "../core/lodash.ts";
11
12
import { kCss, kResources } from "../config/constants.ts";
13
import { Format } from "../config/types.ts";
14
15
import { projectContext } from "../project/project-context.ts";
16
17
import { fileExecutionEngine } from "../execute/engine.ts";
18
import { renderFormats } from "../command/render/render-contexts.ts";
19
import {
20
resolveFileResources,
21
resourcesFromMetadata,
22
} from "../command/render/resources.ts";
23
import { quartoConfig } from "../core/quarto.ts";
24
25
import { cssFileResourceReferences } from "../core/css.ts";
26
import {
27
projectExcludeDirs,
28
projectFileMetadata,
29
projectResolveCodeCellsForFile,
30
withProjectCleanup,
31
} from "../project/project-shared.ts";
32
import { normalizePath, safeExistsSync } from "../core/path.ts";
33
import { kExtensionDir } from "../extension/constants.ts";
34
import {
35
createExtensionContext,
36
extensionFilesFromDirs,
37
} from "../extension/extension.ts";
38
import { withRenderServices } from "../command/render/render-services.ts";
39
import { notebookContext } from "../render/notebook/notebook-context.ts";
40
import { RenderServices } from "../command/render/types.ts";
41
import { singleFileProjectContext } from "../project/types/single-file/single-file.ts";
42
43
import {
44
InspectedConfig,
45
InspectedDocumentConfig,
46
InspectedFile,
47
InspectedProjectConfig,
48
} from "./inspect-types.ts";
49
import { validateDocumentFromSource } from "../core/schema/validate-document.ts";
50
import { error } from "../deno_ral/log.ts";
51
import { ProjectContext } from "../project/types.ts";
52
53
export function isProjectConfig(
54
config: InspectedConfig,
55
): config is InspectedProjectConfig {
56
return (config as InspectedProjectConfig).files !== undefined;
57
}
58
59
export function isDocumentConfig(
60
config: InspectedConfig,
61
): config is InspectedDocumentConfig {
62
return (config as InspectedDocumentConfig).formats !== undefined;
63
}
64
65
export async function inspectConfig(path?: string): Promise<InspectedConfig> {
66
path = path || Deno.cwd();
67
if (!existsSync(path)) {
68
throw new Error(`${path} not found`);
69
}
70
71
const stat = Deno.statSync(path);
72
if (stat.isDirectory) {
73
const nbContext = notebookContext();
74
const ctx = await projectContext(path, nbContext);
75
if (!ctx) {
76
throw new Error(`${path} is not a Quarto project.`);
77
}
78
const config = await withProjectCleanup(ctx, inspectProjectConfig);
79
if (!config) {
80
throw new Error(`${path} is not a Quarto project.`);
81
}
82
return config;
83
} else {
84
return await inspectDocumentConfig(path);
85
}
86
}
87
88
const inspectProjectConfig = async (context: ProjectContext) => {
89
if (!context.config) {
90
return undefined;
91
}
92
const fileInformation: Record<string, InspectedFile> = {};
93
for (const file of context.files.input) {
94
await populateFileInformation(context, fileInformation, file);
95
}
96
const extensions = await populateExtensionInformation(context);
97
const config: InspectedProjectConfig = {
98
quarto: {
99
version: quartoConfig.version(),
100
},
101
dir: context.dir,
102
engines: context.engines,
103
config: context.config,
104
files: context.files,
105
fileInformation,
106
extensions: extensions,
107
};
108
return config;
109
};
110
111
const populateExtensionInformation = async (
112
context: ProjectContext,
113
) => {
114
const extensionContext = createExtensionContext();
115
return await extensionContext.extensions(
116
context.dir,
117
context.config,
118
context.dir,
119
{ builtIn: false },
120
);
121
};
122
123
const populateFileInformation = async (
124
context: ProjectContext,
125
fileInformation: Record<string, InspectedFile>,
126
file: string,
127
) => {
128
const engine = await fileExecutionEngine(file, undefined, context);
129
const src = await context.resolveFullMarkdownForFile(engine, file);
130
if (engine) {
131
const errors = await validateDocumentFromSource(
132
src,
133
engine.name,
134
error,
135
);
136
if (errors.length) {
137
throw new Error(`${file} is not a valid Quarto input document`);
138
}
139
}
140
await projectResolveCodeCellsForFile(context, engine, file);
141
await projectFileMetadata(context, file);
142
const cacheEntry = context.fileInformationCache.get(file);
143
// Output key: project-relative for portability
144
const outputKey = relative(context.dir, normalizePath(file));
145
fileInformation[outputKey] = {
146
includeMap: cacheEntry?.includeMap ?? [],
147
codeCells: cacheEntry?.codeCells ?? [],
148
metadata: cacheEntry?.metadata ?? {},
149
};
150
};
151
152
const inspectDocumentConfig = async (path: string) => {
153
const nbContext = notebookContext();
154
const project = await projectContext(path, nbContext) ||
155
(await singleFileProjectContext(path, nbContext));
156
return withProjectCleanup(project, async (project) => {
157
const engine = await fileExecutionEngine(path, undefined, project);
158
if (!engine) {
159
throw new Error(`${path} is not a valid Quarto input document`);
160
}
161
// partition markdown
162
const partitioned = await engine.partitionedMarkdown(path);
163
const context = project;
164
const src = await context.resolveFullMarkdownForFile(engine, path);
165
if (engine) {
166
const errors = await validateDocumentFromSource(
167
src,
168
engine.name,
169
error,
170
);
171
if (errors.length) {
172
throw new Error(`${path} is not a valid Quarto input document`);
173
}
174
}
175
const formats = await withRenderServices(
176
nbContext,
177
(services: RenderServices) =>
178
renderFormats(path!, services, "all", context),
179
);
180
// accumulate resources from formats then resolve them
181
const resourceConfig: string[] = Object.values(formats).reduce(
182
(resources: string[], format: Format) => {
183
resources = ld.uniq(resources.concat(
184
resourcesFromMetadata(format.metadata[kResources]),
185
));
186
// include css specified in metadata
187
if (format.pandoc[kCss]) {
188
return ld.uniq(resources.concat(
189
resourcesFromMetadata(format.pandoc[kCss]),
190
));
191
} else {
192
return resources;
193
}
194
},
195
[],
196
);
197
198
const fileDir = normalizePath(dirname(path));
199
const excludeDirs = context ? projectExcludeDirs(context) : [];
200
const resources = await resolveResources(
201
context ? context.dir : fileDir,
202
fileDir,
203
excludeDirs,
204
partitioned.markdown,
205
resourceConfig,
206
);
207
208
// if there is an _extensions dir then add it
209
const extensions = join(fileDir, kExtensionDir);
210
if (safeExistsSync(extensions)) {
211
resources.push(
212
...extensionFilesFromDirs([extensions]).map((file) =>
213
relative(fileDir, file)
214
),
215
);
216
}
217
218
await context.resolveFullMarkdownForFile(engine, path);
219
await projectResolveCodeCellsForFile(context, engine, path);
220
await projectFileMetadata(context, path);
221
const fileInformation = context.fileInformationCache.get(path);
222
223
// data to write
224
const config: InspectedDocumentConfig = {
225
quarto: {
226
version: quartoConfig.version(),
227
},
228
engines: [engine.name],
229
formats,
230
resources,
231
fileInformation: {
232
[path]: {
233
includeMap: fileInformation?.includeMap ?? [],
234
codeCells: fileInformation?.codeCells ?? [],
235
metadata: fileInformation?.metadata ?? {},
236
},
237
},
238
};
239
240
// if there is a project then add it
241
if (context?.config) {
242
config.project = await inspectProjectConfig(context);
243
}
244
return config;
245
});
246
};
247
248
async function resolveResources(
249
rootDir: string,
250
fileDir: string,
251
excludeDirs: string[],
252
markdown: string,
253
globs: string[],
254
): Promise<string[]> {
255
const resolved = await resolveFileResources(
256
rootDir,
257
fileDir,
258
excludeDirs,
259
markdown,
260
globs,
261
);
262
const resources = ld.difference(
263
resolved.include,
264
resolved.exclude,
265
) as string[];
266
const allResources = resources.concat(cssFileResourceReferences(resources));
267
return allResources.map((file) => relative(rootDir, file));
268
}
269
270