diff --git a/packages/create-vuestic/src/composables/useFiles.ts b/packages/create-vuestic/src/composables/useFiles.ts new file mode 100644 index 0000000000..59f27acf00 --- /dev/null +++ b/packages/create-vuestic/src/composables/useFiles.ts @@ -0,0 +1,58 @@ +import { resolvePath } from "../utils/resolve-path" +import { dirname, resolve } from "path" +import { useUserAnswers } from "./useUserAnswers" +import { mkdir, readFile, writeFile } from "fs/promises" +import { existsSync } from "fs" + +export const useFiles = async () => { + const { projectName } = await useUserAnswers() + + const resolveCorrectExt = (path: string, ext: string[]) => { + for (const e of ext) { + const resolvedPath = resolve(process.cwd(), projectName, `${path}.${e}`) + + if (resolvedPath) { + return resolvedPath + } + } + + return null + } + + const addFile = async (path: string, content: string) => { + const resolvedPath = resolve(process.cwd(), projectName, path) + + const dir = dirname(resolvedPath) + + if (!existsSync(dir)) { + await mkdir(dir, { recursive: true }) + } + + return writeFile(resolvedPath, content, { encoding: 'utf-8', flag: 'wx' }) + } + + const replaceFileContent = async (path: string, content: (existingContent: string) => string) => { + const resolvedPath = resolvePath(process.cwd(), projectName, path) + + if (!resolvedPath) { + throw new Error(`Unexpected error: Could not find ${path}`) + } + + const existingContent = (await readFile(resolvedPath)).toString() + + return writeFile(resolvedPath, content(existingContent)) + } + + const addToTopOfFile = async (path: string, content: string) => { + return replaceFileContent(path, (existingContent) => { + return `${content}\n${existingContent}` + }) + } + + return { + addToTopOfFile, + addFile, + replaceFileContent, + resolveCorrectExt, + } +} diff --git a/packages/create-vuestic/src/composables/usePackageJson.ts b/packages/create-vuestic/src/composables/usePackageJson.ts index 6568d56a9d..5bb12d23c0 100644 --- a/packages/create-vuestic/src/composables/usePackageJson.ts +++ b/packages/create-vuestic/src/composables/usePackageJson.ts @@ -4,8 +4,8 @@ import { useUserAnswers } from "./useUserAnswers" type Package = { name: string, - dependencies: Record, - devDependencies: Record, + dependencies?: Record, + devDependencies?: Record, } const sortObjectKeys = (obj: Record) => { @@ -29,6 +29,7 @@ export const usePackageJson = async () => { await writeFile(answers.projectName + '/package.json', JSON.stringify(pkg, null, 2)) } + /** @deprecated Use addDependencies to add multiple deps at the same time */ const addDependency = async (name: string, version: string) => { const pkg = await readPackage() if (!pkg.dependencies) { pkg.dependencies = {} } @@ -38,6 +39,7 @@ export const usePackageJson = async () => { await writePackage(pkg) } + /** @deprecated Use addDependencies to add multiple deps at the same time */ const addDevDependency = async (name: string, version: string) => { const pkg = await readPackage() if (!pkg.devDependencies) { pkg.devDependencies = {} } @@ -47,7 +49,44 @@ export const usePackageJson = async () => { await writePackage(pkg) } + const addDependencies = async (deps: { + dependencies?: Record + devDependencies?: Record + }) => { + const pkg = await readPackage() + if (!pkg.dependencies) { pkg.dependencies = {} } + if (!pkg.devDependencies) { pkg.devDependencies = {} } + + if (deps.dependencies) { + pkg.dependencies = { + ...pkg.dependencies, + ...deps.dependencies, + } + } + + if (deps.devDependencies) { + pkg.devDependencies = { + ...pkg.devDependencies, + ...deps.devDependencies, + } + } + + pkg.dependencies = sortObjectKeys(pkg.dependencies) + pkg.devDependencies = sortObjectKeys(pkg.devDependencies) + + if (Object.keys(pkg.dependencies).length === 0) { + delete pkg.dependencies + } + + if (Object.keys(pkg.devDependencies).length === 0) { + delete pkg.devDependencies + } + + await writePackage(pkg) + } + return { + addDependencies, addDependency, addDevDependency, } diff --git a/packages/create-vuestic/src/composables/useVuesticConfig/configDefault.ts b/packages/create-vuestic/src/composables/useVuesticConfig/configDefault.ts index f469d8dfc1..7d5aaef409 100644 --- a/packages/create-vuestic/src/composables/useVuesticConfig/configDefault.ts +++ b/packages/create-vuestic/src/composables/useVuesticConfig/configDefault.ts @@ -4,8 +4,17 @@ export default { import: [ "import { createVuestic } from 'vuestic-ui'", ], - css: [ - 'vuestic-ui/css', - ], + css: (answers) => { + if (answers.vuesticFeatures.includes('tailwind')) { + return [ + 'vuestic-ui/styles/essential.css', + 'vuestic-ui/styles/typography.css' + ] + } + + return [ + 'vuestic-ui/css', + ] + }, plugin: 'createVuestic()' }as Config diff --git a/packages/create-vuestic/src/composables/useVuesticConfig/configTreeShaking.ts b/packages/create-vuestic/src/composables/useVuesticConfig/configTreeShaking.ts index 69a84adf99..d086afc84b 100644 --- a/packages/create-vuestic/src/composables/useVuesticConfig/configTreeShaking.ts +++ b/packages/create-vuestic/src/composables/useVuesticConfig/configTreeShaking.ts @@ -9,11 +9,11 @@ export default { 'vuestic-ui/styles/essential.css', ] - if (answers.treeShaking.includes('grid')) { + if (answers.treeShaking.includes('grid') && !answers.vuesticFeatures.includes('tailwind')) { strings.push('vuestic-ui/styles/grid.css') } - if (answers.treeShaking.includes('normalize')) { + if (answers.treeShaking.includes('normalize') && !answers.vuesticFeatures.includes('tailwind')) { strings.push('vuestic-ui/styles/reset.css') } diff --git a/packages/create-vuestic/src/generators/create-nuxt.ts b/packages/create-vuestic/src/generators/create-nuxt.ts index b51c491996..d25b70d25e 100644 --- a/packages/create-vuestic/src/generators/create-nuxt.ts +++ b/packages/create-vuestic/src/generators/create-nuxt.ts @@ -1,6 +1,11 @@ +import { doesSatisfyNodeVersion } from '../utils/node-version'; import { execp } from './../utils/exacp'; export const createNuxt3 = (projectName: string) => { + if (!doesSatisfyNodeVersion('v16.10.0')) { + throw new Error('Nuxt 3 requires Node.js v16.10.0 or higher. Please upgrade your Node.js version.') + } + const command =`npx nuxi init ${projectName}` return execp(command) diff --git a/packages/create-vuestic/src/generators/create-vue.ts b/packages/create-vuestic/src/generators/create-vue.ts index 52bb9551ff..559ae2be05 100644 --- a/packages/create-vuestic/src/generators/create-vue.ts +++ b/packages/create-vuestic/src/generators/create-vue.ts @@ -1,8 +1,13 @@ +import { doesSatisfyNodeVersion } from '../utils/node-version'; import { execp } from './../utils/exacp'; type CreateVueFeature = 'ts' | 'jsx' | 'router' | 'pinia' | 'tests' | 'vitest' | 'cypress' | 'playwright' | 'eslint' export const createVue = async (projectName: string, features: CreateVueFeature[]) => { + if (!doesSatisfyNodeVersion('v16.0.0')) { + throw new Error('Node.js v16.10.0 or higher required. Please upgrade your Node.js version.') + } + const argsString = features .map((feature) => `--${feature}`) .join(' ') diff --git a/packages/create-vuestic/src/index.ts b/packages/create-vuestic/src/index.ts index c7121029f9..2560762a9d 100644 --- a/packages/create-vuestic/src/index.ts +++ b/packages/create-vuestic/src/index.ts @@ -9,6 +9,7 @@ import { addVuestic } from './steps/2.addVuestic' import { addAgGrid } from './steps/2.1.addAgGrid' import { initGit } from "./steps/3.initGit" import { installDeps } from "./steps/4.installDeps" +import { addTailwind } from "./steps/2.2.addTailwind" export const main = async () => { console.log(primaryColor(logo)) @@ -24,6 +25,7 @@ export const main = async () => { if (['create-vue', 'nuxt'].includes(answers.projectType)) { await addVuestic(answers) await addAgGrid(answers) + await addTailwind(answers) } await initGit() await installDeps() diff --git a/packages/create-vuestic/src/prompts.ts b/packages/create-vuestic/src/prompts.ts index 73ab19606a..4615428a06 100644 --- a/packages/create-vuestic/src/prompts.ts +++ b/packages/create-vuestic/src/prompts.ts @@ -74,19 +74,29 @@ const questions = definePrompts([ message: 'Vuestic features (can be manually added later)', initial: 0, choices: [ + { title: 'Tailwind', value: 'tailwind' as const, description: 'Install Tailwind CSS for styling. We recommend using tailwind instead of grid.css and normalize.css' }, { title: 'AG Grid', value: 'agGrid' as const, description: 'Install Vuestic AG Grid theme for complex data tables' }, { title: 'Tree shaking', value: 'treeShaking' as const, description: 'You will need to register each component manually, but it will decrease bundle size' }, ], }, { - type: skipVuesticAdminFn((prev) => prev.includes('treeShaking') ? 'multiselect' : null), + type: skipVuesticAdminFn((prev) => prev.includes('treeShaking') && !prev.includes('tailwind') ? 'multiselect' : null), + name: 'treeShaking' as const, + message: 'Vuestic CSS modules', + initial: 0, + choices: [ + { title: 'typography.css', value: 'typography' as const}, + { title: 'grid.css', value: 'grid' as const, description: 'grid.css is deprecated, use tailwind instead' }, + { title: 'normalize.css', value: 'normalize' as const, description: 'normalize.css is deprecated, use tailwind instead' }, + ], + }, + { + type: skipVuesticAdminFn((prev) => prev.includes('treeShaking') && prev.includes('tailwind') ? 'multiselect' : null), name: 'treeShaking' as const, message: 'Vuestic CSS modules', initial: 0, choices: [ - { title: 'grid.css', value: 'grid' as const }, { title: 'typography.css', value: 'typography' as const}, - { title: 'normalize.css', value: 'normalize' as const }, ], }, { diff --git a/packages/create-vuestic/src/steps/2.1.addAgGrid.ts b/packages/create-vuestic/src/steps/2.1.addAgGrid.ts index 602ff52cde..1bb7cc3523 100644 --- a/packages/create-vuestic/src/steps/2.1.addAgGrid.ts +++ b/packages/create-vuestic/src/steps/2.1.addAgGrid.ts @@ -8,9 +8,13 @@ export const addAgGrid = async (options: UserAnswers) => { return } - const { addDependency } = await usePackageJson() - + const { addDependencies } = await usePackageJson() + await Promise.all([ - addDependency('@vuestic/ag-grid-theme', versions['@vuestic/ag-grid-theme']) + addDependencies({ + dependencies: { + '@vuestic/ag-grid-theme': versions['@vuestic/ag-grid-theme'] + } + }) ]) } diff --git a/packages/create-vuestic/src/steps/2.2.addTailwind.ts b/packages/create-vuestic/src/steps/2.2.addTailwind.ts new file mode 100644 index 0000000000..f122403361 --- /dev/null +++ b/packages/create-vuestic/src/steps/2.2.addTailwind.ts @@ -0,0 +1,128 @@ +import { versions } from './../versions'; +import { UserAnswers } from './../prompts'; +import { usePackageJson } from "../composables/usePackageJson" +import { useFiles } from '../composables/useFiles'; + +const installInVite = async () => { + const { addFile, resolveCorrectExt, replaceFileContent } = await useFiles() + + const css = resolveCorrectExt('src/assets/main', ['css', 'scss', 'sass']) + + return Promise.all([ + addFile('tailwind.config.js', ` +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{vue,js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + screens: { + xs: '0px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + }, + }, + plugins: [], +} +`.trim()), + addFile('postcss.config.js', ` +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} +`.trim()), + replaceFileContent(css!, (content) => + content.replace("@import './base.css';", ` +@import './base.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; +`.trim()) + ) + ]) +} + +const installInNuxt = async () => { + const { addFile, resolveCorrectExt, replaceFileContent } = await useFiles() + + const nuxtConfig = resolveCorrectExt('nuxt.config', ['ts', 'js']) + + return Promise.all([ + addFile('tailwind.config.js', ` +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./components/**/*.{js,vue,ts}", + "./layouts/**/*.vue", + "./pages/**/*.vue", + "./plugins/**/*.{js,ts}", + "./nuxt.config.{js,ts}", + "./app.vue", + ], + theme: { + extend: {}, + screens: { + xs: '0px', + sm: '576px', + md: '768px', + lg: '992px', + xl: '1200px', + }, + }, + plugins: [], +} +`.trim()), + addFile('./assets/css/main.css', ` +@tailwind base; +@tailwind components; +@tailwind utilities; +`.trim() + ), + replaceFileContent(nuxtConfig!, (content) => + content.replace('export default defineNuxtConfig({', ` +export default defineNuxtConfig({ + css: ['~/assets/css/main.css'], + postcss: { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, + }, +`.trim()) + ), + ]) +} + +export const addTailwind = async (options: UserAnswers) => { + const { vuesticFeatures, projectType } = options + if (vuesticFeatures && !vuesticFeatures.includes('tailwind')) { + return + } + + const { addDependencies } = await usePackageJson() + + if (projectType === 'nuxt') { + await installInNuxt() + } else if (projectType === 'create-vue') { + await installInVite() + } + + await Promise.all([ + addDependencies({ + dependencies: { + '@vuestic/tailwind': versions['@vuestic/tailwind'], + }, + devDependencies: { + tailwindcss: versions['tailwindcss'], + autoprefixer: versions['autoprefixer'], + postcss: versions['postcss'], + } + }), + ]) +} diff --git a/packages/create-vuestic/src/steps/2.addVuestic/insert-nuxt-module.ts b/packages/create-vuestic/src/steps/2.addVuestic/insert-nuxt-module.ts index 0d06a808aa..45522fd4d7 100644 --- a/packages/create-vuestic/src/steps/2.addVuestic/insert-nuxt-module.ts +++ b/packages/create-vuestic/src/steps/2.addVuestic/insert-nuxt-module.ts @@ -36,7 +36,7 @@ export const insertNuxtModule = (source: string, css: string[]) => { } source = source - .replace(/defineNuxtConfig\({([.|\n]*)}\)/m, (a, b) => a.replace(b, createNuxtConfig(vuesticConfig))) + .replace('defineNuxtConfig({', `defineNuxtConfig({\n ${createNuxtConfig(vuesticConfig)}`) return source } diff --git a/packages/create-vuestic/src/steps/2.addVuestic/nuxt-app.ts b/packages/create-vuestic/src/steps/2.addVuestic/nuxt-app.ts index ab265832c4..617734fcc4 100644 --- a/packages/create-vuestic/src/steps/2.addVuestic/nuxt-app.ts +++ b/packages/create-vuestic/src/steps/2.addVuestic/nuxt-app.ts @@ -8,7 +8,7 @@ import { insertNuxtModule } from './insert-nuxt-module'; export const addVuesticToNuxtApp = async () => { // Install vuestic-ui const { addDevDependency } = await usePackageJson() - addDevDependency('@vuestic/nuxt', versions['@vuestic/nuxt']) + await addDevDependency('@vuestic/nuxt', versions['@vuestic/nuxt']) const { projectName, treeShaking } = await useUserAnswers() diff --git a/packages/create-vuestic/src/steps/2.addVuestic/vue-app.ts b/packages/create-vuestic/src/steps/2.addVuestic/vue-app.ts index 2b2023bbda..2a3e51d510 100644 --- a/packages/create-vuestic/src/steps/2.addVuestic/vue-app.ts +++ b/packages/create-vuestic/src/steps/2.addVuestic/vue-app.ts @@ -14,7 +14,7 @@ export const addVuesticToVue3App = async () => { // Install vuestic-ui const { addDependency } = await usePackageJson() - addDependency('vuestic-ui', versions['vuestic-ui']) + await addDependency('vuestic-ui', versions['vuestic-ui']) // Add plugin const mainPath = resolvePath(process.cwd(), projectName, 'src/main.js') || resolvePath(process.cwd(), projectName, 'src/main.ts') diff --git a/packages/create-vuestic/src/steps/4.installDeps.ts b/packages/create-vuestic/src/steps/4.installDeps.ts index 0324925a44..03b804d100 100644 --- a/packages/create-vuestic/src/steps/4.installDeps.ts +++ b/packages/create-vuestic/src/steps/4.installDeps.ts @@ -1,6 +1,7 @@ import { useUserAnswers } from '../composables/useUserAnswers'; import { getPackageManagerName } from '../utils/package-manager'; import { execp } from '../utils/exacp'; +import { withSpinner } from '../utils/with-spinner'; export const installDeps = async () => { const { runInstall, projectName } = await useUserAnswers() @@ -9,7 +10,10 @@ export const installDeps = async () => { const packageManager = getPackageManagerName() - return execp(`${packageManager} install`, { - cwd: `${process.cwd()}/${projectName}`, + return await withSpinner('Installing dependencies...', async () => { + await execp(`${packageManager} install`, { + cwd: `${process.cwd()}/${projectName}`, + }) }) + return } diff --git a/packages/create-vuestic/src/utils/node-version.ts b/packages/create-vuestic/src/utils/node-version.ts new file mode 100644 index 0000000000..569e150b92 --- /dev/null +++ b/packages/create-vuestic/src/utils/node-version.ts @@ -0,0 +1,34 @@ +const parseVersion = (version: string) => { + const versionNumber = version.slice(1).split('.').map((v) => Number(v)) + + return versionNumber +} + +const isVersionGreater = (versionString: string, requiredVersionString: string) => { + const nodeVersion = parseVersion(versionString) + const requiredVersion = parseVersion(requiredVersionString) + + if (nodeVersion[0] > requiredVersion[0]) { + return true + } else if (nodeVersion[0] < requiredVersion[0]) { + return false + } + + if (nodeVersion[1] > requiredVersion[1]) { + return true + } else if (nodeVersion[1] < requiredVersion[1]) { + return false + } + + if (nodeVersion[2] > requiredVersion[2]) { + return true + } else if (nodeVersion[2] < requiredVersion[2]) { + return false + } + + return false +} + +export const doesSatisfyNodeVersion: (version: string) => boolean = (version) => { + return isVersionGreater(process.version, version) +} diff --git a/packages/create-vuestic/src/versions.ts b/packages/create-vuestic/src/versions.ts index 25b55ab081..202fef0d84 100644 --- a/packages/create-vuestic/src/versions.ts +++ b/packages/create-vuestic/src/versions.ts @@ -1,5 +1,9 @@ export const versions = { - 'vuestic-ui': '^1.6.1', - '@vuestic/nuxt': '^1.0.11', + 'vuestic-ui': '^1.7.5', + '@vuestic/nuxt': '^1.0.14', '@vuestic/ag-grid-theme': '^1.1.4', + '@vuestic/tailwind': '^0.1.3', + 'tailwindcss': '^3.3.3', + 'postcss': '^8.4.28', + 'autoprefixer': '^10.4.15' }