├── 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