diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..ed6a4e5 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,94 @@ +const eslintrc = { + "parser": "babel-eslint", + "extends": "airbnb", + "env": { + "browser": true, + "node": true, + "es6": true, + "mocha": true, + "jest": true, + "jasmine": true + }, + "plugins": [ + "react", + "import" + ], + "parserOptions": { + parser: 'babel-eslint', + }, + "rules": { + "linebreak-style": 0, + "lines-between-class-members": 0, + "func-names": 0, + "sort-imports": 0, + "no-else-return": 0, + "operator-linebreak": 0, + "no-multiple-empty-lines": 0, + "arrow-body-style": 0, + "prefer-destructuring": 0, + "max-len": 0, + "consistent-return": 0, + "comma-dangle": [ + "error", + "always-multiline" + ], + "function-paren-newline": 0, + "class-methods-use-this": 0, + "import/extensions": 0, + "import/no-unresolved": 0, + "import/no-extraneous-dependencies": 0, + "import/prefer-default-export": 0, + "jsx-a11y/no-static-element-interactions": 0, + "jsx-a11y/anchor-has-content": 0, + "jsx-a11y/click-events-have-key-events": 0, + "jsx-a11y/anchor-is-valid": 0, + "jsx-a11y/label-has-for": 0, + "jsx-a11y/label-has-associated-control": 0, + "jsx-a11y/no-noninteractive-element-interactions": 0, + "jsx-a11y/mouse-events-have-key-events": 0, + "react/sort-comp": 0, + "react/prop-types": 0, + "react/require-extension": 0, + "react/jsx-filename-extension": [1, + { "extensions": [".js", ".jsx"] } + ], + "react/no-unescaped-entities": 0, + "react/destructuring-assignment": 0, + "react/no-access-state-in-setstate": 0, + "react/no-danger": 0, + "react/jsx-one-expression-per-line": 0, + "react/jsx-first-prop-new-line": 0, + "react/jsx-tag-spacing": 0, + "react/jsx-no-bind": 0, + "react/jsx-wrap-multilines": 0, + "react/forbid-prop-types": 0, + "react/require-default-props": 0, + "react/no-did-mount-set-state": 0, + "react/no-array-index-key": 0, + "react/no-find-dom-node": 0, + "react/no-unused-state": 0, + "react/no-unused-prop-types": 0, + "react/default-props-match-prop-types": 0, + "react/jsx-curly-spacing": 0, + "react/no-render-return-value": 0, + "react/prefer-stateless-function": 0, + "object-curly-newline": 0, + "no-param-reassign": 0, + "no-return-assign": 0, + "no-redeclare": 0, + "no-restricted-globals": 0, + "no-restricted-syntax": 0, + "no-underscore-dangle": 0, + "no-unused-expressions": 0 + } +} + +if (process.env.NODE_ENV === 'development') { + Object.assign(eslintrc.rules, + { + 'no-console': 0, + 'no-unused-vars': 0, + }); +} + +module.exports = eslintrc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e599c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +node_modules +npm-debug.log* +dist +.DS_Store +.cache +.rdoc-dist +.vscode +.next + +*.bak +*.tem +*.temp +#.swp +*.*~ +~*.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..8753b81 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +React SSR +--- + +A baseline for server side rendering for your React application. + +```bash +npm i @kkt/ssr +``` + +## Example + +- [simple](example/simple) A simple for server side rendering for your React application. +- [dynamic-loadable](example/dynamic-loadable) A dynamic loadable for server side rendering for your React application. \ No newline at end of file diff --git a/bin/ssr.js b/bin/ssr.js new file mode 100755 index 0000000..0522c1b --- /dev/null +++ b/bin/ssr.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const program = require('commander'); +const pkg = require('../package.json'); +require('colors-cli/toxic'); + +const logs = console.log; // eslint-disable-line + +process.env.HOST = '0.0.0.0'; +process.env.PORT = 3723; + +program + .description('A baseline for server side rendering for your React application.') + .version(pkg.version, '-v, --version') + .usage(' [options]'); + + +program + .command('build') + .description('Builds the app for production to the dist folder.') + .on('--help', () => { + logs(); + logs(' Examples:'); + logs(); + logs(` $ ${'react-ssr build'.green}`); + logs(); + }) + .action((cmd) => { + require('../script/build')(cmd); // eslint-disable-line + }); + +program + .command('start') + .description('Runs the app in development mode.') + .on('--help', () => { + logs(); + logs(' Examples:'); + logs(); + logs(` $ ${'react-ssr start'.green}`); + logs(); + }) + .action((cmd) => { + require('../script/start')(cmd); // eslint-disable-line + }); + +program.on('--help', () => { + logs('\n Examples:'); + logs(); + logs(` $ ${'react-ssr start'.green}`); + logs(` $ ${'react-ssr build'.green}`); + logs(); + logs(); +}); + +program.parse(process.argv); + +if (!process.argv.slice(2).length) { + program.outputHelp(); +} diff --git a/conf/env.js b/conf/env.js new file mode 100644 index 0000000..2ba5f59 --- /dev/null +++ b/conf/env.js @@ -0,0 +1,95 @@ +const fs = require('fs'); +const path = require('path'); +const paths = require('./'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + `${paths.dotenv}.${NODE_ENV}`, + `${paths.dotenv}.local`, + paths.dotenv, +]; +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. +// https://github.com/motdotla/dotenv +dotenvFiles.forEach((dotenvFile) => { + if (fs.existsSync(dotenvFile)) { + require('dotenv').config({ // eslint-disable-line + path: dotenvFile, + }); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebookincubator/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. +// https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +const nodePath = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and KKT_* environment variables and prepare them to be +// injected into the application via DefinePlugin in Webpack configuration. +const KKT = /^KKT_/i; + +function getClientEnvironment(target) { + const raw = Object.keys(process.env) + .filter(key => KKT.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + PORT: process.env.PORT || 3000, + HOST: process.env.HOST || 'localhost', + KKT_ASSETS_MANIFEST: paths.appManifest, + BUILD_TARGET: target === 'web' ? 'client' : 'server', + // only for production builds. Useful if you need to serve from a CDN + PUBLIC_PATH: process.env.PUBLIC_PATH || '/', + // CLIENT_PUBLIC_PATH is a PUBLIC_PATH for NODE_ENV === 'development' && BUILD_TARGET === 'client' + // It's useful if you're running KKT in a non-localhost container. Ends in a / + CLIENT_PUBLIC_PATH: process.env.CLIENT_PUBLIC_PATH, + // The public dir changes between dev and prod, so we use an environment + // variable available to users. + KKT_PUBLIC_DIR: + process.env.NODE_ENV === 'production' + ? paths.appBuildPublic + : paths.appPublic, + } + ); + // Stringify all values so we can feed into Webpack DefinePlugin + const stringified = Object.keys(raw).reduce((env, key) => { + env[`process.env.${key}`] = JSON.stringify(raw[key]); + return env; + }, {}); + + return { raw, stringified }; +} + +module.exports = { + getClientEnv: getClientEnvironment, + nodePath, +}; diff --git a/conf/index.js b/conf/index.js new file mode 100644 index 0000000..a49c449 --- /dev/null +++ b/conf/index.js @@ -0,0 +1,74 @@ +const PATH = require('path'); +const FS = require('fs'); +const url = require('url'); + +// 确保在项目文件夹中的任何符号都解决了: +const appDirectory = FS.realpathSync(process.cwd()); +const resolveApp = relativePath => PATH.resolve(appDirectory, relativePath); + +const envPublicUrl = process.env.PUBLIC_URL; +const getPublicUrl = appPackageJson => envPublicUrl || require(appPackageJson).homepage; // eslint-disable-line + + +function ensureSlash(inputPath, needsSlash) { + const hasSlash = inputPath.endsWith('/'); + if (hasSlash && !needsSlash) { + return inputPath.substr(0, inputPath.length - 1); + } else if (!hasSlash && needsSlash) { + return `${inputPath}/`; + } else { + return inputPath; + } +} + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// Webpack needs to know it to put the right