diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..7e3649acc2c1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000000..9aeb49c1f994 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,5 @@ +node_modules/ +lib/ +dist/ +template/ +fixtures/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000000..d0c85faaf95f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,48 @@ +{ + "parser": "babel-eslint", + "extends": "airbnb", + "env": { + "mocha": true + }, + "rules": { + "jsx-a11y/href-no-hash": [0], + "generator-star-spacing": [0], + "consistent-return": [0], + "react/react-in-jsx-scope": [0], + "react/forbid-prop-types": [0], + "react/jsx-filename-extension": [1, { "extensions": [".js"] }], + "global-require": [1], + "import/prefer-default-export": [0], + "react/jsx-no-bind": [0], + "react/prop-types": [0], + "react/prefer-stateless-function": [0], + "no-else-return": [0], + "no-restricted-syntax": [0], + "import/no-extraneous-dependencies": [0], + "no-use-before-define": [0], + "jsx-a11y/no-static-element-interactions": [0], + "no-nested-ternary": [0], + "arrow-body-style": [0], + "import/extensions": [0], + "no-bitwise": [0], + "no-cond-assign": [0], + "import/no-unresolved": [0], + "require-yield": [1], + "no-param-reassign": [0], + "no-shadow": [0], + "no-underscore-dangle": [0], + "spaced-comment": [0], + "indent": [0], + "quotes": [0], + "func-names": [0], + "arrow-parens": [0], + "space-before-function-paren": [0], + "no-useless-escape": [0], + "object-curly-newline": [0] + }, + "parserOptions": { + "ecmaFeatures": { + "experimentalObjectRestSpread": true + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..f39211d98444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +tmp +/node_modules +/packages/*/node_modules +/boilerplates/*/node_modules +/boilerplates/*/www +/boilerplates/*/_dist +/boilerplates/*/_package +lib +dist +coverage +.nyc_output +npm-debug.log* +lerna-debug.log +.changelog +.koi +/boilerplates/*/node_modules diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000000..1fd5deb8f98e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +**/fixtures/**/*.js +**/template/*.js +**/bin/*.js diff --git a/README.md b/README.md new file mode 100644 index 000000000000..cf5251318731 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# umi + +> under development. + diff --git a/lerna.json b/lerna.json new file mode 100644 index 000000000000..c959eaa8a4a1 --- /dev/null +++ b/lerna.json @@ -0,0 +1,8 @@ +{ + "lerna": "2.0.0", + "packages": [ + "packages/*" + ], + "npmClient": "npm", + "version": "independent" +} diff --git a/package.json b/package.json new file mode 100644 index 000000000000..5122a9ffc129 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "scripts": { + "build": "lerna exec --bail --parallel -- npm run build --", + "debug": "lerna run debug", + "lint": "eslint --ext .js packages", + "precommit": "lint-staged", + "publish": "./scripts/publish.js" + }, + "lint-staged": { + "*.js": [ + "prettier --trailing-comma all --single-quote --write", + "git add" + ] + }, + "devDependencies": { + "babel-eslint": "^8.0.2", + "eslint": "^4.10.0", + "eslint-config-airbnb": "^16.1.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jsx-a11y": "^6.0.2", + "eslint-plugin-react": "^7.4.0", + "expect": "^1.20.2", + "husky": "^0.14.3", + "lerna": "^2.0.0", + "lint-staged": "^4.0.4", + "prettier": "^1.8.1", + "ruban": "^0.2.1", + "shelljs": "^0.7.8" + } +} diff --git a/packages/af-webpack/Configuration.md b/packages/af-webpack/Configuration.md new file mode 100644 index 000000000000..e3af80acf562 --- /dev/null +++ b/packages/af-webpack/Configuration.md @@ -0,0 +1,43 @@ +# Configuration + +## How to config + +Config via `.webpackrc`, e.g. + +```js +{ + "alias": { "react": "preact-compat" } +} +``` + +If you like to write config in JavaScript, config in `.webpackrc.js`, e.g. + +```js +export default { + alias: { + react: 'preact-compat', + }, +} +``` + +## Options + +| | Default Value | Notes | +| :--- | :--- | :--- | +| entry | null | | +| browsers | [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9' ] | | +| theme | {} | | +| babel | | | +| define | {} | | +| outputPath | null | | +| publicPath | undefined | | +| commons | [] | | +| hash | false | | +| externals | {} | | +| copy | [] | | +| enableCSSModules | false | | +| extraBabelIncludes | [] | | +| extraResolveExtensions | [] | | +| extraPostCSSPlugins | [] | | +| ignoreMomentLocale | false | | +| extraResolveModules | [] | | diff --git a/packages/af-webpack/README.md b/packages/af-webpack/README.md new file mode 100644 index 000000000000..447191620f87 --- /dev/null +++ b/packages/af-webpack/README.md @@ -0,0 +1,85 @@ +# af-webpack + +Unique webpack wrapper for ant financial (af). + +## Why af-webpack ? + +TODO + +## CLIs based on af-webpack + +* umi +* [roadhog@2](https://github.com/sorrycc/roadhog/tree/2.0) + +## Configuration + +See [./Configuration.md](./Configuration.md). + +## API + +### af-webpack/getConfig + +Get webpack config with opts. + +```js +const webpackConfig = getConfig(opts); +// use webpackConfig to dev or build +``` + +### af-webpack/dev + +Run webpack-dev-server more gracefully with [react-dev-utils](https://github.com/facebookincubator/create-react-app/tree/master/packages/react-dev-utils). + +```js +dev({ + webpackConfig, + appName, + extraMiddlewares, + beforeServer, +}); +``` + +webpackConfig is required, other optional. + +Options: + +* `webpackConfig`: the webpack config +* `appName`: the default is "Your Project", text to show after dev server is started +* `extraMiddlewares`: extra middlewares for webpack-dev-server, based on express +* `beforeServer`: the function to execute before dev server is started + +### af-webpack/build + +Run webpack compilation. + +```js +build({ + webpackConfig, + success, +}); +``` + +webpackConfig is required, other optional. + +Options: + +* `webpackConfig`: the webpack config +* `success`: the function to execute after build is done successfully + +### af-webpack/react-dev-utils + +The APIs related to react-dev-utils. + +* webpackHotDevClientPath:the real path of webpackHotDevClient + +### af-webpack/webpack + +The webpack, useful to register extra webpack plugins. + +### af-webpack/registerBabel + +Register babel for extra files. + +## LICENSE + +MIT diff --git a/packages/af-webpack/build.js b/packages/af-webpack/build.js new file mode 100644 index 000000000000..f56078935fe6 --- /dev/null +++ b/packages/af-webpack/build.js @@ -0,0 +1 @@ +module.exports = require('./lib/build'); diff --git a/packages/af-webpack/dev.js b/packages/af-webpack/dev.js new file mode 100644 index 000000000000..a3cb5fa09673 --- /dev/null +++ b/packages/af-webpack/dev.js @@ -0,0 +1 @@ +module.exports = require('./lib/dev'); diff --git a/packages/af-webpack/getConfig.js b/packages/af-webpack/getConfig.js new file mode 100644 index 000000000000..4c941536c898 --- /dev/null +++ b/packages/af-webpack/getConfig.js @@ -0,0 +1 @@ +module.exports = require('./lib/getConfig'); diff --git a/packages/af-webpack/package.json b/packages/af-webpack/package.json new file mode 100644 index 000000000000..4e09544e269a --- /dev/null +++ b/packages/af-webpack/package.json @@ -0,0 +1,81 @@ +{ + "name": "af-webpack", + "version": "0.9.1", + "dependencies": { + "@babel/core": "7.0.0-beta.31", + "@babel/plugin-transform-modules-commonjs": "7.0.0-beta.31", + "@babel/register": "7.0.0-beta.31", + "@babel/runtime": "7.0.0-beta.31", + "assert": "^1.4.1", + "autoprefixer": "^7.1.4", + "babel-eslint": "^8.0.1", + "babel-loader": "^8.0.0-beta.0", + "babel-plugin-add-module-exports": "^0.2.1", + "babel-preset-af-react": "^0.6.0", + "case-sensitive-paths-webpack-plugin": "^2.1.1", + "chalk": "^2.1.0", + "copy-webpack-plugin": "^4.2.0", + "css-loader": "^0.28.7", + "debug": "^3.1.0", + "detect-port": "^1.2.1", + "eslint": "^4.7.1", + "eslint-config-af-react": "^0.4.0", + "eslint-loader": "^1.9.0", + "eslint-plugin-flowtype": "^2.34.1", + "eslint-plugin-import": "^2.6.0", + "eslint-plugin-jsx-a11y": "^5.1.1", + "eslint-plugin-react": "^7.1.0", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^0.11.2", + "inquirer": "^3.3.0", + "is-plain-object": "^2.0.4", + "is-root": "^1.0.0", + "less": "^2.7.2", + "less-loader": "^4.0.5", + "postcss": "^6.0.11", + "postcss-flexbugs-fixes": "^3.2.0", + "postcss-loader": "^2.0.6", + "react-dev-utils": "^4.0.1", + "react-error-overlay": "^3.0.0", + "resolve": "^1.5.0", + "sockjs-client": "1.1.4", + "strip-ansi": "3.0.1", + "style-loader": "^0.18.2", + "system-bell-webpack-plugin": "^1.0.0", + "url-loader": "^0.6.2", + "webpack": "^3.5.6", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-server": "^2.9.4" + }, + "repository": { + "type": "git", + "url": "https://github.com/umijs/umi/tree/master/packages/af-webpack" + }, + "homepage": "https://github.com/umijs/umi/tree/master/packages/af-webpack", + "authors": [ + "chencheng (https://github.com/sorrycc)" + ], + "bugs": { + "url": "https://github.com/umijs/umi/issues" + }, + "scripts": { + "build": "NODE_TARGET=1 ruban build", + "test": "ruban test --timeout 200000", + "debug": "ruban debug --timeout 200000", + "coveralls": "ruban coveralls" + }, + "devDependencies": { + "glob": "^7.1.2" + }, + "files": [ + "lib", + "src", + "build.js", + "dev.js", + "getConfig.js", + "react-dev-utils.js", + "registerBabel.js", + "webpack.js" + ], + "license": "MIT" +} diff --git a/packages/af-webpack/react-dev-utils.js b/packages/af-webpack/react-dev-utils.js new file mode 100644 index 000000000000..68ae80d9a785 --- /dev/null +++ b/packages/af-webpack/react-dev-utils.js @@ -0,0 +1 @@ +module.exports = require('./lib/reactDevUtils'); diff --git a/packages/af-webpack/registerBabel.js b/packages/af-webpack/registerBabel.js new file mode 100644 index 000000000000..e73773bb9035 --- /dev/null +++ b/packages/af-webpack/registerBabel.js @@ -0,0 +1 @@ +module.exports = require('./lib/registerBabel'); diff --git a/packages/af-webpack/src/applyWebpackConfig.js b/packages/af-webpack/src/applyWebpackConfig.js new file mode 100644 index 000000000000..32cd2a0a8f29 --- /dev/null +++ b/packages/af-webpack/src/applyWebpackConfig.js @@ -0,0 +1,30 @@ +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import chalk from 'chalk'; +import webpack from 'webpack'; + +export function warnIfExists() { + const filePath = resolve('webpack.config.js'); + if (existsSync(filePath)) { + console.log( + chalk.yellow( + `⚠️ ⚠️ ⚠️ It\\'s not recommended to use ${chalk.bold( + 'webpack.config.js', + )}, since it\\'s major or minor version upgrades may result in incompatibility. If you insist on doing so, please be careful of the compatibility after upgrading.`, + ), + ); + console.log(); + } +} + +export function applyWebpackConfig(config) { + const filePath = resolve('webpack.config.js'); + if (existsSync(filePath)) { + return require(filePath)(config, { + // eslint-disable-line + webpack, + }); + } else { + return config; + } +} diff --git a/packages/af-webpack/src/build.js b/packages/af-webpack/src/build.js new file mode 100644 index 000000000000..5be462d45237 --- /dev/null +++ b/packages/af-webpack/src/build.js @@ -0,0 +1,88 @@ +import webpack from 'webpack'; +import chalk from 'chalk'; +import { sync as rimraf } from 'rimraf'; +import assert from 'assert'; +import isPlainObject from 'is-plain-object'; +import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; +import printBuildError from 'react-dev-utils/printBuildError'; +import { printFileSizesAfterBuild } from 'react-dev-utils/FileSizeReporter'; +import { warnIfExists as warnIfWebpackConfigExists } from './applyWebpackConfig'; + +const debug = require('debug')('af-webpack:build'); + +process.env.NODE_ENV = 'production'; + +// These sizes are pretty large. We'll warn for bundles exceeding them. +const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; +const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; + +function buildWebpack(webpackConfig) { + debug( + `Clean output path ${webpackConfig.output.path.replace( + `${process.cwd()}/`, + '', + )}`, + ); + rimraf(webpackConfig.output.path); + + const compiler = webpack(webpackConfig); + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + return reject(err); + } + const messages = formatWebpackMessages(stats.toJson({}, true)); + if (messages.errors.length) { + if (messages.errors.length > 1) { + messages.errors.length = 1; + } + return reject(new Error(messages.errors.join('\n\n'))); + } + + return resolve({ + stats, + warnings: messages.warnings, + }); + }); + }); +} + +export default function build({ webpackConfig, success }) { + assert(webpackConfig, 'webpackConfig should be supplied.'); + assert(isPlainObject(webpackConfig), 'webpackConfig should be plain object.'); + + // 存在 webpack.config.js 时提醒用户 + warnIfWebpackConfigExists(); + + buildWebpack(webpackConfig) + .then(({ stats, warnings }) => { + if (warnings.length) { + console.log(chalk.yellow('Compiled with warnings.\n')); + console.log(warnings.join('\n\n')); + } else { + console.log(chalk.green('Compiled successfully.\n')); + } + + console.log('File sizes after gzip:\n'); + printFileSizesAfterBuild( + stats, + { + root: webpackConfig.output.path, + sizes: {}, + }, + webpackConfig.output.path, + WARN_AFTER_BUNDLE_GZIP_SIZE, + WARN_AFTER_CHUNK_GZIP_SIZE, + ); + console.log(); + + if (success) { + success({ stats, warnings }); + } + }) + .catch(err => { + console.log(chalk.red('Failed to compile.\n')); + printBuildError(err); + process.exit(1); + }); +} diff --git a/packages/af-webpack/src/choosePort.js b/packages/af-webpack/src/choosePort.js new file mode 100644 index 000000000000..d8dd1b5982c8 --- /dev/null +++ b/packages/af-webpack/src/choosePort.js @@ -0,0 +1,56 @@ +import detect from 'detect-port'; +import clearConsole from 'react-dev-utils/clearConsole'; +import getProcessForPort from 'react-dev-utils/getProcessForPort'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import isRoot from 'is-root'; + +const isInteractive = process.stdout.isTTY; + +export default function choosePort(defaultPort) { + return detect(defaultPort).then( + port => + new Promise(resolve => { + if (port === defaultPort) { + return resolve(port); + } + const message = + process.platform !== 'win32' && defaultPort < 1024 && !isRoot() + ? `Admin permissions are required to run a server on a port below 1024.` + : `Something is already running on port ${defaultPort}.`; + if (isInteractive) { + clearConsole(); + const existingProcess = getProcessForPort(defaultPort); + const question = { + type: 'confirm', + name: 'shouldChangePort', + message: + chalk.yellow( + `message${existingProcess // eslint-disable-line + ? ` Probably:\n ${existingProcess}` + : ''}`, + ) + '\n\nWould you like to run the app on another port instead?', + default: true, + }; + inquirer.prompt(question).then(answer => { + if (answer.shouldChangePort) { + resolve(port); + } else { + resolve(null); + } + }); + } else { + console.log(chalk.red(message)); + resolve(null); + } + }), + err => { + throw new Error( + chalk.red( + `Could not find an open port.\nNetwork error message: ${err.message || + err}\n`, + ), + ); + }, + ); +} diff --git a/packages/af-webpack/src/debugLoader.js b/packages/af-webpack/src/debugLoader.js new file mode 100644 index 000000000000..9702b011b402 --- /dev/null +++ b/packages/af-webpack/src/debugLoader.js @@ -0,0 +1,17 @@ + +export default function (source) { + const { resourcePath } = this; + const debugLoader = process.env.DEBUG_LOADER; + if (debugLoader && resourcePath.indexOf(debugLoader) > -1) { + console.log(''); + console.log(''); + console.log('-------------------------------'); + console.log(resourcePath); + console.log('==='); + console.log(source); + console.log('-------------------------------'); + console.log(''); + console.log(''); + } + this.callback(null, source); +} diff --git a/packages/af-webpack/src/defaultConfigs/babel.js b/packages/af-webpack/src/defaultConfigs/babel.js new file mode 100644 index 000000000000..72dbb3ef70b3 --- /dev/null +++ b/packages/af-webpack/src/defaultConfigs/babel.js @@ -0,0 +1,3 @@ +export default { + presets: [require.resolve('babel-preset-af-react')], +}; diff --git a/packages/af-webpack/src/defaultConfigs/browsers.js b/packages/af-webpack/src/defaultConfigs/browsers.js new file mode 100644 index 000000000000..8e24f0839a3d --- /dev/null +++ b/packages/af-webpack/src/defaultConfigs/browsers.js @@ -0,0 +1,6 @@ +export default [ + '>1%', + 'last 4 versions', + 'Firefox ESR', + 'not ie < 9', // React doesn't support IE8 anyway +]; diff --git a/packages/af-webpack/src/defaultConfigs/uglifyJS.js b/packages/af-webpack/src/defaultConfigs/uglifyJS.js new file mode 100644 index 000000000000..9fd9e4a6794c --- /dev/null +++ b/packages/af-webpack/src/defaultConfigs/uglifyJS.js @@ -0,0 +1,14 @@ +export default { + compress: { + screw_ie8: true, // React doesn't support IE8 + warnings: false, + }, + mangle: { + screw_ie8: true, + }, + output: { + comments: false, + screw_ie8: true, + ascii_only: true, + }, +}; diff --git a/packages/af-webpack/src/dev.js b/packages/af-webpack/src/dev.js new file mode 100644 index 000000000000..b21d86a485ff --- /dev/null +++ b/packages/af-webpack/src/dev.js @@ -0,0 +1,111 @@ +import { + createCompiler, + prepareUrls, +} from 'react-dev-utils/WebpackDevServerUtils'; +import clearConsole from 'react-dev-utils/clearConsole'; +import webpack from 'webpack'; +import WebpackDevServer from 'webpack-dev-server'; +import chalk from 'chalk'; +import errorOverlayMiddleware from './errorOverlayMiddleware'; +import send, { STARTING, COMPILING, DONE } from './send'; +import choosePort from './choosePort'; + +const isInteractive = process.stdout.isTTY; +const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 8000; +const HOST = '0.0.0.0'; +const PROTOCOL = 'http'; + +process.env.NODE_ENV = 'development'; + +export default function dev({ + webpackConfig, + appName, + extraMiddlewares, + beforeServer, + afterServer, + proxy, +}) { + if (!webpackConfig) { + throw new Error('必须提供 webpackConfig 配置项'); + } + choosePort(DEFAULT_PORT) + .then(port => { + if (port === null) { + return; + } + + if (beforeServer) { + beforeServer(); + } + + const urls = prepareUrls(PROTOCOL, HOST, port); + const compiler = createCompiler( + webpack, + webpackConfig, + appName || 'Your App', + urls, + ); + + // Webpack startup recompilation fix. Remove when @sokra fixes the bug. + // https://github.com/webpack/webpack/issues/2983 + // https://github.com/webpack/watchpack/issues/25 + const timefix = 11000; + compiler.plugin('watch-run', (watching, callback) => { + watching.startTime += timefix; + callback(); + }); + compiler.plugin('done', stats => { + stats.startTime -= timefix; + }); + + compiler.plugin('invalid', () => { + send({ type: COMPILING }); + }); + compiler.plugin('done', () => { + send({ type: DONE }); + }); + const serverConfig = { + disableHostCheck: true, + compress: true, + clientLogLevel: 'none', + hot: true, + quiet: true, + publicPath: webpackConfig.output.publicPath, + watchOptions: { + ignored: /node_modules/, + }, + historyApiFallback: { + disableDotRule: true, + }, + overlay: false, + host: HOST, + proxy, + before(app) { + if (extraMiddlewares) { + extraMiddlewares.forEach(middleware => { + app.use(middleware); + }); + } + app.use(errorOverlayMiddleware()); + }, + }; + const devServer = new WebpackDevServer(compiler, serverConfig); + devServer.listen(port, HOST, err => { + if (err) { + console.log(err); + return; + } + if (isInteractive) { + clearConsole(); + } + console.log(chalk.cyan('Starting the development server...\n')); + send({ type: STARTING }); + if (afterServer) { + afterServer(devServer); + } + }); + }) + .catch(err => { + console.log(err); + }); +} diff --git a/packages/af-webpack/src/errorOverlayMiddleware.js b/packages/af-webpack/src/errorOverlayMiddleware.js new file mode 100644 index 000000000000..6ce0e7e048f0 --- /dev/null +++ b/packages/af-webpack/src/errorOverlayMiddleware.js @@ -0,0 +1,21 @@ +import launchEditor from 'react-dev-utils/launchEditor'; +import launchEditorEndpoint from 'react-dev-utils/launchEditorEndpoint'; +import send, { OPEN_FILE } from './send'; + +export default function createLaunchEditorMiddleware() { + return function launchEditorMiddleware(req, res, next) { + if (req.url.startsWith(launchEditorEndpoint)) { + if (process.env.ALIPAY_EDITOR && process.send) { + send({ + type: OPEN_FILE, + payload: req.query, + }); + } else { + launchEditor(req.query.fileName, req.query.lineNumber); + } + res.end(); + } else { + next(); + } + }; +} diff --git a/packages/af-webpack/src/fork.js b/packages/af-webpack/src/fork.js new file mode 100644 index 000000000000..c8c19c6a98e0 --- /dev/null +++ b/packages/af-webpack/src/fork.js @@ -0,0 +1,15 @@ +import { fork } from 'child_process'; +import send, { RESTART } from './send'; + +export default function start(devScriptPath) { + const devProcess = fork(devScriptPath, process.argv.slice(2)); + + devProcess.on('message', data => { + const type = (data && data.type) || null; + if (type === RESTART) { + devProcess.kill('SIGINT'); + start(devScriptPath); + } + send(data); + }); +} diff --git a/packages/af-webpack/src/getConfig.js b/packages/af-webpack/src/getConfig.js new file mode 100644 index 000000000000..4ed07d3a6900 --- /dev/null +++ b/packages/af-webpack/src/getConfig.js @@ -0,0 +1,484 @@ +import webpack from 'webpack'; +import CaseSensitivePathsPlugin from 'case-sensitive-paths-webpack-plugin'; +import SystemBellWebpackPlugin from 'system-bell-webpack-plugin'; +import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'; +import ExtractTextPlugin from 'extract-text-webpack-plugin'; +import autoprefixer from 'autoprefixer'; +import { dirname, resolve, join } from 'path'; +import { existsSync } from 'fs'; +import eslintFormatter from 'react-dev-utils/eslintFormatter'; +import assert from 'assert'; +import isPlainObject from 'is-plain-object'; +import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import { sync as resolveSync } from 'resolve'; +import uglifyJSConfig from './defaultConfigs/uglifyJS'; +import babelConfig from './defaultConfigs/babel'; +import browsersConfig from './defaultConfigs/browsers'; +import stringifyObject from './stringifyObject'; +import normalizeTheme from './normalizeTheme'; +import { applyWebpackConfig } from './applyWebpackConfig'; + +const debug = require('debug')('af-webpack:getConfig'); + +// opts 包含: +// - cwd +// - browsers +// - extraPostCSSPlugins +// - enableCSSModules +// - theme +// - babel +// - noCompress +// - define +// - alias +// - outputPath +// - publicPath +// - entry +// - extraResolveModules +// - commons +// - hash +// - externals +// - extraBabelIncludes +// - extraResolveExtensions +// - ignoreMomentLocale +// - copy + +function invalidProp(obj, prop) { + return !(prop in obj) || obj[prop] === undefined; +} + +export default function getConfig(opts = {}) { + assert(opts.cwd, 'opts.cwd must be specified'); + assert(opts.outputPath, 'opts.outputPath must be specified'); + assert( + invalidProp(opts, 'browsers') || Array.isArray(opts.browsers), + `opts.browsers must be Array, but got ${opts.browsers}`, + ); + assert( + invalidProp(opts, 'extraPostCSSPlugins') || + Array.isArray(opts.extraPostCSSPlugins), + `opts.extraPostCSSPlugins must be Array, but got ${ + opts.extraPostCSSPlugins + }`, + ); + assert( + invalidProp(opts, 'theme') || + (isPlainObject(opts.theme) || typeof opts.theme === 'string'), + `opts.theme must be Object or String, but got ${opts.theme}`, + ); + assert( + invalidProp(opts, 'babel') || isPlainObject(opts.babel), + `opts.babel must be Object, but got ${opts.babel}`, + ); + assert( + invalidProp(opts, 'enableCSSModules') || + typeof opts.enableCSSModules === 'boolean', + `opts.enableCSSModules must be Boolean, but got ${opts.enableCSSModules}`, + ); + assert( + invalidProp(opts, 'noCompress') || typeof opts.noCompress === 'boolean', + `opts.noCompress must be Boolean, but got ${opts.noCompress}`, + ); + assert( + invalidProp(opts, 'define') || isPlainObject(opts.define), + `opts.define must be Object, but got ${opts.define}`, + ); + assert( + invalidProp(opts, 'publicPath') || typeof opts.publicPath === 'string', + `opts.publicPath must be String, but got ${opts.publicPath}`, + ); + assert( + invalidProp(opts, 'alias') || isPlainObject(opts.alias), + `opts.alias must be Boolean, but got ${opts.alias}`, + ); + assert( + invalidProp(opts, 'outputPath') || typeof opts.outputPath === 'string', + `opts.outputPath must be String, but got ${opts.outputPath}`, + ); + assert( + invalidProp(opts, 'entry') || + (typeof opts.entry === 'string' || isPlainObject(opts.entry)), + `opts.entry must be String or PlainObject, but got ${opts.entry}`, + ); + assert( + invalidProp(opts, 'extraResolveModules') || + Array.isArray(opts.extraResolveModules), + `opts.extraResolveModules must be Array, but got ${ + opts.extraResolveModules + }`, + ); + assert( + invalidProp(opts, 'commons') || Array.isArray(opts.commons), + `opts.commons must be Array, but got ${opts.commons}`, + ); + assert( + invalidProp(opts, 'hash') || typeof opts.hash === 'boolean', + `opts.hash must be Boolean, but got ${opts.hash}`, + ); + assert( + invalidProp(opts, 'extraBabelIncludes') || + Array.isArray(opts.extraBabelIncludes), + `opts.extraBabelIncludes must be Array, but got ${opts.extraBabelIncludes}`, + ); + assert( + invalidProp(opts, 'extraResolveExtensions') || + Array.isArray(opts.extraResolveExtensions), + `opts.extraResolveExtensions must be Array, but got ${ + opts.extraResolveExtensions + }`, + ); + assert( + invalidProp(opts, 'copy') || Array.isArray(opts.copy), + `opts.copy must be Array, but got ${opts.copy}`, + ); + assert( + invalidProp(opts, 'ignoreMomentLocale') || + typeof opts.ignoreMomentLocale === 'boolean', + `opts.ignoreMomentLocale must be Boolean, but got ${ + opts.ignoreMomentLocale + }`, + ); + + const isDev = process.env.NODE_ENV === 'development'; + const postcssOptions = { + // Necessary for external CSS imports to work + // https://github.com/facebookincubator/create-react-app/issues/2677 + ident: 'postcss', + plugins: () => [ + require('postcss-flexbugs-fixes'), + autoprefixer({ + browsers: opts.browsers || browsersConfig, + flexbox: 'no-2009', + }), + ...(opts.extraPostCSSPlugins ? opts.extraPostCSSPlugins : []), + ], + }; + + const theme = normalizeTheme(opts.theme); + const cssModulesConfig = opts.enableCSSModules + ? { + modules: true, + localIdentName: '[local]___[hash:base64:5]', + } + : {}; + const lessOptions = { + modifyVars: theme, + }; + const cssRules = [ + { + test: /\.css$/, + exclude: /node_modules/, + use: [ + require.resolve('style-loader'), + { + loader: require('path').join(__dirname, 'debugLoader.js'), + }, + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 1, + sourceMap: true, + ...cssModulesConfig, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: postcssOptions, + }, + ], + }, + { + test: /\.css$/, + include: /node_modules/, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: postcssOptions, + }, + ], + }, + { + test: /\.less$/, + exclude: /node_modules/, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 1, + sourceMap: true, + ...cssModulesConfig, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: postcssOptions, + }, + { + loader: require.resolve('less-loader'), + options: lessOptions, + }, + ], + }, + { + test: /\.less$/, + include: /node_modules/, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 1, + sourceMap: true, + }, + }, + { + loader: require.resolve('postcss-loader'), + options: postcssOptions, + }, + { + loader: require.resolve('less-loader'), + options: lessOptions, + }, + ], + }, + ]; + + // 生成环境下用 ExtractTextPlugin 提取出来 + if (!isDev) { + cssRules.forEach(rule => { + rule.use = ExtractTextPlugin.extract({ + use: rule.use.slice(1), + }); + }); + } + + // TODO: 根据 opts.hash 自动处理这里的 filename + const commonsPlugins = (opts.commons || []).map(common => { + return new webpack.optimize.CommonsChunkPlugin(common); + }); + + const copyPlugins = opts.copy ? [new CopyWebpackPlugin(opts.copy)] : []; + + // js 和 css 采用不同的 hash 算法 + const jsHash = opts.hash ? '.[chunkhash:8]' : ''; + const cssHash = opts.hash ? '.[contenthash:8]' : ''; + + const babelUse = [ + { + loader: require('path').join(__dirname, 'debugLoader.js'), + }, + { + loader: require.resolve('babel-loader'), + options: { + ...(opts.babel || babelConfig), + // 性能提升有限,但会带来一系列答疑的工作量,所以不开放 + cacheDirectory: false, + }, + }, + ]; + + const eslintOptions = { + formatter: eslintFormatter, + baseConfig: { + extends: [require.resolve('eslint-config-af-react')], + }, + ignore: false, + eslintPath: require.resolve('eslint'), + useEslintrc: false, + }; + + // 用用户的 eslint + try { + const { dependencies, devDependencies } = require(resolve('package.json')); // eslint-disable-line + if (dependencies.eslint || devDependencies) { + const eslintPath = resolveSync('eslint', { + basedir: opts.cwd, + }); + eslintOptions.eslintPath = eslintPath; + debug(`use user's eslint bin: ${eslintPath}`); + } + } catch (e) { + // do nothing + } + // 读用户的 eslintrc + if (existsSync(resolve('.eslintrc'))) { + debug(`use user's .eslintrc: ${resolve('.eslintrc')}`); + eslintOptions.useEslintrc = true; + eslintOptions.baseConfig = false; + eslintOptions.ignore = true; + } + + const config = { + bail: !isDev, + entry: opts.entry || null, + output: { + path: opts.outputPath || null, + // Add /* filename */ comments to generated require()s in the output. + pathinfo: isDev, + filename: `[name]${jsHash}.js`, + publicPath: opts.publicPath || undefined, + chunkFilename: `[name]${jsHash}.async.js`, + }, + resolve: { + modules: [ + resolve(__dirname, '../node_modules'), + 'node_modules', + ...(opts.extraResolveModules || []), + ], + extensions: [ + ...(opts.extraResolveExtensions || []), + '.web.js', + '.web.jsx', + '.web.ts', + '.web.tsx', + '.js', + '.json', + '.jsx', + '.ts', + '.tsx', + ], + alias: { + '@babel/runtime': dirname(require.resolve('@babel/runtime/package')), + ...opts.alias, + }, + }, + module: { + rules: [ + ...(process.env.DISABLE_ESLINT + ? [] + : [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + enforce: 'pre', + use: [ + { + options: eslintOptions, + loader: require.resolve('eslint-loader'), + }, + ], + }, + ]), + { + exclude: [ + /\.html$/, + /\.json$/, + /\.(js|jsx|ts|tsx)$/, + /\.(css|less|scss)$/, + ], + loader: require.resolve('url-loader'), + options: { + limit: 10000, + name: 'static/[name].[hash:8].[ext]', + }, + }, + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + use: babelUse, + }, + ...(opts.extraBabelIncludes + ? opts.extraBabelIncludes.map(include => { + return { + test: /\.(js|jsx)$/, + include: join(opts.cwd, include), + use: babelUse, + }; + }) + : []), + { + test: /\.html$/, + loader: require.resolve('file-loader'), + options: { + name: '[name].[ext]', + }, + }, + ...cssRules, + ], + }, + plugins: [ + ...(isDev + ? [ + new webpack.HotModuleReplacementPlugin(), + new webpack.SourceMapDevToolPlugin({ + columns: false, + moduleFilenameTemplate: info => { + if ( + /\/koi-pkgs\/packages/.test(info.absoluteResourcePath) || + /packages\/koi-core/.test(info.absoluteResourcePath) || + /webpack\/bootstrap/.test(info.absoluteResourcePath) || + /\/node_modules\//.test(info.absoluteResourcePath) + ) { + return `internal:///${info.absoluteResourcePath}`; + } + return resolve(info.absoluteResourcePath).replace(/\\/g, '/'); + }, + }), + new WatchMissingNodeModulesPlugin(join(opts.cwd, 'node_modules')), + new SystemBellWebpackPlugin(), + ] + : [ + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.optimize.ModuleConcatenationPlugin(), + new ExtractTextPlugin({ + filename: `[name]${cssHash}.css`, + allChunks: true, + }), + ]), + ...(isDev || opts.noCompress || process.env.NO_COMPRESS + ? [] + : [new webpack.optimize.UglifyJsPlugin(uglifyJSConfig)]), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify( + // eslint-disable-line + isDev ? 'development' : 'production', + ), // eslint-disable-line + ...stringifyObject(opts.define || {}), + }), + ...(process.env.ANALYZE + ? [ + new BundleAnalyzerPlugin({ + analyzerMode: 'server', + analyzerPort: process.env.ANALYZE_PORT || 8888, + openAnalyzer: true, + }), + ] + : []), + new CaseSensitivePathsPlugin(), + new webpack.LoaderOptionsPlugin({ + options: { + context: __dirname, + }, + }), + ...(opts.ignoreMomentLocale + ? [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)] + : []), + ...commonsPlugins, + ...copyPlugins, + ], + externals: opts.externals, + node: { + dgram: 'empty', + fs: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty', + }, + performance: isDev + ? { + hints: false, + } + : {}, + }; + + return applyWebpackConfig(config); +} diff --git a/packages/af-webpack/src/normalizeTheme.js b/packages/af-webpack/src/normalizeTheme.js new file mode 100644 index 000000000000..8fc7752a30bb --- /dev/null +++ b/packages/af-webpack/src/normalizeTheme.js @@ -0,0 +1,21 @@ +import { existsSync } from 'fs'; +import { resolve } from 'path'; + +export default function(theme, opts = {}) { + const { cwd = process.cwd() } = opts; + if (!theme) return {}; + if (typeof theme === 'string') { + const themePath = resolve(cwd, theme); + if (existsSync(themePath)) { + const themeConfig = require(themePath); // eslint-disable-line + if (typeof themeConfig === 'function') { + return themeConfig(); + } else { + return themeConfig; + } + } else { + throw new Error(`theme file don't exists`); + } + } + return theme; +} diff --git a/packages/af-webpack/src/patchConnection.js b/packages/af-webpack/src/patchConnection.js new file mode 100644 index 000000000000..04d57a506394 --- /dev/null +++ b/packages/af-webpack/src/patchConnection.js @@ -0,0 +1,47 @@ +/* eslint-disable */ + +let el = null; + +function showLoading() { + el = document.createElement('div'); + el.style.position = 'absolute'; + el.style.left = 0; + el.style.top = '50%'; + el.style.marginTop = '-10px'; + el.style.width = '100%'; + el.style.height = '20px'; + el.style.background = 'yellow'; + el.style.zIndex = 2147483647000000; + el.style.color = '#8b0000'; + el.style.textAlign = 'center'; + el.style.fontSize = '18px'; + el.style.padding = '4px 0'; + el.style.boxShadow = '0px 0px 20px #00000054'; + el.innerHTML = '已和开发服务器断开,正在尝试重新连接。。。'; + document.body.appendChild(el); +} + +function hideLoading() { + el.parentNode.removeChild(el); +} + +function connectServer(onSuccess) { + fetch(window.location.href) + .then(onSuccess) + .catch(() => { + setTimeout(() => { + connectServer(onSuccess); + }, 1000); + }); +} + +export default function(connection) { + const oldOnclose = connection.onclose; + connection.onclose = () => { + oldOnclose(); + showLoading(); + connectServer(() => { + window.location.reload(); + }); + }; +} diff --git a/packages/af-webpack/src/reactDevUtils.js b/packages/af-webpack/src/reactDevUtils.js new file mode 100644 index 000000000000..0061f13d17f8 --- /dev/null +++ b/packages/af-webpack/src/reactDevUtils.js @@ -0,0 +1,6 @@ +import clearConsole from 'react-dev-utils/clearConsole'; + +export default { + clearConsole, + webpackHotDevClientPath: require.resolve('./webpackHotDevClient'), +}; diff --git a/packages/af-webpack/src/registerBabel.js b/packages/af-webpack/src/registerBabel.js new file mode 100644 index 000000000000..0b154508e109 --- /dev/null +++ b/packages/af-webpack/src/registerBabel.js @@ -0,0 +1,16 @@ +export default function registerBabel(opts = {}) { + const { only, ignore, babelPreset, disablePreventTest } = opts; + if (disablePreventTest || process.env.NODE_ENV !== 'test') { + require('@babel/register')({ + presets: [babelPreset], + plugins: [ + require.resolve('babel-plugin-add-module-exports'), + require.resolve('@babel/plugin-transform-modules-commonjs'), + ], + only, + ignore, + babelrc: false, + cache: false, // 无效?待排查。 + }); + } +} diff --git a/packages/af-webpack/src/send.js b/packages/af-webpack/src/send.js new file mode 100644 index 000000000000..736d24219f04 --- /dev/null +++ b/packages/af-webpack/src/send.js @@ -0,0 +1,14 @@ +const debug = require('debug')('af-webpack:send'); + +export const COMPILING = 'COMPILING'; +export const DONE = 'DONE'; +export const STARTING = 'STARTING'; +export const RESTART = 'RESTART'; +export const OPEN_FILE = 'OPEN_FILE'; + +export default function send(message) { + if (process.send) { + debug(`send ${JSON.stringify(message)}`); + process.send(message); + } +} diff --git a/packages/af-webpack/src/stringifyObject.js b/packages/af-webpack/src/stringifyObject.js new file mode 100644 index 000000000000..a9e12a6dc1ee --- /dev/null +++ b/packages/af-webpack/src/stringifyObject.js @@ -0,0 +1,6 @@ +export default function(obj) { + return Object.keys(obj).reduce((memo, key) => { + memo[key] = JSON.stringify(obj[key]); + return memo; + }, {}); +} diff --git a/packages/af-webpack/src/webpackHotDevClient.js b/packages/af-webpack/src/webpackHotDevClient.js new file mode 100644 index 000000000000..cdec75a76661 --- /dev/null +++ b/packages/af-webpack/src/webpackHotDevClient.js @@ -0,0 +1,274 @@ +/* eslint-disable */ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +// This alternative WebpackDevServer combines the functionality of: +// https://github.com/webpack/webpack-dev-server/blob/webpack-1/client/index.js +// https://github.com/webpack/webpack/blob/webpack-1/hot/dev-server.js + +// It only supports their simplest configuration (hot updates on same server). +// It makes some opinionated choices on top, like adding a syntax error overlay +// that looks similar to our console output. The error overlay is inspired by: +// https://github.com/glenjamin/webpack-hot-middleware + +var SockJS = require('sockjs-client'); +var stripAnsi = require('strip-ansi'); +var url = require('url'); +var launchEditorEndpoint = require('react-dev-utils/launchEditorEndpoint'); +var formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +var ErrorOverlay = require('react-error-overlay'); + +ErrorOverlay.setEditorHandler(function editorHandler(errorLocation) { + // Keep this sync with errorOverlayMiddleware.js + fetch( + `${launchEditorEndpoint}?fileName=` + + window.encodeURIComponent(errorLocation.fileName) + + '&lineNumber=' + + window.encodeURIComponent(errorLocation.lineNumber || 1), + ); +}); + +// We need to keep track of if there has been a runtime error. +// Essentially, we cannot guarantee application state was not corrupted by the +// runtime error. To prevent confusing behavior, we forcibly reload the entire +// application. This is handled below when we are notified of a compile (code +// change). +// See https://github.com/facebookincubator/create-react-app/issues/3096 +var hadRuntimeError = false; +ErrorOverlay.startReportingRuntimeErrors({ + onError: function() { + hadRuntimeError = true; + }, + filename: '/static/js/bundle.js', +}); + +if (module.hot && typeof module.hot.dispose === 'function') { + module.hot.dispose(function() { + // TODO: why do we need this? + ErrorOverlay.stopReportingRuntimeErrors(); + }); +} + +// Connect to WebpackDevServer via a socket. +var connection = new SockJS( + url.format({ + protocol: window.location.protocol, + hostname: window.location.hostname, + port: window.location.port, + // Hardcoded in WebpackDevServer + pathname: '/sockjs-node', + }), +); + +// Unlike WebpackDevServer client, we won't try to reconnect +// to avoid spamming the console. Disconnect usually happens +// when developer stops the server. +connection.onclose = function() { + if (typeof console !== 'undefined' && typeof console.info === 'function') { + console.info( + 'The development server has disconnected.\nRefresh the page if necessary.', + ); + } +}; + +// Remember some state related to hot module replacement. +var isFirstCompilation = true; +var mostRecentCompilationHash = null; +var hasCompileErrors = false; + +function clearOutdatedErrors() { + // Clean up outdated compile errors, if any. + if (typeof console !== 'undefined' && typeof console.clear === 'function') { + if (hasCompileErrors) { + console.clear(); + } + } +} + +// Successful compilation. +function handleSuccess() { + clearOutdatedErrors(); + + var isHotUpdate = !isFirstCompilation; + isFirstCompilation = false; + hasCompileErrors = false; + + // Attempt to apply hot updates or reload. + if (isHotUpdate) { + tryApplyUpdates(function onHotUpdateSuccess() { + // Only dismiss it when we're sure it's a hot update. + // Otherwise it would flicker right before the reload. + ErrorOverlay.dismissBuildError(); + }); + } +} + +// Compilation with warnings (e.g. ESLint). +function handleWarnings(warnings) { + clearOutdatedErrors(); + + var isHotUpdate = !isFirstCompilation; + isFirstCompilation = false; + hasCompileErrors = false; + + function printWarnings() { + // Print warnings to the console. + var formatted = formatWebpackMessages({ + warnings: warnings, + errors: [], + }); + + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + for (var i = 0; i < formatted.warnings.length; i++) { + if (i === 5) { + console.warn( + 'There were more warnings in other files.\n' + + 'You can find a complete log in the terminal.', + ); + break; + } + console.warn(stripAnsi(formatted.warnings[i])); + } + } + } + + // Attempt to apply hot updates or reload. + if (isHotUpdate) { + tryApplyUpdates(function onSuccessfulHotUpdate() { + // Only print warnings if we aren't refreshing the page. + // Otherwise they'll disappear right away anyway. + printWarnings(); + // Only dismiss it when we're sure it's a hot update. + // Otherwise it would flicker right before the reload. + ErrorOverlay.dismissBuildError(); + }); + } else { + // Print initial warnings immediately. + printWarnings(); + } +} + +// Compilation with errors (e.g. syntax error or missing modules). +function handleErrors(errors) { + clearOutdatedErrors(); + + isFirstCompilation = false; + hasCompileErrors = true; + + // "Massage" webpack messages. + var formatted = formatWebpackMessages({ + errors: errors, + warnings: [], + }); + + // Only show the first error. + ErrorOverlay.reportBuildError(formatted.errors[0]); + + // Also log them to the console. + if (typeof console !== 'undefined' && typeof console.error === 'function') { + for (var i = 0; i < formatted.errors.length; i++) { + console.error(stripAnsi(formatted.errors[i])); + } + } + + // Do not attempt to reload now. + // We will reload on next success instead. +} + +// There is a newer version of the code available. +function handleAvailableHash(hash) { + // Update last known compilation hash. + mostRecentCompilationHash = hash; +} + +// Handle messages from the server. +connection.onmessage = function(e) { + var message = JSON.parse(e.data); + switch (message.type) { + case 'hash': + handleAvailableHash(message.data); + break; + case 'still-ok': + case 'ok': + handleSuccess(); + break; + case 'content-changed': + // Triggered when a file from `contentBase` changed. + window.location.reload(); + break; + case 'warnings': + handleWarnings(message.data); + break; + case 'errors': + handleErrors(message.data); + break; + default: + // Do nothing. + } +}; + +// Is there a newer version of this code available? +function isUpdateAvailable() { + /* globals __webpack_hash__ */ + // __webpack_hash__ is the hash of the current compilation. + // It's a global variable injected by Webpack. + return mostRecentCompilationHash !== __webpack_hash__; +} + +// Webpack disallows updates in other states. +function canApplyUpdates() { + return module.hot.status() === 'idle'; +} + +// Attempt to update code on the fly, fall back to a hard reload. +function tryApplyUpdates(onHotUpdateSuccess) { + if (!module.hot) { + // HotModuleReplacementPlugin is not in Webpack configuration. + window.location.reload(); + return; + } + + if (!isUpdateAvailable() || !canApplyUpdates()) { + return; + } + + function handleApplyUpdates(err, updatedModules) { + if (err || !updatedModules || hadRuntimeError) { + window.location.reload(); + return; + } + + if (typeof onHotUpdateSuccess === 'function') { + // Maybe we want to do something. + onHotUpdateSuccess(); + } + + if (isUpdateAvailable()) { + // While we were updating, there was a new update! Do it again. + tryApplyUpdates(); + } + } + + // https://webpack.github.io/docs/hot-module-replacement.html#check + var result = module.hot.check(/* autoApply */ true, handleApplyUpdates); + + // // Webpack 2 returns a Promise instead of invoking a callback + if (result && result.then) { + result.then( + function(updatedModules) { + handleApplyUpdates(null, updatedModules); + }, + function(err) { + handleApplyUpdates(err, null); + }, + ); + } +} + +require('./patchConnection')(connection); diff --git a/packages/af-webpack/test/fixtures/extract-css/expected/index.css b/packages/af-webpack/test/fixtures/extract-css/expected/index.css new file mode 100644 index 000000000000..b6e589a64a09 --- /dev/null +++ b/packages/af-webpack/test/fixtures/extract-css/expected/index.css @@ -0,0 +1,8 @@ + +body { + background: red; +} + +.a { + color: green; +} diff --git a/packages/af-webpack/test/fixtures/extract-css/expected/index.js b/packages/af-webpack/test/fixtures/extract-css/expected/index.js new file mode 100644 index 000000000000..03cba5ee8374 --- /dev/null +++ b/packages/af-webpack/test/fixtures/extract-css/expected/index.js @@ -0,0 +1,84 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__index_css__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__index_css___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__index_css__); + +console.log(1); + +/***/ }), +/* 1 */ +/***/ (function(module, exports) { + +// removed by extract-text-webpack-plugin + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/packages/af-webpack/test/fixtures/extract-css/index.css b/packages/af-webpack/test/fixtures/extract-css/index.css new file mode 100644 index 000000000000..b6e589a64a09 --- /dev/null +++ b/packages/af-webpack/test/fixtures/extract-css/index.css @@ -0,0 +1,8 @@ + +body { + background: red; +} + +.a { + color: green; +} diff --git a/packages/af-webpack/test/fixtures/extract-css/index.js b/packages/af-webpack/test/fixtures/extract-css/index.js new file mode 100644 index 000000000000..4502c74f8575 --- /dev/null +++ b/packages/af-webpack/test/fixtures/extract-css/index.js @@ -0,0 +1,3 @@ +import './index.css'; + +console.log(1); diff --git a/packages/af-webpack/test/fixtures/import-css/expected/index.css b/packages/af-webpack/test/fixtures/import-css/expected/index.css new file mode 100644 index 000000000000..10af9b333c99 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/expected/index.css @@ -0,0 +1,7 @@ + +.a { + color: red; +} +body { + background: red; +} diff --git a/packages/af-webpack/test/fixtures/import-css/expected/index.js b/packages/af-webpack/test/fixtures/import-css/expected/index.js new file mode 100644 index 000000000000..03cba5ee8374 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/expected/index.js @@ -0,0 +1,84 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__index_css__ = __webpack_require__(1); +/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__index_css___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__index_css__); + +console.log(1); + +/***/ }), +/* 1 */ +/***/ (function(module, exports) { + +// removed by extract-text-webpack-plugin + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/packages/af-webpack/test/fixtures/import-css/index.css b/packages/af-webpack/test/fixtures/import-css/index.css new file mode 100644 index 000000000000..f9c6239124f9 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/index.css @@ -0,0 +1,5 @@ +@import '~a/a.css'; + +body { + background: red; +} diff --git a/packages/af-webpack/test/fixtures/import-css/index.js b/packages/af-webpack/test/fixtures/import-css/index.js new file mode 100644 index 000000000000..4502c74f8575 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/index.js @@ -0,0 +1,3 @@ +import './index.css'; + +console.log(1); diff --git a/packages/af-webpack/test/fixtures/import-css/node_modules/a/a.css b/packages/af-webpack/test/fixtures/import-css/node_modules/a/a.css new file mode 100644 index 000000000000..101ad955970f --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/node_modules/a/a.css @@ -0,0 +1,4 @@ + +.a { + color: red; +} diff --git a/packages/af-webpack/test/fixtures/import-css/node_modules/package.json b/packages/af-webpack/test/fixtures/import-css/node_modules/package.json new file mode 100644 index 000000000000..7d9dd31f9e35 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/node_modules/package.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "0.1.0" +} diff --git a/packages/af-webpack/test/fixtures/import-css/package.json b/packages/af-webpack/test/fixtures/import-css/package.json new file mode 100644 index 000000000000..398b081285c5 --- /dev/null +++ b/packages/af-webpack/test/fixtures/import-css/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "a": "0.1.0" + } +} diff --git a/packages/af-webpack/test/fixtures/normal/expected/index.js b/packages/af-webpack/test/fixtures/normal/expected/index.js new file mode 100644 index 000000000000..6d8df07508ef --- /dev/null +++ b/packages/af-webpack/test/fixtures/normal/expected/index.js @@ -0,0 +1,73 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports) { + +console.log(1); + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/packages/af-webpack/test/fixtures/normal/index.js b/packages/af-webpack/test/fixtures/normal/index.js new file mode 100644 index 000000000000..296d5492b003 --- /dev/null +++ b/packages/af-webpack/test/fixtures/normal/index.js @@ -0,0 +1 @@ +console.log(1); diff --git a/packages/af-webpack/test/fixtures/webpack-config-js/expected/index.js b/packages/af-webpack/test/fixtures/webpack-config-js/expected/index.js new file mode 100644 index 000000000000..dea87bcdab6e --- /dev/null +++ b/packages/af-webpack/test/fixtures/webpack-config-js/expected/index.js @@ -0,0 +1,75 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +if (false) { + console.log('cde'); +} + +/***/ }) +/******/ ]); \ No newline at end of file diff --git a/packages/af-webpack/test/fixtures/webpack-config-js/index.js b/packages/af-webpack/test/fixtures/webpack-config-js/index.js new file mode 100644 index 000000000000..4286546c1812 --- /dev/null +++ b/packages/af-webpack/test/fixtures/webpack-config-js/index.js @@ -0,0 +1,4 @@ + +if (ABC === 'ggg') { + console.log('cde'); +} diff --git a/packages/af-webpack/test/fixtures/webpack-config-js/webpack.config.js b/packages/af-webpack/test/fixtures/webpack-config-js/webpack.config.js new file mode 100644 index 000000000000..422b47e316eb --- /dev/null +++ b/packages/af-webpack/test/fixtures/webpack-config-js/webpack.config.js @@ -0,0 +1,7 @@ + +module.exports = function (webpackConfig, { webpack }) { + webpackConfig.plugins.push(new webpack.DefinePlugin({ + 'ABC': JSON.stringify('cde') + })); + return webpackConfig; +}; diff --git a/packages/af-webpack/test/index-test.js b/packages/af-webpack/test/index-test.js new file mode 100644 index 000000000000..f67a97c56b48 --- /dev/null +++ b/packages/af-webpack/test/index-test.js @@ -0,0 +1,77 @@ +import expect from 'expect'; +import webpack from 'webpack'; +import glob from 'glob'; +import { join } from 'path'; +import { + readFileSync as readFile, + readdirSync as readdir, + existsSync as exists, +} from 'fs'; +import getConfig from '../src/getConfig'; + +function build(env, opts, done) { + if (!opts.cwd) { + throw new Error('opts.cwd must be supplied.'); + } + const configFile = join(opts.cwd, 'config.json'); + const localConfig = exists(configFile) + ? JSON.parse(readFile(configFile, 'utf-8')) + : {}; + const config = getConfig(env, { + ...opts, + noCompress: true, + ...localConfig, + }); + config.entry = { + index: join(opts.cwd, 'index.js'), + }; + config.output.path = join(opts.cwd, 'dist'); + const compiler = webpack(config); + compiler.run(err => { + if (err) { + throw new Error(err); + } else { + done(); + } + }); +} + +function assertBuildResult(cwd) { + const actualDir = join(cwd, 'dist'); + const expectDir = join(cwd, 'expected'); + + const actualFiles = glob.sync('**/*', { cwd: actualDir, nodir: true }); + const expectFiles = glob.sync('**/*', { cwd: actualDir, nodir: true }); + + expect(actualFiles.length).toEqual(expectFiles.length); + + actualFiles.forEach(file => { + const actualFile = readFile(join(actualDir, file), 'utf-8'); + const expectFile = readFile(join(expectDir, file), 'utf-8'); + expect(actualFile).toEqual(expectFile); + }); +} + +describe('index', () => { + const fixtures = join(__dirname, './fixtures'); + readdir(fixtures) + .filter(dir => dir.charAt(0) !== '.') + .forEach(dir => { + const fn = dir.indexOf('-only') > -1 ? it.only : it; + fn(dir, done => { + const cwd = join(fixtures, dir); + process.chdir(cwd); + build( + 'production', + { + cwd, + outputPath: join(cwd, 'dist'), + }, + () => { + assertBuildResult(cwd); + done(); + }, + ); + }); + }); +}); diff --git a/packages/af-webpack/webpack.js b/packages/af-webpack/webpack.js new file mode 100644 index 000000000000..dfe19cee0083 --- /dev/null +++ b/packages/af-webpack/webpack.js @@ -0,0 +1 @@ +module.exports = require('webpack'); diff --git a/packages/babel-preset-umi/README.md b/packages/babel-preset-umi/README.md new file mode 100644 index 000000000000..4bb4ff0a3f84 --- /dev/null +++ b/packages/babel-preset-umi/README.md @@ -0,0 +1,37 @@ +# babel-preset-umi + +Yet another babel preset. + +## Options + +### browsers + +Notice: browsers don't work if targets is specified. + +```js +["umi", { + "browsers": ['last 2 versions'] +}] +``` + +### targets + +e.g. + +```js +["umi", { + "targets": { + "browsers": ['last 2 versions'] + } +}] +``` + +or + +```js +["umi", { + "targets": { + "node": "6" + } +}] +``` diff --git a/packages/babel-preset-umi/package.json b/packages/babel-preset-umi/package.json new file mode 100644 index 000000000000..3d134d2fd72f --- /dev/null +++ b/packages/babel-preset-umi/package.json @@ -0,0 +1,47 @@ +{ + "name": "babel-preset-umi", + "version": "0.1.0", + "main": "./lib/index", + "repository": { + "type": "git", + "url": "https://github.com/umijs/umi/tree/master/packages/babel-preset-umi" + }, + "homepage": "https://github.com/umijs/umi/tree/master/packages/babel-preset-umi", + "authors": [ + "chencheng (https://github.com/sorrycc)" + ], + "bugs": { + "url": "https://github.com/umijs/umi/issues" + }, + "scripts": { + "build": "ruban build", + "test": "ruban test", + "debug": "ruban debug", + "coveralls": "ruban coveralls" + }, + "dependencies": { + "@babel/plugin-proposal-class-properties": "7.0.0-beta.31", + "@babel/plugin-proposal-decorators": "7.0.0-beta.31", + "@babel/plugin-proposal-do-expressions": "7.0.0-beta.31", + "@babel/plugin-proposal-export-default": "7.0.0-beta.31", + "@babel/plugin-proposal-export-namespace": "7.0.0-beta.31", + "@babel/plugin-proposal-function-bind": "7.0.0-beta.31", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.0.0-beta.31", + "@babel/plugin-proposal-object-rest-spread": "7.0.0-beta.31", + "@babel/plugin-proposal-optional-catch-binding": "7.0.0-beta.31", + "@babel/plugin-proposal-optional-chaining": "7.0.0-beta.31", + "@babel/plugin-proposal-pipeline-operator": "7.0.0-beta.31", + "@babel/plugin-syntax-dynamic-import": "7.0.0-beta.31", + "@babel/plugin-transform-react-constant-elements": "7.0.0-beta.31", + "@babel/plugin-transform-react-inline-elements": "7.0.0-beta.31", + "@babel/plugin-transform-runtime": "7.0.0-beta.31", + "@babel/preset-env": "7.0.0-beta.31", + "@babel/preset-react": "7.0.0-beta.31", + "babel-plugin-react-require": "^3.0.0", + "babel-plugin-transform-react-remove-prop-types": "^0.4.8" + }, + "files": [ + "lib", + "src" + ] +} diff --git a/packages/babel-preset-umi/src/index.js b/packages/babel-preset-umi/src/index.js new file mode 100644 index 000000000000..9c0502a16f08 --- /dev/null +++ b/packages/babel-preset-umi/src/index.js @@ -0,0 +1,70 @@ +const env = process.env.NODE_ENV; + +export default function(context, opts = {}) { + const plugins = [ + // adds React import declaration if file contains JSX tags + require.resolve('babel-plugin-react-require'), + require.resolve('@babel/plugin-syntax-dynamic-import'), + require.resolve('@babel/plugin-proposal-object-rest-spread'), + require.resolve('@babel/plugin-proposal-optional-catch-binding'), + require.resolve('@babel/plugin-proposal-decorators'), + [ + require.resolve('@babel/plugin-proposal-class-properties'), + { loose: true }, + ], + require.resolve('@babel/plugin-proposal-export-namespace'), + require.resolve('@babel/plugin-proposal-export-default'), + require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), + require.resolve('@babel/plugin-proposal-optional-chaining'), + require.resolve('@babel/plugin-proposal-pipeline-operator'), + require.resolve('@babel/plugin-proposal-do-expressions'), + require.resolve('@babel/plugin-proposal-function-bind'), + require.resolve('@babel/plugin-transform-react-constant-elements'), + ]; + + // transform-react-inline-element don't support preact + if (!opts.preact) { + plugins.push( + require.resolve('@babel/plugin-transform-react-inline-elements'), + ); + } + + if (env !== 'test' && !opts.disableTransform) { + plugins.push(require.resolve('@babel/plugin-transform-runtime')); + } + + if (env === 'production') { + plugins.push.apply(plugins, [ + require.resolve('babel-plugin-transform-react-remove-prop-types'), + ]); + } + + const browsers = opts.browsers || ['last 2 versions']; + return { + presets: [ + [ + require.resolve('@babel/preset-env'), + { + targets: opts.targets || { browsers }, + debug: opts.debug, + modules: false, + exclude: [ + 'transform-typeof-symbol', + 'transform-unicode-regex', + 'transform-sticky-regex', + 'transform-object-super', + 'transform-new-target', + 'transform-modules-umd', + 'transform-modules-systemjs', + 'transform-modules-amd', + 'transform-literals', + 'transform-exponentiation-operator', + 'transform-duplicate-keys', + ], + }, + ], + require.resolve('@babel/preset-react'), + ], + plugins, + }; +} diff --git a/packages/eslint-config-umi/README.md b/packages/eslint-config-umi/README.md new file mode 100644 index 000000000000..cf727e6529a5 --- /dev/null +++ b/packages/eslint-config-umi/README.md @@ -0,0 +1,4 @@ + +# eslint-config-umi + +Eslint config for umi projects. diff --git a/packages/eslint-config-umi/package.json b/packages/eslint-config-umi/package.json new file mode 100644 index 000000000000..1d48fa1a9929 --- /dev/null +++ b/packages/eslint-config-umi/package.json @@ -0,0 +1,31 @@ +{ + "name": "eslint-config-umi", + "version": "0.1.0", + "main": "./lib/index", + "repository": { + "type": "git", + "url": "https://github.com/umijs/umi/tree/master/packages/eslint-config-umi" + }, + "homepage": "https://github.com/umijs/umi/tree/master/packages/eslint-config-umi", + "authors": [ + "chencheng (https://github.com/sorrycc)" + ], + "bugs": { + "url": "https://github.com/umijs/umi/issues" + }, + "scripts": { + "build": "DISABLE_TRANSFORM_RUNTIME=1 NODE_TARGET=1 ruban build", + "test": "ruban test", + "debug": "ruban debug", + "coveralls": "ruban coveralls" + }, + "dependencies": { + "eslint-config-react-app": "^2.0.0" + }, + "peerDependencies": { + "eslint-plugin-flowtype": "^2.34.1", + "eslint-plugin-import": "^2.6.0", + "eslint-plugin-jsx-a11y": "^5.1.1", + "eslint-plugin-react": "^7.1.0" + } +} diff --git a/packages/eslint-config-umi/src/index.js b/packages/eslint-config-umi/src/index.js new file mode 100644 index 000000000000..12ef019ed62e --- /dev/null +++ b/packages/eslint-config-umi/src/index.js @@ -0,0 +1,13 @@ +import reactAppConfig from 'eslint-config-react-app'; + +export default { + ...reactAppConfig, + rules: { + ...reactAppConfig.rules, + strict: [0], + 'no-sequences': [0], + 'no-mixed-operators': [0], + 'react/react-in-jsx-scope': [0], + 'no-useless-escape': [0], + }, +}; diff --git a/packages/umi-router/README.md b/packages/umi-router/README.md new file mode 100644 index 000000000000..3184be1a9601 --- /dev/null +++ b/packages/umi-router/README.md @@ -0,0 +1,3 @@ +# koi-router + +路由方案。 diff --git a/packages/umi-router/index.js b/packages/umi-router/index.js new file mode 100644 index 000000000000..1f3f4f1a1289 --- /dev/null +++ b/packages/umi-router/index.js @@ -0,0 +1,5 @@ +module.exports = { + Router: require('./lib/Router'), + Route: require('./lib/Route'), + Switch: require('./lib/Switch'), +}; diff --git a/packages/umi-router/package.json b/packages/umi-router/package.json new file mode 100644 index 000000000000..f09821107689 --- /dev/null +++ b/packages/umi-router/package.json @@ -0,0 +1,31 @@ +{ + "name": "umi-router", + "version": "0.1.0", + "dependencies": { + "path-to-regexp": "^2.0.0", + "prop-types": "^15.5.10", + "query-string": "^5.0.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/umijs/umi/tree/master/packages/umi-router" + }, + "homepage": "https://github.com/umijs/umi/tree/master/packages/umi-router", + "authors": [ + "chencheng (https://github.com/sorrycc)" + ], + "bugs": { + "url": "https://github.com/umijs/umi/issues" + }, + "scripts": { + "build": "ruban build", + "test": "ruban test", + "debug": "ruban debug", + "coveralls": "ruban coveralls" + }, + "files": [ + "lib", + "src", + "index.js" + ] +} diff --git a/packages/umi-router/src/Route.js b/packages/umi-router/src/Route.js new file mode 100644 index 000000000000..9d3a855623b6 --- /dev/null +++ b/packages/umi-router/src/Route.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import querystring from 'query-string'; + +class Router extends Component { + static contextTypes = { + router: PropTypes.shape({ + history: PropTypes.object.isRequired, + route: PropTypes.object.isRequired, + }), + }; + + static childContextTypes = { + router: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + match: props.computedMatch, + }; + } + + getChildContext() { + return { + router: { + ...this.context.router, + route: { + location: this.props.location || this.context.router.route.location, + match: this.state.match, + }, + }, + }; + } + + componentWillReceiveProps(nextProps) { + this.setState({ + match: nextProps.computedMatch, + }); + } + + render() { + const { match } = this.state; + const { component } = this.props; + const props = { + match, + location: this.props.location, + history: this.context.router.history, + }; + + props.location.query = querystring.parse(props.location.search); + return match ? React.createElement(component, props) : null; + } +} + +export default Router; diff --git a/packages/umi-router/src/Router.js b/packages/umi-router/src/Router.js new file mode 100644 index 000000000000..df83c4d35fd6 --- /dev/null +++ b/packages/umi-router/src/Router.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class Router extends Component { + static childContextTypes = { + router: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + match: {}, + }; + } + + getChildContext() { + return { + router: { + ...this.context.router, + history: this.props.history, + route: { + location: this.props.history.location, + match: this.state.match, + }, + }, + }; + } + + componentWillMount() { + const { history } = this.props; + this.unlisten = history.listen(() => { + this.setState({ + match: {}, + }); + }); + } + + componentWillUnmount() { + this.unlisten(); + } + + render() { + const { children } = this.props; + return children ? React.Children.only(children) : null; + } +} + +export default Router; diff --git a/packages/umi-router/src/Switch.js b/packages/umi-router/src/Switch.js new file mode 100644 index 000000000000..7d2ac18a30dc --- /dev/null +++ b/packages/umi-router/src/Switch.js @@ -0,0 +1,49 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import matchPath from './matchPath'; + +class Router extends Component { + static contextTypes = { + router: PropTypes.shape({ + route: PropTypes.object.isRequired, + }).isRequired, + }; + + componentWillMount() { + if (!this.context.router) { + throw new Error('You should not use outside a '); + } + } + + render() { + const { route } = this.context.router; + const { children } = this.props; + const location = this.props.location || route.location; + + let match = null; + let child; + + React.Children.forEach(children, element => { + if (!React.isValidElement(element)) return; + + const { path: pathProp, exact, strict, sensitive, from } = element.props; + const path = pathProp || from; + + if (match === null) { + child = element; + match = matchPath(location.pathname, { + path, + exact, + strict, + sensitive, + }); + } + }); + + return match + ? React.cloneElement(child, { location, computedMatch: match }) + : null; + } +} + +export default Router; diff --git a/packages/umi-router/src/matchPath.js b/packages/umi-router/src/matchPath.js new file mode 100644 index 000000000000..d6e1f60a035a --- /dev/null +++ b/packages/umi-router/src/matchPath.js @@ -0,0 +1,49 @@ +import pathToRegexp from 'path-to-regexp'; + +const patternCache = {}; +const cacheLimit = 10000; +let cacheCount = 0; + +const compilePath = (pattern, options) => { + const cacheKey = `${options.end}`; + const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {}); + + if (cache[pattern]) { + return cache[pattern]; + } + + const keys = []; + const re = pathToRegexp(pattern, keys, options); + const compiledPattern = { re, keys }; + + if (cacheCount < cacheLimit) { + cache[pattern] = compiledPattern; + cacheCount += 1; + } + + return compiledPattern; +}; + +/** + * Public API for matching a URL pathname to a path pattern. + */ +const matchPath = (pathname, options = {}) => { + const { path = '/', exact = false } = options; + const { re } = compilePath(path, { end: exact }); + const match = re.exec(pathname); + + if (!match) return null; + + const [url] = match; + const isExact = pathname === url; + + if (exact && !isExact) return null; + + return { + path, // the path pattern used to match + url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL + isExact, // whether or not we matched exactly + }; +}; + +export default matchPath; diff --git a/packages/umi/README.md b/packages/umi/README.md new file mode 100644 index 000000000000..2cf1e191fe80 --- /dev/null +++ b/packages/umi/README.md @@ -0,0 +1,3 @@ +# umi + +> under development. diff --git a/packages/umi/package.json b/packages/umi/package.json new file mode 100644 index 000000000000..14b3158e871b --- /dev/null +++ b/packages/umi/package.json @@ -0,0 +1,4 @@ +{ + "name": "umi", + "version": "1.0.0-alpha.0" +} diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 000000000000..6949ad672aab --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node + +const shell = require('shelljs'); +const { join } = require('path'); +const { fork } = require('child_process'); + +if ( + shell + .exec('npm config get registry') + .stdout.indexOf('https://registry.npmjs.org/') === -1 +) { + console.error('Failed: set npm registry to https://registry.npmjs.org/ first'); + process.exit(1); +} + +const cwd = process.cwd(); +const ret = shell.exec('./node_modules/.bin/lerna updated').stdout; +const updatedRepos = ret + .split('\n') + .map(line => line.replace('- ', '')) + .filter(line => line !== ''); + +if (updatedRepos.length === 0) { + console.log('No package is updated.'); + process.exit(0); +} + +const { code: buildCode } = shell.exec('npm run build'); +if (buildCode === 1) { + console.error('Failed: npm run build'); + process.exit(1); +} + +const { code: buildCode } = shell.exec('npm run build'); +if (buildCode === 1) { + console.error('Failed: npm run build'); + process.exit(1); +} + +const cp = fork( + join(process.cwd(), 'node_modules/.bin/lerna'), + ['publish', '--skip-npm'].concat(process.argv.slice(2)), + { + stdio: 'inherit', + cwd: process.cwd(), + }, +); +cp.on('error', err => { + console.log(err); +}); +cp.on('close', code => { + console.log('code', code); + if (code === 1) { + console.error('Failed: lerna publish'); + process.exit(1); + } + + publishToNpm(); +}); + +function publishToNpm() { + console.log(`repos to publish: ${updatedRepos.join(', ')}`); + updatedRepos.forEach(repo => { + shell.cd(join(cwd, 'packages', repo)); + console.log(`[${repo}] npm publish`); + shell.exec(`npm publish`); + }); +}