Skip to content

Commit e1544aa

Browse files
committed
feat: create simple angular skeleton
1 parent 6084706 commit e1544aa

15 files changed

Lines changed: 149 additions & 159 deletions

File tree

.editorconfig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ root = true
33

44
[*]
55
charset = utf-8
6-
end_of_line = lf
7-
indent_size = 2
86
indent_style = space
7+
indent_size = 2
98
insert_final_newline = true
10-
max_line_length = 140
119
trim_trailing_whitespace = true
10+
end_of_line = lf
11+
max_line_length = 140
1212

1313
[*.md]
14-
indent_size = 4
14+
max_line_length = off
1515

1616
[*.ts]
1717
quote_type = single

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"author": "Marcel Hendrich",
33
"bin": {
4-
"swagstack": "dist/cli/index.js"
4+
"swagstack": "dist/index.js"
55
},
66
"contributors": [
77
{
@@ -11,10 +11,10 @@
1111
],
1212
"dependencies": {
1313
"@inquirer/prompts": "8.3.2",
14-
"commander": "14.0.3",
1514
"chalk": "5.6.2",
16-
"figlet": "1.11.0",
15+
"commander": "14.0.3",
1716
"execa": "9.6.1",
17+
"figlet": "1.11.0",
1818
"fs-extra": "11.3.4",
1919
"handlebars": "4.7.9"
2020
},
@@ -35,9 +35,9 @@
3535
"private": true,
3636
"repository": "https://github.com/inpercima/swagstack",
3737
"scripts": {
38-
"build": "tsc",
39-
"dev": "tsx src/cli/index.ts",
40-
"start": "node dist/cli/index.js"
38+
"build": "tsc && node scripts/copy-templates.js",
39+
"dev": "tsx src/index.ts",
40+
"start": "node dist/index.js"
4141
},
4242
"type": "module",
4343
"version": "3.0.0-SNAPSHOT"

scripts/copy-templates.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import fs from 'fs-extra';
2+
import path from 'node:path';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
6+
7+
const source = path.resolve(__dirname, '../src/templates');
8+
const target = path.resolve(__dirname, '../dist/templates');
9+
10+
await fs.ensureDir(path.dirname(target));
11+
await fs.copy(source, target, { overwrite: true });
12+
13+
console.log(`Copied templates: ${source} -> ${target}`);

src/commands/init.ts

Lines changed: 72 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import chalk from 'chalk';
12
import { Command } from 'commander';
23
import { execa } from 'execa';
34
import fs from 'fs-extra';
@@ -8,21 +9,23 @@ import {
89
choosePackageManager,
910
choosePreset,
1011
} from '../utils/prompts.js';
11-
import type { InitOptions, PackageManager, Preset } from '../utils/types.js';
12+
import { PRESETS, type InitOptions, type PackageManager, type Preset } from '../utils/types.js';
1213
import { resolvePm, resolvePreset } from '../utils/utils.js';
14+
import { ANGULAR_CLI_VERSION } from '../utils/versions.js';
15+
import { fileURLToPath } from 'node:url';
1316

1417
/**
1518
* Ensures that the target directory exists and is empty.
1619
* If the directory already exists and is not empty, prompts the user to confirm whether to continue.
1720
* Returns true if the directory is ready for use, false if the user aborted.
1821
**/
19-
async function ensureCleanEmptyDir(dir: string, projectName: string): Promise<boolean> {
22+
async function ensureCleanEmptyDir(dir: string): Promise<boolean> {
2023
if (await fs.pathExists(dir)) {
2124
const entries = await fs.readdir(dir);
2225
if (entries.length > 0) {
23-
const ok = await askContinueInNonEmptyDir(projectName);
26+
const ok = await askContinueInNonEmptyDir(dir);
2427
if (!ok) {
25-
console.log('Aborted.');
28+
console.log('❌ ' + chalk.red('Aborted.'));
2629
return false;
2730
}
2831
}
@@ -34,11 +37,11 @@ async function ensureCleanEmptyDir(dir: string, projectName: string): Promise<bo
3437
/** Logs the starting info for the initialization process. */
3538
function logStarting(repoRoot: string, preset: Preset, pm: PackageManager): void {
3639
console.log('');
37-
console.log('🚀 Starting swagstack project initialization...');
40+
console.log('🚀 ' + chalk.cyanBright('Starting swagstack project initialization...'));
3841
console.log('');
39-
console.log(`Creating monorepo: ${repoRoot}`);
40-
console.log(`Preset: ${preset}`);
41-
console.log(`Package Manager: ${pm}`);
42+
console.log(`Creating monorepo: ${chalk.green(repoRoot)}`);
43+
console.log(`Preset: ${chalk.green(preset)}`);
44+
console.log(`Package Manager: ${chalk.green(pm)}`);
4245
console.log('');
4346
}
4447

@@ -47,60 +50,68 @@ async function run(cmd: string, args: string[], cwd: string): Promise<void> {
4750
await execa(cmd, args, { cwd, stdio: 'inherit' });
4851
}
4952

50-
async function writeRootFiles(repoRoot: string, projectName: string, pm: PackageManager): Promise<void> {
51-
const rootPkg = {
52-
name: projectName,
53-
private: true,
54-
workspaces: ['apps/*'],
55-
scripts: {
56-
'dev:frontend': `${pm} --prefix apps/frontend start`,
57-
},
58-
};
59-
await fs.writeJson(path.join(repoRoot, 'package.json'), rootPkg, { spaces: 2 });
60-
61-
if (pm === 'pnpm') {
62-
await fs.writeFile(path.join(repoRoot, 'pnpm-workspace.yaml'), `packages:\n - 'apps/*'\n`);
63-
}
64-
65-
await fs.writeFile(
66-
path.join(repoRoot, 'README.md'),
67-
[
68-
`# ${name}`,
69-
'',
70-
'Generated with [swagstack](https://github.com/inpercima/swagstack).',
71-
'',
72-
'## Next steps',
73-
'',
74-
`\`\`\`bash`,
75-
`cd ${name}`,
76-
`${pm} install`,
77-
`${pm} run dev:frontend`,
78-
`\`\`\``,
79-
'',
80-
].join('\n'),
81-
);
53+
/** Writes the root-level files like package.json and README.md. */
54+
async function writeRootFiles(
55+
repoRoot: string,
56+
projectName: string,
57+
pm: PackageManager,
58+
): Promise<void> {
59+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
60+
fs.copyFile(path.join(__dirname, '../templates/root/.gitattributes'), path.join(repoRoot, '.gitattributes'));
61+
// const rootPkg = {
62+
// name: projectName,
63+
// private: true,
64+
// workspaces: ['apps/*'],
65+
// scripts: {
66+
// 'dev:frontend': `${pm} --prefix apps/frontend start`,
67+
// },
68+
// };
69+
// await fs.writeJson(path.join(repoRoot, 'package.json'), rootPkg, { spaces: 2 });
70+
// if (pm === 'pnpm') {
71+
// await fs.writeFile(path.join(repoRoot, 'pnpm-workspace.yaml'), `packages:\n - 'apps/*'\n`);
72+
// }
73+
// await fs.writeFile(
74+
// path.join(repoRoot, 'README.md'),
75+
// [
76+
// `# ${projectName}`,
77+
// '',
78+
// 'Generated with [swagstack](https://github.com/inpercima/swagstack).',
79+
// '',
80+
// '## Next steps',
81+
// '',
82+
// `\`\`\`bash`,
83+
// `cd ${projectName}`,
84+
// `${pm} install`,
85+
// `${pm} run dev:frontend`,
86+
// `\`\`\``,
87+
// '',
88+
// ].join('\n'),
89+
// );
8290
}
8391

84-
async function createAngularFrontend(repoRoot: string, pm: PackageManager): Promise<void> {
85-
const appsDir = path.join(repoRoot, 'apps');
86-
const frontendDir = path.join(appsDir, 'frontend');
87-
await fs.ensureDir(appsDir);
88-
92+
async function createAngularFrontendSkeleton(
93+
repoRoot: string,
94+
projectName: string,
95+
preset: Preset,
96+
pm: PackageManager,
97+
): Promise<void> {
98+
const isAngularOnly = preset === PRESETS.ANGULAR_ONLY;
99+
const workingDir = isAngularOnly ? process.cwd() : repoRoot;
89100
await run(
90101
'npx',
91102
[
92103
'-y',
93-
'@angular/cli@latest',
104+
`@angular/cli@${ANGULAR_CLI_VERSION}`,
94105
'new',
95-
'frontend',
96-
'--directory',
97-
frontendDir,
106+
isAngularOnly ? projectName : 'frontend',
107+
'--interactive=false',
108+
'--skip-git',
109+
'--style=css',
110+
'--routing',
98111
'--package-manager',
99112
pm,
100-
'--skip-git',
101-
'--skip-tests',
102113
],
103-
repoRoot,
114+
workingDir,
104115
);
105116
}
106117

@@ -170,37 +181,36 @@ async function startInitialization(
170181
logStarting(repoRoot, preset, pm);
171182

172183
await writeRootFiles(repoRoot, projectName, pm);
173-
await createAngularFrontend(repoRoot, pm);
184+
await createAngularFrontendSkeleton(repoRoot, projectName, preset, pm);
174185

175-
if (preset === 'preset-angular-java') {
186+
if (preset === PRESETS.ANGULAR_JAVA) {
176187
await createJavaBackendSkeleton(repoRoot);
177-
} else if (preset === 'preset-angular-php') {
188+
} else if (preset === PRESETS.ANGULAR_PHP) {
178189
await createPhpBackendSkeleton(repoRoot);
179190
}
180191

181192
console.log('');
182-
console.log('✔ Done! Next steps:');
193+
console.log(`✔ ${chalk.green('Done! Next steps:')}`);
183194
console.log('');
184-
console.log(` cd ${projectName}`);
185-
console.log(` ${pm} install`);
186-
console.log(` ${pm} run dev:frontend`);
195+
console.log(` cd ${chalk.green(projectName)}`);
196+
console.log(` ${chalk.green(pm)} install`);
197+
console.log(` ${chalk.green(pm)} run dev:frontend`);
187198
console.log('');
188199
}
189200

190201
export function initCommand(): Command {
191202
const cmd = new Command('init');
192203
cmd
193204
.argument('[name]', 'Project folder name (monorepo root)')
194-
.option('--preset <preset>', 'preset-angular-only | preset-angular-java | preset-angular-php')
205+
.option('--preset <preset>', 'Preset to use (e.g. preset-angular-only)')
195206
.option('--pm <pm>', 'pnpm | npm | yarn (default: pnpm)')
196207
.action(async (name: string | undefined, opts: InitOptions) => {
197-
198208
const projectName = name ?? (await askProjectName());
199209
const preset: Preset = resolvePreset(opts.preset) ?? (await choosePreset());
200210
const pm: PackageManager = resolvePm(opts.pm) ?? (await choosePackageManager());
201211
const repoRoot = path.resolve(process.cwd(), projectName);
202212

203-
if (await ensureCleanEmptyDir(repoRoot, projectName)) {
213+
if (await ensureCleanEmptyDir(repoRoot)) {
204214
startInitialization(repoRoot, projectName, preset, pm).catch((err) => {
205215
console.error('Initialization failed:', err);
206216
process.exit(1);

src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { Command } from 'commander';
44
import figlet from 'figlet';
55
import { initCommand } from './commands/init.js';
66

7+
const tagline =
8+
'An [s]mart [w]eb [a]pps monorepo [g]enerator for Angular with optional backend - full[stack] included.';
9+
710
console.log(chalk.cyan(figlet.textSync('swagstack')));
8-
console.log(chalk.gray('An [s]mart [w]eb [a]pps monorepo [g]enerator for Angular with optional backend - full[stack] included.'));
11+
console.log(chalk.gray(tagline));
912
console.log('');
1013

1114
const program = new Command();
12-
program
13-
.name('swagstack')
14-
.description('An [s]mart [w]eb [a]pps monorepo [g]enerator for Angular with optional backend - full[stack] included.')
15-
.version('3.0.0-SNAPSHOT');
15+
program.name('swagstack').description(tagline).version('3.0.0-SNAPSHOT');
1616
program.addCommand(initCommand());
1717

1818
await program.parseAsync(process.argv);

src/templates/root/.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Set the default behavior, in case people don't have core.autocrlf set.
2+
* text=auto

src/utils/prompts.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { confirm, input, select } from '@inquirer/prompts';
2-
import { PackageManager, Preset } from './types.js';
2+
import { CSS_FRAMEWORKS, PACKAGE_MANAGERS, PackageManager, Preset, PRESETS } from './types.js';
33

44
/** Prompts the user to enter a project name. */
55
export function askProjectName(): Promise<string> {
@@ -13,19 +13,19 @@ export function choosePreset(): Promise<Preset> {
1313
choices: [
1414
{
1515
name: 'Angular only',
16-
value: 'preset-angular-only',
16+
value: PRESETS.ANGULAR_ONLY,
1717
},
1818
{
1919
name: 'Angular + Java',
20-
value: 'preset-angular-java',
20+
value: PRESETS.ANGULAR_JAVA,
2121
},
2222
{
2323
name: 'Angular + PHP',
24-
value: 'preset-angular-php',
24+
value: PRESETS.ANGULAR_PHP,
2525
},
2626
{
2727
name: 'Angular + Nest.js',
28-
value: 'preset-angular-nestjs',
28+
value: PRESETS.ANGULAR_NESTJS,
2929
},
3030
],
3131
});
@@ -36,9 +36,21 @@ export function choosePackageManager(): Promise<PackageManager> {
3636
return select<PackageManager>({
3737
message: 'Choose a package manager',
3838
choices: [
39-
{ name: 'pnpm (recommended)', value: 'pnpm' },
40-
{ name: 'npm', value: 'npm' },
41-
{ name: 'yarn', value: 'yarn' },
39+
{ name: `${PACKAGE_MANAGERS.PNPM} (recommended)`, value: PACKAGE_MANAGERS.PNPM },
40+
{ name: PACKAGE_MANAGERS.NPM, value: PACKAGE_MANAGERS.NPM },
41+
{ name: PACKAGE_MANAGERS.YARN, value: PACKAGE_MANAGERS.YARN },
42+
],
43+
});
44+
}
45+
46+
export function chooseCssFramework(): Promise<CSS_FRAMEWORKS> {
47+
return select<CSS_FRAMEWORKS>({
48+
message: 'Choose a CSS framework',
49+
choices: [
50+
{ name: `${CSS_FRAMEWORKS.TAILWIND_CSS}`, value: CSS_FRAMEWORKS.TAILWIND_CSS },
51+
{ name: `${CSS_FRAMEWORKS.DAISYUI}`, value: CSS_FRAMEWORKS.DAISYUI },
52+
{ name: `${CSS_FRAMEWORKS.SHADCN_UI}`, value: CSS_FRAMEWORKS.SHADCN_UI },
53+
{ name: `${CSS_FRAMEWORKS.BOOTSTRAP}`, value: CSS_FRAMEWORKS.BOOTSTRAP },
4254
],
4355
});
4456
}

src/utils/types.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
export const CSS_FRAMEWORKS = [
2-
'Tailwind CSS',
3-
'DaisyUI',
4-
'Bootstrap',
5-
'Shadcn UI',
6-
] as const;
7-
export type CssFramework = (typeof CSS_FRAMEWORKS)[number];
1+
export enum CSS_FRAMEWORKS {
2+
TAILWIND_CSS = 'Tailwind CSS',
3+
DAISYUI = 'DaisyUI',
4+
SHADCN_UI = 'Shadcn UI',
5+
BOOTSTRAP = 'Bootstrap',
6+
}
7+
export type CssFramework = `${CSS_FRAMEWORKS}`;
88

9-
export const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn'] as const;
10-
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
9+
export enum PACKAGE_MANAGERS {
10+
PNPM = 'pnpm',
11+
NPM = 'npm',
12+
YARN = 'yarn',
13+
}
14+
export type PackageManager = `${PACKAGE_MANAGERS}`;
1115

12-
export const PRESETS = [
13-
'preset-angular-only',
14-
'preset-angular-java',
15-
'preset-angular-php',
16-
'preset-angular-nestjs',
17-
] as const;
18-
export type Preset = (typeof PRESETS)[number];
16+
export enum PRESETS {
17+
ANGULAR_ONLY = 'preset-angular-only',
18+
ANGULAR_JAVA = 'preset-angular-java',
19+
ANGULAR_PHP = 'preset-angular-php',
20+
ANGULAR_NESTJS = 'preset-angular-nestjs',
21+
}
22+
export type Preset = `${PRESETS}`;
1923

2024
export interface InitOptions {
2125
preset?: string;

0 commit comments

Comments
 (0)