├── cli ├── configs │ ├── html │ │ ├── prefetch.ts │ │ ├── preload.ts │ │ ├── index.ts │ │ └── render.ts │ ├── package │ │ └── index.ts │ ├── webpack │ │ ├── clien.dev.ts │ │ ├── client.prod.ts │ │ ├── server.ts │ │ ├── helper.ts │ │ ├── client.ts │ │ └── index.ts │ ├── typescript │ │ ├── compiler.shim.ts │ │ └── index.ts │ ├── parallel │ │ └── index.ts │ ├── vuniversal │ │ ├── index.ts │ │ ├── transformer.ts │ │ ├── default.ts │ │ └── interface.ts │ ├── eslint │ │ └── index.ts │ ├── error │ │ ├── index.ts │ │ └── transformer.ts │ ├── prerender │ │ └── index.ts │ ├── terser │ │ └── index.ts │ ├── wds │ │ └── index.ts │ ├── babel │ │ └── index.ts │ └── css │ │ └── index.ts ├── templates │ ├── index.html │ └── dev.html ├── scripts │ ├── init │ │ └── index.ts │ ├── build │ │ ├── index.ts │ │ ├── spa.ts │ │ └── ssr.ts │ └── dev │ │ ├── index.ts │ │ ├── spa.ts │ │ └── ssr.ts ├── texts.ts ├── services │ ├── notifier.ts │ ├── stdout.ts │ ├── banner.ts │ └── logger.ts ├── environment.ts ├── utils.ts └── paths.ts ├── vuniversal ├── helmet │ ├── transformer.ts │ ├── helmet.ts │ ├── constant.ts │ └── index.ts ├── index.ts ├── creater.ts ├── env.ts ├── utils.ts ├── helper.ts ├── middleware.ts └── render.ts ├── examples └── spa-typescript-jest │ ├── jest.config.js │ ├── src │ ├── app.vue │ └── main.ts │ ├── tsconfig.js │ └── vun.config.js ├── presses ├── logo.png ├── logo.psd └── notify.png ├── .eslintignore ├── webpack └── server.webpack.config.js ├── .editorconfig ├── tsconfig.vun.json ├── tsconfig.cli.json ├── scripts └── release.sh ├── .github └── workflows │ ├── release.yml │ └── publish.yml ├── CHANGELOG.md ├── .eslintrc.js ├── LICENSE ├── tsconfig.json ├── .gitignore ├── TODO.md ├── bin └── vun.js ├── README.md └── package.json /cli/configs/html/prefetch.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli/configs/html/preload.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vuniversal/helmet/transformer.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spa-typescript-jest/jest.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spa-typescript-jest/src/app.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spa-typescript-jest/src/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spa-typescript-jest/tsconfig.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/spa-typescript-jest/vun.config.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vuniversal/index.ts: -------------------------------------------------------------------------------- 1 | export * from './render' 2 | export * from './middleware' 3 | -------------------------------------------------------------------------------- /presses/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surmon-china/vuniversal/HEAD/presses/logo.png -------------------------------------------------------------------------------- /presses/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surmon-china/vuniversal/HEAD/presses/logo.psd -------------------------------------------------------------------------------- /presses/notify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surmon-china/vuniversal/HEAD/presses/notify.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # deeps 2 | node_modules 3 | 4 | # dist 5 | cli-dist 6 | vun-dist 7 | 8 | # dev 9 | vuniversal/helmet 10 | 11 | *.js 12 | -------------------------------------------------------------------------------- /webpack/server.webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO: with mocha test?/prod?/dev? 3 | module.exports = require('../cli-dist/configs/webpack').getWebpackConfig({ target: 'server' }) 4 | -------------------------------------------------------------------------------- /cli/configs/package/index.ts: -------------------------------------------------------------------------------- 1 | import { resolveAppRoot, resolveVunRoot } from '../../paths' 2 | 3 | export const appPackageJSON = require(resolveAppRoot('package.json')) 4 | export const vunPackageJSON = require(resolveVunRoot('package.json')) 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /cli/configs/webpack/clien.dev.ts: -------------------------------------------------------------------------------- 1 | import webpack, { Configuration } from 'webpack' 2 | 3 | export function modifyClientDevConfig(webpackConfig: Configuration): void { 4 | // Add client-only development plugins 5 | webpackConfig.plugins?.push( 6 | new webpack.HotModuleReplacementPlugin() 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /cli/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Welcome to vuniversal! ⚡ 5 | {{ HEAD }} 6 | 7 | 8 |
{{ APP }}
9 | {{ FOOTER }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.vun.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./vun-dist", 5 | "declaration": true, 6 | "declarationDir": "./vun-dist", 7 | "tsBuildInfoFile": "./.cache/vun.tsbuildinfo" 8 | }, 9 | "include": [ 10 | "vuniversal" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /vuniversal/creater.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CreateAppFunction } from 'vue' 3 | import { Router } from 'vue-router' 4 | // import { Helme } from './helmet' 5 | 6 | export type AppCreater = () => { 7 | app: ReturnType> 8 | router: Router 9 | meta: any 10 | [key: string]: any 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "./cli-dist", 6 | "tsBuildInfoFile": "./.cache/cli.tsbuildinfo", 7 | "plugins": [{ "transform": "typescript-transform-paths" }] 8 | }, 9 | "include": [ 10 | "cli" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # https://github.com/mikeal/merge-release/blob/v4.0.7/merge-release-run.js 6 | PKG_VERSION=$(jq -r '.version' package.json) 7 | 8 | git fetch origin v"$PKG_VERSION" || { 9 | type standard-version || npm i -g standard-version 10 | standard-version --skip.changelog -a --release-as "$PKG_VERSION" 11 | git push --follow-tags origin master 12 | } 13 | -------------------------------------------------------------------------------- /cli/configs/typescript/compiler.shim.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-typescript/index.js#L82 2 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-typescript/vue-compiler-sfc-shim.js 3 | 4 | // @ts-ignore 5 | const compilerSFC = require('@vue/compiler-sfc') 6 | 7 | module.exports = { 8 | parseComponent(content: any, options: any) { 9 | return compilerSFC.parse(content, options) 10 | } 11 | } -------------------------------------------------------------------------------- /cli/scripts/init/index.ts: -------------------------------------------------------------------------------- 1 | import readline from 'readline' 2 | // import program from 'program' 3 | 4 | const rl = readline.createInterface({ 5 | input: process.stdin, 6 | output: process.stdout 7 | }) 8 | 9 | // 检测到你已经存在 vun.config.js 文件了,是否覆盖 10 | // TODO: 选择语言 11 | // TODO: 检测到当前是 vue 项目,是否从 vue.config.js 自动推断 12 | // TODO: 检测到当前是 nuxt 项目,是否从 nuxt.config.js 自动推断 13 | 14 | // TODO: xxx 15 | rl.question('检测到你使用了 Typescript 创建项目? ?', (answer) => { 16 | console.log(`感谢您的宝贵意见:${answer}`) 17 | // fs.writeFile('/xxx/vun.config.js', `module.exports = { ${config} }`) 18 | rl.close() 19 | }) 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/actions/create-release 2 | name: Create Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | build: 11 | name: Create Release 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@master 17 | 18 | - name: Create Release 19 | id: create_release 20 | uses: actions/create-release@latest 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | tag_name: ${{ github.ref }} 25 | release_name: ${{ github.ref }} 26 | draft: false 27 | prerelease: false 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ### 0.0.13 (2020-05-14) 5 | 6 | **Feature** 7 | - Analyze supported 8 | 9 | ### 0.0.12 (2020-05-14) 10 | 11 | **Feature** 12 | - Server chunk max length === 1 13 | 14 | **Fix** 15 | - Hash of chunk and assets 16 | 17 | ### 0.0.10 (2020-05-08) 18 | 19 | **NIP** 20 | - Logo 21 | 22 | **Upgrade** 23 | - Webpack 5 24 | 25 | ### 0.0.5 (2020-05-08) 26 | 27 | **Fix** 28 | - Eslint 29 | - lint 30 | 31 | ### 0.0.3 (2020-05-08) 32 | 33 | **Fix** 34 | - Add `clean-webpack-plugin` 35 | - Add `break` for `scripts` 36 | 37 | ### 0.0.2 (2020-05-08) 38 | 39 | **Feature** 40 | - Support SPA mode 41 | -------------------------------------------------------------------------------- /cli/configs/parallel/index.ts: -------------------------------------------------------------------------------- 1 | import { RuleSetRule } from 'webpack' 2 | import { requireResolve } from '@cli/utils' 3 | import { VunLibConfig } from '../vuniversal' 4 | 5 | export function enableParallel(vunConfig: VunLibConfig) { 6 | return !!vunConfig.build.parallel 7 | } 8 | export function isNumberParallel(vunConfig: VunLibConfig) { 9 | return typeof vunConfig.build.parallel === 'number' 10 | } 11 | 12 | export function getThreadLoader(vunConfig: VunLibConfig): RuleSetRule { 13 | return !enableParallel(vunConfig) ? {} : { 14 | loader: requireResolve('thread-loader'), 15 | options: isNumberParallel(vunConfig) 16 | ? { workers: vunConfig.build.parallel } 17 | : {} 18 | } 19 | } -------------------------------------------------------------------------------- /cli/templates/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vuniversal dev server 5 | 25 | 26 | 27 |
28 |

Vuniversal dev server

29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /vuniversal/env.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Environment 3 | * @module environment 4 | * @author Surmon 5 | */ 6 | 7 | export enum VueEnv { 8 | Client = 'client', 9 | Server = 'server' 10 | } 11 | 12 | export enum NodeEnv { 13 | Development = 'development', 14 | Production = 'production', 15 | Test = 'test' 16 | } 17 | 18 | export const NODE_ENV = process.env.NODE_ENV as NodeEnv 19 | export const isDev = process.env.NODE_ENV === NodeEnv.Development 20 | export const isProd = process.env.NODE_ENV === NodeEnv.Production 21 | export const isTest = process.env.NODE_ENV === NodeEnv.Test 22 | 23 | export const VUE_ENV = process.env.VUE_ENV as VueEnv 24 | export const isClient = process.env.VUE_ENV === VueEnv.Client 25 | export const isServer = process.env.VUE_ENV === VueEnv.Server 26 | -------------------------------------------------------------------------------- /cli/scripts/build/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeEnv, UniversalMode } from '@cli/environment' 2 | 3 | // @ts-ignore 4 | process.noDeprecation = true // turns off that loadQuery clutter. 5 | process.env.NODE_ENV = NodeEnv.Production 6 | 7 | import fs from 'fs-extra' 8 | import { command } from '@cli/utils' 9 | import { vunConfig } from '@cli/configs/vuniversal' 10 | import { headBanner } from '@cli/services/banner' 11 | import { startBuildSSR } from './ssr' 12 | import { startBuildSPA } from './spa' 13 | 14 | fs.removeSync(vunConfig.dir.build) 15 | 16 | // Banner 17 | headBanner({ 18 | command, 19 | memory: false, 20 | runningIn: NodeEnv.Production, 21 | univservalMode: vunConfig.universal 22 | ? UniversalMode.UNIVERSAL 23 | : UniversalMode.SPA 24 | }) 25 | 26 | vunConfig.universal 27 | ? startBuildSSR() 28 | : startBuildSPA() 29 | -------------------------------------------------------------------------------- /cli/configs/webpack/client.prod.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack' 2 | import CopyWebpackPlugin from 'copy-webpack-plugin' 3 | import { getClientBuildDir } from '@cli/paths' 4 | import { getTerserConfig } from '../terser' 5 | import { vunConfig } from '../vuniversal' 6 | 7 | const TerserPlugin = require('terser-webpack-plugin') 8 | 9 | export function modifyClientProdConfig(webpackConfig: Configuration): void { 10 | const clientBuildPath = getClientBuildDir(vunConfig) 11 | 12 | webpackConfig.plugins?.push( 13 | new CopyWebpackPlugin([ 14 | { from: vunConfig.dir.public, to: clientBuildPath } 15 | ]) 16 | ) 17 | 18 | webpackConfig.optimization = { 19 | ...webpackConfig.optimization, 20 | minimize: true, 21 | minimizer: [ 22 | new TerserPlugin(getTerserConfig(vunConfig)) 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cli/texts.ts: -------------------------------------------------------------------------------- 1 | export const VUN_COMMND = 'vun' 2 | export const VUNIVERSAL_NAME = 'Vuniversal' 3 | export const DEV_SERVER_RUN_FAILED = 'Dev server runs failed!' 4 | export const DEV_SERVER_RUN_SUCCESSFULLY = 'Dev server runs successfully!' 5 | 6 | export const FAILED_TO_COMPILE = 'Failed to compile!' 7 | export const COMPILED_SUCCESSFULLY = 'Compiled successfully!' 8 | export const FAILED_TO_VALIDATION = 'Failed to validation!' 9 | 10 | export function projectIsRunningAt(url: string) { 11 | return `Project is running at: ${url}` 12 | } 13 | 14 | export function compiledSuccessfully(name = '') { 15 | return `Compiled ${name} successfully`.replace(/\s+/g, ' ') 16 | } 17 | 18 | export function compiling(name = '') { 19 | const text = `${name} compiling...`.replace(/\s+/g, ' ') 20 | return text.charAt(0).toUpperCase() + text.slice(1) 21 | } 22 | -------------------------------------------------------------------------------- /cli/configs/vuniversal/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import { VunLibConfig } from './interface' 3 | import { defaultConfig } from './default' 4 | import { transformConfig, normalizeConfig } from './transformer' 5 | import { APP_VUN_CONFIG_PATH } from '@cli/paths' 6 | import logger from '@cli/services/logger' 7 | 8 | export * from './default' 9 | export * from './interface' 10 | export * from './transformer' 11 | 12 | export function getVunConfig(): VunLibConfig { 13 | // Check for vuniversal.config.js file 14 | if (!fs.existsSync(APP_VUN_CONFIG_PATH)) { 15 | return transformConfig(defaultConfig) 16 | } 17 | 18 | try { 19 | return normalizeConfig(require(APP_VUN_CONFIG_PATH)) 20 | } catch (error) { 21 | logger.error('Invalid vuniversal config file!', error) 22 | process.exit(1) 23 | } 24 | } 25 | 26 | export const vunConfig: VunLibConfig = getVunConfig() 27 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: [ 8 | '@typescript-eslint/eslint-plugin' 9 | ], 10 | extends: [ 11 | 'plugin:@typescript-eslint/eslint-recommended', 12 | 'plugin:@typescript-eslint/recommended' 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/member-delimiter-style': 'off', 21 | '@typescript-eslint/ban-ts-ignore': 'off', 22 | '@typescript-eslint/interface-name-prefix': 'off', 23 | '@typescript-eslint/explicit-function-return-type': 'off', 24 | '@typescript-eslint/no-explicit-any': 'off', 25 | '@typescript-eslint/camelcase': 'off', 26 | '@typescript-eslint/no-use-before-define': 'off', 27 | '@typescript-eslint/no-var-requires': 'off', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | # Fork from: https://github.com/actions/starter-workflows/blob/master/ci/npm-publish.yml 4 | 5 | name: Publish 6 | 7 | on: 8 | release: 9 | types: [created] 10 | push: 11 | tags: 12 | - 'v*' 13 | 14 | jobs: 15 | publish-npm: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: '12' 22 | registry-url: https://registry.npmjs.org/ 23 | - run: npm i yarn -g 24 | - run: yarn 25 | - run: yarn rebirth 26 | - run: npm publish 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /cli/configs/html/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { Configuration } from 'webpack' 3 | import HtmlWebpackPlugin from 'html-webpack-plugin' 4 | import { SPA_TEMPLATE_FILE, getClientBuildDir } from '@cli/paths' 5 | import { vunConfig } from '../vuniversal' 6 | import { spaTemplateRender } from './render' 7 | 8 | // HTML plugin config 9 | export function modifyHTMLConfig(webpackConfig: Configuration) { 10 | webpackConfig.plugins?.push(new HtmlWebpackPlugin({ 11 | filename: path.resolve(getClientBuildDir(vunConfig), SPA_TEMPLATE_FILE), 12 | templateContent: spaTemplateRender, 13 | inject: false, 14 | minify: { 15 | removeComments: true, 16 | collapseWhitespace: true, 17 | removeAttributeQuotes: true, 18 | collapseBooleanAttributes: true, 19 | removeScriptTypeAttributes: true 20 | // more options: 21 | // https://github.com/kangax/html-minifier#options-quick-reference 22 | } 23 | })) 24 | } 25 | -------------------------------------------------------------------------------- /cli/configs/eslint/index.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack' 2 | import { requireResolve } from '@cli/utils' 3 | import { vunConfig } from '../vuniversal' 4 | 5 | export function modifyEslintConfig(webpackConfig: Configuration) { 6 | const { lintOnSave } = vunConfig 7 | const hasTypeScript = !!vunConfig.typescript 8 | const baseExtensions = ['.js', '.jsx', '.vue'] 9 | const extensions = hasTypeScript 10 | ? ['.ts', '.tsx', ...baseExtensions] 11 | : baseExtensions 12 | 13 | webpackConfig.module?.rules?.push({ 14 | enforce: 'pre', 15 | test: /\.(vue|(j|t)sx?)$/, 16 | include: [vunConfig.dir.source], 17 | exclude: [/node_modules/], 18 | loader: requireResolve('eslint-loader'), 19 | options: { 20 | extensions, 21 | cache: true, 22 | // only emit errors in production mode. 23 | emitError: lintOnSave === 'error', 24 | emitWarning: lintOnSave === true || lintOnSave === 'warning' 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /cli/configs/error/index.ts: -------------------------------------------------------------------------------- 1 | import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' 2 | import notifier from '@cli/services/notifier' 3 | import { FAILED_TO_COMPILE } from '@cli/texts' 4 | import { transformer, formatter } from './transformer' 5 | 6 | export const createFriendlyErrorsWebpackPlugin = (options?: FriendlyErrorsWebpackPlugin.Options) => { 7 | return new FriendlyErrorsWebpackPlugin({ 8 | clearConsole: false, 9 | additionalTransformers: [transformer], 10 | additionalFormatters: [formatter], 11 | onErrors: (severity, errors) => { 12 | if (severity === 'error') { 13 | const [error] = errors 14 | const { name, file, webpackError } = error as any 15 | const { rawMessage } = webpackError 16 | notifier.notify( 17 | name 18 | ? `SEVERITY: ${name}` 19 | : rawMessage || `FILE: ${file}`, 20 | FAILED_TO_COMPILE 21 | ) 22 | } 23 | }, 24 | ...options 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /cli/services/notifier.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/mikaelbr/node-notifier 2 | import path from 'path' 3 | import notifier from 'node-notifier' 4 | import { VUN_NAME } from '../paths' 5 | 6 | export function notify(message: string, title?: string, options?: any) { 7 | notifier.notify({ 8 | title: title || `${VUN_NAME} message!`, 9 | message, 10 | timeout: 2600, 11 | icon: path.join(__dirname, '..', '..', 'presses', 'notify.png'), 12 | sound: true, 13 | ...options 14 | // wait: true // Wait with callback, until user action is taken against notification, does not apply to Windows Toasters as they always wait or notify-send as it does not support the wait option 15 | }) 16 | } 17 | 18 | export function successfully(message: string) { 19 | notify(`${VUN_NAME} successfully!`, message) 20 | } 21 | 22 | export function failed(message: string) { 23 | notify(`${VUN_NAME} failed!`, message) 24 | } 25 | 26 | export default { 27 | notify, 28 | successfully, 29 | failed 30 | } 31 | -------------------------------------------------------------------------------- /cli/environment.ts: -------------------------------------------------------------------------------- 1 | import { VunLibConfig } from './configs/vuniversal' 2 | 3 | export enum UniversalMode { 4 | SPA = 'spa', 5 | UNIVERSAL = 'universal' 6 | } 7 | 8 | export enum VueEnv { 9 | Client = 'client', 10 | Server = 'server' 11 | } 12 | 13 | export enum NodeEnv { 14 | Development = 'development', 15 | Production = 'production', 16 | Test = 'test' 17 | } 18 | 19 | export function isUniversal(vunConfig: VunLibConfig): boolean { 20 | return vunConfig.universal !== false 21 | } 22 | 23 | export function isDev(environment: NodeEnv): boolean { 24 | return environment === NodeEnv.Development 25 | } 26 | 27 | export function isProd(environment: NodeEnv): boolean { 28 | return environment === NodeEnv.Production 29 | } 30 | 31 | export function isTest(environment: NodeEnv): boolean { 32 | return environment === NodeEnv.Test 33 | } 34 | 35 | export function isClientTarget(target: VueEnv): boolean { 36 | return target === VueEnv.Client 37 | } 38 | 39 | export function isServerTarget(target: VueEnv): boolean { 40 | return target === VueEnv.Server 41 | } 42 | -------------------------------------------------------------------------------- /cli/scripts/dev/index.ts: -------------------------------------------------------------------------------- 1 | import { VUN_DEV_CACHE_PATH } from '@cli/paths' 2 | import { NodeEnv, UniversalMode } from '@cli/environment' 3 | 4 | // @ts-ignore 5 | process.noDeprecation = true // turns off that loadQuery clutter. 6 | process.env.NODE_ENV = NodeEnv.Development 7 | 8 | import fs from 'fs-extra' 9 | import { command } from '@cli/utils' 10 | import { vunConfig } from '@cli/configs/vuniversal' 11 | import { headBanner } from '@cli/services/banner' 12 | import { getDevServerUrl } from '@cli/configs/webpack/helper' 13 | import { startSSRServer } from './ssr' 14 | import { startSPAServer } from './spa' 15 | 16 | // Delete assets.json and chunks.json to always have a manifest up to date 17 | fs.removeSync(VUN_DEV_CACHE_PATH) 18 | 19 | // Banner 20 | headBanner({ 21 | command, 22 | memory: true, 23 | runningIn: NodeEnv.Development, 24 | univservalMode: vunConfig.universal 25 | ? UniversalMode.UNIVERSAL 26 | : UniversalMode.SPA, 27 | listeningOn: getDevServerUrl(vunConfig.dev.host, vunConfig.dev.port) 28 | }) 29 | 30 | // Run dev server 31 | vunConfig.universal 32 | ? startSSRServer() 33 | : startSPAServer() 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Surmon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "allowSyntheticDefaultImports": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "downlevelIteration": true, 8 | "sourceMap": false, 9 | "noEmit": false, 10 | "target": "es2017", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "allowJs": true, 14 | "noUnusedLocals": true, 15 | "strictNullChecks": true, 16 | "skipLibCheck": true, 17 | "noImplicitAny": true, 18 | "noImplicitThis": true, 19 | "noImplicitReturns": true, 20 | "strict": true, 21 | "isolatedModules": false, 22 | "resolveJsonModule": true, 23 | "esModuleInterop": true, 24 | "removeComments": false, 25 | "baseUrl": "./", 26 | "lib": [ 27 | "esnext", 28 | "DOM" 29 | ], 30 | "paths": { 31 | "@cli/*": ["cli/*"] 32 | } 33 | }, 34 | "include": [ 35 | "cli", 36 | "vuniversal" 37 | ], 38 | "exclude": [ 39 | "node_modules", 40 | "cli-dist", 41 | "vun-dist" 42 | ], 43 | "typeRoots": [ 44 | "node_modules/@types" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /cli/services/stdout.ts: -------------------------------------------------------------------------------- 1 | let lastLineIsEmptyLine = false 2 | const handleOutput = (...args: any[]) => { 3 | const chunk = args[0] 4 | if (typeof chunk === 'string') { 5 | if (chunk.trim() === '') { 6 | // console.log() === br 7 | lastLineIsEmptyLine = true 8 | } else if (chunk.trim().endsWith('\n\n')) { 9 | // console.log('\n\n') === br 10 | lastLineIsEmptyLine = true 11 | } else { 12 | // not br 13 | lastLineIsEmptyLine = false 14 | } 15 | } 16 | } 17 | 18 | export const isBrLastLine = () => lastLineIsEmptyLine 19 | export const init = () => { 20 | // stdout 21 | const originalStdoutWrite = process.stdout.write.bind(process.stdout) 22 | process.stdout.write = (...args: any[]) => { 23 | handleOutput(...args) 24 | // @ts-ignore 25 | return originalStdoutWrite(...args) 26 | } 27 | 28 | // stderr 29 | // keep default: webpackbar use stderr ... 30 | // const originalStderrWrite = process.stderr.write.bind(process.stderr) 31 | // process.stderr.write = (...args: any[]) => { 32 | // handleOutput(...args) 33 | // // @ts-ignore 34 | // return originalStderrWrite(...args) 35 | // } 36 | } 37 | -------------------------------------------------------------------------------- /cli/configs/vuniversal/transformer.ts: -------------------------------------------------------------------------------- 1 | import lodash from 'lodash' 2 | import { resolveAppRoot } from '@cli/paths' 3 | import { VuniversalConfig, VunLibConfig } from './interface' 4 | import { defaultConfig } from './default' 5 | 6 | export function transformConfig(config: VunLibConfig): VunLibConfig { 7 | return { 8 | ...config, 9 | clientEntry: resolveAppRoot(config.clientEntry), 10 | serverEntry: resolveAppRoot(config.serverEntry), 11 | // TODO: 或需要单独获取以便于 watch 12 | template: config.template 13 | ? resolveAppRoot(config.template) 14 | : defaultConfig.template, 15 | dir: { 16 | build: resolveAppRoot(config.dir.build), 17 | public: resolveAppRoot(config.dir.public), 18 | source: resolveAppRoot(config.dir.source), 19 | root: resolveAppRoot(config.dir.root), 20 | modules: config.dir.modules.map(resolveAppRoot) 21 | } 22 | } 23 | } 24 | 25 | export function mergeDefaultConfig(config: VuniversalConfig): VunLibConfig { 26 | return lodash.merge({}, defaultConfig, config) 27 | } 28 | 29 | export function normalizeConfig(config: VuniversalConfig): VunLibConfig { 30 | return transformConfig(mergeDefaultConfig(config)) 31 | } -------------------------------------------------------------------------------- /vuniversal/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | // import { APP_VUN_CONFIG_PATH } from '../base/paths' 3 | // import { VunLibConfig, VuniversalConfig } from '../base/config/interface' 4 | // import { normalizeConfig } from '../base/config/transformer' 5 | 6 | /* 7 | export function getVunConfig(): VunLibConfig { 8 | let vunConfig = {} as any as VuniversalConfig 9 | try { 10 | // - dev: tsc -> ts -> js 11 | // - prod: app -> webpack -> commonjs -> webpack compiler -> commonjs 12 | // For webpack env 13 | // https://webpack.js.org/api/module-variables/#__non_webpack_require__-webpack-specific 14 | // @ts-ignore 15 | vunConfig = __non_webpack_require__(APP_VUN_CONFIG_PATH) 16 | } catch (_) { 17 | vunConfig = {} as any as VuniversalConfig 18 | } 19 | return normalizeConfig(vunConfig) 20 | } 21 | */ 22 | 23 | export interface Assets { 24 | js: string[] 25 | css: string[] 26 | } 27 | 28 | export function getAssets(): Assets { 29 | // TODO: 用 ENV 进行通信才能避免两者之间的交互 30 | // 开发模式使用环境变量获取,生产模式,使用约定文件夹,vueConfig.dir.build + vunConfig.build.assestDir + 'server' + MANIFEST 31 | return fs.readJSONSync(process.env.VUN_CLIENT_MANIFEST as string) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /cli/configs/html/render.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import templateParser from 'lodash/template' 3 | import { vunConfig } from '../vuniversal' 4 | 5 | const htmlTemplate = fs.readFileSync(vunConfig.template) 6 | const templateRender = templateParser(htmlTemplate.toString(), { 7 | interpolate: /{{([\s\S]+?)}}/g, 8 | evaluate: /{%([\s\S]+?)%}/g 9 | }) 10 | 11 | export function spaTemplateRender({ htmlWebpackPlugin }: any) { 12 | const { crossorigin } = vunConfig.build.html 13 | const CROSSORIGIN = crossorigin == false || crossorigin == null 14 | ? '' 15 | : crossorigin === '' 16 | ? `crossorigin` 17 | : `crossorigin=${crossorigin}` 18 | 19 | const HTML_ATTRS = '' 20 | const HEAD_ATTRS = '' 21 | const BODY_ATTRS = '' 22 | const APP = '' 23 | const HEAD = htmlWebpackPlugin.files.css 24 | .map((css: string) => ``) 25 | .join('\n') 26 | const FOOTER = htmlWebpackPlugin.files.js 27 | .map((js: string) => ``) 28 | .join('\n') 29 | 30 | return templateRender({ 31 | HTML_ATTRS, 32 | HEAD_ATTRS, 33 | BODY_ATTRS, 34 | HEAD, 35 | APP, 36 | FOOTER 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /cli/scripts/build/spa.ts: -------------------------------------------------------------------------------- 1 | import { NodeEnv, VueEnv } from '@cli/environment' 2 | import { getWebpackConfig } from '@cli/configs/webpack' 3 | import { COMPILED_SUCCESSFULLY, compiledSuccessfully } from '@cli/texts' 4 | import { compileConfig, runPromise } from '@cli/configs/webpack/helper' 5 | import { modifyHTMLConfig } from '@cli/configs/html' 6 | import { modifyPrerenderConfig } from '@cli/configs/prerender' 7 | import { prerenderFallback, TODO_fixPrerenderMkdirp } from '@cli/configs/prerender' 8 | import { vunConfig } from '@cli/configs/vuniversal' 9 | import notifier from '@cli/services/notifier' 10 | import logger from '@cli/services/logger' 11 | 12 | export function startBuildSPA() { 13 | const clientConfig = getWebpackConfig({ 14 | target: VueEnv.Client, 15 | environment: NodeEnv.Production 16 | }) 17 | 18 | // HTML 19 | modifyHTMLConfig(clientConfig) 20 | 21 | // SPA + Prerender 22 | if (vunConfig.prerender) { 23 | modifyPrerenderConfig(clientConfig) 24 | } 25 | 26 | // TODO: prefetch & preload plugins 27 | 28 | // Compile 29 | const compiler = TODO_fixPrerenderMkdirp(compileConfig(clientConfig)) 30 | 31 | // Run 32 | runPromise(compiler).then(() => { 33 | // Prerender fallback 34 | if (vunConfig.prerender) { 35 | prerenderFallback() 36 | } 37 | 38 | logger.done(compiledSuccessfully()) 39 | notifier.successfully(COMPILED_SUCCESSFULLY) 40 | process.exit() 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /vuniversal/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CreateAppFunction, createApp, createSSRApp } from 'vue' 3 | import { createWebHistory, createMemoryHistory, RouterHistory } from 'vue-router' 4 | import { isClient } from './env' 5 | 6 | export * from './env' 7 | export * from './creater' 8 | export * from './helmet' 9 | 10 | // Get function's arguments type 11 | type ArgumentTypes = F extends (...args: infer A) => any ? A : never 12 | 13 | // Only run in client env 14 | export function clientOnly(callback: () => any): void { 15 | if (isClient) { 16 | return callback() 17 | } 18 | } 19 | 20 | // Auto create app with env 21 | export const createUniversalApp: CreateAppFunction = (...args) => { 22 | // console.log('------createUniversalApp', 'isClient:', isClient) 23 | return isClient 24 | ? createApp(...args) 25 | : createSSRApp(...args) 26 | } 27 | 28 | // Auto create history with env 29 | // Use hashHistory: isClient ? createWebHashHistory() : createMemoryHistory() 30 | export function createUniversalHistory(...args: ArgumentTypes): RouterHistory 31 | export function createUniversalHistory(...args: ArgumentTypes): RouterHistory 32 | export function createUniversalHistory(...args: any): RouterHistory { 33 | // console.log('------createUniversalHistory', 'isClient:', isClient) 34 | return isClient 35 | ? createWebHistory(...args) 36 | : createMemoryHistory(...args) 37 | } 38 | -------------------------------------------------------------------------------- /cli/scripts/build/ssr.ts: -------------------------------------------------------------------------------- 1 | import { getWebpackConfig } from '@cli/configs/webpack' 2 | import { compileConfig, runPromise } from '@cli/configs/webpack/helper' 3 | import { vunConfig } from '@cli/configs/vuniversal' 4 | import { modifyHTMLConfig } from '@cli/configs/html' 5 | import { modifyPrerenderConfig, prerenderFallback, TODO_fixPrerenderMkdirp } from '@cli/configs/prerender' 6 | import { COMPILED_SUCCESSFULLY, compiledSuccessfully } from '@cli/texts' 7 | import { NodeEnv, VueEnv } from '@cli/environment' 8 | import notifier from '@cli/services/notifier' 9 | import logger from '@cli/services/logger' 10 | 11 | export function startBuildSSR() { 12 | const clientConfig = getWebpackConfig({ 13 | target: VueEnv.Client, 14 | environment: NodeEnv.Production 15 | }) 16 | const serverConfig = getWebpackConfig({ 17 | target: VueEnv.Server, 18 | environment: NodeEnv.Production 19 | }) 20 | 21 | // SSR + Prerender 22 | if (vunConfig.prerender) { 23 | modifyHTMLConfig(clientConfig) 24 | modifyPrerenderConfig(clientConfig) 25 | } 26 | 27 | Promise.all([ 28 | runPromise(TODO_fixPrerenderMkdirp(compileConfig(clientConfig))), 29 | runPromise(compileConfig(serverConfig)) 30 | ]).then(() => { 31 | // Prerender fallback 32 | if (vunConfig.prerender) { 33 | prerenderFallback() 34 | } 35 | 36 | logger.done(compiledSuccessfully()) 37 | notifier.successfully(COMPILED_SUCCESSFULLY) 38 | process.exit() 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /vuniversal/helmet/helmet.ts: -------------------------------------------------------------------------------- 1 | 2 | // import { HelmetComputedData } from './constant' 3 | 4 | // export type Helme = ReturnType 5 | // export const getHelmet = (state: any) => { 6 | // return { 7 | // // 设置新的值 8 | // set(): void { 9 | 10 | // }, 11 | // clear(): void { 12 | // // state 13 | // }, 14 | // // 重置所有至默认值 15 | // reset(): void { 16 | // Object.keys(state).forEach(key => { 17 | // // @ts-ignore 18 | // state[key] = initState[key] 19 | // }) 20 | // }, 21 | // // 刷新 DOM 22 | // refresh(): void {}, 23 | // // 暂停自动更新 24 | // pause() { 25 | 26 | // }, 27 | // // 重新开始自动更新 28 | // resume(): void { 29 | 30 | // }, 31 | // get state() { 32 | // return { ...state } 33 | // }, 34 | // get html() { 35 | // return { 36 | // title: `${state.title}`, 37 | // // meta: state.meta.map(meta => ``) 38 | // } 39 | // // htmlAttributes: {}, 40 | // // bodyAttributes: {}, 41 | // // meta: [], 42 | // // link: [], 43 | // // style: [], 44 | // // script: [], 45 | // // noscript: '' 46 | // }, 47 | // // 应该支持响应式的值 48 | // title(title: string | (() => string)) { 49 | // if (state) { 50 | // if (typeof title === 'string') { 51 | // state.title = title 52 | // } else { 53 | // state.title = computed(title) 54 | // } 55 | // } 56 | // } 57 | // } 58 | // } 59 | 60 | -------------------------------------------------------------------------------- /vuniversal/helmet/constant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ComputedRef, 3 | UnwrapRef, 4 | HTMLAttributes, 5 | HtmlHTMLAttributes, 6 | BaseHTMLAttributes, 7 | MetaHTMLAttributes, 8 | LinkHTMLAttributes, 9 | StyleHTMLAttributes, 10 | ScriptHTMLAttributes, 11 | } from 'vue' 12 | 13 | interface Base { 14 | [key: string]: any 15 | } 16 | 17 | interface HelmeHTMLAttributes extends HtmlHTMLAttributes, Base { 18 | xmlns?: string 19 | lang?: string 20 | } 21 | 22 | export type HelmetComputedData = UnwrapRef 23 | export type HelmetState = Partial 24 | export interface HelmetData { 25 | title: string | ComputedRef 26 | base: BaseHTMLAttributes & Base 27 | htmlAttributes: HelmeHTMLAttributes 28 | bodyAttributes: HTMLAttributes & Base 29 | meta: Array 30 | link: Array 31 | style: Array 32 | script: Array 33 | noscript: string 34 | } 35 | export const DEFAULT_STATE: HelmetData = { 36 | title: '', 37 | base: {}, 38 | htmlAttributes: {}, 39 | bodyAttributes: {}, 40 | meta: [], 41 | link: [], 42 | style: [], 43 | script: [], 44 | noscript: '' 45 | } 46 | 47 | export const HELMET_KEY = Symbol('helmet') 48 | 49 | export interface HelmetConfig { 50 | autoRefresh?: boolean 51 | attribute?: string 52 | ssrAttribute?: string 53 | } 54 | export const DEFAULT_CONFIG: HelmetConfig = { 55 | autoRefresh: true, 56 | attribute: 'data-vun', 57 | ssrAttribute: 'data-vun-server-rendered', 58 | } 59 | -------------------------------------------------------------------------------- /vuniversal/middleware.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path' 3 | import express, { Handler } from 'express' 4 | import { createProxyMiddleware } from 'http-proxy-middleware' 5 | import { AppCreater } from './creater' 6 | import { isDev } from './env' 7 | import { render } from './render' 8 | 9 | export interface VuniversalMiddlewareOptions { 10 | appCreater: AppCreater 11 | // static?: string 12 | // redner?: boolean 13 | } 14 | 15 | export function vuniversal(options: VuniversalMiddlewareOptions): Handler { 16 | return (request, response) => { 17 | const renderer = () => { 18 | render({ 19 | appCreater: options.appCreater, 20 | url: request.originalUrl, 21 | }) 22 | .then(html => response.status(200).send(html)) 23 | .catch(error => response.status(404).send(String(error))) 24 | } 25 | 26 | if (isDev) { 27 | const proxyer = () => { 28 | if ( 29 | request.path.startsWith(`/${process.env.VUN_ASSETS_DIR}/`) || 30 | request.path.includes('hot-update') 31 | ) { 32 | // TODO: 反代有问题,有时候会响应失败,测热更新就可以了 33 | createProxyMiddleware({ 34 | target: process.env.VUN_DEV_SERVER_URL, 35 | changeOrigin: true 36 | })(request, response, renderer) 37 | } else { 38 | renderer() 39 | } 40 | } 41 | express.static(process.env.VUN_PUBLIC_DIR as string)(request, response, proxyer) 42 | } else { 43 | express.static(path.join(__dirname, '..', 'client'))(request, response, renderer) 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /cli/configs/error/transformer.ts: -------------------------------------------------------------------------------- 1 | // Fork from: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/util/resolveLoaderError.js 2 | import chalk from 'chalk' 3 | 4 | const rules = [ 5 | { 6 | type: 'cant-resolve-loader', 7 | re: /Can't resolve '(.*loader)'/, 8 | msg: (_: any, match: any) => ( 9 | `Failed to resolve loader: ${chalk.yellow(match[1])}\n` + 10 | `You may need to install it.` 11 | ) 12 | } 13 | ] 14 | 15 | export const transformer = (error: any) => { 16 | if (error.webpackError) { 17 | const message = typeof error.webpackError === 'string' 18 | ? error.webpackError 19 | : error.webpackError.message || '' 20 | for (const { re, msg, type } of rules) { 21 | const match = message.match(re) 22 | if (match) { 23 | return Object.assign({}, error, { 24 | // type is necessary to avoid being printed as defualt error 25 | // by friendly-error-webpack-plugin 26 | type, 27 | shortMessage: msg(error, match) 28 | }) 29 | } 30 | } 31 | // no match, unknown webpack error without a message. 32 | // friendly-error-webpack-plugin fails to handle this. 33 | if (!error.message) { 34 | return Object.assign({}, error, { 35 | type: 'unknown-webpack-error', 36 | shortMessage: message 37 | }) 38 | } 39 | } 40 | return error 41 | } 42 | 43 | export const formatter = (errors: any[]): any => { 44 | errors = errors.filter(e => e.shortMessage) 45 | if (errors.length) { 46 | return errors.map(e => e.shortMessage) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cli/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import logger from './services/logger' 3 | import { vunConfig } from './configs/vuniversal' 4 | import { resolveAppRoot } from './paths' 5 | import { VUN_COMMND } from './texts' 6 | import { VueEnv } from './environment' 7 | 8 | export const isWindows = process.platform === 'win32' 9 | 10 | export const args = process.argv.slice(2) 11 | export const command = [VUN_COMMND, ...args].join(' ') 12 | 13 | export function findExistingFile(files: string[]): any { 14 | for (const file of files) { 15 | if (fs.existsSync(resolveAppRoot(file))) { 16 | return file 17 | } 18 | } 19 | } 20 | 21 | const removeError = (text: string) => text.replace('Error: ', '') 22 | export function requireResolve(file: string): string { 23 | try { 24 | return require.resolve(file) 25 | } catch (error) { 26 | error.stack = removeError(error.stack) 27 | error.message = removeError(error.message) 28 | logger.error(error) 29 | process.exit(1) 30 | } 31 | } 32 | 33 | export function resolveEntry(file: string, target: VueEnv): string { 34 | try { 35 | return require.resolve(file) 36 | } catch (error) { 37 | error.stack = removeError(error.stack) 38 | error.message = removeError(error.message) 39 | if (target === VueEnv.Client && !vunConfig.universal) { 40 | // TODO: text 41 | logger.warn('你是不是没有指定正确的客户端入口:clientEntry') 42 | } 43 | if (vunConfig.universal) { 44 | // TODO: text 45 | logger.warn('你是不是没有指定正确的两端入口:clientEntry,或者说你应该确定一下自己确定要开发 SSR 应用吗,如果要的话两端入口分别是 client 和 server') 46 | } 47 | logger.error(error) 48 | process.exit(1) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # development components 11 | development 12 | 13 | # local env files 14 | .env.local 15 | .env.*.local 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # TypeScript v1 declaration files 58 | typings/ 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Vuniversal 82 | .vun 83 | cli-dist 84 | vun-dist 85 | 86 | # Serverless directories 87 | .serverless 88 | 89 | # Service worker 90 | sw.* 91 | 92 | # Mac OSX 93 | .DS_Store 94 | 95 | # Vim swap files 96 | *.swp 97 | -------------------------------------------------------------------------------- /cli/configs/prerender/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import { Configuration, Compiler } from 'webpack' 4 | import { SPA_TEMPLATE_FILE, DEFAULT_FALLBACK_FILE, getClientBuildDir } from '@cli/paths' 5 | import { vunConfig } from '../vuniversal' 6 | 7 | export function TODO_fixPrerenderMkdirp(compiler: Compiler) { 8 | // WORKAROUND: https://github.com/chrisvfritz/prerender-spa-plugin/blob/v3.4.0/es6/index.js#L60 9 | // TODO: Remove when prerender-spa-plugin upgrade 10 | // @ts-ignore 11 | compiler.outputFileSystem.mkdirp = require('mkdirp') 12 | return compiler 13 | } 14 | 15 | // Prerender config 16 | export function modifyPrerenderConfig(webpackConfig: Configuration) { 17 | if (typeof vunConfig.prerender === 'object') { 18 | const { routes } = vunConfig.prerender 19 | if (Array.isArray(routes) && routes.length) { 20 | const PrerenderSPAPlugin = require('prerender-spa-plugin') 21 | webpackConfig.plugins?.push(new PrerenderSPAPlugin({ 22 | staticDir: getClientBuildDir(vunConfig), 23 | routes, 24 | server: { 25 | port: vunConfig.dev.port, 26 | proxy: vunConfig.dev.proxy 27 | }, 28 | ...vunConfig.prerender?.options 29 | })) 30 | } 31 | } 32 | } 33 | 34 | // Prerender fallback 35 | export function prerenderFallback() { 36 | if (typeof vunConfig.prerender === 'object') { 37 | const { fallback } = vunConfig.prerender 38 | if (fallback) { 39 | const clientBuildDir = getClientBuildDir(vunConfig) 40 | const fallbackFile = fallback === true 41 | ? DEFAULT_FALLBACK_FILE 42 | : fallback 43 | fs.copySync( 44 | path.resolve(clientBuildDir, SPA_TEMPLATE_FILE), 45 | path.resolve(clientBuildDir, fallbackFile) 46 | ) 47 | } 48 | } 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /cli/configs/terser/index.ts: -------------------------------------------------------------------------------- 1 | import { VunLibConfig } from '../vuniversal' 2 | 3 | export function getTerserConfig(vunConfig: VunLibConfig) { 4 | return { 5 | terserOptions: { 6 | compress: { 7 | // Disabled because of an issue with Uglify breaking seemingly valid code: 8 | // https://github.com/facebook/create-react-app/issues/2376 9 | // Pending further investigation: 10 | // https://github.com/mishoo/UglifyJS2/issues/2011 11 | comparisons: false, 12 | // turn off flags with small gains to speed up minification 13 | arrows: false, 14 | collapse_vars: false, // 0.3kb 15 | computed_props: false, 16 | hoist_funs: false, 17 | hoist_props: false, 18 | hoist_vars: false, 19 | inline: false, 20 | loops: false, 21 | negate_iife: false, 22 | properties: false, 23 | reduce_funcs: false, 24 | reduce_vars: false, 25 | switches: false, 26 | toplevel: false, 27 | typeofs: false, 28 | 29 | // a few flags with noticable gains/speed ratio 30 | // numbers based on out of the box vendor bundle 31 | booleans: true, // 0.7kb 32 | if_return: true, // 0.4kb 33 | sequences: true, // 0.7kb 34 | unused: true, // 2.3kb 35 | 36 | // required features to drop conditional branches 37 | conditionals: true, 38 | dead_code: true, 39 | evaluate: true 40 | }, 41 | mangle: { 42 | safari10: true, 43 | } 44 | }, 45 | // Enable file caching 46 | cache: true, 47 | extractComments: false, 48 | // Use multi-process parallel running to improve the build speed 49 | // Default number of concurrent runs: os.cpus().length - 1 50 | parallel: vunConfig.build.parallel, 51 | sourceMap: vunConfig.build.productionSourceMap, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /vuniversal/render.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path' 3 | import fs from 'fs-extra' 4 | import templateParser from 'lodash/template' 5 | import { renderToString } from '@vue/server-renderer' 6 | import { NodeEnv, VueEnv, isDev } from './env' 7 | import { AppCreater } from './creater' 8 | 9 | export interface RenderOptions { 10 | appCreater: AppCreater 11 | url: string 12 | } 13 | 14 | export interface TemplateRenderParameters { 15 | target: VueEnv 16 | environment: NodeEnv 17 | state?: any, 18 | meta?: any 19 | appHTML?: string 20 | assets: { 21 | js: string[] 22 | css: string[] 23 | } 24 | } 25 | 26 | export async function render(options: RenderOptions): Promise { 27 | const { app, router, state, ...rest } = options.appCreater() 28 | // TODO: 404 路由会如何反应呢,会不会是 404,这里应该区分 404 和 500 29 | await router.push(options.url) 30 | 31 | console.log('-----ssr render', typeof rest) 32 | 33 | const template = fs.readFileSync( 34 | isDev 35 | ? process.env.VUN_SSR_TEMPLATE as string 36 | : path.join(__dirname, 'template.html') 37 | ).toString() 38 | const assets = fs.readJSONSync( 39 | isDev 40 | ? process.env.VUN_CLIENT_MANIFEST as string 41 | : path.join(__dirname, 'client.manifest.json') 42 | ) 43 | 44 | const HTML_ATTRS = '' 45 | const HEAD_ATTRS = '' 46 | const BODY_ATTRS = '' 47 | 48 | const APP = await renderToString(app) 49 | 50 | const HEAD = [ 51 | `Welcome to vuniversal! ⚡`, 52 | ...assets.css.map((css: string) => ``) 53 | ].join('\n') 54 | 55 | const FOOTER = [ 56 | ``, 57 | ...assets.js.map((js: string) => ``) 58 | ].join('\n') 59 | 60 | return templateParser(template, { 61 | interpolate: /{{([\s\S]+?)}}/g, 62 | evaluate: /{%([\s\S]+?)%}/g 63 | })({ 64 | HTML_ATTRS, 65 | HEAD_ATTRS, 66 | BODY_ATTRS, 67 | HEAD, 68 | APP, 69 | FOOTER 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /cli/scripts/dev/spa.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin' 2 | import { getWebpackConfig } from '@cli/configs/webpack' 3 | import { createWebpackDevServer } from '@cli/configs/wds' 4 | import { compileConfig, compilerToPromise, getDevServerUrl } from '@cli/configs/webpack/helper' 5 | import { NodeEnv, VueEnv } from '@cli/environment' 6 | import { spaTemplateRender } from '@cli/configs/html/render' 7 | import { DEV_SERVER_RUN_FAILED, DEV_SERVER_RUN_SUCCESSFULLY, projectIsRunningAt } from '@cli/texts' 8 | import { vunConfig } from '@cli/configs/vuniversal' 9 | import notifier from '@cli/services/notifier' 10 | import logger from '@cli/services/logger' 11 | 12 | export function startSPAServer() { 13 | const clientConfig = getWebpackConfig({ 14 | target: VueEnv.Client, 15 | environment: NodeEnv.Development 16 | }) 17 | 18 | clientConfig.plugins?.push(new HtmlWebpackPlugin({ 19 | inject: false, 20 | minify: false, 21 | chunks: 'all', 22 | templateContent: spaTemplateRender 23 | })) 24 | 25 | const clientCompiler = compileConfig(clientConfig) 26 | const devServer = createWebpackDevServer( 27 | clientCompiler, 28 | { 29 | host: vunConfig.dev.host, 30 | port: vunConfig.dev.port, 31 | historyApiFallback: true, 32 | open: true, 33 | ...vunConfig.dev.devServer 34 | }, 35 | clientConfig 36 | ) 37 | 38 | compilerToPromise(clientCompiler, VueEnv.Client) 39 | .catch(() => null) 40 | .finally(() => { 41 | devServer.listen(vunConfig.dev.port, vunConfig.dev.host, (error?: Error) => { 42 | if (error) { 43 | logger.error(DEV_SERVER_RUN_FAILED, error) 44 | notifier.notify('', DEV_SERVER_RUN_FAILED) 45 | process.exit(1) 46 | } else { 47 | const serverUrl = getDevServerUrl(vunConfig.dev.host, vunConfig.dev.port) 48 | const projectIsRunningAtUrl = projectIsRunningAt(serverUrl) 49 | logger.done(projectIsRunningAtUrl) 50 | notifier.notify(projectIsRunningAtUrl, DEV_SERVER_RUN_SUCCESSFULLY) 51 | } 52 | }) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /cli/configs/wds/index.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import WebpackDevServer from 'webpack-dev-server' 3 | import logger from '@cli/services/logger' 4 | import { vunConfig } from '../vuniversal' 5 | 6 | const defaultDevServerConfig: WebpackDevServer.Configuration = { 7 | hot: true, 8 | inline: true, 9 | overlay: true, 10 | disableHostCheck: true, 11 | // Browser log 12 | // clientLogLevel: 'error', 13 | // Hidden webpack bundle info 14 | noInfo: !vunConfig.dev.verbose, 15 | // Only output init info when true 16 | quiet: true, 17 | // HACK: logLevel for WebpackDevMiddleware 18 | // @ts-ignore 19 | logLevel: 'silent', 20 | headers: { 21 | 'Access-Control-Allow-Origin': '*' 22 | }, 23 | watchContentBase: true, 24 | // historyApiFallback: true, 25 | // Enable gzip compression of generated files. 26 | compress: true, 27 | proxy: vunConfig.dev.proxy, 28 | publicPath: vunConfig.build.publicPath, 29 | contentBase: vunConfig.dir.public, 30 | } 31 | 32 | export const createWebpackDevServer = ( 33 | webpackCompiler: webpack.Compiler, 34 | wdsConfig: WebpackDevServer.Configuration, 35 | webpackConfig?: webpack.Configuration 36 | ): WebpackDevServer => { 37 | if (webpackConfig) { 38 | WebpackDevServer.addDevServerEntrypoints(webpackConfig, wdsConfig) 39 | } 40 | // https://webpack.docschina.org/configuration/dev-server 41 | const wds = new WebpackDevServer(webpackCompiler, { 42 | ...defaultDevServerConfig, 43 | ...wdsConfig 44 | }) 45 | // HACK: Overlay the WebpackDevServer's log & remove status info 46 | // TODO: Rrmove when webpack-dev-server 4.x publish 47 | // https://github.com/webpack/webpack-dev-server/blob/master/lib/Server.js#L52 48 | // https://github.com/webpack/webpack-dev-server/blob/master/lib/utils/status.js#L56 49 | // https://github.com/webpack/webpack-dev-server/blob/master/lib/utils/runOpen.js 50 | // @ts-ignore 51 | wds.showStatus = () => { 52 | const self = wds as any 53 | const createDomain = require('webpack-dev-server/lib/utils/createDomain') 54 | require('webpack-dev-server/lib/utils/runOpen')( 55 | createDomain(self.options, self.listeningApp), 56 | self.options, 57 | { 58 | ...self.log, 59 | info: logger.info 60 | } 61 | ) 62 | } 63 | 64 | return wds 65 | } 66 | -------------------------------------------------------------------------------- /cli/configs/vuniversal/default.ts: -------------------------------------------------------------------------------- 1 | import { VunLibConfig } from './interface' 2 | import { VUN_DEFAULT_HTML_TEMPLATE } from '@cli/paths' 3 | import { NodeEnv, isProd, isDev } from '@cli/environment' 4 | import { appPackageJSON } from '@cli/configs/package' 5 | 6 | const dependencies = Object.keys(appPackageJSON.dependencies) 7 | const devDependencies = Object.keys(appPackageJSON.devDependencies) 8 | const allDependencies = [...dependencies, ...devDependencies] 9 | 10 | export const defaultConfig: VunLibConfig = { 11 | universal: true, 12 | modern: true, 13 | clientEntry: 'src/client', 14 | serverEntry: 'src/server', 15 | template: VUN_DEFAULT_HTML_TEMPLATE, 16 | prerender: false, 17 | inspect: false, 18 | get lintOnSave() { 19 | return isDev(process.env.NODE_ENV as NodeEnv) && allDependencies.includes('eslint') 20 | ? 'default' 21 | : false 22 | }, 23 | env: {}, 24 | dir: { 25 | build: 'dist', 26 | public: 'public', 27 | source: 'src', 28 | root: '.', 29 | modules: [] 30 | }, 31 | dev: { 32 | host: 'localhost', 33 | port: 3000, 34 | verbose: false, 35 | proxy: {}, 36 | devServer: {}, 37 | }, 38 | build: { 39 | publicPath: '/', 40 | assetsDir: 'vun', 41 | analyze: false, 42 | runtimeCompiler: false, 43 | productionSourceMap: true, 44 | transpileDependencies: [], 45 | get filenameHashing() { 46 | return isProd(process.env.NODE_ENV as NodeEnv) 47 | }, 48 | get parallel() { 49 | try { 50 | return require('os').cpus().length > 1 51 | } catch (error) { 52 | return false 53 | } 54 | }, 55 | html: { 56 | crossorigin: false, 57 | preload: false, 58 | ext: {} 59 | }, 60 | css: { 61 | get extract() { 62 | return isProd(process.env.NODE_ENV as NodeEnv) 63 | }, 64 | requireModuleExtension: true, 65 | sourceMap: false, 66 | styleResources: { 67 | scss: [], 68 | sass: [], 69 | less: [], 70 | stylus: [] 71 | } 72 | }, 73 | loaders: {} as any, 74 | optimization: {} 75 | }, 76 | babel: {}, 77 | webpack: {}, 78 | typescript: !allDependencies.includes('typescript') 79 | ? false 80 | : { 81 | tsLoader: {}, 82 | forkTsChecker: true 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ### CLI 4 | - [x] Build with tsc & abc 5 | - [x] Webpack5 6 | - [x] Remove cache-loader 7 | - [x] [Upgrade](https://juejin.im/post/5df5cdf66fb9a0161a0c3092) 8 | - [x] [ModuleFederationPlugin](https://juejin.im/post/5eb382c26fb9a04388075b45?utm_source=gold_browser_extension) 9 | - ~~autofix (cache-loader)~~ 10 | - ~~autofix (pnp-loader)~~ 11 | - [x] block by vue-loader & webpack 5 12 | - [x] fix bugs with html-webpack-plugin@^4.0.0-beta.11 13 | - [x] helper command 14 | - [x] fix prerender plugin with webpack 5 15 | - [x] wds message 16 | - [x] notifier logo & texts 17 | - [x] logger auto line 18 | - [x] asynv chunk number: -> id | name 19 | - [x] cors & ~~integrity~~ option support 20 | - [x] ~~integrity has bug with preload~~ [Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) 21 | - [x] prerender option e.g. wait 22 | - [x] OptimizeCSSAssetsPlugin 23 | - [x] extractOptions (duplicates) & OptimizeCSSAssetsPlugin (map) [so](https://stackoverflow.com/questions/52564625/cssnano-doesnt-remove-duplicates) 24 | - [x] server prod optimize (max chunk length = 1) 25 | - [x] analyze support [options](https://github.com/webpack-contrib/webpack-bundle-analyzer#options-for-plugin) 26 | - [x] babel config 27 | - [x] prerender with ssr 28 | - [x] bin script 29 | - [x] jest support (example) 30 | - [ ] mocha support (example) & webpack.config.js interface with root 31 | - [ ] [FYI](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-unit-mocha/index.js#L59) 32 | - [ ] [FYI](https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/config/app.js#L37) 33 | - [ ] ssr app.template.html copy to dist & default build spa.html 34 | - [WIP] html (cors/ext/preload/prefetch) option 35 | - [script-ext-html-webpack-plugin](https://github.com/numical/script-ext-html-webpack-plugin) 36 | - [preload-webpack-plugin](https://github.com/GoogleChromeLabs/preload-webpack-plugin) 37 | - [ ] [modern support](https://cli.vuejs.org/zh/guide/browser-compatibility.html#%E7%8E%B0%E4%BB%A3%E6%A8%A1%E5%BC%8F) 38 | - [ ] init script 39 | - [ ] server boundle `require.resolveWeak` [FYI](https://github.com/faceyspacey/react-universal-component) 40 | - [ ] server boundle async to html [FYI](https://github.com/jamiebuilds/react-loadable) 41 | 42 | ### Vuniversal 43 | - [ ] Meta (vue-meta?) 44 | - [ ] universal 404 context 45 | - [ ] universal cache 46 | - [ ] `no-prerender` component 47 | - [ ] `client-only` component 48 | - [ ] `only-run` component? 49 | -------------------------------------------------------------------------------- /cli/configs/webpack/server.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import webpack, { Configuration } from 'webpack' 3 | import WebpackBar from 'webpackbar' 4 | import ManifestPlugin from 'webpack-manifest-plugin' 5 | import nodeExternals from 'webpack-node-externals' 6 | import { vunConfig } from '../vuniversal' 7 | import { VueEnv, isDev } from '@cli/environment' 8 | import { 9 | SERVER_ENTRY, 10 | SERVER_JS_FILE, 11 | SERVER_MANIFEST_FILE, 12 | VUN_DEV_CACHE_PATH, 13 | WEBPACK_HOT_POLL_ENTRY, 14 | getManifestDir, 15 | getServerBuildDir 16 | } from '@cli/paths' 17 | import { resolveEntry } from '@cli/utils' 18 | import { BuildContext } from '.' 19 | 20 | export function modifyServerConfig(webpackConfig: Configuration, buildContext: BuildContext): void { 21 | const IS_DEV = isDev(buildContext.environment) 22 | 23 | webpackConfig.entry = { 24 | [SERVER_ENTRY]: [resolveEntry(vunConfig.serverEntry, VueEnv.Server)] 25 | } 26 | 27 | // Specify webpack Node.js output path and filename 28 | webpackConfig.output = { 29 | path: IS_DEV 30 | ? VUN_DEV_CACHE_PATH 31 | : getServerBuildDir(vunConfig), 32 | publicPath: vunConfig.build.publicPath, 33 | filename: SERVER_JS_FILE, 34 | libraryTarget: 'commonjs2' 35 | } 36 | 37 | // We need to tell webpack what to bundle into our Node bundle. 38 | const whitelist = [ 39 | /\.(eot|woff|woff2|ttf|otf)$/, 40 | /\.(svg|png|jpg|jpeg|gif|ico)$/, 41 | /\.(mp4|mp3|ogg|swf|webp)$/, 42 | /\.(css|scss|sass|sss|less)$/ 43 | ] 44 | webpackConfig.node = false 45 | webpackConfig.externals = [ 46 | nodeExternals({ 47 | whitelist: IS_DEV 48 | ? [WEBPACK_HOT_POLL_ENTRY, ...whitelist] 49 | : whitelist 50 | }) 51 | ] 52 | 53 | webpackConfig.plugins?.push( 54 | new WebpackBar({ 55 | color: 'orange', 56 | name: VueEnv.Server 57 | }), 58 | // Output our JS and CSS files in a manifest file called chunks.json 59 | // in the build directory. 60 | // based on https://github.com/danethurber/webpack-manifest-plugin/issues/181#issuecomment-467907737 61 | // @ts-ignore 62 | new ManifestPlugin({ 63 | fileName: path.join( 64 | getManifestDir(buildContext.environment, vunConfig), 65 | SERVER_MANIFEST_FILE 66 | ), 67 | writeToFileEmit: true 68 | }), 69 | // Prevent creating multiple chunks for the server 70 | new webpack.optimize.LimitChunkCountPlugin({ 71 | maxChunks: 1 72 | }) 73 | ) 74 | 75 | if (IS_DEV) { 76 | webpackConfig.plugins?.push( 77 | // Add hot module replacement 78 | new webpack.HotModuleReplacementPlugin() 79 | ) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /cli/configs/babel/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { RuleSetRule } from 'webpack' 3 | import { loadPartialConfig } from '@babel/core' 4 | import { isWindows, requireResolve } from '@cli/utils' 5 | import { VunLibConfig } from '../vuniversal' 6 | import { BuildContext } from '../webpack' 7 | 8 | const defaultOptions = { 9 | presets: [requireResolve('@vue/babel-preset-app')] 10 | } 11 | 12 | export function getBabelLoader(vunConfig: VunLibConfig, buildContext: BuildContext): RuleSetRule { 13 | // Validate a user's config 14 | loadPartialConfig() 15 | 16 | let loaderOptions = defaultOptions 17 | if (typeof vunConfig.babel === 'object') { 18 | loaderOptions = { 19 | ...defaultOptions, 20 | ...vunConfig.babel 21 | } 22 | } else if (typeof vunConfig.babel === 'function') { 23 | loaderOptions = vunConfig.babel({ target: buildContext.target }) as any 24 | } 25 | 26 | return { 27 | loader: requireResolve('babel-loader'), 28 | options: loaderOptions 29 | } 30 | } 31 | 32 | export function getExcluder(vunConfig: VunLibConfig) { 33 | const transpileDepRegex = genTranspileDepRegex(vunConfig.build.transpileDependencies) 34 | return (filepath: string) => { 35 | // always transpile js in vue files 36 | if (/\.vue\.jsx?$/.test(filepath)) { 37 | return false 38 | } 39 | 40 | // only include @babel/runtime when the @vue/babel-preset-app preset is used 41 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/babel-preset-app/index.js#L218 42 | // https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel#L50 43 | if (filepath.includes(path.join('@babel', 'runtime'))) { 44 | return false 45 | } 46 | 47 | // check if this is something the user explicitly wants to transpile 48 | if (transpileDepRegex && transpileDepRegex.test(filepath)) { 49 | return false 50 | } 51 | // Don't transpile node_modules 52 | return /node_modules/.test(filepath) 53 | } 54 | } 55 | 56 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-babel/index.js#L5 57 | function genTranspileDepRegex(transpileDependencies: VunLibConfig['build']['transpileDependencies']) { 58 | // @ts-ignore 59 | const deps = transpileDependencies.map(dep => { 60 | if (typeof dep === 'string') { 61 | const depPath = path.join('node_modules', dep, '/') 62 | return isWindows 63 | ? depPath.replace(/\\/g, '\\\\') // double escape for windows style path 64 | : depPath 65 | } else if (dep instanceof RegExp) { 66 | return dep.source 67 | } 68 | }) 69 | return deps.length ? new RegExp(deps.join('|')) : null 70 | } 71 | -------------------------------------------------------------------------------- /bin/vun.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Init stdout interceptor 4 | require('../cli-dist/services/stdout').init() 5 | const open = require('open') 6 | const semver = require('semver') 7 | const program = require('commander') 8 | const didYouMean = require('didyoumean') 9 | const loggerService = require('../cli-dist/services/logger') 10 | const { vunPackageJSON } = require('../cli-dist/configs/package') 11 | const { APP_VUN_CONFIG_FILE, VUN_DOC_URL } = require('../cli-dist/paths') 12 | const { VUN_COMMND, VUNIVERSAL_NAME } = require('../cli-dist/texts') 13 | const { default: logger, yellowText, greenText, linkText, redText } = loggerService 14 | 15 | // Overlay global console 16 | global.console.warn = logger.warn 17 | global.console.error = logger.error 18 | 19 | didYouMean.threshold = 0.6 20 | 21 | // Check Node version 22 | const currentVersion = process.version 23 | const targetVersion = vunPackageJSON.engines.node 24 | if (!semver.satisfies(currentVersion, targetVersion)) { 25 | logger.error(`You are using Node ${currentVersion}, but this version of ${VUNIVERSAL_NAME} requires Node ${targetVersion}.\n\nPlease upgrade your Node version.`) 26 | process.exit(1) 27 | } 28 | 29 | program 30 | .name(VUN_COMMND) 31 | .usage('') 32 | .version(vunPackageJSON.version, '-v, --version', `Output the ${VUNIVERSAL_NAME} version number`) 33 | .helpOption('-h, --help', `Display help for ${VUN_COMMND} command`) 34 | 35 | program 36 | .command('init') 37 | .description(`Create ${VUNIVERSAL_NAME} config file ${greenText('(' + APP_VUN_CONFIG_FILE + ')')}`) 38 | .action(() => require('../cli-dist/scripts/init')) 39 | 40 | program 41 | .command('dev') 42 | .description(`Start the application in ${greenText('development')} mode (e.g. hot-code reloading, error reporting)`) 43 | .action(() => require('../cli-dist/scripts/dev')) 44 | 45 | program 46 | .command('build') 47 | .description(`Compiles the application for ${greenText('production')} deployment ${greenText('(ssr/spa/prerender)')}`) 48 | .action(() => require('../cli-dist/scripts/build')) 49 | 50 | program 51 | .command('doc') 52 | .description(`Open documentation site: ${linkText(VUN_DOC_URL)}`) 53 | .action(() => open(VUN_DOC_URL)) 54 | 55 | program 56 | .arguments('[command]') 57 | .action(command => { 58 | if (command === undefined) { 59 | require('../cli-dist/scripts/dev') 60 | } else { 61 | const warnText = redText(`Unknown command ${yellowText(VUN_COMMND + ' ' + command)}`) 62 | const suggestion = didYouMean(command, program.commands.map(cmd => cmd._name)) 63 | logger.br() 64 | logger.log( 65 | suggestion 66 | ? warnText + ',' + redText(` Did you mean ${yellowText(VUN_COMMND + ' ' + suggestion)} ?`) 67 | : warnText + '.' 68 | ) 69 | logger.br() 70 | program.outputHelp() 71 | logger.br() 72 | } 73 | }) 74 | 75 | program.parse(process.argv) 76 | -------------------------------------------------------------------------------- /cli/configs/webpack/helper.ts: -------------------------------------------------------------------------------- 1 | import webpack, { Configuration } from 'webpack' 2 | import logger from '@cli/services/logger' 3 | import notifier from '@cli/services/notifier' 4 | import { FAILED_TO_VALIDATION } from '@cli/texts' 5 | import { VueEnv } from '@cli/environment' 6 | import { VunEnvObject, VunLibConfig } from '../vuniversal' 7 | import { BuildContext } from '.' 8 | 9 | export function isClientTarget(buildContext: BuildContext): boolean { 10 | return buildContext.target === VueEnv.Client 11 | } 12 | 13 | // Webpack compile in a try-catch 14 | export function compileConfig(config: Configuration) { 15 | let compiler 16 | try { 17 | compiler = webpack(config) 18 | } catch (error) { 19 | logger.error(FAILED_TO_VALIDATION, error) 20 | notifier.failed(FAILED_TO_VALIDATION) 21 | process.exit(1) 22 | } 23 | return compiler 24 | } 25 | 26 | export function handleCompiler(successHandler: (stats?: webpack.Stats) => void) { 27 | return (error?: Error, stats?: webpack.Stats) => { 28 | // https://github.com/geowarin/friendly-errors-webpack-plugin/blob/v2.0.0-beta.2/src/friendly-errors-plugin.js#L52 29 | if (error || stats?.hasErrors()) { 30 | return 31 | } 32 | // https://github.com/geowarin/friendly-errors-webpack-plugin/blob/v2.0.0-beta.2/src/friendly-errors-plugin.js#L83 33 | successHandler(stats) 34 | } 35 | } 36 | 37 | export function runPromise(compiler: webpack.Compiler) { 38 | return new Promise((resolve) => { 39 | compiler.run(handleCompiler(resolve)) 40 | }) 41 | } 42 | 43 | export function compilerToPromise(compiler: webpack.Compiler, name: string) { 44 | return new Promise((resolve, reject) => { 45 | compiler.hooks.done.tap(name, stats => { 46 | stats.hasErrors() 47 | ? reject(stats.toJson().errors) 48 | : resolve(stats) 49 | }) 50 | }) 51 | } 52 | 53 | export function stringifyEnvObject(envObject: VunEnvObject): VunEnvObject { 54 | const codeValueObject: VunEnvObject = {} 55 | Object.keys(envObject).forEach(key => { 56 | codeValueObject[key] = JSON.stringify(envObject[key]) 57 | }) 58 | return codeValueObject 59 | } 60 | 61 | export function transformToProcessEnvObject(envObject: VunEnvObject): VunEnvObject { 62 | return { 63 | 'process.env': stringifyEnvObject(envObject) 64 | } 65 | } 66 | 67 | export function autoHash(vunConfig: VunLibConfig) { 68 | return vunConfig.build.filenameHashing ? '.[hash:8]' : '' 69 | } 70 | 71 | export function autoContentHash(vunConfig: VunLibConfig) { 72 | return vunConfig.build.filenameHashing ? '.[contenthash:8]' : '' 73 | } 74 | 75 | export function autoChunkHash(vunConfig: VunLibConfig) { 76 | return vunConfig.build.filenameHashing ? '.[chunkhash:8]' : '' 77 | } 78 | 79 | export function getDevServerUrl(host: string, port: number): string { 80 | return `http://${host}:${port}` 81 | } 82 | 83 | export function getAssetsServerPort(port: number): number { 84 | return port + 1 85 | } 86 | 87 | export function getAssetsServerUrl(host: string, port: number): string { 88 | return `http://${host}:${getAssetsServerPort(port)}` 89 | } 90 | -------------------------------------------------------------------------------- /cli/configs/typescript/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import path from 'path' 3 | import { Configuration } from 'webpack' 4 | import { Options as TsLoaderOptions } from 'ts-loader' 5 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' 6 | import { requireResolve } from '@cli/utils' 7 | import { isProd, isClientTarget } from '@cli/environment' 8 | import { BuildContext } from '../webpack' 9 | import { getBabelLoader } from '../babel' 10 | import { enableParallel, getThreadLoader } from '../parallel' 11 | import { vunConfig } from '../vuniversal' 12 | import logger from '@cli/services/logger' 13 | 14 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-typescript/index.js 15 | export function modifyTypeScriptConfig(webpackConfig: Configuration, buildContext: BuildContext) { 16 | const IS_PROD = isProd(buildContext.environment) 17 | const IS_CLIENT = isClientTarget(buildContext.target) 18 | 19 | // Threads 20 | const useThreads = IS_PROD && enableParallel(vunConfig) 21 | 22 | // ts-loader 23 | webpackConfig.resolve?.extensions?.unshift('.ts', '.tsx') 24 | webpackConfig.module?.rules?.unshift({ 25 | test: /\.(ts|tsx)$/, 26 | include: [vunConfig.dir.source], 27 | exclude: [/node_modules/], 28 | use: [ 29 | getBabelLoader(vunConfig, buildContext), 30 | { 31 | loader: requireResolve('ts-loader'), 32 | options: { 33 | transpileOnly: true, 34 | experimentalWatchApi: true, 35 | appendTsSuffixTo: [/\.vue$/], 36 | // https://github.com/TypeStrong/ts-loader#happypackmode-boolean-defaultfalse 37 | happyPackMode: useThreads, 38 | ...( 39 | typeof vunConfig.typescript === 'object' 40 | ? vunConfig.typescript.tsLoader 41 | : {} 42 | ) 43 | } as TsLoaderOptions 44 | }, 45 | getThreadLoader(vunConfig) 46 | ] 47 | }) 48 | 49 | // ForkTsChecker 50 | const enableForkTsChecker = ( 51 | typeof vunConfig.typescript === 'object' && 52 | !!vunConfig.typescript.forkTsChecker 53 | ) 54 | // 服务端和客户端大部分代码是重合的,但是运行了两个实例,也就是说同样的错误会报两遍 55 | if (IS_CLIENT && enableForkTsChecker) { 56 | const vunForkTsCheckerOptions = ( 57 | typeof vunConfig.typescript === 'object' && 58 | typeof vunConfig.typescript.forkTsChecker === 'object' 59 | ) ? vunConfig.typescript.forkTsChecker : {} 60 | 61 | webpackConfig.plugins?.push( 62 | new ForkTsCheckerWebpackPlugin({ 63 | // Not friendly & duplicate with eslint-loader 64 | // eslint: !!vunConfig.lintOnSave, 65 | logger, 66 | // Disable logger 67 | silent: true, 68 | // Emit errors to webpack: https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#options 69 | async: false, 70 | formatter: 'codeframe', 71 | // https://github.com/TypeStrong/ts-loader#happypackmode-boolean-defaultfalse 72 | checkSyntacticErrors: useThreads, 73 | vue: { 74 | // https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-plugin-typescript/index.js#L82 75 | enabled: true, 76 | compiler: requireResolve(path.resolve(__dirname, 'compiler.shim')) 77 | }, 78 | ...vunForkTsCheckerOptions 79 | }) 80 | ) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /cli/scripts/dev/ssr.ts: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin' 2 | import StartServerPlugin from 'start-server-webpack-plugin' 3 | import { vunConfig } from '@cli/configs/vuniversal' 4 | import { getWebpackConfig } from '@cli/configs/webpack' 5 | import { createWebpackDevServer } from '@cli/configs/wds' 6 | import { VUN_DEV_TEMPLATE, SERVER_ENTRY, WEBPACK_HOT_POLL_ENTRY, SERVER_JS_FILE } from '@cli/paths' 7 | import { compileConfig, compilerToPromise, handleCompiler, getAssetsServerPort } from '@cli/configs/webpack/helper' 8 | import { DEV_SERVER_RUN_FAILED, COMPILED_SUCCESSFULLY } from '@cli/texts' 9 | import { NodeEnv, VueEnv } from '@cli/environment' 10 | import { args } from '@cli/utils' 11 | import logger from '@cli/services/logger' 12 | import notifier from '@cli/services/notifier' 13 | 14 | export function startSSRServer() { 15 | const assetsServerPost = getAssetsServerPort(vunConfig.dev.port) 16 | const clientConfig = getWebpackConfig({ target: VueEnv.Client, environment: NodeEnv.Development }) 17 | const serverConfig = getWebpackConfig({ target: VueEnv.Server, environment: NodeEnv.Development }) 18 | 19 | // Client -------------------------------------------------------------- 20 | clientConfig.output = { 21 | ...clientConfig.output, 22 | // MARK: publicPath -> chunks url & socket url host & hot-upload url 23 | // publicPath: assetsServerUrl + '/' 24 | publicPath: '/' 25 | } 26 | clientConfig.plugins?.push(new HtmlWebpackPlugin({ 27 | template: VUN_DEV_TEMPLATE, 28 | inject: false 29 | })) 30 | 31 | const clientCompiler = compileConfig(clientConfig) 32 | const clientServer = createWebpackDevServer( 33 | clientCompiler, 34 | { 35 | port: assetsServerPost, 36 | historyApiFallback: false, 37 | open: false, 38 | ...vunConfig.dev.devServer 39 | }, 40 | clientConfig 41 | ) 42 | 43 | // Server -------------------------------------------------------------- 44 | // Start HMR server 45 | // @ts-ignore 46 | serverConfig.entry[SERVER_ENTRY].unshift(WEBPACK_HOT_POLL_ENTRY) 47 | // Auro run ssr server when build done. 48 | // https://github.com/ericclemmons/start-server-webpack-plugin 49 | // @ts-ignore 50 | serverConfig.plugins?.push(new StartServerPlugin({ 51 | // https://github.com/ericclemmons/start-server-webpack-plugin/blob/master/src/StartServerPlugin.js#L110 52 | name: SERVER_JS_FILE, 53 | // Capture any --inspect or --inspect-brk flags (with optional values) so that we 54 | // can pass them when we invoke nodejs 55 | nodeArgs: args, 56 | keyboard: true 57 | })) 58 | 59 | const serverCompiler = compileConfig(serverConfig) 60 | serverCompiler.watch( 61 | serverConfig.watchOptions || {}, 62 | handleCompiler(() => { 63 | // logger.log(`${VueEnv.Server} status: ${stats?.toString(serverConfig.stats)}`) 64 | }) 65 | ) 66 | 67 | // Run 68 | Promise.all([ 69 | compilerToPromise(clientCompiler, VueEnv.Client), 70 | compilerToPromise(serverCompiler, VueEnv.Server) 71 | ]) 72 | .then(() => notifier.notify(COMPILED_SUCCESSFULLY)) 73 | .catch(() => null) 74 | .finally(() => { 75 | clientServer.listen(assetsServerPost, vunConfig.dev.host, error => { 76 | if (error) { 77 | logger.error(DEV_SERVER_RUN_FAILED, error) 78 | notifier.notify('', DEV_SERVER_RUN_FAILED) 79 | process.exit(1) 80 | } 81 | }) 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /cli/configs/webpack/client.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { Configuration } from 'webpack' 3 | import WebpackBar from 'webpackbar' 4 | import ManifestPlugin from 'webpack-manifest-plugin' 5 | import { vunConfig } from '../vuniversal' 6 | import { modifyClientDevConfig } from './clien.dev' 7 | import { modifyClientProdConfig } from './client.prod' 8 | import { VueEnv, isDev, isUniversal } from '@cli/environment' 9 | import { CLIENT_ENTRY, CLIENT_MANIFEST_FILE, getManifestDir, getClientBuildDir } from '@cli/paths' 10 | import { resolveEntry } from '@cli/utils' 11 | import { autoChunkHash } from './helper' 12 | import { BuildContext } from '.' 13 | 14 | export function modifyClientConfig(webpackConfig: Configuration, buildContext: BuildContext): void { 15 | const IS_DEV = isDev(buildContext.environment) 16 | const clientBuildDir = getClientBuildDir(vunConfig) 17 | 18 | // TODO: morden 19 | // isLegacyBundle ? '[name]-legacy.js' : '[name].js' 20 | // const outputFilename = getAssetPath( 21 | // options, 22 | // `js/[name]${isLegacyBundle ? `-legacy` : ``}${isProd && options.filenameHashing ? '.[contenthash:8]' : ''}.js` 23 | 24 | // specify our client entry point src/client 25 | webpackConfig.entry = { 26 | [CLIENT_ENTRY]: [resolveEntry(vunConfig.clientEntry, VueEnv.Client)] 27 | } 28 | 29 | // Specify the client output directory and paths. 30 | webpackConfig.output = { 31 | path: clientBuildDir, 32 | publicPath: vunConfig.build.publicPath, 33 | filename: `${vunConfig.build.assetsDir}/js/[name]${autoChunkHash(vunConfig)}.js`, 34 | chunkFilename: `${vunConfig.build.assetsDir}/js/[name]${autoChunkHash(vunConfig)}.js` 35 | } 36 | 37 | webpackConfig.node = false 38 | webpackConfig.optimization = { 39 | ...webpackConfig.optimization, 40 | splitChunks: { 41 | cacheGroups: { 42 | vendors: { 43 | name: 'chunk-vendors', 44 | test: /[\\/]node_modules[\\/]/, 45 | priority: -10, 46 | chunks: 'initial' 47 | }, 48 | common: { 49 | name: 'chunk-common', 50 | minChunks: 2, 51 | priority: -20, 52 | chunks: 'initial', 53 | reuseExistingChunk: true 54 | } 55 | } 56 | } 57 | } 58 | 59 | webpackConfig.plugins?.push( 60 | new WebpackBar({ 61 | color: 'green', 62 | name: VueEnv.Client 63 | }), 64 | // Output our JS and CSS files in a manifest file called chunks.json 65 | // in the build directory. 66 | // based on https://github.com/danethurber/webpack-manifest-plugin/issues/181#issuecomment-467907737 67 | // @ts-ignore 68 | new ManifestPlugin({ 69 | fileName: path.join( 70 | getManifestDir(buildContext.environment, vunConfig), 71 | CLIENT_MANIFEST_FILE 72 | ), 73 | writeToFileEmit: isUniversal(vunConfig), 74 | filter: item => item.isChunk, 75 | generate(_, __, entrypoints) { 76 | return { 77 | js: entrypoints[CLIENT_ENTRY] 78 | .filter(file => file.endsWith('.js')) 79 | .map(file => webpackConfig.output?.publicPath + file) 80 | , 81 | css: entrypoints[CLIENT_ENTRY] 82 | .filter(file => file.endsWith('.css')) 83 | .map(file => webpackConfig.output?.publicPath + file) 84 | } 85 | } 86 | }) 87 | ) 88 | 89 | IS_DEV 90 | ? modifyClientDevConfig(webpackConfig) 91 | : modifyClientProdConfig(webpackConfig) 92 | } 93 | -------------------------------------------------------------------------------- /cli/paths.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { NodeEnv, isDev, isUniversal } from './environment' 4 | import { VunLibConfig } from './configs/vuniversal' 5 | 6 | // --------------------------------------------------------- 7 | // Make sure any symlinks in the project folder are resolved 8 | export const APP_ROOT_DIRECTORY_PATH = fs.realpathSync(process.cwd()) 9 | export const resolveAppRoot = (relativePath: string): string => { 10 | return path.resolve(APP_ROOT_DIRECTORY_PATH, relativePath) 11 | } 12 | export const APP_BABEL_RC_PATH = resolveAppRoot('.babelrc') 13 | export const APP_NODE_MODULES_PATH = resolveAppRoot('node_modules') 14 | export const APP_VUN_CONFIG_PATH = resolveAppRoot('vun.config.js') 15 | export const APP_PACKAGE_JSON_PATH = resolveAppRoot('package.json') 16 | export const APP_JS_CONFIG_PATH = resolveAppRoot('jsconfig.json') 17 | export const APP_TS_CONFIG_PATH = resolveAppRoot('tsconfig.json') 18 | 19 | // --------------------------------------------------------- 20 | // Vun constants 21 | export const resolveVunRoot = (...relativePath: string[]): string => { 22 | return path.resolve(__dirname, '..', ...relativePath) 23 | } 24 | export const VUN_NAME = 'Vuniversal' 25 | export const VUN_DOC_URL = 'https://github.surmon.me/vuniversal' 26 | export const VUN_ROOT_PATH = resolveVunRoot('.') 27 | export const VUN_NODE_MODULES_PATH = resolveVunRoot('node_modules') 28 | export const VUN_DEV_CACHE_PATH = resolveVunRoot('.vun') 29 | 30 | export const VUN_DEV_TEMPLATE = resolveVunRoot('cli', 'templates', 'dev.html') 31 | export const VUN_DEFAULT_HTML_TEMPLATE = resolveVunRoot('cli', 'templates', 'index.html') 32 | 33 | // --------------------------------------------------------- 34 | // Butid time constants 35 | export const SERVER_JS_FILE = 'server.js' 36 | export const CLIENT_ENTRY = 'client' 37 | export const SERVER_ENTRY = 'server' 38 | export const WEBPACK_HOT_POLL_ENTRY = 'webpack/hot/poll?100' 39 | export const CLIENT_MANIFEST_FILE = 'client.manifest.json' 40 | export const SERVER_MANIFEST_FILE = 'server.manifest.json' 41 | export const SPA_TEMPLATE_FILE = 'index.html' 42 | export const DEFAULT_FALLBACK_FILE = '404.html' 43 | export const SSR_TEMPLATE_FILE = 'template.html' 44 | 45 | // Butid time paths 46 | export function getServerBuildDir(vunConfig: VunLibConfig): string { 47 | return path.join(vunConfig.dir.build, SERVER_ENTRY) 48 | } 49 | 50 | export function getClientBuildDir(vunConfig: VunLibConfig): string { 51 | return isUniversal(vunConfig) 52 | ? path.join(vunConfig.dir.build, CLIENT_ENTRY) 53 | : vunConfig.dir.build 54 | } 55 | 56 | export function getManifestDir(environment: NodeEnv, vunConfig: VunLibConfig): string { 57 | return isDev(environment) 58 | ? VUN_DEV_CACHE_PATH 59 | : isUniversal(vunConfig) 60 | ? getServerBuildDir(vunConfig) 61 | : vunConfig.dir.build 62 | } 63 | 64 | // --------------------------------------------------------- 65 | // We support resolving modules according to `NODE_PATH`. 66 | // This lets you use absolute paths in imports inside large monorepos: 67 | // https://github.com/facebookincubator/create-react-app/issues/253. 68 | // It works similar to `NODE_PATH` in Node itself: 69 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 70 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 71 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 72 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 73 | // We also resolve them to make sure all tools using them work consistently. 74 | export const NODE_PATH = (process.env.NODE_PATH || '') 75 | .split(path.delimiter) 76 | .filter(folder => folder && !path.isAbsolute(folder)) 77 | .map(folder => path.resolve(APP_ROOT_DIRECTORY_PATH, folder)) 78 | .join(path.delimiter) 79 | 80 | export const NODE_PATHS = (process.env.NODE_PATH || '') 81 | .split(process.platform === 'win32' ? ';' : ':') 82 | .filter(Boolean) 83 | .filter(folder => !path.isAbsolute(folder)) 84 | .map(resolveAppRoot) 85 | -------------------------------------------------------------------------------- /cli/services/banner.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import wrapAnsi from 'wrap-ansi' 3 | import prettyBytes from 'pretty-bytes' 4 | import boxen, { BorderStyle } from 'boxen' 5 | import { vunPackageJSON } from '@cli/configs/package' 6 | import { NodeEnv, UniversalMode } from '../environment' 7 | import { VUN_NAME } from '../paths' 8 | import { linkText } from './logger' 9 | 10 | // 80% of terminal column width 11 | // this is a fn because console width can have changed since startup 12 | const maxCharsPerLine = () => (process.stdout.columns || 100) * 80 / 100 13 | 14 | function indent(count: number, chr = ' ') { 15 | return chr.repeat(count) 16 | } 17 | 18 | function foldLines(string: string, spaces: number, firstLineSpaces: number, charsPerLine = maxCharsPerLine()) { 19 | const lines = wrapAnsi(string, charsPerLine).split('\n') 20 | let result = '' 21 | if (lines.length) { 22 | const i0 = indent(firstLineSpaces === undefined ? spaces : firstLineSpaces) 23 | result = i0 + lines.shift() 24 | } 25 | if (lines.length) { 26 | const i = indent(spaces) 27 | result += '\n' + lines.map(l => i + l).join('\n') 28 | } 29 | return result 30 | } 31 | 32 | export default function bannerBox(message: string, title?: string, options?: boxen.Options) { 33 | const content = [title || chalk.white(`${VUN_NAME} Message`)] 34 | if (!!message) { 35 | content.push( 36 | '', 37 | chalk.white(foldLines(message, 0, 0, maxCharsPerLine())) 38 | ) 39 | } 40 | 41 | return boxen( 42 | content.join('\n'), 43 | { 44 | borderColor: 'white', 45 | borderStyle: BorderStyle.Round, 46 | padding: 1, 47 | margin: 1, 48 | ...options 49 | } 50 | ) + '\n' 51 | } 52 | 53 | export function success(message: string, title?: string) { 54 | return bannerBox(message, title || chalk.green(`✔ ${VUN_NAME} Success`), { 55 | borderColor: 'green' 56 | }) 57 | } 58 | 59 | export function warning(message: string, title?: string) { 60 | return bannerBox(message, title || chalk.yellow(`⚠ ${VUN_NAME} Warning`), { 61 | borderColor: 'yellow' 62 | }) 63 | } 64 | 65 | export function error(message: string, title?: string) { 66 | return bannerBox(message, title || chalk.red(`✖ ${VUN_NAME} Error`), { 67 | borderColor: 'red' 68 | }) 69 | } 70 | 71 | export interface IHeadBannerOptions { 72 | univservalMode?: UniversalMode 73 | command?: string 74 | memory?: boolean 75 | runningIn?: string 76 | listeningOn?: string 77 | } 78 | 79 | export function headBanner(options: IHeadBannerOptions = {}) { 80 | const messages = [] 81 | const titles = [ 82 | `${chalk.green.bold(VUN_NAME)} v${chalk(vunPackageJSON.version)}`, 83 | '' 84 | ] 85 | 86 | // Execute command 87 | if (options.command) { 88 | titles.push(`Execute ${chalk.green.bold(options.command)}`) 89 | } 90 | 91 | if (options.runningIn) { 92 | // Running mode 93 | const environment = !options.runningIn 94 | ? '' 95 | : options.runningIn === NodeEnv.Development 96 | ? chalk.green.bold(NodeEnv.Development) 97 | : chalk.green.bold(NodeEnv.Production) 98 | const runningMode = environment && `Running in ${environment} mode` 99 | // Univserval mode 100 | const univservalMode = options.univservalMode 101 | ? ` (${chalk.green.bold(options.univservalMode)})` 102 | : '' 103 | titles.push(runningMode + univservalMode) 104 | } 105 | 106 | // Memory 107 | if (options.memory) { 108 | // https://nodejs.org/api/process.html#process_process_memoryusage 109 | const { heapUsed, rss } = process.memoryUsage() 110 | titles.push(`Memory usage: ${chalk.green.bold(prettyBytes(heapUsed))} (RSS: ${prettyBytes(rss)})`) 111 | } 112 | 113 | // Listeners 114 | if (options.listeningOn) { 115 | messages.push(`Listening on: ${linkText(options.listeningOn)}`) 116 | } 117 | 118 | process.stdout.write(success( 119 | messages.length ? messages.join('\n') : '', 120 | titles.join('\n') 121 | )) 122 | } 123 | -------------------------------------------------------------------------------- /cli/services/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Console } from 'console' 3 | import { isBrLastLine } from './stdout' 4 | 5 | export enum LogTypes { 6 | Warn = 'warn', 7 | Debug = 'debug', 8 | Info = 'info', 9 | Error = 'error', 10 | Start = 'start', 11 | Done = 'done' 12 | } 13 | 14 | export const logStyles = { 15 | [LogTypes.Warn]: { 16 | bg: chalk.bgYellow, 17 | text: chalk.yellow, 18 | msg: ' WARNING ' 19 | }, 20 | [LogTypes.Debug]: { 21 | bg: chalk.bgMagenta, 22 | text: chalk.magenta, 23 | msg: ' DEBUG ' 24 | }, 25 | [LogTypes.Info]: { 26 | bg: chalk.bgCyan, 27 | text: chalk.cyan, 28 | msg: ' INFO ' 29 | }, 30 | [LogTypes.Error]: { 31 | bg: chalk.bgRed, 32 | text: chalk.red, 33 | msg: ' ERROR ' 34 | }, 35 | [LogTypes.Start]: { 36 | bg: chalk.bgBlue, 37 | text: chalk.blue, 38 | msg: ' WAIT ' 39 | }, 40 | [LogTypes.Done]: { 41 | bg: chalk.bgGreen, 42 | text: chalk.green, 43 | msg: ' DONE ' 44 | } 45 | } 46 | 47 | export const yellowText = (text: string) => logStyles[LogTypes.Warn].text(text) 48 | export const greenText = (text: string) => logStyles[LogTypes.Done].text(text) 49 | export const blueText = (text: string) => logStyles[LogTypes.Info].text(text) 50 | export const linkText = (text: string) => logStyles[LogTypes.Info].text.underline(text) 51 | export const redText = (text: string) => logStyles[LogTypes.Error].text(text) 52 | export const loggerConsole = new Console(process.stdout, process.stderr) 53 | 54 | const br = () => { 55 | loggerConsole.log() 56 | } 57 | 58 | const clear = () => { 59 | process.stdout.write( 60 | process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H' 61 | ) 62 | } 63 | 64 | const autoPreBr = () => { 65 | if (!isBrLastLine()) { 66 | br() 67 | } 68 | } 69 | 70 | const write = (type: LogTypes, text: string, verbose?: any) => { 71 | const logType = logStyles[type] 72 | const isObjectVerbose = typeof verbose === 'object' 73 | const textToLog = logType.bg.black(logType.msg) + ' ' + logType.text(text) 74 | autoPreBr() 75 | if (isObjectVerbose) { 76 | loggerConsole.log(textToLog) 77 | loggerConsole.dir(verbose, { depth: 15 }) 78 | } else { 79 | loggerConsole.log( 80 | !!verbose 81 | ? textToLog + `\n${verbose}` 82 | : textToLog 83 | ) 84 | } 85 | br() 86 | } 87 | 88 | // Printing any statements 89 | const log = (...text: string[]) => loggerConsole.log(...text) 90 | // Starting a process 91 | const start = (text: string) => write(LogTypes.Start, text) 92 | // Ending a process 93 | const done = (text: string) => write(LogTypes.Done, text) 94 | // Info about a process task 95 | const info = (text: string) => write(LogTypes.Info, text) 96 | // Verbose output 97 | const debug = (text: string, data?: any) => write(LogTypes.Debug, text, data) 98 | // Warn output 99 | const warn = (text: string, data?: any) => write(LogTypes.Warn, text, data) 100 | // Error output 101 | const error = (text: string | Error, error?: Error) => { 102 | const errorLog = logStyles[LogTypes.Error] 103 | let logContent = errorLog.bg.black(errorLog.msg) + ' ' 104 | if (typeof text === 'string') { 105 | logContent += errorLog.text(text) 106 | if (error) { 107 | logContent += `\n\n${errorLog.text(error.stack || error.message || error)}` 108 | } 109 | } else { 110 | logContent += errorLog.text(text.stack || text.message || text) 111 | } 112 | autoPreBr() 113 | loggerConsole.error(logContent) 114 | br() 115 | } 116 | // Errors output 117 | const errors = (text: string, errors: Array) => { 118 | const errorLog = logStyles[LogTypes.Error] 119 | autoPreBr() 120 | loggerConsole.error(errorLog.bg.black(errorLog.msg) + ' ' + errorLog.text(text) + `(${errors.length} errors)`) 121 | errors.forEach(_error => error(_error)) 122 | } 123 | 124 | export default { 125 | br, 126 | autoPreBr, 127 | clear, 128 | log, 129 | info, 130 | debug, 131 | warn, 132 | start, 133 | done, 134 | error, 135 | errors 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | 5 | 6 |

