diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 32046f25..c773cade 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -49,13 +49,13 @@ jobs:
node-version: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
verification-script:
- - pnpm --filter "\!*typescript*" build
- - pnpm --filter "*typescript*" build
- - pnpm --filter "*vitest*" test:unit
- - pnpm --filter "*eslint*" lint --no-fix --max-warnings=0
- - pnpm --filter "*prettier*" format --write --check
+ - pnpm --filter '!*typescript*' build
+ - pnpm --filter '*typescript*' build
+ - pnpm --filter '*vitest*' test:unit
+ - pnpm --filter '*eslint*' lint --no-fix --max-warnings=0
+ - pnpm --filter '*prettier*' format --write --check
# FIXME: it's failing now
- # - pnpm --filter "*with-tests*" test:unit
+ # - pnpm --filter '*with-tests*' test:unit
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.os == 'windows-latest' }}
env:
@@ -163,11 +163,12 @@ jobs:
- name: Run build script
working-directory: ./playground
- run: pnpm --filter "*${{ matrix.e2e-framework }}*" build
+ run: pnpm --filter '*${{ matrix.e2e-framework }}*' build
- name: Run e2e test script
working-directory: ./playground
- run: pnpm --filter "*${{ matrix.e2e-framework }}*" --workspace-concurrency 1 test:e2e
+ # bare templates can't pass e2e tests because their page structures don't match the example tests
+ run: pnpm --filter '*${{ matrix.e2e-framework }}*' --filter '!*bare*' --workspace-concurrency 1 test:e2e
- name: Cypress component testing for projects without Vitest
if: ${{ contains(matrix.e2e-framework, 'cypress') }}
diff --git a/index.ts b/index.ts
index 40f70304..bf6031eb 100755
--- a/index.ts
+++ b/index.ts
@@ -18,6 +18,7 @@ import generateReadme from './utils/generateReadme'
import getCommand from './utils/getCommand'
import getLanguage from './utils/getLanguage'
import renderEslint from './utils/renderEslint'
+import trimBoilerplate from './utils/trimBoilerplate'
function isValidPackageName(projectName) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
@@ -83,7 +84,9 @@ async function init() {
// --playwright
// --eslint
// --eslint-with-prettier (only support prettier through eslint for simplicity)
- // --force (for force overwriting)
+ // in addition to the feature flags, you can also pass the following options:
+ // --bare (for a barebone template without example code)
+ // --force (for force overwriting without confirming)
const args = process.argv.slice(2)
@@ -319,8 +322,8 @@ async function init() {
packageName = projectName ?? defaultProjectName,
shouldOverwrite = argv.force,
needsJsx = argv.jsx,
- needsTypeScript = argv.ts || argv.typescript,
- needsRouter = argv.router || argv['vue-router'],
+ needsTypeScript = (argv.ts || argv.typescript) as boolean,
+ needsRouter = (argv.router || argv['vue-router']) as boolean,
needsPinia = argv.pinia,
needsVitest = argv.vitest || argv.tests,
needsPrettier = argv['eslint-with-prettier'],
@@ -563,6 +566,25 @@ async function init() {
)
}
+ if (argv.bare) {
+ trimBoilerplate(root, { needsTypeScript, needsRouter })
+ render('bare/base')
+
+ // TODO: refactor the `render` utility to avoid this kind of manual mapping?
+ if (needsTypeScript) {
+ render('bare/typescript')
+ }
+ if (needsVitest) {
+ render('bare/vitest')
+ }
+ if (needsCypressCT) {
+ render('bare/cypress-ct')
+ }
+ if (needsNightwatchCT) {
+ render('bare/nightwatch-ct')
+ }
+ }
+
// Instructions:
// Supported package managers: pnpm > yarn > bun > npm
const userAgent = process.env.npm_config_user_agent ?? ''
diff --git a/scripts/snapshot.mjs b/scripts/snapshot.mjs
index de5de0b0..1b523a0f 100644
--- a/scripts/snapshot.mjs
+++ b/scripts/snapshot.mjs
@@ -8,6 +8,7 @@ if (!/pnpm/.test(process.env.npm_config_user_agent ?? ''))
throw new Error("Please use pnpm ('pnpm run snapshot') to generate snapshots!")
const featureFlags = [
+ 'bare',
'typescript',
'jsx',
'router',
@@ -54,12 +55,7 @@ function fullCombination(arr) {
}
let flagCombinations = fullCombination(featureFlags)
-flagCombinations.push(
- ['default'],
- ['router', 'pinia'],
- ['eslint'],
- ['eslint-with-prettier'],
-)
+flagCombinations.push(['default'], ['bare', 'default'], ['eslint'], ['eslint-with-prettier'])
// `--with-tests` are equivalent of `--vitest --cypress`
// Previously it means `--cypress` without `--vitest`.
@@ -85,10 +81,15 @@ for (const flags of flagCombinations) {
}
// Filter out combinations that are not allowed
-flagCombinations = flagCombinations.filter(
- (combination) =>
- !featureFlagsDenylist.some((denylist) => denylist.every((flag) => combination.includes(flag))),
-)
+flagCombinations = flagCombinations
+ .filter(
+ (combination) =>
+ !featureFlagsDenylist.some((denylist) =>
+ denylist.every((flag) => combination.includes(flag)),
+ ),
+ )
+ // `--bare` is a supplementary flag and should not be used alone
+ .filter((combination) => !(combination.length === 1 && combination[0] === 'bare'))
const bin = path.posix.relative('../playground/', '../outfile.cjs')
diff --git a/template/bare/base/src/App.vue b/template/bare/base/src/App.vue
new file mode 100644
index 00000000..6ca279f5
--- /dev/null
+++ b/template/bare/base/src/App.vue
@@ -0,0 +1,7 @@
+
+
+
+ Hello World
+
+
+
diff --git a/template/bare/cypress-ct/src/__tests__/App.cy.js b/template/bare/cypress-ct/src/__tests__/App.cy.js
new file mode 100644
index 00000000..55f8caa1
--- /dev/null
+++ b/template/bare/cypress-ct/src/__tests__/App.cy.js
@@ -0,0 +1,8 @@
+import App from '../App.vue'
+
+describe('App', () => {
+ it('mounts and renders properly', () => {
+ cy.mount(App)
+ cy.get('h1').should('contain', 'Hello World')
+ })
+})
diff --git a/template/bare/nightwatch-ct/src/__tests__/App.spec.js b/template/bare/nightwatch-ct/src/__tests__/App.spec.js
new file mode 100644
index 00000000..86cd9e12
--- /dev/null
+++ b/template/bare/nightwatch-ct/src/__tests__/App.spec.js
@@ -0,0 +1,14 @@
+describe('App', function () {
+ before((browser) => {
+ browser.init()
+ })
+
+ it('mounts and renders properly', async function () {
+ const appComponent = await browser.mountComponent('/src/App.vue');
+
+ browser.expect.element(appComponent).to.be.present;
+ browser.expect.element('h1').text.to.contain('Hello World');
+ })
+
+ after((browser) => browser.end())
+})
diff --git a/template/bare/typescript/src/App.vue b/template/bare/typescript/src/App.vue
new file mode 100644
index 00000000..c2903a62
--- /dev/null
+++ b/template/bare/typescript/src/App.vue
@@ -0,0 +1,7 @@
+
+
+
+ Hello World
+
+
+
diff --git a/template/bare/vitest/src/__tests__/App.spec.js b/template/bare/vitest/src/__tests__/App.spec.js
new file mode 100644
index 00000000..607fbfba
--- /dev/null
+++ b/template/bare/vitest/src/__tests__/App.spec.js
@@ -0,0 +1,11 @@
+import { describe, it, expect } from 'vitest'
+
+import { mount } from '@vue/test-utils'
+import App from '../App.vue'
+
+describe('App', () => {
+ it('mounts renders properly', () => {
+ const wrapper = mount(App)
+ expect(wrapper.text()).toContain('Hello World')
+ })
+})
diff --git a/utils/trimBoilerplate.ts b/utils/trimBoilerplate.ts
new file mode 100644
index 00000000..1a9fd704
--- /dev/null
+++ b/utils/trimBoilerplate.ts
@@ -0,0 +1,36 @@
+import * as fs from 'node:fs'
+import * as path from 'path'
+
+function replaceContent(filepath: string, replacer: (content: string) => string) {
+ const content = fs.readFileSync(filepath, 'utf8')
+ fs.writeFileSync(filepath, replacer(content))
+}
+
+export default function trimBoilerplate(rootDir: string, features: Record) {
+ const isTs = features.needsTypeScript
+ const srcDir = path.resolve(rootDir, 'src')
+
+ for (const filename of fs.readdirSync(srcDir)) {
+ // Keep `main.js/ts`, `router`, and `stores` directories
+ // `App.vue` would be re-rendered in the next step
+ if (['main.js', 'main.ts', 'router', 'stores'].includes(filename)) {
+ continue
+ }
+ const fullpath = path.resolve(srcDir, filename)
+ fs.rmSync(fullpath, { recursive: true })
+ }
+
+ // Remove CSS import in the entry file
+ const entryPath = path.resolve(rootDir, isTs ? 'src/main.ts' : 'src/main.js')
+ replaceContent(entryPath, (content) => content.replace("import './assets/main.css'\n\n", ''))
+
+ // If `router` feature is selected, use an empty router configuration
+ if (features.needsRouter) {
+ const routerEntry = path.resolve(srcDir, isTs ? 'router/index.ts' : 'router/index.js')
+ replaceContent(routerEntry, (content) =>
+ content
+ .replace(`import HomeView from '../views/HomeView.vue'\n`, '')
+ .replace(/routes:\s*\[[\s\S]*?\],/, 'routes: [],'),
+ )
+ }
+}