Skip to content

Commit

Permalink
feat: CLI option new (#4273)
Browse files Browse the repository at this point in the history
  • Loading branch information
zanettin committed Jun 12, 2023
1 parent 54b22f5 commit 221c1e5
Show file tree
Hide file tree
Showing 18 changed files with 429 additions and 20 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ packages/docs/src/routes/playground/app/**/*
packages/docs/src/routes/tutorial/**/*
starters/apps/base
starters/apps/library
starters/templates
vite.config.ts
1 change: 1 addition & 0 deletions packages/qwik/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"server/package.json",
"starters/adapters",
"starters/features",
"templates",
"testing/index.cjs",
"testing/index.mjs",
"testing/index.d.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/cli/add/update-files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FsUpdates, UpdateAppOptions } from '../types';
import fs from 'node:fs';
import type { FsUpdates, UpdateAppOptions } from '../types';
import { extname, join } from 'node:path';
import { getPackageManager } from '../utils/utils';

Expand Down
36 changes: 36 additions & 0 deletions packages/qwik/src/cli/new/print-new-help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { cyan, magenta, gray } from 'kleur/colors';
import { loadTemplates } from '../utils/templates';
import { pmRunCmd, note } from '../utils/utils';
import { POSSIBLE_TYPES } from './utils';

export async function printNewHelp() {
const pmRun = pmRunCmd();
const templates = await loadTemplates();

const outString = [];
outString.push(`${cyan('Interactive')}`);
outString.push(` ${pmRun} qwik ${magenta(`new --[template] ...`)}`);
outString.push(``);

outString.push(`${cyan('Complete command')}`);
outString.push(` ${pmRun} qwik ${magenta(`new [type] [name] --[template] ...`)}`);
outString.push(``);

outString.push(`${cyan('Available types')}`);
for (const t of POSSIBLE_TYPES) {
outString.push(` ${t}`);
}
outString.push(``);

outString.push(`${cyan('Available templates')}`);
for (const t of templates) {
let postfix = '';
if (t.id === 'qwik') {
postfix = ' (default)';
}

outString.push(` ${t.id}${gray(postfix)}`);
}

note(outString.join('\n'), 'Available commands');
}
195 changes: 195 additions & 0 deletions packages/qwik/src/cli/new/run-new-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { green, bgMagenta } from 'kleur/colors';
import fs from 'node:fs';
import { join } from 'path';
import { isCancel, select, text, log, intro } from '@clack/prompts';
import { bye } from '../utils/utils';
import type { Template } from '../types';
import type { AppCommand } from '../utils/app-command';
import { loadTemplates } from '../utils/templates';
import { printNewHelp } from './print-new-help';
import { POSSIBLE_TYPES } from './utils';

const SLUG_KEY = '[slug]';
const NAME_KEY = '[name]';

export async function runNewCommand(app: AppCommand) {
try {
// render help
if (app.args.length > 1 && app.args[1] === 'help') {
intro(`🔭 ${bgMagenta(' Qwik Help ')}`);
await printNewHelp();
bye();
} else {
intro(`✨ ${bgMagenta(' Create a new Qwik component or route ')}`);
}

const args = app.args.filter((a) => !a.startsWith('--'));

let typeArg = args[1] as (typeof POSSIBLE_TYPES)[number];
let nameArg = args.slice(2).join(' ');
const templateArg = app.args
.filter((a) => a.startsWith('--'))
.map((a) => a.substring(2))
.join('');

if (!typeArg) {
typeArg = await selectType();
}

if (!POSSIBLE_TYPES.includes(typeArg)) {
throw new Error(`Invalid type: ${typeArg}`);
}

if (!nameArg) {
nameArg = await selectName(typeArg);
}

const { name, slug } = parseInputName(nameArg);

const writers: Promise<void>[] = [];

let template: Template | undefined;
if (!templateArg) {
template = await selectTemplate(typeArg);
} else {
const allTemplates = await loadTemplates();
const templates = allTemplates.filter(
(i) => i.id === templateArg && i[typeArg] && i[typeArg].length
);

if (!templates.length) {
log.error(`Template "${templateArg}" not found`);
bye();
}

template = templates[0][typeArg][0];
}

const outDir = join(app.rootDir, 'src', `${typeArg}s`);
writers.push(writeToFile(name, slug, template as unknown as Template, outDir));

await Promise.all(writers);

log.success(`${green(`${toPascal([typeArg])} "${name}" created!`)}`);
} catch (e) {
log.error(String(e));
await printNewHelp();
}
bye();
}

async function selectType() {
const typeAnswer = await select({
message: 'What would you like to create?',
options: [
{ value: 'component', label: 'Component' },
{ value: 'route', label: 'Route' },
],
});

if (isCancel(typeAnswer)) {
bye();
}

return typeAnswer as (typeof POSSIBLE_TYPES)[number];
}

async function selectName(type: string) {
const nameAnswer = await text({
message: `Name your ${type}`,
});

if (isCancel(nameAnswer)) {
bye();
}

return nameAnswer as string;
}

async function selectTemplate(typeArg: (typeof POSSIBLE_TYPES)[number]) {
const allTemplates = await loadTemplates();

const templates = allTemplates.filter((i) => i[typeArg] && i[typeArg].length);

const templateAnswer = await select({
message: 'Which template would you like to use?',
options: templates.map((t) => ({ value: t[typeArg][0], label: t.id })),
});

if (isCancel(templateAnswer)) {
bye();
}

return templateAnswer as Template;
}

async function writeToFile(name: string, slug: string, template: Template, outDir: string) {
const relativeDirMatches = template.relative.match(/.+?(?=(\/[^/]+$))/);
const relativeDir = relativeDirMatches ? relativeDirMatches[0] : undefined;
const fileDir = inject(join(outDir, relativeDir ?? ''), [[SLUG_KEY, slug]]);

// Build the full output file path + name
const outFile = join(outDir, template.relative);

// String replace the file path
const fileOutput = inject(outFile, [
[SLUG_KEY, slug],
['.template', ''],
]);

// Exit if the module already exists
if (fs.existsSync(fileOutput)) {
const filename = fileOutput.split('/').pop();
throw new Error(`"${filename}" already exists in "${fileDir}"`);
}

// Get the template content
const text = await fs.promises.readFile(template.absolute, { encoding: 'utf-8' });

// String replace the template content
const templateOut = inject(text, [
[SLUG_KEY, slug],
[NAME_KEY, name],
]);

// Create recursive folders
await fs.promises.mkdir(fileDir, { recursive: true });

// Write to file
await fs.promises.writeFile(fileOutput, templateOut, { encoding: 'utf-8' });
}

function inject(raw: string, vars: string[][]) {
let output = raw;

for (const v of vars) {
output = replaceAll(output, v[0], v[1]);
}

return output;
}

function parseInputName(input: string) {
const parts = input.split(/[-_\s]/g);

return {
slug: toSlug(parts),
name: toPascal(parts),
};
}

function toSlug(list: string[]) {
return list.join('-').toLowerCase();
}

function toPascal(list: string[]) {
return list.map((p) => p[0].toUpperCase() + p.substring(1).toLowerCase()).join('');
}

function escapeRegExp(val: string) {
return val.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function replaceAll(val: string, find: string, replace: string) {
return val.replace(new RegExp(escapeRegExp(find), 'g'), replace);
}
1 change: 1 addition & 0 deletions packages/qwik/src/cli/new/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const POSSIBLE_TYPES = ['component', 'route'] as const;
30 changes: 27 additions & 3 deletions packages/qwik/src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { red, dim, cyan, bgMagenta } from 'kleur/colors';
import { AppCommand } from './utils/app-command';
import { runAddCommand } from './add/run-add-command';
import { runNewCommand } from './new/run-new-command';
import { note, panic, pmRunCmd, printHeader, bye } from './utils/utils';
import { runBuildCommand } from './build/run-build-command';
import { intro, isCancel, select, confirm } from '@clack/prompts';
Expand Down Expand Up @@ -29,6 +30,13 @@ const COMMANDS = [
run: (app: AppCommand) => runBuildCommand(app),
showInHelp: true,
},
{
value: 'new',
label: 'new',
hint: 'Create a new component or route',
run: (app: AppCommand) => runNewCommand(app),
showInHelp: true,
},
{
value: 'help',
label: 'help',
Expand Down Expand Up @@ -62,9 +70,25 @@ export async function runCli() {
}

async function runCommand(app: AppCommand) {
for (const value of COMMANDS) {
if (value.value === app.task && typeof value.run === 'function') {
await value.run(app);
switch (app.task) {
case 'add': {
await runAddCommand(app);
return;
}
case 'build': {
await runBuildCommand(app);
return;
}
case 'help': {
printHelp(app);
return;
}
case 'new': {
await runNewCommand(app);
return;
}
case 'version': {
printVersion();
return;
}
}
Expand Down
12 changes: 12 additions & 0 deletions packages/qwik/src/cli/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface IntegrationPackageJson {
exports?: any;
module?: string;
qwik?: string;
qwikTemplates?: string[];
types?: string;
type?: string;
__qwik__?: {
Expand All @@ -96,3 +97,14 @@ export interface ViteConfigUpdates {
vitePlugins?: string[];
qwikViteConfig?: { [key: string]: string };
}

export interface Template {
absolute: string;
relative: string;
}

export interface TemplateSet {
id: string;
component: Template[];
route: Template[];
}
61 changes: 61 additions & 0 deletions packages/qwik/src/cli/utils/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fs from 'node:fs';
import { join } from 'node:path';
import type { TemplateSet } from '../types';
import { getFilesDeep } from './utils';

let templates: TemplateSet[] | null = null;

export async function loadTemplates() {
if (!templates) {
const allTemplates: TemplateSet[] = [];

const templatesDir = join(__dirname, 'templates');
const templatesDirNames = await fs.promises.readdir(templatesDir);

await Promise.all(
templatesDirNames.map(async (templatesDirName) => {
const dir = join(templatesDir, templatesDirName);
const files = await readTemplates(dir);
const template = { id: templatesDirName, ...files };
allTemplates.push(template);
})
);

// Sort qwik templates first so they can be overridden, then alphabetical
allTemplates.sort((a, b) => {
if (a.id === 'qwik') {
return -1;
} else if (b.id === 'qwik') {
return 1;
}

return a.id > b.id ? 1 : -1;
});

templates = allTemplates;
}

return templates;
}

export async function readTemplates(rootDir: string) {
const componentDir = join(rootDir, 'component');
const routeDir = join(rootDir, 'route');

const component = await getFilesDeep(componentDir);
const route = await getFilesDeep(routeDir);

return {
component: component.map((c) => parseTemplatePath(c, 'component')),
route: route.map((r) => parseTemplatePath(r, 'route')),
};
}

function parseTemplatePath(path: string, type: string) {
const parts = path.split(`/${type}/`);

return {
absolute: path,
relative: parts[1],
};
}

0 comments on commit 221c1e5

Please sign in to comment.