Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/myst-cli/src/build/site/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './logger.js';
export * from './manifest.js';
export * from './prepare.js';
export * from './start.js';
Expand Down
54 changes: 13 additions & 41 deletions packages/myst-cli/src/build/site/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ import chalk from 'chalk';
import cors from 'cors';
import express from 'express';
import getPort, { portNumbers } from 'get-port';
import { makeExecutable } from 'myst-cli-utils';
import type child_process from 'child_process';
import { nanoid } from 'nanoid';
import { join } from 'node:path';
import type WebSocket from 'ws';
import { WebSocketServer } from 'ws';
import type { ProcessSiteOptions } from '../../process/site.js';
import type { ISession } from '../../session/types.js';
import version from '../../version.js';
import { createServerLogger } from './logger.js';
import { buildSite } from './prepare.js';
import { installSiteTemplate, getSiteTemplate } from './template.js';
import { installSiteTemplate, startSiteTemplate } from '../../templates/site.js';
import { getSiteTemplate } from './template.js';
import { watchContent } from './watch.js';

const DEFAULT_START_COMMAND = 'npm run start';
import type { AppServer } from '../../templates/site.js';

type ServerOptions = {
serverPort?: number;
Expand Down Expand Up @@ -110,13 +107,6 @@ export function warnOnHostEnvironmentVariable(session: ISession, opts?: StartOpt
}
}
}

export type AppServer = {
port: number;
process: child_process.ChildProcess;
stop: () => void;
};