7 |

Create vue (3) universal web applications quickly.

8 |
9 |

10 | 11 |   12 | 13 | 14 | 15 |   16 | 17 | 18 | 19 |   20 | 21 | 22 | 23 |   24 | 25 | 26 | 27 |

28 | 29 | 30 | --- 31 | 32 | ### Usage 33 | 34 | ``` bash 35 | yarn add vuniversal 36 | 37 | # init vun config file 38 | npx vun init 39 | ``` 40 | 41 | **package.json** 42 | 43 | ```json 44 | "scripts": { 45 | "dev": "vun", 46 | "build": "vun build", 47 | "test": "vun test", 48 | "lint": "eslint --ext .js,.ts,.vue src" 49 | } 50 | ``` 51 | 52 | ### Config 53 | 54 | **vun.config.js** 55 | 56 | ```Ts 57 | export interface VunLibConfig { 58 | universal: boolean 59 | modern: boolean 60 | clientEntry: string 61 | serverEntry: string 62 | template: string 63 | inspect: boolean 64 | prerender: false | { 65 | routes: string[] 66 | fallback: true | string 67 | options: object 68 | } 69 | lintOnSave: boolean | 'default' | 'warning' | 'error' 70 | dir: { 71 | build: string 72 | public: string 73 | source: string 74 | root: string 75 | modules: string[] 76 | } 77 | env: VunEnvObject 78 | dev: { 79 | host: string 80 | port: number 81 | verbose: boolean 82 | proxy: WebpackDevServer.ProxyConfigMap | WebpackDevServer.ProxyConfigArray 83 | devServer: WebpackDevServer.Configuration 84 | } 85 | build: { 86 | analyze: boolean | object 87 | publicPath: string 88 | assetsDir: string 89 | filenameHashing: boolean 90 | runtimeCompiler: boolean 91 | transpileDependencies: Array 92 | productionSourceMap: boolean 93 | parallel: boolean | number 94 | crossorigin: false | '' | 'anonymous' | 'use-credentials' 95 | optimization: webpack.Configuration['optimization'] 96 | css: { 97 | requireModuleExtension: boolean 98 | extract: boolean | { 99 | filename: string; 100 | chunkFilename: string 101 | } 102 | sourceMap: boolean 103 | styleResources: { 104 | scss: string[] 105 | sass: string[] 106 | less: string[] 107 | stylus: string[] 108 | } 109 | } 110 | loaders: { 111 | vue: LoaderOptions 112 | imgUrl: LoaderOptions 113 | fontUrl: LoaderOptions 114 | mediaUrl: LoaderOptions 115 | svgFile: LoaderOptions 116 | css: LoaderOptions 117 | scss: LoaderOptions 118 | sass: LoaderOptions 119 | less: LoaderOptions 120 | stylus: LoaderOptions 121 | postcss: LoaderOptions 122 | vueStyle: LoaderOptions 123 | } 124 | } 125 | babel: any 126 | webpack: ((config: webpack.Configuration, buildContext: BuildContext) => (webpack.Configuration | void)) 127 | typescript: boolean | { 128 | tsLoader: Partial 129 | forkTsChecker: boolean | Partial 130 | } 131 | } 132 | ``` 133 | 134 | ### Changelog 135 | 136 | Detailed changes for each release are documented in the [release notes](https://github.com/surmon-china/vuniversal/blob/master/CHANGELOG.md). 137 | 138 | ### License 139 | 140 | [MIT](https://github.com/surmon-china/vuniversal/blob/master/LICENSE) 141 | -------------------------------------------------------------------------------- /cli/configs/vuniversal/interface.ts: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack' 2 | import WebpackDevServer from 'webpack-dev-server' 3 | import WebpackBundleAnalyzer from 'webpack-bundle-analyzer' 4 | import { Options as TsLoaderOptions } from 'ts-loader' 5 | import { Options as ForkTsCheckerOptions } from 'fork-ts-checker-webpack-plugin' 6 | import { VueEnv } from '@cli/environment' 7 | 8 | export interface VunEnvObject { 9 | [key: string]: any 10 | } 11 | 12 | interface LoaderOptions { 13 | [key: string]: any 14 | } 15 | 16 | type BuildContext = { 17 | target: VueEnv 18 | } 19 | 20 | type RecursivePartial = { 21 | [P in keyof T]?: 22 | T[P] extends (infer U)[] ? RecursivePartial[] : 23 | T[P] extends object ? RecursivePartial : 24 | T[P] 25 | } 26 | 27 | // For user 28 | export type VuniversalConfig = RecursivePartial 29 | // For vun 30 | export interface VunLibConfig { 31 | // 是否 SSR 32 | universal: boolean 33 | // 是否 modern 34 | modern: boolean 35 | // 客户端入口 36 | clientEntry: string 37 | // 服务端入口 38 | serverEntry: string 39 | // 模板 40 | template: string 41 | // 是否打印 webpack 配置 42 | inspect: boolean 43 | // 转换配置 44 | prerender: false | { 45 | // 需要转换的路由 46 | routes: string[] 47 | // 是否回退为 SPA 48 | fallback: true | string 49 | // options of https://github.com/chrisvfritz/prerender-spa-plugin 50 | options: object 51 | } 52 | // Eslint 53 | lintOnSave: boolean | 'default' | 'warning' | 'error' 54 | // 目录配置 55 | dir: { 56 | // 构建出的路径 57 | build: string 58 | // 静态资源路径 59 | public: string 60 | // 项目源码路径 61 | source: string 62 | // 项目根目录 63 | root: string 64 | // node_modules 路径 65 | modules: string[] 66 | } 67 | // 环境配置 68 | env: VunEnvObject 69 | dev: { 70 | // 端口地址 71 | host: string 72 | port: number 73 | // 各种信息 74 | verbose: boolean 75 | // 代理 76 | proxy: WebpackDevServer.ProxyConfigMap | WebpackDevServer.ProxyConfigArray 77 | // webpack dev server 78 | devServer: WebpackDevServer.Configuration 79 | } 80 | build: { 81 | // 统计配置 82 | analyze: boolean | WebpackBundleAnalyzer.BundleAnalyzerPlugin.Options 83 | // CDN PATH 84 | publicPath: string 85 | // 输出的 assets 文件夹,相对于 build 的路径 86 | assetsDir: string 87 | // 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存 88 | filenameHashing: boolean 89 | // 是否包含运行时编译 90 | runtimeCompiler: boolean 91 | // 默认情况下 babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来 92 | transpileDependencies: Array 93 | // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 94 | productionSourceMap: boolean 95 | // 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。 96 | parallel: boolean | number 97 | html: { 98 | // https://cli.vuejs.org/zh/config/#crossorigin 99 | crossorigin: false | '' | 'anonymous' | 'use-credentials' 100 | preload: boolean | 'preload' | 'prefetch' | 'auto' 101 | // TODO 102 | ext: any 103 | }, 104 | // webpack optimization 105 | optimization: webpack.Configuration['optimization'] 106 | // 有关样式的配置项 107 | css: { 108 | // 默认情况下,只有 *.module.[ext] 结尾的文件才会被视作 CSS Modules 模块。设置为 false 后你就可以去掉文件名中的 .module 并将所有的 *.(css|scss|sass|less|styl(us)?) 文件视为 CSS Modules 模块。 109 | requireModuleExtension: boolean 110 | // mini-css-extract-plugin options 111 | extract: boolean | { 112 | filename: string; 113 | chunkFilename: string 114 | } 115 | sourceMap: boolean 116 | styleResources: { 117 | scss: string[] 118 | sass: string[] 119 | less: string[] 120 | stylus: string[] 121 | } 122 | } 123 | loaders: { 124 | vue: LoaderOptions 125 | imgUrl: LoaderOptions 126 | fontUrl: LoaderOptions 127 | mediaUrl: LoaderOptions 128 | svgFile: LoaderOptions 129 | css: LoaderOptions 130 | scss: LoaderOptions 131 | sass: LoaderOptions 132 | less: LoaderOptions 133 | stylus: LoaderOptions 134 | postcss: LoaderOptions 135 | vueStyle: LoaderOptions 136 | } 137 | } 138 | babel: object | ((buildContext: BuildContext) => object) 139 | webpack: webpack.Configuration | ((config: webpack.Configuration, buildContext: BuildContext) => (webpack.Configuration | void)) 140 | typescript: boolean | { 141 | tsLoader: Partial 142 | forkTsChecker: boolean | Partial 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuniversal", 3 | "version": "0.0.13", 4 | "description": "Create vue universal app", 5 | "homepage": "https://github.com/surmon-china/vuniversal#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/surmon-china/vuniversal.git" 9 | }, 10 | "keywords": [ 11 | "vue", 12 | "ssr", 13 | "universal" 14 | ], 15 | "author": { 16 | "name": "Surmon", 17 | "email": "surmon@foxmail.com", 18 | "url": "https://github.com/surmon-china" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/surmon-china/vuniversal/issues" 22 | }, 23 | "license": "MIT", 24 | "main": "vun-dist/index.js", 25 | "types": "vun-dist/index.d.ts", 26 | "files": [ 27 | "bin", 28 | "cli-dist", 29 | "vun-dist" 30 | ], 31 | "bin": { 32 | "vun": "bin/vun.js" 33 | }, 34 | "scripts": { 35 | "cleanup:cli": "rm -rf ./cli-dist ./.cache/cli.tsbuildinfo", 36 | "cleanup:vun": "rm -rf ./vun-dist ./.cache/vun.tsbuildinfo", 37 | "cleanup": "npm run cleanup:cli && npm run cleanup:vun && rm -rf ./.cache", 38 | "dev:cli": "ttsc -p ./tsconfig.cli.json -w", 39 | "dev:vun": "tsc -p ./tsconfig.vun.json -w", 40 | "dev": "npm run dev:cli & npm run dev:vun", 41 | "build:cli": "npm run cleanup:cli && ttsc -p ./tsconfig.cli.json", 42 | "build:vun": "npm run cleanup:vun && tsc -p ./tsconfig.vun.json", 43 | "build": "npm run build:cli && npm run build:vun", 44 | "lint": "eslint --ext .ts cli vuniversal", 45 | "test": "jest", 46 | "test:watch": "jest --watch -i", 47 | "rebirth": "npm run lint && npm run build", 48 | "release": ". ./scripts/release.sh" 49 | }, 50 | "peerDependencies": { 51 | "@vue/compiler-sfc": "3.x", 52 | "@vue/server-renderer": "3.x", 53 | "vue": "3.x" 54 | }, 55 | "dependencies": { 56 | "@babel/core": "^7.9.6", 57 | "@babel/plugin-transform-runtime": "^7.9.6", 58 | "@types/commander": "^2.12.2", 59 | "@vue/babel-preset-app": "^4.3.1", 60 | "assets-webpack-plugin": "^3.9.12", 61 | "babel-loader": "^8.1.0", 62 | "boxen": "^4.2.0", 63 | "case-sensitive-paths-webpack-plugin": "^2.3.0", 64 | "chalk": "4.0.0", 65 | "clean-webpack-plugin": "^3.0.0", 66 | "cli-highlight": "^2.1.4", 67 | "commander": "^5.1.0", 68 | "connect-history-api-fallback": "^1.6.0", 69 | "copy-webpack-plugin": "^5.1.1", 70 | "core-js": "^3.6.5", 71 | "css-loader": "^3.5.3", 72 | "didyoumean": "^1.2.1", 73 | "eslint-loader": "^4.0.2", 74 | "express": "^4.17.1", 75 | "file-loader": "^6.0.0", 76 | "fork-ts-checker-webpack-plugin": "^4.1.3", 77 | "friendly-errors-webpack-plugin": "^2.0.0-beta.2", 78 | "fs-extra": "9.0.0", 79 | "fs-monkey": "^1.0.0", 80 | "html-webpack-plugin": "^4.3.0", 81 | "http-proxy-middleware": "^1.0.3", 82 | "javascript-stringify": "^2.0.1", 83 | "lodash": "^4.17.15", 84 | "mini-css-extract-plugin": "^0.9.0", 85 | "mkdirp": "^0.5.5", 86 | "node-notifier": "^7.0.0", 87 | "open": "^7.0.4", 88 | "optimize-css-assets-webpack-plugin": "^5.0.3", 89 | "pnp-webpack-plugin": "^1.6.4", 90 | "postcss-flexbugs-fixes": "^4.2.1", 91 | "postcss-loader": "^3.0.0", 92 | "postcss-preset-env": "^6.7.0", 93 | "postcss-safe-parser": "^4.0.2", 94 | "prerender-spa-plugin": "^3.4.0", 95 | "pretty-bytes": "^5.3.0", 96 | "semver": "^7.3.2", 97 | "start-server-webpack-plugin": "^2.2.5", 98 | "strip-ansi": "6.0.0", 99 | "style-loader": "^1.2.1", 100 | "style-resources-loader": "^1.3.3", 101 | "terser-webpack-plugin": "^3.0.1", 102 | "thread-loader": "^2.1.3", 103 | "ts-loader": "^7.0.3", 104 | "url-loader": "^4.1.0", 105 | "vue-loader": "^16.0.0-beta.1", 106 | "vue-style-loader": "^4.1.2", 107 | "webpack": "^v5.0.0-beta.16", 108 | "webpack-bundle-analyzer": "^3.7.0", 109 | "webpack-dev-server": "^3.10.3", 110 | "webpack-manifest-plugin": "^3.0.0-rc.0", 111 | "webpack-merge": "^4.2.2", 112 | "webpack-node-externals": "^1.7.2", 113 | "webpackbar": "^4.0.0", 114 | "wrap-ansi": "^7.0.0" 115 | }, 116 | "devDependencies": { 117 | "@types/assets-webpack-plugin": "^3.9.0", 118 | "@types/case-sensitive-paths-webpack-plugin": "^2.1.4", 119 | "@types/chalk": "^2.2.0", 120 | "@types/clean-webpack-plugin": "^0.1.3", 121 | "@types/connect-history-api-fallback": "^1.3.3", 122 | "@types/copy-webpack-plugin": "^5.0.0", 123 | "@types/express": "^4.17.6", 124 | "@types/friendly-errors-webpack-plugin": "^0.1.2", 125 | "@types/fs-extra": "^8.1.0", 126 | "@types/http-proxy-middleware": "^0.19.3", 127 | "@types/lodash": "^4.14.150", 128 | "@types/mini-css-extract-plugin": "^0.9.1", 129 | "@types/node": "^13.13.5", 130 | "@types/node-notifier": "^6.0.1", 131 | "@types/optimize-css-assets-webpack-plugin": "^5.0.1", 132 | "@types/start-server-webpack-plugin": "^2.2.0", 133 | "@types/webpack-bundle-analyzer": "^2.13.3", 134 | "@types/webpack-dev-server": "^3.10.1", 135 | "@types/webpack-manifest-plugin": "^2.1.0", 136 | "@types/webpack-merge": "^4.1.5", 137 | "@types/webpack-node-externals": "^1.7.1", 138 | "@types/webpackbar": "^4.0.0", 139 | "@types/wrap-ansi": "^3.0.0", 140 | "@typescript-eslint/eslint-plugin": "^2.31.0", 141 | "@typescript-eslint/parser": "^2.31.0", 142 | "@vue/compiler-sfc": "^3.0.0-beta.10", 143 | "@vue/server-renderer": "^3.0.0-beta.10", 144 | "eslint": "^6.8.0", 145 | "jest": "^26.0.1", 146 | "ttypescript": "^1.5.10", 147 | "typescript": "^3.8.3", 148 | "typescript-transform-paths": "^1.1.14", 149 | "vue": "^3.0.0-beta.10", 150 | "vue-router": "^4.0.0-alpha.10" 151 | }, 152 | "engines": { 153 | "node": ">=12" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /vuniversal/helmet/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | inject, 3 | reactive, 4 | computed, 5 | watchEffect, 6 | // onMounted, 7 | onActivated, 8 | onDeactivated, 9 | App, 10 | Plugin, 11 | WatchStopHandle, 12 | UnwrapRef, 13 | HTMLAttributes, 14 | HtmlHTMLAttributes, 15 | BaseHTMLAttributes, 16 | MetaHTMLAttributes, 17 | LinkHTMLAttributes, 18 | StyleHTMLAttributes, 19 | ScriptHTMLAttributes, 20 | } from 'vue' 21 | 22 | interface Base { 23 | [key: string]: any 24 | } 25 | 26 | interface HelmeHTMLAttributes extends HtmlHTMLAttributes, Base { 27 | xmlns?: string 28 | lang?: string 29 | } 30 | 31 | interface HelmetData { 32 | title: string 33 | base: BaseHTMLAttributes & Base 34 | htmlAttributes: HelmeHTMLAttributes 35 | bodyAttributes: HTMLAttributes & Base 36 | meta: Array 37 | link: Array 38 | style: Array 39 | script: Array 40 | noscript: string 41 | } 42 | 43 | const DEFAULT_STATE: HelmetData = { 44 | title: '', 45 | base: {}, 46 | htmlAttributes: {}, 47 | bodyAttributes: {}, 48 | meta: [], 49 | link: [], 50 | style: [], 51 | script: [], 52 | noscript: '' 53 | } 54 | 55 | export type HelmetComputedState = UnwrapRef 56 | export type HelmetState = Partial 57 | export type HelmetConfig = Partial 58 | 59 | interface HelmetConfigData { 60 | autoRefresh?: boolean 61 | attribute?: string 62 | ssrAttribute?: string 63 | } 64 | 65 | const DEFAULT_CONFIG: HelmetConfig = { 66 | autoRefresh: true, 67 | attribute: 'data-vun', 68 | ssrAttribute: 'data-vun-server-rendered', 69 | } 70 | 71 | // 1. 靠注册不断地驱动和注册,不响应式,当一个组件的数据释放后,我的本地无法移除对应 dom 72 | // 2. 靠生命周期维护,当一个组件应用,则注册;当组件释放,则删除 73 | // 维护一个栈,当有新的注册时,放进队列,当组件释放时,移除,计算出一份数据,这份数据是合并后的数据,并实施更新 74 | const HELMET_KEY = Symbol('helmet') 75 | 76 | interface HelmeStackItem { 77 | id: string 78 | state: HelmetState 79 | } 80 | 81 | export type Helme = ReturnType 82 | function getHelmet(initState: HelmetState, initConfig?: HelmetConfig) { 83 | 84 | // Base state 85 | const baseStateObject = Object.freeze({ 86 | ...DEFAULT_STATE, 87 | ...initState 88 | }) 89 | const baseState = { 90 | id: '1', 91 | state: reactive({ ...baseStateObject }) 92 | } 93 | 94 | const helmetStateStack = reactive([baseState]) 95 | const helmetOption = { 96 | ...DEFAULT_CONFIG, 97 | ...initConfig, 98 | disposer: null as WatchStopHandle | null 99 | } 100 | 101 | // 1. 现在以堆栈的顺序为合成名称,是有问题的,应该以组件的嵌套层级为单位 102 | // 2. 以堆栈顺序为合成条件,进入到一个页面,meta 就会被更新为最新,如果是子组件控制,子组件就应该自己控制 push remove 103 | const currentState = computed(() => { 104 | return { 105 | title: helmetStateStack[helmetStateStack.length - 1].state.title || '', 106 | base: {}, 107 | htmlAttributes: {}, 108 | bodyAttributes: {}, 109 | meta: [], 110 | link: [], 111 | style: [], 112 | script: [], 113 | noscript: '' 114 | } 115 | }) 116 | 117 | const autoUpdate = () => { 118 | helmetOption.disposer?.() 119 | helmetOption.disposer = watchEffect(() => { 120 | document.title = currentState.value.title 121 | // TODO: 其他的更改 122 | }) 123 | } 124 | const stopUpdate = () => { 125 | helmetOption.disposer?.() 126 | helmetOption.disposer = null 127 | } 128 | 129 | if (helmetOption.autoRefresh) { 130 | autoUpdate() 131 | } 132 | 133 | return { 134 | push(id: string, state: HelmetState): void { 135 | helmetStateStack.push({ id, state }) 136 | }, 137 | remove(id: string): void { 138 | const index = helmetStateStack.findIndex(item => item.id === id) 139 | helmetStateStack.splice(index, 1) 140 | }, 141 | clear(): void { 142 | helmetStateStack.splice(0, helmetStateStack.length) 143 | }, 144 | reset(): void { 145 | helmetStateStack.splice(0, helmetStateStack.length, baseState) 146 | }, 147 | pause() { 148 | stopUpdate() 149 | }, 150 | resume(): void { 151 | autoUpdate() 152 | }, 153 | refresh(): void { 154 | // 刷新 DOM 155 | }, 156 | get state() { 157 | // TODO: 不应该是响应式数据 158 | return { ...currentState.value } 159 | }, 160 | get html() { 161 | // 获取最新的混合后的结果的 html 化内容 162 | return { 163 | get title() { 164 | return `${currentState.value.title}` 165 | }, 166 | meta: baseStateObject.meta.map(meta => ``) 167 | } 168 | // htmlAttributes: {}, 169 | // bodyAttributes: {}, 170 | // meta: [], 171 | // link: [], 172 | // style: [], 173 | // script: [], 174 | // noscript: '' 175 | } 176 | } 177 | } 178 | 179 | export function createHelmet(initState: HelmetState, config?: HelmetConfig): Helme & Plugin { 180 | let installed = false 181 | const helmet = getHelmet(initState, config) 182 | return { 183 | ...helmet, 184 | install(app: App) { 185 | if (!installed) { 186 | app.provide(HELMET_KEY, helmet) 187 | app.mixin({ 188 | created () { 189 | 190 | }, 191 | mounted () { 192 | // init() 193 | }, 194 | beforeDestroy () { 195 | // destroy() 196 | } 197 | }) 198 | installed = true 199 | } 200 | } 201 | } 202 | } 203 | 204 | export function useHelmet(state?: HelmetState) { 205 | const helmet = inject(HELMET_KEY) 206 | if (helmet && state) { 207 | const id = new Date().getTime().toString() 208 | const push = () => helmet.push(id, state) 209 | const remove = () => helmet.remove(id) 210 | 211 | push() 212 | onActivated(push) 213 | onDeactivated(remove) 214 | } 215 | // TODO: 216 | // onmounted -> push 217 | // ondestory -> unshift 218 | return helmet as Helme 219 | } 220 | -------------------------------------------------------------------------------- /cli/configs/css/index.ts: -------------------------------------------------------------------------------- 1 | // Fork form: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/config/css.js 2 | import path from 'path' 3 | import { Configuration, RuleSetRule } from 'webpack' 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin' 5 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin' 6 | import { isDev, isClientTarget } from '@cli/environment' 7 | import { findExistingFile, requireResolve } from '@cli/utils' 8 | import { BuildContext } from '../webpack' 9 | import { appPackageJSON } from '../package' 10 | import { autoContentHash } from '../webpack/helper' 11 | import { vunConfig } from '../vuniversal' 12 | 13 | export function modifyCSSConfig(webpackConfig: Configuration, buildContext: BuildContext): void { 14 | const IS_DEV = isDev(buildContext.environment) 15 | const IS_CLIENT = isClientTarget(buildContext.target) 16 | 17 | const buildOptions = vunConfig.build 18 | const loaderOptions = buildOptions.loaders || {} 19 | const cssOptions = buildOptions.css 20 | const styleResources = cssOptions.styleResources || {} 21 | const sourceMap = cssOptions.sourceMap 22 | 23 | // About css module 24 | let { requireModuleExtension } = cssOptions 25 | if (typeof requireModuleExtension === 'undefined') { 26 | if (loaderOptions.css?.modules) { 27 | throw new Error('`css.requireModuleExtension` is required when custom css modules options provided') 28 | } 29 | requireModuleExtension = true 30 | } 31 | 32 | const { extract } = buildOptions.css 33 | const shouldExtract = extract !== false 34 | const canExtract = IS_CLIENT 35 | 36 | const filename = path.posix.join( 37 | buildOptions.assetsDir, 38 | `css/[name]${autoContentHash(vunConfig)}.css` 39 | ) 40 | 41 | const extractOptions = { 42 | filename, 43 | chunkFilename: filename, 44 | ...(typeof extract === 'object' ? extract : {}) 45 | } 46 | 47 | // check if the project has a valid postcss config 48 | // if it doesn't, don't use postcss-loader for direct style imports 49 | // because otherwise it would throw error when attempting to load postcss config 50 | const hasPostCSSConfig = !!( 51 | loaderOptions.postcss || 52 | appPackageJSON.postcss || 53 | findExistingFile([ 54 | '.postcssrc', 55 | '.postcssrc.js', 56 | 'postcss.config.js', 57 | '.postcssrc.yaml', 58 | '.postcssrc.json' 59 | ]) 60 | ) 61 | 62 | if (!hasPostCSSConfig) { 63 | loaderOptions.postcss = { 64 | plugins: () => [require('autoprefixer')] 65 | } 66 | } 67 | 68 | // if building for production but not extracting CSS, we need to minimize 69 | // the embbeded inline CSS as they will not be going through the optimizing 70 | // plugin. 71 | const needInlineMinification = !IS_DEV && !shouldExtract 72 | 73 | // https://cssnano.co/guides/optimisations 74 | // MRAK: duplicated!! https://github.com/NMFR/optimize-css-assets-webpack-plugin/issues/45#issuecomment-380738707 75 | const cssnanoOptions: any = { 76 | preset: ['default', { 77 | mergeLonghand: false, 78 | cssDeclarationSorter: false, 79 | autoprefixer: { disable: true }, 80 | }] 81 | } 82 | if (buildOptions.productionSourceMap && sourceMap) { 83 | cssnanoOptions.map = { inline: false } 84 | } 85 | 86 | function createCSSRule(params: { test: RegExp, loader?: string, options?: any, resources?: string[] }): RuleSetRule { 87 | 88 | const createLoaders = (rule: RuleSetRule, isCssModule: boolean): RuleSetRule => { 89 | rule.use = [] 90 | 91 | // extract 92 | if (shouldExtract && canExtract) { 93 | rule.use.push({ 94 | loader: MiniCssExtractPlugin.loader, 95 | options: { 96 | hmr: IS_DEV, 97 | // TODO: test 98 | // use relative publicPath in extracted CSS based on extract location 99 | publicPath: '../'.repeat( 100 | extractOptions.filename 101 | .replace(/^\.[\/\\]/, '') 102 | .split(/[\/\\]/g) 103 | .length - 1 104 | ) 105 | } 106 | }) 107 | } else { 108 | rule.use.push({ 109 | loader: requireResolve('vue-style-loader'), 110 | options: { 111 | sourceMap, 112 | ...loaderOptions.vueStyle 113 | } 114 | }) 115 | } 116 | 117 | const cssLoaderOptions: any = { 118 | sourceMap, 119 | importLoaders: ( 120 | 1 + // stylePostLoader injected by vue-loader 121 | 1 + // postcss-loader 122 | (needInlineMinification ? 1 : 0) 123 | ), 124 | ...loaderOptions.css 125 | } 126 | 127 | // css-loader options 128 | if (isCssModule) { 129 | cssLoaderOptions.modules = { 130 | localIdentName: '[name]_[local]_[hash:base64:5]', 131 | ...cssLoaderOptions.modules 132 | } 133 | } else { 134 | delete cssLoaderOptions.modules 135 | } 136 | 137 | // css-loader 138 | rule.use.push({ 139 | loader: requireResolve('css-loader'), 140 | options: cssLoaderOptions 141 | }) 142 | 143 | // inline 144 | if (needInlineMinification) { 145 | rule.use.push({ 146 | loader: requireResolve('postcss-loader'), 147 | options: { 148 | sourceMap, 149 | plugins: () => [require('cssnano')(cssnanoOptions)] 150 | } 151 | }) 152 | } 153 | 154 | // postcss 155 | rule.use.push({ 156 | loader: requireResolve('postcss-loader'), 157 | options: { sourceMap, ...loaderOptions.postcss } 158 | }) 159 | 160 | // loader 161 | if (params.loader) { 162 | let resolvedLoader 163 | try { 164 | resolvedLoader = require.resolve(params.loader) 165 | } catch (error) { 166 | resolvedLoader = params.loader 167 | } 168 | 169 | rule.use.push({ 170 | loader: resolvedLoader, 171 | options: { sourceMap, ...params.options } 172 | }) 173 | } 174 | 175 | // style-resource-loader 176 | if (params.resources?.length) { 177 | rule.use.push({ 178 | // https://github.com/yenshih/style-resources-loader 179 | loader: requireResolve('style-resources-loader'), 180 | options: { 181 | patterns: params.resources 182 | } 183 | }) 184 | } 185 | 186 | return rule 187 | } 188 | 189 | return { 190 | test: params.test, 191 | oneOf: [ 192 | // rules for