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 @@ + + + + + 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 @@ + + + + + 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: [],'), + ) + } +}