diff --git a/src/commands/make/component.ts b/src/commands/make/component.ts
index b39269a..9049b91 100644
--- a/src/commands/make/component.ts
+++ b/src/commands/make/component.ts
@@ -24,9 +24,9 @@ export default async function makeComponent(filePath?: string) {
p.text({
message: 'Directory to place it in',
placeholder: './components',
+ initialValue: './components',
validate: value => {
if (!value) return 'Please enter a path.'
- if (value[0] !== '.') return 'Please enter a relative path.'
},
}),
},
diff --git a/src/commands/make/config.ts b/src/commands/make/config.ts
index 3c0cd29..16ce5f1 100644
--- a/src/commands/make/config.ts
+++ b/src/commands/make/config.ts
@@ -18,7 +18,7 @@ export default async function makeConfig(name?: string) {
placeholder: 'production',
validate: value => {
if (!value) return 'Please enter a config name.'
- if (value.includes(' ')) return 'Use - instead of spaces.'
+ if (value.includes(' ')) return 'Use - or . instead of spaces.'
},
}),
},
diff --git a/src/commands/make/layout.ts b/src/commands/make/layout.ts
index b0fe329..0bd5909 100644
--- a/src/commands/make/layout.ts
+++ b/src/commands/make/layout.ts
@@ -4,7 +4,7 @@ import { scaffold, onCancel } from './scaffold.ts'
export default async function makeLayout(filePath?: string) {
if (filePath) {
- await scaffold(filePath, 'layout.vue')
+ await scaffold(filePath, 'Layout.vue')
return
}
@@ -24,14 +24,14 @@ export default async function makeLayout(filePath?: string) {
p.text({
message: 'Directory to place it in',
placeholder: './components',
+ initialValue: './components',
validate: value => {
if (!value) return 'Please enter a path.'
- if (value[0] !== '.') return 'Please enter a relative path.'
},
}),
},
{ onCancel },
)
- await scaffold(`${result.path}/${result.filename}`, 'layout.vue')
+ await scaffold(`${result.path}/${result.filename}`, 'Layout.vue')
}
diff --git a/src/commands/make/scaffold.ts b/src/commands/make/scaffold.ts
index 84b7792..11e673c 100644
--- a/src/commands/make/scaffold.ts
+++ b/src/commands/make/scaffold.ts
@@ -30,6 +30,6 @@ export async function scaffold(filePath: string, stubName: string) {
}
export function onCancel() {
- p.cancel('Cancelled.')
+ p.cancel('Canceled.')
process.exit(0)
}
diff --git a/src/commands/make/stubs/Layout.vue b/src/commands/make/stubs/Layout.vue
new file mode 100644
index 0000000..448728e
--- /dev/null
+++ b/src/commands/make/stubs/Layout.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/commands/make/stubs/component.vue b/src/commands/make/stubs/component.vue
index e9f0816..1d21767 100644
--- a/src/commands/make/stubs/component.vue
+++ b/src/commands/make/stubs/component.vue
@@ -1,7 +1,5 @@
-
diff --git a/src/commands/make/stubs/config.ts b/src/commands/make/stubs/config.ts
index 23e727e..e2d1c84 100644
--- a/src/commands/make/stubs/config.ts
+++ b/src/commands/make/stubs/config.ts
@@ -1,9 +1,5 @@
import { defineConfig } from '@maizzle/framework'
export default defineConfig({
- css: {
- purge: true,
- inline: true,
- shorthand: true,
- },
+
})
diff --git a/src/commands/make/stubs/layout.vue b/src/commands/make/stubs/layout.vue
deleted file mode 100644
index c1648f7..0000000
--- a/src/commands/make/stubs/layout.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/commands/make/template.ts b/src/commands/make/template.ts
index 723b77b..ad2c29f 100644
--- a/src/commands/make/template.ts
+++ b/src/commands/make/template.ts
@@ -24,9 +24,9 @@ export default async function makeTemplate(filePath?: string) {
p.text({
message: 'Directory to place it in',
placeholder: './emails',
+ initialValue: './emails',
validate: value => {
if (!value) return 'Please enter a path.'
- if (value[0] !== '.') return 'Please enter a relative path.'
},
}),
},
diff --git a/src/commands/new.ts b/src/commands/new.ts
index 11a248d..f63eed1 100644
--- a/src/commands/new.ts
+++ b/src/commands/new.ts
@@ -125,9 +125,9 @@ export default async function newProject(starterArg?: string, dirArg?: string, o
p.text({
message: 'Where should we create your project?',
placeholder: './maizzle',
+ initialValue: './maizzle',
validate: value => {
if (!value) return 'Please enter a path.'
- if (value[0] !== '.') return 'Please enter a relative path.'
if (existsSync(value)) return 'That directory already exists. Please enter a different path.'
},
}),
@@ -186,24 +186,9 @@ export default async function newProject(starterArg?: string, dirArg?: string, o
) as unknown as Project
}
- const spinner = p.spinner()
-
- spinner.start('Creating project')
-
const starter = starters.find(s => s.value === project.starter)
const source = starter ? starter.path : project.starter
- await downloadTemplate(source.includes(':') ? source : `gh:${source}`, {
- dir: project.path,
- })
-
- await rm(`${project.path}/.github`, {
- recursive: true,
- force: true
- })
-
- spinner.stop(`Created project in ${project.path}`)
-
if (project.install) {
try {
execSync(`${project.pm} --version`, { stdio: 'ignore' })
@@ -211,19 +196,34 @@ export default async function newProject(starterArg?: string, dirArg?: string, o
p.log.error(`${project.pm} is not installed. Please install it first.`)
process.exit(1)
}
-
- spinner.start('Installing dependencies')
- const startTime = Date.now()
-
- await installDependencies({
- cwd: project.path,
- silent: true,
- packageManager: project.pm as any,
- })
-
- spinner.stop(`Installed dependencies ${color.gray((Date.now() - startTime) / 1000 + 's')}`)
}
+ await p.tasks([
+ {
+ title: 'Creating project',
+ task: async () => {
+ await downloadTemplate(source.includes(':') ? source : `gh:${source}`, {
+ dir: project.path,
+ })
+ await rm(`${project.path}/.github`, { recursive: true, force: true })
+ return `Created project in ${project.path}`
+ },
+ },
+ {
+ title: 'Installing dependencies',
+ enabled: project.install,
+ task: async () => {
+ const startTime = Date.now()
+ await installDependencies({
+ cwd: project.path,
+ silent: true,
+ packageManager: project.pm as any,
+ })
+ return `Installed dependencies ${color.gray((Date.now() - startTime) / 1000 + 's')}`
+ },
+ },
+ ])
+
const pm = project.pm || 'npm'
const runCmd = pm === 'yarn' ? 'yarn dev' : `${pm} run dev`
diff --git a/src/index.ts b/src/index.ts
index a4866f9..fe7250d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -17,13 +17,14 @@ export default async function bootstrap(framework?: Framework) {
program
.name('maizzle')
.description('Maizzle CLI')
- .version('1.0.0')
+ .version('1.2.0')
if (framework) {
program
.command('serve')
+ .alias('dev')
.description('Start the Maizzle dev server with HMR')
- .option('-c, --config ', 'Path to maizzle config file')
+ .option('-c, --config ', 'Path to a Maizzle config file')
.option('-p, --port ', 'Dev server port')
.option('--host [address]', 'Expose on network')
.action(async (options) => {
@@ -37,19 +38,34 @@ export default async function bootstrap(framework?: Framework) {
program
.command('build')
.description('Build email templates to HTML')
- .option('-c, --config ', 'Path to maizzle config file')
+ .option('-c, --config ', 'Path to a Maizzle config file')
.option('-o, --output ', 'Output directory')
+ .option('--pretty', 'Pretty-print HTML output')
+ .option('--minify', 'Minify HTML output')
+ .option('--plaintext', 'Generate plaintext versions')
+ .option('--dir ', 'Source directory for email templates')
+ .option('--ext ', 'Output file extension')
.action(async (options) => {
- await framework.build({
- config: options.config,
- output: options.output,
- })
+ if (options.config) {
+ await framework.build(options.config)
+ return
+ }
+
+ const overrides: Record = {}
+ if (options.output) overrides.output = { ...overrides.output, path: options.output }
+ if (options.ext) overrides.output = { ...overrides.output, extension: options.ext }
+ if (options.pretty) overrides.html = { ...overrides.html, format: true }
+ if (options.minify) overrides.html = { ...overrides.html, minify: true }
+ if (options.plaintext) overrides.plaintext = true
+ if (options.dir) overrides.content = [`${options.dir}/**/*.{vue,md}`]
+
+ await framework.build(Object.keys(overrides).length ? overrides : undefined)
})
program
.command('prepare')
.description('Generate IDE type definitions in .maizzle/')
- .option('-c, --config ', 'Path to maizzle config file')
+ .option('-c, --config ', 'Path to a Maizzle config file')
.action(async (options) => {
await framework.prepare({
config: options.config,
diff --git a/src/tests/index.test.ts b/src/tests/index.test.ts
index 65d7646..a809cc9 100644
--- a/src/tests/index.test.ts
+++ b/src/tests/index.test.ts
@@ -18,6 +18,11 @@ vi.mock('../commands/new.ts', () => ({
default: vi.fn(),
}))
+vi.mock('../commands/make/config.ts', () => ({ default: vi.fn() }))
+vi.mock('../commands/make/layout.ts', () => ({ default: vi.fn() }))
+vi.mock('../commands/make/template.ts', () => ({ default: vi.fn() }))
+vi.mock('../commands/make/component.ts', () => ({ default: vi.fn() }))
+
import bootstrap from '../index.ts'
const mockFramework = {
@@ -64,25 +69,98 @@ describe('bootstrap', () => {
})
})
- it('calls framework.build for the build command', async () => {
+ it('calls framework.build with no args for bare build command', async () => {
process.argv = ['node', 'maizzle', 'build']
await bootstrap(mockFramework)
+ expect(mockFramework.build).toHaveBeenCalledWith(undefined)
+ })
+
+ it('passes config path as a string when -c is set', async () => {
+ process.argv = ['node', 'maizzle', 'build', '-c', 'custom.config.ts']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith('custom.config.ts')
+ })
+
+ it('passes -o as output.path override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '-o', 'dist']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({ output: { path: 'dist' } })
+ })
+
+ it('ignores override flags when -c is set', async () => {
+ process.argv = ['node', 'maizzle', 'build', '-c', 'custom.config.ts', '-o', 'dist', '--pretty']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith('custom.config.ts')
+ })
+
+ it('passes --pretty as html.format override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '--pretty']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({ html: { format: true } })
+ })
+
+ it('combines --pretty with -o overrides', async () => {
+ process.argv = ['node', 'maizzle', 'build', '-o', 'dist', '--pretty']
+
+ await bootstrap(mockFramework)
+
expect(mockFramework.build).toHaveBeenCalledWith({
- config: undefined,
- output: undefined,
+ output: { path: 'dist' },
+ html: { format: true },
+ })
+ })
+
+ it('passes --plaintext as plaintext override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '--plaintext']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({ plaintext: true })
+ })
+
+ it('passes --ext as output.extension override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '--ext', 'blade.php']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({ output: { extension: 'blade.php' } })
+ })
+
+ it('combines -o and --ext into output overrides', async () => {
+ process.argv = ['node', 'maizzle', 'build', '-o', 'dist', '--ext', 'blade.php']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({
+ output: { path: 'dist', extension: 'blade.php' },
})
})
- it('passes config and output options to build', async () => {
- process.argv = ['node', 'maizzle', 'build', '-c', 'custom.config.ts', '-o', 'dist']
+ it('passes --minify as html.minify override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '--minify']
+
+ await bootstrap(mockFramework)
+
+ expect(mockFramework.build).toHaveBeenCalledWith({ html: { minify: true } })
+ })
+
+ it('passes --dir as content override', async () => {
+ process.argv = ['node', 'maizzle', 'build', '--dir', 'templates']
await bootstrap(mockFramework)
expect(mockFramework.build).toHaveBeenCalledWith({
- config: 'custom.config.ts',
- output: 'dist',
+ content: ['templates/**/*.{vue,md}'],
})
})
@@ -116,6 +194,42 @@ describe('bootstrap', () => {
expect(newProject).toHaveBeenCalled()
})
+ it('dispatches make:config to its handler', async () => {
+ const makeConfig = (await import('../commands/make/config.ts')).default
+ process.argv = ['node', 'maizzle', 'make:config', 'production']
+
+ await bootstrap()
+
+ expect(makeConfig).toHaveBeenCalledWith('production')
+ })
+
+ it('dispatches make:layout to its handler', async () => {
+ const makeLayout = (await import('../commands/make/layout.ts')).default
+ process.argv = ['node', 'maizzle', 'make:layout', './components/Layout.vue']
+
+ await bootstrap()
+
+ expect(makeLayout).toHaveBeenCalledWith('./components/Layout.vue')
+ })
+
+ it('dispatches make:template to its handler', async () => {
+ const makeTemplate = (await import('../commands/make/template.ts')).default
+ process.argv = ['node', 'maizzle', 'make:template', './emails/welcome.vue']
+
+ await bootstrap()
+
+ expect(makeTemplate).toHaveBeenCalledWith('./emails/welcome.vue')
+ })
+
+ it('dispatches make:component to its handler', async () => {
+ const makeComponent = (await import('../commands/make/component.ts')).default
+ process.argv = ['node', 'maizzle', 'make:component', './components/Button.vue']
+
+ await bootstrap()
+
+ expect(makeComponent).toHaveBeenCalledWith('./components/Button.vue')
+ })
+
it('does not register serve/build without framework', async () => {
process.argv = ['node', 'maizzle', 'serve']
diff --git a/src/tests/make-interactive.test.ts b/src/tests/make-interactive.test.ts
index 26eda64..5ae0d20 100644
--- a/src/tests/make-interactive.test.ts
+++ b/src/tests/make-interactive.test.ts
@@ -73,7 +73,7 @@ describe('make:config interactive', () => {
await makeConfig()
const nameField = textCalls.find(c => c.message === 'Config name')
- expect(nameField.validate('has space')).toBe('Use - instead of spaces.')
+ expect(nameField.validate('has space')).toBe('Use - or . instead of spaces.')
})
})
@@ -99,11 +99,11 @@ describe('make:layout interactive', () => {
expect(field.validate('')).toBe('Please enter a path.')
})
- it('validates non-relative path', async () => {
+ it('accepts any non-empty path', async () => {
await makeLayout()
const field = textCalls.find(c => c.message === 'Directory to place it in')
- expect(field.validate('components')).toBe('Please enter a relative path.')
+ expect(field.validate('components')).toBeUndefined()
expect(field.validate('./components')).toBeUndefined()
})
})
@@ -127,7 +127,7 @@ describe('make:template interactive', () => {
const field = textCalls.find(c => c.message === 'Directory to place it in')
expect(field.validate('')).toBe('Please enter a path.')
- expect(field.validate('emails')).toBe('Please enter a relative path.')
+ expect(field.validate('emails')).toBeUndefined()
expect(field.validate('./emails')).toBeUndefined()
})
})
@@ -151,7 +151,7 @@ describe('make:component interactive', () => {
const field = textCalls.find(c => c.message === 'Directory to place it in')
expect(field.validate('')).toBe('Please enter a path.')
- expect(field.validate('components')).toBe('Please enter a relative path.')
+ expect(field.validate('components')).toBeUndefined()
expect(field.validate('./components')).toBeUndefined()
})
})
diff --git a/src/tests/make.test.ts b/src/tests/make.test.ts
index 16c3176..facbc4d 100644
--- a/src/tests/make.test.ts
+++ b/src/tests/make.test.ts
@@ -139,8 +139,8 @@ describe('make:component', () => {
expect(existsSync(filePath)).toBe(true)
const content = readFileSync(filePath, 'utf-8')
+ expect(content).toContain('script setup')
expect(content).toContain('')
- expect(content).toContain('defineProps')
})
it('creates nested directories if needed', async () => {
diff --git a/src/tests/scaffold.test.ts b/src/tests/scaffold.test.ts
index 059adad..fa53cae 100644
--- a/src/tests/scaffold.test.ts
+++ b/src/tests/scaffold.test.ts
@@ -71,7 +71,7 @@ describe('onCancel', () => {
it('calls p.cancel and exits with 0', () => {
onCancel()
- expect(p.cancel).toHaveBeenCalledWith('Cancelled.')
+ expect(p.cancel).toHaveBeenCalledWith('Canceled.')
expect(mockExit).toHaveBeenCalledWith(0)
})
})