export async function startServer(
session: ISession,
opts: StartOptions,
Expand All @@ -138,34 +128,16 @@ export async function startServer(
);
return undefined;
}
session.log.info(
`\n\n\t✨✨✨ Starting ${mystTemplate.getValidatedTemplateYml().title} ✨✨✨\n\n`,
);
const port = opts?.port ?? (await getPort({ port: portNumbers(3000, 3100) }));
const appServer = { port } as AppServer;
await new Promise<void>((resolve) => {
const start = makeExecutable(
mystTemplate.getValidatedTemplateYml().build?.start ?? DEFAULT_START_COMMAND,
createServerLogger(session, resolve),
{
cwd: mystTemplate.templatePath,
env: {
...process.env,
CONTENT_CDN_PORT: String(server.port),
PORT: String(port),
MODE: opts.buildStatic ? 'static' : 'app',
BASE_URL: opts.baseurl || undefined,
},
getProcess(process) {
appServer.process = process;
},
},
);
start().catch((e) => session.log.debug(e));
const appServer = await startSiteTemplate(session, mystTemplate, {
...opts,
cdn: `http://localhost:${server.port}`,
});
appServer.stop = () => {
appServer.process.kill();
server.stop();
};
if (appServer) {
const appStop = appServer.stop;
appServer.stop = () => {
appStop();
server.stop();
};
}
return appServer;
}
24 changes: 1 addition & 23 deletions packages/myst-cli/src/build/site/template.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import fs from 'node:fs';
import { join } from 'node:path';
import { RuleId, TemplateKind } from 'myst-common';
import { createNpmLogger, makeExecutable, tic } from 'myst-cli-utils';
import MystTemplate from 'myst-templates';
import type { ISession } from '../../session/types.js';
import { selectors } from '../../store/index.js';
import { addWarningForFile } from '../../utils/addWarningForFile.js';
import { castSession } from '../../session/cache.js';

const DEFAULT_TEMPLATE = 'book-theme';
const DEFAULT_INSTALL_COMMAND = 'npm install';
import { DEFAULT_TEMPLATE } from '../../templates/site.js';

export async function getSiteTemplate(
session: ISession,
Expand Down Expand Up @@ -40,20 +35,3 @@ export async function getSiteTemplate(
cache.$siteTemplate = mystTemplate;
return mystTemplate;
}

export async function installSiteTemplate(
session: ISession,
mystTemplate: MystTemplate,
): Promise<void> {
if (fs.existsSync(join(mystTemplate.templatePath, 'node_modules'))) return;
const toc = tic();
session.log.info('⤵️ Installing web libraries (can take up to 60 s)');
await makeExecutable(
mystTemplate.getValidatedTemplateYml().build?.install ?? DEFAULT_INSTALL_COMMAND,
createNpmLogger(session),
{
cwd: mystTemplate.templatePath,
},
)();
session.log.info(toc('📦 Installed web libraries in %s'));
}
4 changes: 4 additions & 0 deletions packages/myst-cli/src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ function parseInt(value: any) {
return parsedValue;
}

export function makeCDNOption(description: string) {
return new Option('--cdn <string>', description).default('http://localhost:3100');
}

export function makePdfOption(description: string) {
return new Option('--pdf', description).default(false);
}
Expand Down
1 change: 1 addition & 0 deletions packages/myst-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './config.js';
export * from './init/index.js';
export * from './frontmatter.js';
export * from './plugins.js';
export * from './templates/index.js';
export * from './process/index.js';
export * from './project/index.js';
export * from './session/index.js';
Expand Down
189 changes: 189 additions & 0 deletions packages/myst-cli/src/templates/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import chalk from 'chalk';
import fs from 'node:fs';
import { join } from 'node:path';
import yaml from 'js-yaml';
import { tic } from 'myst-cli-utils';
import type { TemplateYmlResponse } from 'myst-templates';
import MystTemplate, {
downloadTemplate,
fetchPublicTemplate,
listPublicTemplates,
resolveInputs,
TEMPLATE_YML,
} from 'myst-templates';
import type { ISession } from '../session/types.js';
import { installSiteTemplate, startSiteTemplate, ensureSiteTemplateExistsOnPath } from './site.js';

import { TemplateKind } from 'myst-common';

type TemplateKinds = {
pdf?: boolean;
tex?: boolean;
typst?: boolean;
docx?: boolean;
site?: boolean;
};

const allTemplates = [TemplateKind.tex, TemplateKind.typst, TemplateKind.docx, TemplateKind.site];

function getKindFromName(name: string) {
return name.match(/^(tex|typst|docx|site)\//)?.[1] ?? undefined;
}
function getKind(session: ISession, kinds?: TemplateKinds): TemplateKind[] | undefined {
if (!kinds) return undefined;
const { pdf, tex, typst, docx, site } = kinds;
if (pdf) session.log.warn('PDF templates may be either "tex" or "typst", including both.');
const flags = {
[TemplateKind.tex]: (tex || pdf) ?? false,
[TemplateKind.typst]: (typst || pdf) ?? false,
[TemplateKind.docx]: docx ?? false,
[TemplateKind.site]: site ?? false,
};
const filteredKinds = Object.entries(flags)
.filter(([, v]) => !!v)
.map(([k]) => k);
if (!filteredKinds || filteredKinds.length === 0) return undefined;
return filteredKinds as TemplateKind[];
}

export async function startTemplateCLI(
session: ISession,
template: string,
opts?: { force?: true; cdn: string },
) {
if (!opts) return;
const mystTemplate = new MystTemplate(session, {
kind: TemplateKind.site,
template,
buildDir: session.buildPath(),
validateFiles: true,
});

await ensureSiteTemplateExistsOnPath(session, mystTemplate, opts.force);
await startSiteTemplate(session, mystTemplate, { cdn: opts.cdn });
}

export async function downloadTemplateCLI(
session: ISession,
template: string,
path?: string,
opts?: TemplateKinds & { force?: true },
) {
const templateKind = getKindFromName(template);
const kinds = templateKind ? [templateKind] : getKind(session, opts);
if (!kinds?.length) {
throw new Error('Cannot lookup a template without specifying a kind (e.g. typst).');
}

if (kinds.length > 1) {
throw new Error('Cannot lookup a template when more than one kind is specified.');
}
const kind = kinds[0] as TemplateKind;
const { templatePath: defaultTemplatePath, templateUrl } = resolveInputs(session, {
template,
kind,
buildDir: session.buildPath(),
});
const templatePath = path || defaultTemplatePath;
if (!templateUrl) {
throw new Error(`Unresolved template URL for "${template}"`);
}
if (fs.existsSync(templatePath)) {
if (!opts?.force) {
session.log.error(`The template download path already exists: "${templatePath}"`);
process.exit(1);
}
session.log.info(`🗑 Deleting path ${templatePath} due to "force" option`);
fs.rmSync(templatePath, { recursive: true });
}
await downloadTemplate(session, { templatePath, templateUrl });
}

export async function listTemplatesCLI(
session: ISession,
name?: string,
opts?: { tag?: string } & TemplateKinds,
) {
const toc = tic();
const kinds = getKind(session, opts);
if (name) {
if (kinds && kinds?.length > 1) {
throw new Error('Cannot lookup a template with more than one kind.');
}
const isLocal = fs.existsSync(name)
? name.endsWith('.yml')
? name
: join(name, TEMPLATE_YML)
: false;
// Load the template from disk or remotely
const template = isLocal
? (yaml.load(fs.readFileSync(isLocal).toString()) as TemplateYmlResponse)
: await fetchPublicTemplate(session, name, kinds?.[0]);
if (!template.id) template.id = name;
session.log.debug(toc(`Found ${template.id} template in %s`));
session.log.info(
`${chalk.bold.green((template.title ?? '').padEnd(30))}${chalk.bold.blueBright(
template.id.replace(/^(tex|typst|site|docx)\//, '').replace(/^myst\//, ''),
)}`,
);
session.log.info(
`ID: ${chalk.dim(template.id)}\nVersion: ${chalk.dim(template.version ?? '')}`,
);
session.log.info(
`Authors: ${chalk.dim(template.authors?.map((a) => a.name).join(', ') ?? '')}`,
);
session.log.info(`Description: ${chalk.dim(template.description ?? '')}`);
session.log.info(`Tags: ${chalk.dim(template.tags?.join(', ') ?? '')}`);
session.log.info(chalk.bold.blueBright(`\nParts:`));
template.parts?.map((p) =>
session.log.info(
`${chalk.cyan(p.id)}${p.required ? chalk.dim(' (required)') : ''} - ${p.description
?.trim()
.replace(/\n/g, '\n\t')}`,
),
);
session.log.info(chalk.bold.blueBright(`\nOptions:`));
template.options?.map((p) =>
session.log.info(
`${chalk.cyan(p.id)} (${p.type})${
p.required ? chalk.dim(' (required)') : ''
} - ${p.description?.trim().replace(/\n/g, '\n\t')}`,
),
);
return;
}
const templates = await listPublicTemplates(session, kinds ?? allTemplates);
let filtered = templates;
if (opts?.tag) {
const tags = opts.tag.split(',').map((t) => t.trim());
filtered = templates.filter((t) => {
const templateTags = new Set(t.tags);
const intersection = tags.filter((x) => templateTags.has(x));
return intersection.length > 0;
});
}
session.log.debug(
toc(
`Found ${templates.length} templates in %s${
opts?.tag ? `, filtered by ${opts?.tag} to ${filtered.length}` : ''
}`,
),
);
if (filtered.length === 0) {
session.log.error(
`No templates found for kinds "${(kinds ?? allTemplates).join(', ')}"${
opts?.tag ? ` and tag "${opts.tag}"` : ''
}`,
);
process.exit(1);
}
filtered.forEach((template) => {
session.log.info(
`\n${chalk.bold.green((template.title ?? '').padEnd(30))}${chalk.bold.blueBright(
template.id.replace(/^(tex|typst|site|docx)\//, '').replace(/^myst\//, ''),
)}\nDescription: ${chalk.dim(template.description ?? '')}\nTags: ${chalk.dim(
template.tags?.join(', ') ?? '',
)}`,
);
});
}
2 changes: 2 additions & 0 deletions packages/myst-cli/src/templates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './site.js';
export * from './cli.js';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import chalk from 'chalk';
import type { LoggerDE } from 'myst-cli-utils';
import type { ISession } from '../../session/types.js';
import type { ISession } from '../session/types.js';

export function createServerLogger(session: ISession, ready: () => void): LoggerDE {
const logger = {
Expand Down
Loading
Loading