Path: blob/main/src/command/create/artifacts/extension.ts
3589 views
/*1* extension.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import {7ArtifactCreator,8CreateContext,9CreateDirective,10} from "../cmd-types.ts";1112import { ejsData, renderAndCopyArtifacts } from "./artifact-shared.ts";1314import { resourcePath } from "../../../core/resources.ts";1516import { Input, Select } from "cliffy/prompt/mod.ts";17import { join } from "../../../deno_ral/path.ts";18import { existsSync } from "../../../deno_ral/fs.ts";1920const kType = "type";21const kSubType = "subtype";22const kName = "name";2324const kTypeExtension = "extension";2526interface ExtensionType {27name: string;28value: string;29openfiles: string[];30}3132const kExtensionTypes: Array<string | ExtensionType> = [33{ name: "shortcode", value: "shortcode", openfiles: ["example.qmd"] },34{ name: "filter", value: "filter", openfiles: ["example.qmd"] },35{36name: "revealjs plugin",37value: "revealjs-plugin",38openfiles: ["example.qmd"],39},40{ name: "journal format", value: "journal", openfiles: ["template.qmd"] },41{ name: "custom format", value: "format", openfiles: ["template.qmd"] },42{ name: "metadata", value: "metadata", openfiles: [] },43{ name: "brand", value: "brand", openfiles: [] },44];4546const kExtensionSubtypes: Record<string, string[]> = {47"format": ["html", "pdf", "docx", "revealjs", "typst"],48};4950const kExtensionValues = kExtensionTypes.filter((t) => typeof t === "object")51.map((t) => (t as { name: string; value: string }).value);5253export const extensionArtifactCreator: ArtifactCreator = {54displayName: "Extension",55type: kTypeExtension,56resolveOptions,57finalizeOptions,58nextPrompt,59createArtifact,60};6162function resolveOptions(args: string[]): Record<string, unknown> {63// The first argument is the extension type64// The second argument is the name65const typeRaw = args.length > 0 ? args[0] : undefined;66const nameRaw = args.length > 1 ? args[1] : undefined;6768const options: Record<string, unknown> = {};6970// Populate the type data71if (typeRaw) {72const [type, template] = typeRaw.split(":");73options[kType] = type;74options[kSubType] = template;75}7677// Populate a directory, if provided78if (nameRaw) {79options[kName] = nameRaw;80}8182return options;83}8485function finalizeOptions(createOptions: CreateContext) {86// There should be a name87if (!createOptions.options.name) {88throw new Error("Required property 'name' is missing.");89}9091// Is the type valid92const type = createOptions.options[kType] as string;93if (!kExtensionValues.includes(type)) {94throw new Error(95`The type ${type} isn't valid. Expected one of ${96kExtensionValues.join(", ")97}`,98);99}100101// Is the subtype valid102const subType = createOptions.options[kSubType] as string;103const subTypes = kExtensionSubtypes[type];104if (subTypes && !subTypes.includes(subType)) {105throw new Error(106`The sub type ${subType} isn't valid. Expected one of ${107subTypes.join(", ")108}`,109);110}111112// Form a template113const template = createOptions.options[kSubType]114? `${createOptions.options[kType]}:${createOptions.options[kSubType]}`115: createOptions.options[kType];116117// Provide a directory and title118return {119displayType: "extension",120name: createOptions.options[kName],121directory: join(122createOptions.cwd,123createOptions.options[kName] as string,124),125template,126} as CreateDirective;127}128129function nextPrompt(130createOptions: CreateContext,131) {132// First ensure that there is a type133if (!createOptions.options[kType]) {134return {135name: kType,136message: "Type",137type: Select,138options: kExtensionTypes.map((t) => {139if (t === "---") {140return Select.separator("--------");141} else {142return t;143}144}),145};146}147148const subTypes = kExtensionSubtypes[createOptions.options[kType] as string];149if (150!createOptions.options[kSubType] &&151subTypes && subTypes.length > 0152) {153return {154name: kSubType,155message: "Base Format",156type: Select,157options: subTypes.map((t) => {158return {159name: t,160value: t,161};162}),163};164}165166// Collect a title167if (!createOptions.options[kName]) {168return {169name: kName,170message: "Extension Name",171type: Input,172};173}174}175176function typeFromTemplate(template: string) {177return template.split(":")[0];178}179180async function createArtifact(181createDirective: CreateDirective,182quiet?: boolean,183) {184// Find the type using the template185const createType = typeFromTemplate(createDirective.template);186const extType = kExtensionTypes.find((type) => {187if (typeof type === "object") {188return type.value === createType;189} else {190return false;191}192});193if (!extType) {194throw new Error(`Unrecognized extension type ${createType}`);195}196const openfiles = extType ? (extType as ExtensionType).openfiles : [];197198// Create the extension199await createExtension(createDirective, quiet);200return {201path: createDirective.directory,202openfiles,203};204}205206async function createExtension(207createDirective: CreateDirective,208quiet?: boolean,209) {210// The folder for this extension211const artifact = templateFolder(createDirective);212213// The target directory214const target = createDirective.directory;215216if (existsSync(target)) {217// The target directory already exists218throw new Error(219`The directory ${target} already exists. Quarto extensions must have unique names - please modify the existing extension or use a unique name.`,220);221}222223// Data for this extension224const data = await ejsData(createDirective);225226// Render or copy the artifact227const filesCreated = renderAndCopyArtifacts(228target,229artifact,230createDirective,231data,232quiet,233);234235return filesCreated[0];236}237238function templateFolder(createDirective: CreateDirective) {239const basePath = resourcePath(join("create", "extensions"));240const artifactFolderName = createDirective.template.replace(":", "-");241return join(basePath, artifactFolderName);242}243244245