]> git.ipfire.org Git - thirdparty/vuejs/create-vue.git/commitdiff
feat: use a `--bare` flag to generate a template without too much boilerplate (#636)
authorHaoqun Jiang <haoqunjiang@gmail.com>
Mon, 23 Dec 2024 06:31:32 +0000 (14:31 +0800)
committerGitHub <noreply@github.com>
Mon, 23 Dec 2024 06:31:32 +0000 (14:31 +0800)
Closes #112
Closes #186
Closes #300
Closes #637

.github/workflows/ci.yml
index.ts
scripts/snapshot.mjs
template/bare/base/src/App.vue [new file with mode: 0644]
template/bare/cypress-ct/src/__tests__/App.cy.js [new file with mode: 0644]
template/bare/nightwatch-ct/src/__tests__/App.spec.js [new file with mode: 0644]
template/bare/typescript/src/App.vue [new file with mode: 0644]
template/bare/vitest/src/__tests__/App.spec.js [new file with mode: 0644]
utils/trimBoilerplate.ts [new file with mode: 0644]

index 32046f255920941a4e5b68307cd37bc162be6fc6..c773caded42833b2469ad6835adbf7a264fba719 100644 (file)
@@ -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') }}
index 40f70304b9bd091f5dbf1e4225cafcd0670f0c57..bf6031eb5121e70a560e70c293455c2ca090632d 100755 (executable)
--- 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 ?? ''
index de5de0b0df617b08aecac1d8b93e2d67bb33dccf..1b523a0fbe816cfe8611d5f1b90b7233cd54b384 100644 (file)
@@ -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 (file)
index 0000000..6ca279f
--- /dev/null
@@ -0,0 +1,7 @@
+<script setup></script>
+
+<template>
+  <h1>Hello World</h1>
+</template>
+
+<style scoped></style>
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 (file)
index 0000000..55f8caa
--- /dev/null
@@ -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 (file)
index 0000000..86cd9e1
--- /dev/null
@@ -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 (file)
index 0000000..c2903a6
--- /dev/null
@@ -0,0 +1,7 @@
+<script setup lang="ts"></script>
+
+<template>
+  <h1>Hello World</h1>
+</template>
+
+<style scoped></style>
diff --git a/template/bare/vitest/src/__tests__/App.spec.js b/template/bare/vitest/src/__tests__/App.spec.js
new file mode 100644 (file)
index 0000000..607fbfb
--- /dev/null
@@ -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 (file)
index 0000000..1a9fd70
--- /dev/null
@@ -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<string, boolean>) {
+  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: [],'),
+    )
+  }
+}