diff --git a/README.md b/README.md index d12f9d0..015db4c 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,12 @@ Build for production: npm run build ``` +Non-interactive setup (for automation): + +```bash +npm run init -- --script ts --style scss --markup pug --normalize --reset +``` + Quality checks: ```bash diff --git a/_gulp/gulpSetup.ts b/_gulp/gulpSetup.ts index d378cc3..ff78f91 100644 --- a/_gulp/gulpSetup.ts +++ b/_gulp/gulpSetup.ts @@ -3,6 +3,7 @@ import { writeFile } from 'fs/promises'; import { copyVendorCSS, createProjectFiles, createProjectStructure } from './modules/fileSetup'; import { generateGulpfile } from './modules/gulpfileGenerator'; import { confirmProjectDeletion, promptUser } from './modules/setupQuestions'; +import { parseSetupOptions } from './modules/setupCliOptions'; import { assertUserChoices } from './modules/userChoicesValidation'; import { UserChoices } from './types'; import { deleteDirectory, fileExists } from './utils/fileSystem'; @@ -21,7 +22,8 @@ async function setup(): Promise { deleteDirectory('dist'); } - const rawChoices = await promptUser(); + const parsedOptions = parseSetupOptions(process.argv.slice(2)); + const rawChoices = parsedOptions.shouldPrompt ? await promptUser() : parsedOptions.choices; const choices: UserChoices = assertUserChoices(rawChoices); await writeFile('_gulp/user-choices.json', JSON.stringify(choices, null, 2)); diff --git a/_gulp/modules/setupCliOptions.test.ts b/_gulp/modules/setupCliOptions.test.ts new file mode 100644 index 0000000..e3cec57 --- /dev/null +++ b/_gulp/modules/setupCliOptions.test.ts @@ -0,0 +1,40 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { parseSetupOptions } from './setupCliOptions'; + +test('parseSetupOptions returns interactive mode when no flags are provided', () => { + const parsed = parseSetupOptions([]); + assert.equal(parsed.shouldPrompt, true); + assert.equal(parsed.choices, undefined); +}); + +test('parseSetupOptions parses non-interactive flags with aliases and booleans', () => { + const parsed = parseSetupOptions(['--script', 'ts', '--style=scss', '--markup', 'pug', '--normalize']); + + assert.equal(parsed.shouldPrompt, false); + assert.deepEqual(parsed.choices, { + script: 'TypeScript', + style: 'SCSS', + markup: 'Pug', + addNormalize: true, + addReset: false, + }); +}); + +test('parseSetupOptions throws when non-interactive flags are partially provided', () => { + assert.throws( + () => parseSetupOptions(['--script', 'js', '--style', 'sass']), + /provide --script, --style, and --markup together/, + ); +}); + +test('parseSetupOptions throws when a flag has no value', () => { + assert.throws(() => parseSetupOptions(['--script']), /Missing value for --script/); +}); + +test('parseSetupOptions throws when a value is invalid', () => { + assert.throws( + () => parseSetupOptions(['--script', 'coffee', '--style', 'scss', '--markup', 'html']), + /Invalid user choices/, + ); +}); diff --git a/_gulp/modules/setupCliOptions.ts b/_gulp/modules/setupCliOptions.ts new file mode 100644 index 0000000..5c9210f --- /dev/null +++ b/_gulp/modules/setupCliOptions.ts @@ -0,0 +1,77 @@ +import { UserChoices } from '../types'; +import { assertUserChoices } from './userChoicesValidation'; + +const scriptAliases: Record = { + javascript: 'JavaScript', + js: 'JavaScript', + typescript: 'TypeScript', + ts: 'TypeScript', +}; + +const styleAliases: Record = { + sass: 'Sass', + scss: 'SCSS', +}; + +const markupAliases: Record = { + html: 'HTML', + pug: 'Pug', +}; + +export interface ParsedSetupOptions { + choices?: UserChoices; + shouldPrompt: boolean; +} + +function readFlagValue(argv: string[], flagName: string): string | undefined { + const flagPrefix = `${flagName}=`; + const inline = argv.find((arg) => arg.startsWith(flagPrefix)); + if (inline) { + return inline.slice(flagPrefix.length); + } + + const index = argv.indexOf(flagName); + if (index === -1) { + return undefined; + } + + const next = argv[index + 1]; + if (!next || next.startsWith('--')) { + throw new Error(`Missing value for ${flagName}.`); + } + + return next; +} + +function normalizeFlagValue(value: string | undefined): string | undefined { + return value?.trim().toLowerCase(); +} + +export function parseSetupOptions(argv: string[]): ParsedSetupOptions { + const rawScript = normalizeFlagValue(readFlagValue(argv, '--script')); + const rawStyle = normalizeFlagValue(readFlagValue(argv, '--style')); + const rawMarkup = normalizeFlagValue(readFlagValue(argv, '--markup')); + + const hasChoiceFlag = rawScript !== undefined || rawStyle !== undefined || rawMarkup !== undefined; + if (!hasChoiceFlag) { + return { shouldPrompt: true }; + } + + if (!rawScript || !rawStyle || !rawMarkup) { + throw new Error('When using non-interactive flags, provide --script, --style, and --markup together.'); + } + + const candidateChoices = { + script: scriptAliases[rawScript], + style: styleAliases[rawStyle], + markup: markupAliases[rawMarkup], + addNormalize: argv.includes('--normalize'), + addReset: argv.includes('--reset'), + }; + + const choices = assertUserChoices(candidateChoices); + return { + choices, + shouldPrompt: false, + }; +}