diff --git a/.circleci/config.yml b/.circleci/config.yml index 10a7c4d5..c815527b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,6 @@ version: 2 - jobs: - lint: + build: working_directory: ~/tmp docker: - image: circleci/node:10.11.0 @@ -25,35 +24,15 @@ jobs: - run: name: Lint command: yarn lint - - build: - working_directory: ~/tmp - docker: - - image: circleci/node:10.11.0 - steps: - - checkout - run: - name: Install Latest Yarn - command: 'sudo npm install --global yarn@latest' - - restore_cache: - name: Restore Yarn Package Cache - keys: - - yarn-packages-{{ checksum "yarn.lock" }} - - run: - name: Install Dependencies - command: yarn install --frozen-lockfile - - save_cache: - name: Save Yarn Package Cache - key: yarn-packages-{{ checksum "yarn.lock" }} - paths: - - ~/.cache/yarn - - run: - name: Build Test + name: Run Build command: yarn build - - + - run: + name: Run Unit Test + command: yarn test + workflows: version: 2 - install_and_lint: + lint-build: jobs: - - lint \ No newline at end of file + - build diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 00000000..d7138ccf --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,38 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 365 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: 'outdated' + +# Comment to post before locking. Set to `false` to disable +lockComment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b1b48bed..a816f7ed 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,4 @@ - + @@ -8,8 +8,8 @@ -- [ ] Your pull request targets the `canary` branch of Maleo.JS. -- [ ] Branch starts with either `fix/`, `feature/`, `optimization/`, or `translate/` (e.g. `fix/build-issue`) +- [ ] Your pull request targets the `canary` branch of Maleo.js. +- [ ] Branch starts with either `fix/`, `feature/`, etc (e.g. `fix/build-issue`. [more info](https://github.com/airyrooms/maleo.js/blob/canary/CONTRIBUTING.md)) - [ ] You have only one commit (if not, [squash](http://forum.freecodecamp.org/t/how-to-squash-multiple-commits-into-one-with-git/13231) them into one commit). - [ ] All new and existing tests pass the command `yarn test`. Use `git commit --amend` to amend any fixes. @@ -20,6 +20,8 @@ - [ ] Optimization (non-breaking change which provides better optimization) - [ ] Breaking change (fix or feature that would change existing functionality) - [ ] Add new translation (feature adding new translations) +- [ ] Update Documentation (added new docs or doc updates) +- [ ] Other ( ) #### Checklist: diff --git a/.gitignore b/.gitignore index f261f80c..ef8191bd 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,10 @@ package-lock.json @types .vscode + +# ignore yarn lock on example +example/*/yarn.lock +example/*/package-lock.json + +# OSX +.DS_Store \ No newline at end of file diff --git a/.grenrc b/.grenrc new file mode 100644 index 00000000..b3580929 --- /dev/null +++ b/.grenrc @@ -0,0 +1,12 @@ +{ + "dataSource": "commits", + "groupBy": { + "Features:": ["feat"], + "Enhancements:": ["refactor", "perf", "build"], + "Bug Fixes:": ["fix"] + }, + "ignoreIssuesWith": [ + "wontfix", + "duplicate" + ] +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..8d0628c4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,37 @@ +language: node_js +sudo: required +git: + quiet: true +branches: + only: + - master + - canary +cache: + yarn: true + +before_install: + - npm install --global yarn@1.10.1 + +script: + - yarn build + - yarn test + +before_deploy: + - yarn test:cov + - echo "access=public" >> $HOME/.npmrc 2> /dev/null + - echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" >> $HOME/.npmrc 2> /dev/null + - bash ./ci/travis-checkout-branch.sh + +deploy: + - provider: script + skip_cleanup: true + script: + - bash ./ci/publish-canary.sh + on: + branch: canary + - provider: script + skip_cleanup: true + script: + - bash ./ci/publish-stable.sh + on: + branch: master \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..f7da5540 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# Contributing Guidelines + +Hello 👋! + +Maleo.js is an un-opinionated framework to enable Universal Rendering in JavaScript using React with no hassle. + +We are here to solve the time consuming setups Universal Rendering Required. + +Feel free to contribute to this project. We are grateful for your contributions and we are excited to welcome you abroad! + +Happy contributing 🎉! + +### Setting Up Maleo.js in Local Environment + +**Clone Repo to Local Machine** + +[Fork](https://help.github.com/articles/fork-a-repo/) this repository to your GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local machine from forked repo. + +```bash +$ git clone https://github.com//maleo.js.git +$ cd maleo.js +``` + +Add Maleo.js repo as upstream to keep your fork up to date + +```bash +$ git remote add upstream https://github.com/airyrooms/maleo.js +``` + +Make sure you are currently on branch `canary`, if not you can run this command +```bash +$ git checkout canary +$ git pull upstream canary # sync with Maleo.js repo +``` + +**Setup** + +Make sure you are using the same or higher version of [Node.js](https://nodejs.org/en/) from `.nvmrc` file. (more info about [nvm](https://github.com/creationix/nvm)) + +We are using [Yarn](https://yarnpkg.com/en/) as package manager + +Install yarn as global dependency +```bash +$ npm install --global yarn +``` +Should the install fails, use `sudo` + +And then, install all the dependencies required by Maleo.js + +```bash +$ yarn +``` + +After the installation finished, run `fix:bin` command to fix binary symlink issue +```bash +$ yarn fix:bin +``` + +--- + +***Before making any changes, please check our [issues list](https://github.com/airyrooms/maleo.js/issues), for issues that you want to solve.*** + +Create a new branch based on what kind of contribution you are going to do. + +Here is the draft: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Contributing Type Branch Prefix Description
Fix fix/< name > Fixing current bug or issues
Feature feature/< name > Adding new feature
Optimization optimization/< name > Optimize current unoptimized code
Documentation docs/< name > Add or edit documentation related file
Translation translate/< name > Add or edit documentation's translation
+ +For example if you want to contribute to fix bug in Maleo.js you need to create new branch with `fix/` as the prefix. + +For example: +```bash +$ git checkout -b fix/webpack-bug +``` + +--- + +**Development** + +You are now ready to contribute to Maleo.js. Yaay 🤓! + +To build the module you can run this command: +```bash +$ yarn build +``` + +During development we are more likely watch our code changes, therefore we use this command: +```bash +$ yarn watch +``` + +The command above watches for every changes from folder `/packages/plugins` and `/packages/Maleo.js` + +**Test** + +For every new feature you are required to add unit test. + +You can add the unit test on folder `test` with filename having `[feature-name].test.js` prefix. + + +Running test: +```bash +$ yarn test +``` + +**Commit and Push Changes** + +Awesome 🎉! + +You have arrived at this stage, you are almost ready to make your changes available for other people. + +After you have made changes and tested, please don't commit the changes using `$ git commit`, instead use this command: +```bash +$ yarn commit +``` + +The command above will display a wizard for you to fill, it uses our standard commit message. After you have finished filling the answers, we will run [linting](https://stackoverflow.com/questions/8503559/what-is-linting) and `prettify` process to your code and stage those changes to the commit to make sure your code have the same formatting as the other. + +If you have passed all the process above you can now push your changes! 😙 + +```bash +$ git push +``` + +**Making Pull Request** + +YEAH!! 🎉🎉 You are ready to make your changes available for other people + +Your code are now available in your repository, but it's time to make a [Pull Request](https://help.github.com/articles/about-pull-requests/) to Maleo.js + +## FAQ +
+ How to test Maleo.js on local development machine in the example directory? + Maleo.js is utilizing Lerna and Yarn Workspace to manage the mono repo structure. + So you can use the Maleo.js inside package folder or example folder. Because Yarn Workspace and Lerna has hoisted all the dependencies into root directory. Therefore every app inside example able to add symlinked Maleo.js as dependency. +
+ +
+ +
+ How to test Maleo.js on your own app during development? + You can run this command inside packages/Maleo.js directory +
+  $ yarn link # if you are using yarn on your app
+  $ npm link # if you are using npm on your app
+ And then go to your app directory and add @airy/maleo.js to your own app's package.json and run this command: +
$ yarn link @airy/maleo.js
+ And you are good to go! Maleo.js are now living in your node_modules directory as a symlinked module + + more: + +
\ No newline at end of file diff --git a/README.md b/README.md new file mode 120000 index 00000000..4a1f29ae --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +packages/Maleo.js/README.md \ No newline at end of file diff --git a/ci/publish-canary.sh b/ci/publish-canary.sh new file mode 100755 index 00000000..723035d4 --- /dev/null +++ b/ci/publish-canary.sh @@ -0,0 +1,4 @@ +echo "Publishing Canary" +yarn publish:prepublish +yarn publish:version-canary +yarn publish:canary \ No newline at end of file diff --git a/ci/publish-stable.sh b/ci/publish-stable.sh new file mode 100644 index 00000000..45bc5721 --- /dev/null +++ b/ci/publish-stable.sh @@ -0,0 +1,4 @@ +echo "Publishing Stable" +yarn publish:prepublish +yarn publish:version-stable +yarn publish:stable \ No newline at end of file diff --git a/ci/travis-checkout-branch.sh b/ci/travis-checkout-branch.sh new file mode 100644 index 00000000..683fcbbf --- /dev/null +++ b/ci/travis-checkout-branch.sh @@ -0,0 +1,44 @@ +# Travis olways runs on detached HEAD mode +# we need to get it attach first + +# Set the user name and email to match the API token holder +# This will make sure the git commits will have the correct photo +# and the user gets the credit for a checkin +git config --global user.email "airyrooms-engineering@users.noreply.github.com" +git config --global user.name "airyrooms-engineering" +git config --global push.default matching + +# Get the credentials from a file +git config credential.helper "store --file=.git/credentials" + +# This associates the API Key with the account +echo "https://${GH_TOKEN}:@github.com" > .git/credentials + +# Make sure that the workspace is clean +# It could be "dirty" if +# 1. yarn.lock is not aligned with package.json +# 2. yarn install is run +git checkout -- . + +# Echo the status to the log so that we can see it is OK +git status + +head_ref=$(git rev-parse HEAD) +if [[ $? -ne 0 || ! $head_ref ]]; then + err "failed to get HEAD reference" + exit 1 +fi +branch_ref=$(git rev-parse "$TRAVIS_BRANCH") +if [[ $? -ne 0 || ! $branch_ref ]]; then + err "failed to get $TRAVIS_BRANCH reference" + exit 1 +fi +if [[ $head_ref != $branch_ref ]]; then + msg "HEAD ref ($head_ref) does not match $TRAVIS_BRANCH ref ($branch_ref)" + msg "someone may have pushed new commits before this build cloned the repo" + exit 1 +fi +if ! git checkout "$TRAVIS_BRANCH"; then + err "failed to checkout $TRAVIS_BRANCH" + exit 1 +fi diff --git a/example/playground/_document.tsx b/example/playground/_document.tsx index 3f6eb133..25d77f4e 100644 --- a/example/playground/_document.tsx +++ b/example/playground/_document.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Document, Header, Main, Scripts } from '@airy/maleo/lib/render/_document'; -import { ReduxScript } from '@airy/with-redux-plugin'; +import { default as Document, Header, Main, Scripts } from '@airy/maleo/document'; +import { ReduxScript } from '@airy/maleo-redux-plugin'; -export class MyDocument extends Document { +export default class MyDocument extends Document { static getInitialProps = async (ctx) => { const initialProps = await Document.getInitialProps(ctx); diff --git a/example/playground/_wrap.tsx b/example/playground/_wrap.tsx index 73237321..8c3f5e43 100644 --- a/example/playground/_wrap.tsx +++ b/example/playground/_wrap.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { _Wrap } from '@airy/maleo/lib/render/_wrap'; -import pageWithStyles from '@airy/css-plugin/lib/pageWithStyles'; -import { withRedux } from '@airy/with-redux-plugin'; +import Wrap from '@airy/maleo/wrap'; +import pageWithStyles from '@airy/maleo-css-plugin/lib/pageWithStyles'; +import { withRedux } from '@airy/maleo-redux-plugin'; import { makeStoreClient } from './store'; @pageWithStyles @withRedux(makeStoreClient) -export class Wrap extends _Wrap { +export default class extends Wrap { static getInitialProps = ({ store }) => { console.log('\nGIP wrap store', store); }; @@ -25,4 +25,4 @@ export class Wrap extends _Wrap { ); } -} +} \ No newline at end of file diff --git a/example/playground/client.ts b/example/playground/client.ts deleted file mode 100644 index 04f4c93b..00000000 --- a/example/playground/client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { init } from '@airy/maleo/lib/client/Client'; - -import { routes } from './routes'; -import { Wrap } from './_wrap'; - -init(routes, module, { Wrap }); diff --git a/example/playground/interface/AppContext.ts b/example/playground/interface/AppContext.ts deleted file mode 100644 index 0df45919..00000000 --- a/example/playground/interface/AppContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Store } from 'redux'; - -export interface AppContext { - url: string; - store: Store; -} diff --git a/example/playground/maleo.config.js b/example/playground/maleo.config.js index a10c230e..dd7e90bf 100644 --- a/example/playground/maleo.config.js +++ b/example/playground/maleo.config.js @@ -1,5 +1,5 @@ -const tsPlugin = require('@airy//typescript-plugin'); -const cssPlugin = require('@airy//css-plugin'); +const tsPlugin = require('@airy/maleo-typescript-plugin'); +const cssPlugin = require('@airy/maleo-css-plugin'); module.exports = tsPlugin( cssPlugin({ diff --git a/example/playground/package.json b/example/playground/package.json index 7293eb0f..0aa859a5 100644 --- a/example/playground/package.json +++ b/example/playground/package.json @@ -4,21 +4,20 @@ "description": "", "main": "index.js", "scripts": { - "dev": "maleo run", - "build:prod": "rm -rf .maleo && export NODE_ENV=production && maleo build", - "start": "export NODE_ENV=production && node .maleo/server.js" + "dev": "maleo dev", + "build": "export NODE_ENV=production && maleo build", + "start": "export NODE_ENV=production && maleo run" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@airy/css-plugin": "1.0.0", - "@airy/typescript-plugin": "1.0.0", - "@airy/with-redux-plugin": "^1.0.0", - "@airy/maleo": "1.0.0", - "react": "16.5.2", + "@airy/maleo-css-plugin": "latest", + "@airy/maleo-typescript-plugin": "latest", + "@airy/maleo-redux-plugin": "latest", + "@airy/maleo": "latest", + "react": "latest", "react-redux": "^5.1.1", - "react-router-dom": "^4.4.0-beta.1", "redux": "^4.0.1" }, "devDependencies": { diff --git a/example/playground/routes.json b/example/playground/routes.json new file mode 100644 index 00000000..579b7e4e --- /dev/null +++ b/example/playground/routes.json @@ -0,0 +1,28 @@ +[ + { + "page": "./src/MainApp", + "routes": [ + { + "path": "/", + "page": "./src/Search", + "exact": true + }, + { + "path": "/search", + "page": "./src/Search", + "routes": [ + { + "path": "/search/hello", + "page": "./src/Detail", + "exact": true + } + ] + }, + { + "path": "/detail", + "page": "./src/Detail", + "exact": true + } + ] + } +] \ No newline at end of file diff --git a/example/playground/routes.tsx b/example/playground/routes.tsx deleted file mode 100644 index c08e71cc..00000000 --- a/example/playground/routes.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import Dynamic from '@airy/maleo/lib/utils/dynamicImport'; - -import { RoomsMainApp } from './src/MainApp'; - -const Loading = () =>
Loading ...
; - -// console.log('Loading', Loading, Loading.prototype, typeof Loading); -// console.log('RoomsMainApp', RoomsMainApp, RoomsMainApp.prototype, typeof RoomsMainApp); -// console.log('Dynamic Component', (() => import('./src/Detail')).prototype); - -const RoomsSearch = Dynamic({ - loader: () => import('./src/Search' /* webpackChunkName:"Search" */), - loading: Loading, - modules: ['./src/Search'], -}); - -const RoomsDetail = Dynamic({ - loader: () => import('./src/Detail' /* webpackChunkName:"Detail" */), - loading: Loading, - modules: ['./src/Detail'], -}); - -// import { RoomsDetail } from 'src/Detail'; -// import { RoomsSearch } from 'src/Search'; - -export const routes = [ - { - path: '/', - component: RoomsMainApp, - key: 'rootWrapper', - routes: [ - { - path: '/', - key: 'rootPage', - component: () =>

This is root path

, - exact: true, - }, - { - path: '/search', - key: 'root-search', - component: RoomsSearch, - routes: [ - { - path: '/search/hello', - key: 'search-hello', - component: RoomsDetail, - exact: true, - }, - ], - // exact: true, - }, - { - path: '/detail', - key: 'rootDetail', - component: RoomsDetail, - exact: true, - }, - { - path: '*', - key: '404', - component: () =>
not found
, - }, - ], - }, -]; diff --git a/example/playground/server.ts b/example/playground/server.ts deleted file mode 100644 index 0def294c..00000000 --- a/example/playground/server.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Server } from '@airy/maleo/lib/server/Server'; -import path from 'path'; - -import { MyDocument } from './_document'; -import { routes } from './routes'; -import { Wrap } from './_wrap'; - -const PORT = process.env.PORT || 8080; - -const maleoServer = Server.init({ - port: PORT, - assetDir: path.resolve('.', '.maleo', 'client'), - distDir: path.resolve('.', 'dist'), - - routes, - - _document: MyDocument, - _wrap: Wrap, -}); - -maleoServer.run(() => { - // tslint:disable-next-line:no-console - console.log('Server running on port :' + PORT); -}); diff --git a/example/playground/src/Detail/detail.css b/example/playground/src/Detail/detail.css index 9e64b039..03e8f875 100644 --- a/example/playground/src/Detail/detail.css +++ b/example/playground/src/Detail/detail.css @@ -1,6 +1,5 @@ .detail { background-color: blue; display: inline-block; - float: left; max-width: 50%; } \ No newline at end of file diff --git a/example/playground/src/Detail/index.tsx b/example/playground/src/Detail/index.tsx index d07f53c9..38c5b067 100644 --- a/example/playground/src/Detail/index.tsx +++ b/example/playground/src/Detail/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import withStyles from '@airy//css-plugin/lib/withStyles'; +import withStyles from '@airy/maleo-css-plugin/lib/withStyles'; -import { AppContext } from '../../interface/AppContext'; const style = require('./detail.css'); @@ -9,7 +8,7 @@ const style = require('./detail.css'); export class RoomsDetail extends React.Component { static displayName = 'RoomsDetail'; - static getInitialProps = async (appContext: AppContext) => { + static getInitialProps = async (appContext) => { const { store } = appContext; return { a: 5, store }; diff --git a/example/playground/src/MainApp.tsx b/example/playground/src/MainApp.tsx index 589b1190..5ee32142 100644 --- a/example/playground/src/MainApp.tsx +++ b/example/playground/src/MainApp.tsx @@ -17,7 +17,6 @@ export class RoomsMainApp extends React.Component { }; render() { - console.log('Data', this.props.data); return (
rooms main app diff --git a/example/playground/src/Search/index.tsx b/example/playground/src/Search/index.tsx index d871e2b4..c0a25b96 100644 --- a/example/playground/src/Search/index.tsx +++ b/example/playground/src/Search/index.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import { AppContext } from '../../interface/AppContext'; import Loadable from 'react-loadable'; const DetailComponent = Loadable({ @@ -11,7 +10,7 @@ const DetailComponent = Loadable({ export class RoomsSearch extends React.Component { static displayName = 'RoomsSearch'; - static getInitialProps = async (appContext: AppContext) => { + static getInitialProps = async (appContext) => { const { store } = appContext; if (store) { store.dispatch({ type: 'TEST', data: 'searchhh' }); diff --git a/example/playground/store.ts b/example/playground/store.ts index 78cec890..e9732857 100644 --- a/example/playground/store.ts +++ b/example/playground/store.ts @@ -1,6 +1,6 @@ import { Action, combineReducers } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension'; -import { makeStore } from '@airy/with-redux-plugin'; +import { makeStore } from '@airy/maleo-redux-plugin'; interface State { initVal: string; diff --git a/example/playground/tsconfig.json b/example/playground/tsconfig.json index a6036f07..24344d21 100644 --- a/example/playground/tsconfig.json +++ b/example/playground/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../node_modules/@airy/typescript-plugin/tsconfig.base.json", + "extends": "../../node_modules/@airy/maleo-typescript-plugin/tsconfig.base.json", "compilerOptions": { "baseUrl": ".", "rootDir": ".", diff --git a/lerna.json b/lerna.json index d0612c70..e6a1a878 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,6 @@ { - "version": "fixed", + "lerna": "3.7.2", + "version": "independent", "packages": [ "packages/**" ], @@ -8,13 +9,44 @@ "--no-lockfile" ], "useWorkspaces": true, + "publishConfig": { + "access": "public" + }, + "ignoreChanges": [ + "*.md", + "*.test.*", + "example/**", + "**/__test__/**" + ], "command": { + "version": { + "exact": true, + "githubRelease": true, + "noCommitHooks": true, + "message": "chore(release): publish %s [skip ci]", + "conventionalCommits": true, + "allowBranch": [ + "master", + "canary" + ], + "yes": true + }, "publish": { + "forcePublish": false, + "npmClient": "npm", + "noChangelog": true, + "allowBranch": [ + "master", + "canary" + ], + "yes": true, + "registry": "https://registry.npmjs.org/", "ignoreChanges": [ "*.md", "*.test.*", - "example/**" + "example/**", + "**/__test__/**" ] } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index ecc06ac6..4ed4246b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "airy-maleojs", - "version": "1.0.0", "description": "Un-opinionated Universal Rendering Javascript Framework", "author": "Airy Engineering ", "license": "MIT", @@ -22,8 +21,7 @@ "webpack" ], "workspaces": [ - "packages/**", - "example/**" + "packages/**" ], "typeScriptVersion": "3.1.1", "engines": { @@ -42,7 +40,14 @@ "ts:lint": "tslint -c tslint.json 'packages/**/**.ts'", "lint": "yarn ts:lint", "pretty:fix": "prettier --config-precedence file-override --config .prettierrc --write 'packages/**/**+(.jsx|.tsx|.js|.ts)'", - "test": "yarn build && jest" + "test": "jest --config ./setup-test/jest.config.js --coverage", + "test:cov": "jest --config ./setup-test/jest.config.js --coverage --coverageReporters=text-lcov | coveralls", + "build:test": "yarn build && yarn test", + "publish:prepublish": "lerna run prepublish", + "publish:version-canary": "lerna version prerelease --preid canary", + "publish:canary": "lerna publish from-package --no-git-reset --canary --dist-tag canary", + "publish:version-stable": "lerna version", + "publish:stable": "lerna publish from-package --no-git-reset" }, "husky": { "hooks": { @@ -59,6 +64,7 @@ "babel-core": "7.0.0-bridge.0" }, "devDependencies": { + "@babel/plugin-transform-modules-commonjs": "^7.2.0", "@commitlint/cli": "^7.3.2", "@commitlint/config-conventional": "^7.3.1", "@types/express": "^4.16.0", @@ -66,13 +72,16 @@ "@types/react": "^16.4.18", "@types/react-dom": "^16.0.9", "@types/react-loadable": "^5.4.1", - "@types/react-router": "^4.0.31", "@types/react-router-dom": "^4.3.1", "@types/tapable": "^1.0.4", "@types/webpack": "^4.4.14", "babel-jest": "^23.6.0", "commitizen": "^3.0.5", + "coveralls": "^3.0.2", "cz-conventional-changelog": "^2.1.0", + "enzyme": "^3.8.0", + "enzyme-adapter-react-16": "^1.8.0", + "github-release-notes": "^0.17.0", "husky": "^1.3.1", "jest": "^23.6.0", "lerna": "^3.4.3", diff --git a/packages/Maleo.js/.npmignore b/packages/Maleo.js/.npmignore index d8db1a2a..d92980ae 100644 --- a/packages/Maleo.js/.npmignore +++ b/packages/Maleo.js/.npmignore @@ -1,2 +1,2 @@ -!dist +!lib !package.json \ No newline at end of file diff --git a/packages/Maleo.js/CHANGELOG.md b/packages/Maleo.js/CHANGELOG.md new file mode 100644 index 00000000..aac15932 --- /dev/null +++ b/packages/Maleo.js/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.8-canary.3](https://github.com/airyrooms/maleo.js/compare/@airy/maleo@0.0.8-canary.2...@airy/maleo@0.0.8-canary.3) (2019-03-19) + +**Note:** Version bump only for package @airy/maleo + + + + + +## [0.0.8-canary.2](https://github.com/airyrooms/maleo.js/compare/@airy/maleo@0.0.8-alpha.0...@airy/maleo@0.0.8-canary.2) (2019-03-18) + +**Note:** Version bump only for package @airy/maleo diff --git a/packages/Maleo.js/README.md b/packages/Maleo.js/README.md new file mode 100644 index 00000000..b2a638de --- /dev/null +++ b/packages/Maleo.js/README.md @@ -0,0 +1,675 @@ +[![Build Status](https://travis-ci.org/airyrooms/maleo.js.svg?branch=master)](https://travis-ci.org/airyrooms/maleo.js) +[![Coverage Status](https://coveralls.io/repos/github/airyrooms/maleo.js/badge.svg?branch=master)](https://coveralls.io/github/airyrooms/maleo.js?branch=master) +[![npm version](https://badge.fury.io/js/%40airy%2Fmaleo.svg)](https://badge.fury.io/js/%40airy%2Fmaleo) +[![Issues](http://img.shields.io/github/issues/airyrooms/maleo.js.svg)]( https://github.com/airyrooms/maleo.js/issues) +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/airyrooms/maleo.js/blob/canary/LICENSE) +[![Discord Chat](https://img.shields.io/discord/550498214789513218.svg)](https://discord.gg/9eArCQn) + + +# Welcome to Maleo.js + +Maleo.js is an un-opinionated framework to enable Universal Rendering in JavaScript using React with no hassle. + +We are here to solve the time consuming setups Universal Rendering Required. + +--- + +Readme below is the documentation for the `canary` (prerelease) branch. To view the documentation for the latest stable Maleo.js version change branch to `master` + +--- + +# Table of Contents +- [Features](#features) +- [Setup](#setup) +- [Component Lifecycle](#component-lifecycle) +- [Routing](#routing) +- [Dynamic Import Component](#dynamic-import-component) + - [Tips](#tips) + - [Preloading](#preloading) +- [Customizable Component](#customizable-component) + - [Custom Document](#custom-document) + - [Custom Wrap](#custom-wrap) +- [Custom Configuration](#custom-configuration) + - [Customize Server](#customize-server) + - [Customize Webpack](#customize-webpack) + - [Customize Babel Config](#customize-babel-config) +- [CDN Support](#cdn-support) +- [Plugins](#plugins) +- [FAQ](#faq) +- [Contributing](#contributing) + +--- + +## Features +- Universal Rendering +- Plugin based framework +- Customizable + +## Setup + +Install Maleo.js + +>*for now change `@airy/maleo` to `@airy/maleo@canary` until we publish the stable version of this package* + + +**NPM** +```bash +$ npm install --save @airy/maleo react +``` +**Yarn** +```bash +$ yarn add @airy/maleo react +``` + +Add this script to your `package.json` +```json +{ + "scripts": { + "dev": "maleo dev", + "build": "export NODE_ENV=production && maleo build", + "start": "export NODE_ENV=production && node .maleo/server.js" + } +} +``` + +Create a page Root component +```jsx +// ./src/Root.jsx +import React from 'react'; + +// Export default is required for registering page +export default class RootComponent extends React.Component { + render() { + return ( +

Hello World!

+ ) + } +} +``` + +And lastly, create a routing file on your project root directory called `routes.json` and register your page component +```json +[ + { + "path": "/", + "page": "./src/Root" + } +] +``` + +After that you can now run `$ npm run dev` and go to `http://localhost:3000`. + +You should now see your app running on your browser. + +By now you should see +- Automatic transpilation and bundling (with webpack and babel) +- ~~Hot code reloading~~ [#17](https://github.com/airyrooms/maleo.js/issues/17) +- Server rendering + +To see how simple this is, check out the sample app! + +## Component Lifecycle + +Maleo.js added a new component lifecycle hook called `getInitialProps`, this function is called during Server Side Rendering (SSR). + +This is useful especially for SEO purposes. + +Example for stateful component: +```jsx +import React from 'react'; + +export default class extends React.Component { + static getInitialProps = async (ctx) => { + const { req } = ctx; + + const userAgent = req ? req.headers['user-agent'] : navigator.userAgent; + + // the return value will be passed as props for this component + return { userAgent }; + } + + render() { + return ( +
+ Hello World {this.props.userAgent} +
+ ); + } +} +``` + +Example for stateless component: +```jsx +const Component = (props) => ( +
+ Hello World {props.userAgent} +
+); + +Component.getInitialprops = async (ctx) => { + const { req } = ctx; + + const userAgent = req ? req.headers['user-agent'] : navigator.userAgent; + + return { userAgent }; +}; + +export default Component; +``` + +`getInitialProps` receives a context object with the following properties: +- `req` - HTTP request object (server only) +- `res` - HTTP response object (server only) +- `...wrapProps` - Spreaded properties from custom Wrap +- `...appProps` - Spreaded properties from custom App + +## Routing + +Routing is declared in a centralized route config. +Register all the route config in `routes.json` file. + +If you put the `routes.json` files on root directory, Maleo will automatically register your route. Otherwise put path to your routes on [Maleo config](#custom-configuration). + +Routes file has to export default the route configuration. +The route object **expected to have distinct key** to indicate the route. + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
pathString!Routes path
pageString!Path to React Component for this route path
exactBoolean? [false]To make url has to match exactly the path in order to render the component. Give false value if the route is a wrapper route component
routesRouteObject?Nested route
+ + +For example: +```json +[ + { + "page": "./src/MainApp", + "routes": [ + { + "path": "/", + "page": "./src/Search", + "exact": true + }, + { + "path": "/search", + "page": "./src/Search", + "routes": [ + { + "path": "/search/hello", + "page": "./src/Detail", + "exact": true + } + ] + }, + { + "path": "/detail", + "page": "./src/Detail", + "exact": true + } + ] + } +] +``` + +## Dynamic Import Component +Maleo.js supports TC39 [dynamic import proposal](https://github.com/tc39/proposal-dynamic-import) for JavaScript. + +You can think dynamic import as another way to split your code into manageable chunks. You can use our `Dynamic` function which utilizes [react loadable](https://github.com/jamiebuilds/react-loadable) + +For Example +```js +// DynamicComponent.js +import Dynamic from '@airy/maleo/dynamic'; + +export default Dynamic({ + loader: () => import( /* webpackChunkName:"DynamicComponent" */ './component/DynamicLoad'), +}) +``` + +### **Preloading** + +For optimization purposes, you can also preload a component even before the component got rendered. + +For example, if you want to load component when a button get pressed, you can start preloading the component when the user hovers the mouse over the button. + +The component created by `Dynamic` exposes a [static `preload` method](https://github.com/jamiebuilds/react-loadable#loadablecomponentpreload). + +```jsx +import React from 'react'; +import Dynamic from '@airy/maleo/dynamic'; + +const DynamicBar = Dynamic({ + loader: () => import('./Bar'), + loading: LoadingCompoinent +}); + +class MyComponent extends React.Component { + state = { showBar: false }; + + onClick = () => { + this.setState({ showBar: true }); + }; + + onMouseOver = () => DynamicBar.preload(); + + render() { + return ( +
+ + { this.state.showBar && } +
+ ) + } +} +``` + +## Customizable Component + +### Custom Document + +Highly inspired by what [Next.js](https://github.com/zeit/next.js) has done on their awesome template customization. + +Maleo.js also enable customization on `Document` as document's markup. So you don't need to include tags like ``, ``, ``, etc. + +To override the default behavior, you'll need to create a component that extends the `Document` React class provided by Maleo. + +```jsx +// document.jsx +import React from 'react'; +import { Document, Header, Main, Scripts } from '@airy/maleo/document'; + +export default class extends Document { + render() { + return ( + +
+ Maleo JS + + + + +
+ + +
+ + + + + + + ); + } +} +``` + +### Custom Wrap + +Maleo.js uses the `Wrap` component to initialize pages. `Wrap` contains React Router's Component. You can add HoC here to wrap the application and control the page initialization. Which allows you to do amazing things like: +- Persisting layour between page changes +- Keeping state when navigating pages +- Custom error handling using `componentDidCatch` +- Inject additional data into pages (like Redux provider, etc) + +To override the default behavior, you'll need to create a component that extends the `Wrap` React class provided by Maleo. + +```jsx +// wrap.jsx +import React from 'react'; +import { Wrap } from '@airy/maleo/wrap'; + +// Redux plugin for Maleo.js +// Hoc that creates store and add Redux Provider +import { withRedux } from '@airy/maleo-redux-plugin'; + +// Custom Wrapper that will be rendered for the whole Application +import CustomWrapper from './component/CustomWrapper'; + +import { createStore } from './store'; + +@withRedux(createStore) +export default class extends Wrap { + static getInitialProps = (ctx) => { + const { store } = ctx + // you receive store from context + // you can access or do something with the store here + console.log('Initialized Redux Store', store); + return {} + } + + render() { + return ( + + {super.render()} + + ) + } +} + +``` +If you put `document.jsx` and `wrap.jsx` on root directory (the same level with `package.json`), then Maleo will automatically register your custom Document and Wrap. Otherwise, you can add the path to your custom Document and Wrap on [Maleo config](#custom-configuration) + +--- +***We are also working on adding default and customizable `Error` component page*** + +--- + +## Custom Configuration + +For more advanced configuration of Maleo.js, like `webpack` config, registering `plugins`, path to your routes, custom Document and Wrap, and adding `path alias`, you can create a `maleo.config.js` in the root of your project directory. (same directory with `package.json`) + +```js +// maleo.config.js + +module.exports = { + /* config options here */ +} +``` + +Here are the API's for the configuration: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyTypeDescription
buildDirString? [.maleo]Directory to put Maleo.js' build assets
cacheBoolean? [true]Enable webpack build caching
sourceMapsBoolean? [true]Enable webpack to generate source maps
aliasObject?A key value pair for aliasing path directory +
+ + { 'component': './src/component' } + +
publicPathString? [/_assets/]To customize webpack's publicPath +
Comes in handy if using CDN to put built assets
analyzeBundleBoolean? [false]To enable webpack's bundle analyzer, for analyzing bundle sizes during bundle debugging should Maleo.js' build process got slow
webpackFunction?To customize webpack configuration, more details here
routesstring? [rootDir/routes.jsx]Path to your routes file
customDocumentstring? [rootDir/document.jsx]Path to your custom document file
customWrapstring? [rootDir/wrap.jsx]Path to your custom wrap file
customAppstring? [rootDir/app.jsx]Path to your custom app file
+ +#### Customize Server + +Create a `server.js` file on root directory where your `package.json` lives. +Here you can customize Maleo's server. +```js +import { Server } from '@airy/maleo/server'; +import path from 'path'; + +import routeConfig from './routes'; + +const PORT = process.env.PORT || 8080; + +const maleoServer = Server.init({ + port: PORT, +}); + +maleoServer.run(() => { + console.log('Server running on port :', PORT); +}); +``` + +Here are the API's for the configuration: + + + + + + + + + + + + + + + + + +
KeyTypeDescription
portNumber? [3000]Port to run Maleo server
assetDirString? ['/.maleo/client']Directory for all client related assets
+ +#### Customize Webpack + +You are able to extend Maleo.js' default webpack configuration by defining a function on `maleo.config.js` + +```js +// maleo.config.js + +module.exports = { + webpack(config, context, next) { + // Perform customizations to webpack config + // Important: This call is required in order to let Maleo pass the config to webpack + return next(); + }, +}; +``` + +Webpack function will receive three arguments: + + + + + + + + + + + + + + + + + + +
ArgumentDetails
configThis contains webpack configuration object that you can manipulate
contextThis contains some keys that are useful for the build context +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
isDevBooleanCheck if current build is for development
publicPathStringPublic Path of user defined or default's value
analyzeBundleBooleanCheck if analyze bundle is enabled
buildDirectoryStringBuild Directory of user defined or default's value
nameStringBuild name 'server' || 'client'
+
nextA callback required to be called to pass the custom configuration
+ +Example of adding `ts-loader` through `maleo.config.js`: + +```js +// maleo.config.js + +// Partly taken and modified from @airy/maleo-ts-plugin source +// for simplicity purposes + +module.exports = { + webpack(config, context, next) { + const { isDev } = context + + config.module.rules.push({ + test: /\.tsx?/, + exclude: /node_modules/, + use: [ + require.resolve('@airy/maleo/lib/build/loaders/maleo-babel-loader'), + { + loader: 'ts-loader', + options: { + transpileOnly: true, + }, + }, + ], + + if (isDev) { + config.plugins.push(new ForkTSCheckerWebpackPlugin()); + } + + return next(); + }) + }, +}; +``` + +#### Customize Babel Config + +Maleo.js also let you have your own babel config. Just simply add `.babelrc` file at the root directory of your app. + +You can include Maleo.js' babel preset in order to have latest JavaScript preset. + +Here's an example of `.babelrc` file: +```json +{ + "presets": ["@airy/maleo/babel"], + "plugins": [] +} +``` + +The `@airy/maleo/babel` preset includes everything you need to get your development started. The preset includes: +- `@babel/preset-env` +- `@babel/preset-react` +- `@babel/plugin-proposal-class-properties` +- `@babel/plugin-proposal-decorators` +- `@babel/plugin-proposal-object-rest-spread` +- `@babel/plugin-transform-runtime` +- `react-loadable/babel` + +## CDN Support + +If you are using a CDN, you can set up the `publicPath` setting and configure your CDN's origin to resolve to the domain that Maleo.js is hosted on. + +```js +// maleo.config.js + +const isProd = process.env.NODE_ENV === 'production'; + +module.exports = { + assetPrefix: isProd && 'https://cdn.example.com'; +} +``` + +## FAQ + +== TO BE DETERMINED == + +## Plugins +- [css-plugin](https://github.com/airyrooms/maleo.js/tree/canary/packages/plugins/css-plugin) +- [typescript-plugin](https://github.com/airyrooms/maleo.js/tree/canary/packages/plugins/typescript-plugin) +- [redux-plugin](https://github.com/airyrooms/maleo.js/tree/canary/packages/plugins/redux-plugin) + + +## Contributing +[Please follow these steps to contribute to Maleo.js](https://github.com/airyrooms/maleo.js/blob/canary/CONTRIBUTING.md) + +[Please follow these steps to contribute to Maleo.js' plugins](https://github.com/airyrooms/maleo.js/tree/canary/packages/plugins) + + +## Contributors + +This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md). + +Many thanks to our **[contributors](https://github.com/airyrooms/maleo.js/graphs/contributors)**! + + + +## License + +MIT + diff --git a/packages/Maleo.js/__test__/render/_document.test.js b/packages/Maleo.js/__test__/render/_document.test.js new file mode 100644 index 00000000..4bf0673c --- /dev/null +++ b/packages/Maleo.js/__test__/render/_document.test.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { shallow } from 'enzyme'; + +import { Document, Header, Main, Scripts } from '@airy/maleo/document'; + +describe('', () => { + const preloadScripts = [ + { name: 'main', filename: 'main.js' }, + { name: 'common', filename: 'commons.js' }, + ]; + + it('renders Header component', () => { + const context = { + preloadScripts, + }; + const header = shallow(
, { context }); + + expect(header.context()).toStrictEqual(context); + expect(header.containsMatchingElement()).toBe(true); + }); +}); diff --git a/packages/Maleo.js/__test__/server/loadInitialProps.test.js b/packages/Maleo.js/__test__/server/loadInitialProps.test.js new file mode 100644 index 00000000..79d032fb --- /dev/null +++ b/packages/Maleo.js/__test__/server/loadInitialProps.test.js @@ -0,0 +1,157 @@ +import React from 'react'; + +import { matchingRoutes } from '@airy/maleo/lib/server/routeHandler'; +import { loadInitialProps, loadComponentProps } from '@airy/maleo/lib/server/loadInitialProps'; + +const initialProps = { + root: { + loadFromServer: [1, 2, 3, 4, 5], + }, + match: { + matchData: { + a: 1, + b: true, + c: '3', + }, + }, +}; + +class RootComponent extends React.Component { + static getInitialProps = (context) => { + return initialProps.root; + }; + + render() { + return ( +
+

Wrapper

+ {this.props.children} +
+ ); + } +} + +class MatchComponent extends React.Component { + static getInitialProps = (context) => { + return initialProps.match; + }; + + render() { + return

Match Component

; + } +} + +class ComponentNoInitialProps extends React.Component { + render() { + return

No initial props

; + } +} + +describe('[Load Initial Props] Simple', () => { + let routes = []; + + beforeEach(async () => { + routes = [ + { + path: '/not-matched', + component: () =>

Not Matched

, + key: 'not-matched', + }, + { + path: '/match', + component: MatchComponent, + key: 'match', + isExact: true, + }, + ]; + }); + + test('Should return matched route', async () => { + const pathname = '/match'; + const mr = matchingRoutes(routes, pathname); + const { branch } = await loadInitialProps(mr, {}); + + expect(branch).toHaveProperty('route'); + expect(branch.route).toEqual(routes.find((r) => r.key === 'match')); + expect(branch).toHaveProperty('match'); + expect(branch.match).toEqual({ + path: '/match', + url: '/match', + isExact: true, + params: {}, + }); + }); + + test('Should return inital props', async () => { + const pathname = '/match'; + const mr = matchingRoutes(routes, pathname); + const { data } = await loadInitialProps(mr, {}); + + expect(data).toEqual({ match: initialProps.match }); + }); + + test('Should return no matched route', async () => { + const pathname = '/404'; + const mr = matchingRoutes(routes, pathname); + const { branch, data } = await loadInitialProps(mr, {}); + + expect(branch).toBe(undefined); + expect(data).toEqual({}); + }); +}); + +describe('[Load Initial Props] Nested', () => { + let routes = []; + + beforeEach(() => { + routes = [ + { + path: '/', + component: RootComponent, + key: 'root', + routes: [ + { + path: '/match', + component: MatchComponent, + key: 'match', + }, + { + path: '/not-matched', + component: () =>

Not Matched

, + key: 'not-matched', + }, + ], + }, + ]; + }); + + test('Should render matched route(s)', async () => { + const pathname = '/match'; + const mr = matchingRoutes(routes, pathname); + const { branch } = await loadInitialProps(mr, {}); + + expect(branch.route.routes.length).toBe(2); + }); + + test('Should return inital props', async () => { + const pathname = '/match'; + const mr = matchingRoutes(routes, pathname); + const { data } = await loadInitialProps(mr, {}); + + expect(data).toEqual(initialProps); + }); +}); + +describe('[Load Component Props] Check component has getInitialProps properties', () => { + test('Should return null', async () => { + const result = await loadComponentProps(ComponentNoInitialProps, {}); + + expect(result).toBe(null); + }); + + test('Should return result', async () => { + const result = await loadComponentProps(MatchComponent, {}); + + expect(result).toEqual(initialProps.match); + }); +}); diff --git a/packages/Maleo.js/__test__/server/routeHandler.test.js b/packages/Maleo.js/__test__/server/routeHandler.test.js new file mode 100644 index 00000000..4cc57d6d --- /dev/null +++ b/packages/Maleo.js/__test__/server/routeHandler.test.js @@ -0,0 +1,45 @@ +import React from 'react'; + +import { matchingRoutes } from '@airy/maleo/lib/server/routeHandler'; + +describe('[Route Handler] Simple', () => { + let routes = []; + beforeEach(() => { + routes = [ + { + path: '/not-matched', + component: () =>

Not Matched

, + key: 'not-matched', + }, + { + path: '/match', + component: () =>

Match

, + key: 'match', + isExact: true, + }, + ]; + }); + + test('Should return matched route', () => { + const pathname = '/match'; + const mr = matchingRoutes(routes, pathname); + + const { route, match } = mr[0]; + + expect(route).toEqual(routes[1]); + expect(match).toEqual({ + path: routes[1].path, + url: routes[1].path, + isExact: routes[1].isExact, + params: {}, + }); + }); + + test('Should not return any matched route', () => { + const pathname = '/'; + const mr = matchingRoutes(routes, pathname); + + expect(mr).toEqual([]); + expect(mr.length).toBe(0); + }); +}); diff --git a/packages/Maleo.js/__test__/utils/index.test.js b/packages/Maleo.js/__test__/utils/index.test.js new file mode 100644 index 00000000..9df9ca16 --- /dev/null +++ b/packages/Maleo.js/__test__/utils/index.test.js @@ -0,0 +1,64 @@ +import { isPromise, isFunction, isObject } from '@airy/maleo/lib/utils/index'; + +describe('[Utilities] Index', () => { + test('isPromise should return true', () => { + expect(isPromise(Promise.resolve())).toBe(true); + expect( + isPromise( + new Promise((resolve) => { + return resolve(''); + }), + ), + ).toBe(true); + expect(isPromise(Promise.reject())).toBe(true); + }); + + test('isPromise should return false', () => { + expect(isPromise(setTimeout(new Function(), 0))).toBe(false); + expect(isPromise({})).toBe(false); + expect(isPromise([])).toBe(false); + expect(isPromise(1)).toBe(false); + expect(isPromise(true)).toBe(false); + expect(isPromise('')).toBe(false); + expect(isPromise()).toBe(false); + }); + + test('isFunction should return true', () => { + expect(isFunction(new Function())).toBe(true); + expect( + isFunction(() => { + return void 0; + }), + ).toBe(true); + expect( + isFunction(function() { + return void 0; + }), + ).toBe(true); + }); + + test('isFunction should return false', () => { + expect(isFunction({})).toBe(false); + expect(isFunction([])).toBe(false); + expect(isFunction(1)).toBe(false); + expect(isFunction(true)).toBe(false); + expect(isFunction('')).toBe(false); + expect(isFunction()).toBe(false); + }); + + test('isObject should return true', () => { + expect(isObject({})).toBe(true); + expect(isObject([])).toBe(true); + expect(isObject(new Object())).toBe(true); + expect(isObject(new Array())).toBe(true); + expect(isObject(new String())).toBe(true); + }); + + test('isObject should return false', () => { + expect(isObject(new Function())).toBe(false); + expect(isObject('')).toBe(false); + expect(isObject(1)).toBe(false); + expect(isObject(null)).toBe(false); + expect(isObject()).toBe(false); + }); +}); diff --git a/packages/Maleo.js/app.js b/packages/Maleo.js/app.js new file mode 100644 index 00000000..269dbf94 --- /dev/null +++ b/packages/Maleo.js/app.js @@ -0,0 +1 @@ +module.exports = require('./lib/render/_app'); diff --git a/packages/Maleo.js/babel.config.js b/packages/Maleo.js/babel.config.js index 92d16376..91cbc20f 100644 --- a/packages/Maleo.js/babel.config.js +++ b/packages/Maleo.js/babel.config.js @@ -1,15 +1,18 @@ -const { resolve } = require('path'); - const { paths } = require('./tsconfig.json').compilerOptions; const aliases = {}; Object.keys(paths).forEach((item) => { const key = item.replace('/*', ''); - const path = paths[item][0].replace(/src\/(.+)\/\*/, '$1/'); - const value = resolve('lib', path); - aliases[key] = value; + let path = ''; + if (paths[item][0] === './*') { + path = './'; + } else { + path = paths[item][0].replace(/src\/(.+)\/\*/, './lib/$1/'); + } + + aliases[key] = path; }); module.exports = (api) => { diff --git a/packages/Maleo.js/client.js b/packages/Maleo.js/client.js new file mode 100644 index 00000000..49a285d6 --- /dev/null +++ b/packages/Maleo.js/client.js @@ -0,0 +1 @@ +module.exports = require('./lib/client/client'); diff --git a/packages/Maleo.js/document.js b/packages/Maleo.js/document.js new file mode 100644 index 00000000..29ff33db --- /dev/null +++ b/packages/Maleo.js/document.js @@ -0,0 +1 @@ +module.exports = require('./lib/render/_document'); diff --git a/packages/Maleo.js/dynamic.js b/packages/Maleo.js/dynamic.js new file mode 100644 index 00000000..2d441801 --- /dev/null +++ b/packages/Maleo.js/dynamic.js @@ -0,0 +1 @@ +module.exports = require('./lib/utils/dynamicImport'); diff --git a/packages/Maleo.js/gulpfile.js b/packages/Maleo.js/gulpfile.js index cc78b074..f0c0a79c 100644 --- a/packages/Maleo.js/gulpfile.js +++ b/packages/Maleo.js/gulpfile.js @@ -18,6 +18,8 @@ const paths = { server: 'src/server/**', client: 'src/client/**', bin: 'src/bin/*.ts', + routes: 'src/routes/**', + default: 'src/default/*.ts', }; let tasks = Object.keys(paths); @@ -31,7 +33,7 @@ tasks.map((p) => { .src(paths[p]) .pipe(sourcemaps.init()) .pipe(babel()) - // .pipe(uglify()) + .pipe(uglify()) .pipe(sourcemaps.write('.')) .pipe(gulp.dest(dest)), ); diff --git a/packages/Maleo.js/package.json b/packages/Maleo.js/package.json index a0352bdf..6af2e90d 100644 --- a/packages/Maleo.js/package.json +++ b/packages/Maleo.js/package.json @@ -1,20 +1,32 @@ { "name": "@airy/maleo", - "version": "1.0.0", - "description": "", - "bin": "lib/bin/maleo.js", - "scripts": { - "build": "rm -rf dist && gulp", - "watch": "gulp watch" - }, + "version": "0.0.8-canary.3", + "description": "Un-opinionated Universal Rendering Javascript Framework", "repository": { "type": "git", - "url": "git+https://github.com/airyrooms/maleo.js" + "url": "git+https://github.com/airyrooms/maleo.js", + "bugs": "https://github.com/airyrooms/maleo.js/issues" }, - "author": "", - "license": "ISC", + "author": "Airy", + "license": "MIT", "private": false, "sideEffects": false, + "scripts": { + "build": "rm -rf dist && gulp", + "watch": "gulp watch", + "prepublish": "yarn build" + }, + "bin": "lib/bin/maleo.js", + "files": [ + "lib", + "app.js", + "babel.js", + "client.js", + "document.js", + "dynamic.js", + "server.js", + "wrap.js" + ], "dependencies": { "@babel/core": "7.1.2", "@babel/plugin-proposal-class-properties": "7.1.0", @@ -29,31 +41,33 @@ "babel-loader": "8.0.4", "case-sensitive-paths-webpack-plugin": "2.1.2", "es6-promise": "4.2.5", + "eventsource": "^1.0.7", "express": "4.16.4", - "hard-source-webpack-plugin": "0.12.0", + "figlet": "1.2.1", + "friendly-errors-webpack-plugin": "1.7.0", + "hard-source-webpack-plugin": "0.13.1", "helmet": "3.14.0", "isomorphic-fetch": "2.2.1", "loader-utils": "1.1.0", "node-notifier": "5.2.1", "nodemon": "1.18.4", - "react-dom": "16.5.2", + "react-dom": "16.8.4", "react-helmet": "5.2.0", - "react-hot-loader": "4.3.12", "react-loadable": "5.5.0", - "react-router": "4.4.0-beta.1", - "react-router-config": "4.4.0-beta.6", + "react-router-config": "5.0.0", + "react-router-dom": "5.0.0", + "rimraf": "2.6.3", "source-map-support": "0.5.9", "terser-webpack-plugin": "1.1.0", - "webpack": "4.19.0", + "webpack": "4.29.0", + "webpack-bundle-analyzer": "3.0.3", "webpack-cli": "3.1.2", "webpack-dev-middleware": "3.4.0", "webpack-hot-middleware": "2.24.3", - "webpack-node-externals": "1.7.2", "webpackbar": "2.6.3" }, "peerDependencies": { - "react": "^16.5.2", - "react-router-dom": "^4.4.0-beta.1" + "react": "^16.8.4" }, "devDependencies": { "@babel/preset-typescript": "^7.1.0", @@ -65,7 +79,6 @@ "gulp-sourcemaps": "^2.6.4", "gulp-uglify": "^3.0.1", "gulp-watch": "^5.0.1", - "webpack-bundle-analyzer": "^3.0.3", - "webpack-stats-plugin": "^0.2.1" + "react": "^16.8.4" } } diff --git a/packages/Maleo.js/server.js b/packages/Maleo.js/server.js index 0b139434..dc01d12b 100644 --- a/packages/Maleo.js/server.js +++ b/packages/Maleo.js/server.js @@ -1 +1 @@ -module.exports = require('.lib/server/Server.js'); +module.exports = require('./lib/server/server.js'); diff --git a/packages/Maleo.js/src/bin/maleo.ts b/packages/Maleo.js/src/bin/maleo.ts index 6a28c5d7..d9333a0d 100755 --- a/packages/Maleo.js/src/bin/maleo.ts +++ b/packages/Maleo.js/src/bin/maleo.ts @@ -15,54 +15,61 @@ const isDev = env === 'development'; const binaryPath = path.resolve(__dirname); const projectPath = path.resolve(process.cwd()); -const buildDirectory = path.join(projectPath, '.maleo'); // bin codes starts here // Importing node's own dependencies import path from 'path'; +import figlet from 'figlet'; +import rimraf from 'rimraf'; import { spawn } from 'child_process'; // Importing required bin dependencies -import { build } from '../build/index'; +import { build } from '@build/index'; +import { loadUserConfig } from '@build/webpack/webpack'; +import { BUILD_DIR } from '@constants/index'; -const figletZones = ` -============================================ -============================================ -== |__ / / _ \\ | \\ | | | ____| / ___| == -== / / | | | | | \\| | | _| \\___ \\ == -== / /_ | |_| | | |\\ | | |___ ___) | == -== /____| \\___/ |_| \\_| |_____| |____/ == -============================================ -============================================ -`; +console.log( + figlet.textSync('Maleo.js', { + horizontalLayout: 'default', + verticalLayout: 'default', + }), +); -console.log(figletZones); -console.log('==> Current Working Directory: ', projectPath); -console.log('==> Running Command: ', type); -console.log('==> Command Args: ', args); +const userConfig = loadUserConfig(projectPath, true); +const buildDirectory = userConfig.buildDir || BUILD_DIR; -if (type === 'build') { - console.log('==> Running build'); - build({ - env, - buildType, - }); -} else if (type === 'run') { +// Generating server execution +const serverPath = path.join(buildDirectory, 'server.js'); +const exec = spawn.bind(null, 'node', [serverPath], { + stdio: 'inherit', +}); + +if (type === 'run') { console.log('[MALEO] Running Application'); + exec(); +} else { + // Clean up the folder + rimraf(path.join(projectPath, buildDirectory), {}, () => { + console.log('==> Current Working Directory: ', projectPath); + console.log('==> Current Build Directory: ', buildDirectory); + console.log('==> Environment (isDevelopment): ', env, '(' + isDev + ')'); + console.log('==> Running Command: ', type); + console.log('==> Command Args: ', args); - const serverPath = path.join(buildDirectory, 'server.js'); + if (type === 'build') { + console.log('==> Running build'); + build({ + env, + buildType, + }); + } else if (type === 'dev') { + console.log('[MALEO] Running Development'); - const exec = spawn.bind(null, 'node', [serverPath], { - stdio: 'inherit', + build({ + env, + buildType: 'server', + callback: exec, + }); + } }); - - if (isDev) { - build({ - env, - buildType: 'server', - callback: exec, - }); - } else { - exec(); - } } diff --git a/packages/Maleo.js/src/build/babel/preset.ts b/packages/Maleo.js/src/build/babel/preset.ts index 6897a82c..88e43e13 100644 --- a/packages/Maleo.js/src/build/babel/preset.ts +++ b/packages/Maleo.js/src/build/babel/preset.ts @@ -48,7 +48,6 @@ export const preset = (context, opts = {}) => { useESModules: false, }, ], - require('react-hot-loader/babel'), // for production // require('@babel/runtime'), ].filter(Boolean); diff --git a/packages/Maleo.js/src/build/loaders/maleo-babel-loader.ts b/packages/Maleo.js/src/build/loaders/maleo-babel-loader.ts index 95cfa28f..e8d684c4 100644 --- a/packages/Maleo.js/src/build/loaders/maleo-babel-loader.ts +++ b/packages/Maleo.js/src/build/loaders/maleo-babel-loader.ts @@ -9,6 +9,15 @@ export default babelLoader.custom((babel) => { const configs = new Set(); return { + customOptions(opts) { + const loader = { + cacheCompression: false, + cacheDirectory: true, + ...opts, + }; + + return { loader }; + }, config(config) { const options = { ...config.options }; if (config.hasFilesystemConfig()) { diff --git a/packages/Maleo.js/src/build/loaders/maleo-register-loader.ts b/packages/Maleo.js/src/build/loaders/maleo-register-loader.ts new file mode 100644 index 00000000..55eb0a53 --- /dev/null +++ b/packages/Maleo.js/src/build/loaders/maleo-register-loader.ts @@ -0,0 +1,23 @@ +import { loader } from 'webpack'; +import loaderUtils from 'loader-utils'; +import { REGISTERS } from '@constants/index'; + +const maleoRegisterLoader: loader.Loader = function(source) { + const { absolutePagePath, page }: any = loaderUtils.getOptions(this); + const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath); + const stringifiedPage = JSON.stringify(page); + + return ` + (window.${REGISTERS.WINDOW_VAR_NAME}=window.${ + REGISTERS.WINDOW_VAR_NAME + }||[]).push([${stringifiedPage}, function() { + var page = require(${stringifiedAbsolutePagePath}); + if (module.hot) { + module.hot.accept(); + } + return page.default || page; + }]); + `; +}; + +export default maleoRegisterLoader; diff --git a/packages/Maleo.js/src/build/loaders/maleo-routes-split.ts b/packages/Maleo.js/src/build/loaders/maleo-routes-split.ts new file mode 100644 index 00000000..c6ea8ab1 --- /dev/null +++ b/packages/Maleo.js/src/build/loaders/maleo-routes-split.ts @@ -0,0 +1,62 @@ +import { getOptions } from 'loader-utils'; +import { REGISTERS } from '@constants/index'; + +// Converts routes.json file into automatic dynamic import every page and transform it into react-router +export default function loader(source) { + this.cacheable(); + const { server } = getOptions(this); + + const routes = replaceStringPage(source); + + if (!server) { + // similar to maleo-register-loader for client + return ` + const Dynamic = require('@airy/maleo/dynamic').default; + (window.${REGISTERS.WINDOW_VAR_NAME} = window.${ + REGISTERS.WINDOW_VAR_NAME + } || []).push(['routes', function() { + if (module.hot) { + module.hot.accept(); + } + return ${routes} + }]) + `; + } + + return ` + const Dynamic = require('@airy/maleo/dynamic').default; + export default ${routes} + `; +} + +// Replaces routes string into Dynamic loading string +// using path and hashed path string as unique webpackChunkName identifier +function replaceStringPage(routes: string, key = 0) { + const pageRegex = /"page"(.*):(.*)"(.+)"/; + const page = routes.match(pageRegex); + + if (page) { + const [match, , , path] = page; + let keyName = path.split('/').pop(); + keyName = 'dynamic.' + keyName + '-' + hashString(path); + + const newRoutes = routes.replace( + match, + `"component": Dynamic({ loader: () => import(/* webpackChunkName: "${keyName}" */ '${path}'), modules: ['${path}'] }), "key": "${keyName}-${key}"`, + ); + + return replaceStringPage(newRoutes, ++key); + } + + return routes; +} + +// Produce a correct unique hash for each page path +function hashString(s) { + const uniqueHash = s.split('').reduce(function(a, b) { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0); + + return uniqueHash.toString(36).substring(2, 15) + uniqueHash.toString(36).substring(2, 15); +} diff --git a/packages/Maleo.js/src/build/webpack/plugins/require-cache-hmr.ts b/packages/Maleo.js/src/build/webpack/plugins/require-cache-hmr.ts index 1081d194..69fab89d 100644 --- a/packages/Maleo.js/src/build/webpack/plugins/require-cache-hmr.ts +++ b/packages/Maleo.js/src/build/webpack/plugins/require-cache-hmr.ts @@ -1,3 +1,13 @@ +/** + The MIT License (MIT) + Copyright (c) 2016-present ZEIT, Inc. + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + https://github.com/zeit/next.js/blob/canary/packages/next/build/webpack/plugins/nextjs-require-cache-hot-reloader.js + */ + function deleteCache(path: string) { delete require.cache[path]; } @@ -30,3 +40,5 @@ export class RequireCacheHMR { }); } } + +export default RequireCacheHMR; diff --git a/packages/Maleo.js/src/build/webpack/plugins/stats-writer.ts b/packages/Maleo.js/src/build/webpack/plugins/stats-writer.ts index 7e8be4bd..be679c1a 100644 --- a/packages/Maleo.js/src/build/webpack/plugins/stats-writer.ts +++ b/packages/Maleo.js/src/build/webpack/plugins/stats-writer.ts @@ -1,13 +1,10 @@ -import path from 'path'; import { Compiler } from 'webpack'; -import { AUTODLL_PATH } from '@src/constants'; const INDENT = 2; const DEFAULT_TRANSFORM = async (data) => JSON.stringify(data, null, INDENT); export interface Opts { filename: string; - fields: string[]; isDev: boolean; transform: (data: any, compiler?: any) => Promise; } @@ -19,7 +16,6 @@ export class StatsWriterPlugin { constructor(opts?) { this.opts = { filename: 'stats.json', - fields: ['assetsByChunkName'], isDev: false, transform: DEFAULT_TRANSFORM, ...opts, @@ -27,39 +23,20 @@ export class StatsWriterPlugin { } apply = (compiler: Compiler) => { - try { - compiler.plugin('autodll-stats-retrieved', this.getAutoDLLPath); - } catch (err) { - // @ts-ignore - } finally { - compiler.hooks.emit.tapPromise('maleo-stats-writer-plugin', this.emitStats); - } + compiler.hooks.emit.tapPromise('maleo-stats-writer-plugin', this.emitStats); }; emitStats = async (compilation, callback): Promise => { let stats = compilation.getStats().toJson(); // Filter to fields - if (this.opts.fields) { - stats = this.opts.fields.reduce( - (memo, key) => ({ - ...memo, - [key]: stats[key], - }), - {}, - ); - } + // Extract dynamic assets to dynamic key - if (this.opts.isDev) { - stats = { - ...stats, - development: this.dllFilename - ? { - dll: [path.join(AUTODLL_PATH, this.dllFilename)], - } - : {}, - }; - } + const key = 'assetsByChunkName'; + stats = { + static: this.extractDynamic(stats[key], false), + dynamic: this.extractDynamic(stats[key], true), + }; // Transform to string const [err, statsStr] = await to( @@ -92,8 +69,23 @@ export class StatsWriterPlugin { } }; - getAutoDLLPath = (stats) => { - this.dllFilename = stats.assetsByChunkName.dll; + // Extract dynamic assets + extractDynamic = (stats, isDynamic = false) => { + return Object.keys(stats) + .filter((k) => { + const regex = /dynamic\./; + if (isDynamic) { + return !!regex.test(k); + } + return !regex.test(k); + }) + .reduce( + (p, c) => ({ + ...p, + [c]: stats[c], + }), + {}, + ); }; } diff --git a/packages/Maleo.js/src/build/webpack/utils/webpackNodeExternals.ts b/packages/Maleo.js/src/build/webpack/utils/webpackNodeExternals.ts new file mode 100644 index 00000000..4a8ddd19 --- /dev/null +++ b/packages/Maleo.js/src/build/webpack/utils/webpackNodeExternals.ts @@ -0,0 +1,172 @@ +/** + Copyright (c) 2016 Liad Yosef + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + */ + +import fs from 'fs'; +import path from 'path'; + +function contains(arr, val) { + return arr && arr.indexOf(val) !== -1; +} + +const atPrefix = new RegExp('^@', 'g'); +function readDir(dirName) { + if (!fs.existsSync(dirName)) { + return []; + } + + try { + return fs + .readdirSync(dirName) + .map(function(module) { + if (atPrefix.test(module)) { + // reset regexp + atPrefix.lastIndex = 0; + try { + return fs.readdirSync(path.join(dirName, module)).map(function(scopedMod) { + return module + '/' + scopedMod; + }); + } catch (e) { + return [module]; + } + } + return module; + }) + .reduce(function(prev, next) { + return prev.concat(next); + }, []); + } catch (e) { + return []; + } +} + +let packageJsonCache = {}; + +function readFromPackageJson(options) { + if (typeof options !== 'object') { + options = {}; + } + // read the file + let packageJson; + try { + const fileName = options.fileName || 'package.json'; + if (!packageJsonCache[fileName]) { + const packageJsonString = fs.readFileSync(fileName, 'utf8'); + packageJsonCache = { + ...packageJsonCache, + [fileName]: JSON.parse(packageJsonString), + }; + } + + packageJson = packageJsonCache[fileName]; + } catch (e) { + return []; + } + // sections to search in package.json + let sections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']; + if (options.include) { + sections = [].concat(options.include); + } + if (options.exclude) { + sections = sections.filter(function(section) { + return [].concat(options.exclude).indexOf(section) === -1; + }); + } + // collect dependencies + const deps = {}; + sections.forEach(function(section) { + Object.keys(packageJson[section] || {}).forEach(function(dep) { + deps[dep] = true; + }); + }); + + return Object.keys(deps); +} + +function containsPattern(arr, val) { + return ( + arr && + arr.some(function(pattern) { + if (pattern instanceof RegExp) { + return pattern.test(val); + } else if (typeof pattern === 'function') { + return pattern(val); + } else { + return pattern === val; + } + }) + ); +} + +const scopedModuleRegex = new RegExp( + '@[a-zA-Z0-9][\\w-.]+/[a-zA-Z0-9][\\w-.]+([a-zA-Z0-9./]+)?', + 'g', +); + +function getModuleName(request, includeAbsolutePaths) { + let req = request; + const delimiter = '/'; + + if (includeAbsolutePaths) { + req = req.replace(/^.*?\/node_modules\//, ''); + } + // check if scoped module + if (scopedModuleRegex.test(req)) { + // reset regexp + scopedModuleRegex.lastIndex = 0; + return req.split(delimiter, 2).join(delimiter); + } + return req.split(delimiter)[0]; +} + +export default function nodeExternals(options) { + options = options || {}; + const whitelist = [].concat(options.whitelist || []); + const binaryDirs = [].concat(options.binaryDirs || ['.bin']); + const importType = options.importType || 'commonjs'; + const modulesDir = options.modulesDir || 'node_modules'; + const modulesFromFile = !!options.modulesFromFile; + const includeAbsolutePaths = !!options.includeAbsolutePaths; + + // helper function + function isNotBinary(x) { + return !contains(binaryDirs, x); + } + + // create the node modules list + const nodeModules = modulesFromFile + ? readFromPackageJson(options.modulesFromFile) + : readDir(modulesDir).filter(isNotBinary); + + // return an externals function + return function(context, request, callback) { + const moduleName = getModuleName(request, includeAbsolutePaths); + if (contains(nodeModules, moduleName) && !containsPattern(whitelist, request)) { + if (typeof importType === 'function') { + return callback(null, importType(request)); + } + // mark this module as external + // https://webpack.js.org/configuration/externals/ + return callback(null, importType + ' ' + request); + } + callback(); + }; +} diff --git a/packages/Maleo.js/src/build/webpack/webpack.ts b/packages/Maleo.js/src/build/webpack/webpack.ts index 592ab9f7..69d3d270 100644 --- a/packages/Maleo.js/src/build/webpack/webpack.ts +++ b/packages/Maleo.js/src/build/webpack/webpack.ts @@ -1,5 +1,4 @@ // tslint:disable:no-console - import path from 'path'; import { Configuration, @@ -17,13 +16,15 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import HardSourcePlugin from 'hard-source-webpack-plugin'; // Webpack required plugins -import { StatsWriterPlugin } from 'webpack-stats-plugin'; +import { StatsWriterPlugin } from './plugins/stats-writer'; import { ReactLoadablePlugin } from './plugins/react-loadable'; -import nodeExternals from 'webpack-node-externals'; +import nodeExternals from './utils/webpackNodeExternals'; // Other Webpack Plugins import WebpackBar from 'webpackbar'; import CaseInsensitivePathPlugin from 'case-sensitive-paths-webpack-plugin'; +import FriendlyError from 'friendly-errors-webpack-plugin'; +import RequireCacheHMR from './plugins/require-cache-hmr'; import { REACT_LOADABLE_MANIFEST, @@ -34,7 +35,12 @@ import { RUNTIME_CHUNK_FILE, SERVER_ASSETS_ROUTE, MALEO_PROJECT_ROOT_NODE_MODULES, -} from '@src/constants'; + PROJECT_ROOT_NODE_MODULES, + ROUTES_ENTRY_NAME, + DOCUMENT_ENTRY_NAME, + WRAP_ENTRY_NAME, + APP_ENTRY_NAME, +} from '@constants/index'; import { Context, CustomConfig, @@ -42,6 +48,7 @@ import { WebpackCustomConfigCallback, } from '@interfaces/build/IWebpackInterfaces'; import { requireRuntime } from '@utils/require'; +import { fileExist } from '@utils/index'; // Default Config if user doesn't have maleo.config.js const defaultUserConfig: CustomConfig = { @@ -92,7 +99,7 @@ export const createWebpackConfig = (context: Context, customConfig: CustomConfig getDefaultRules, getDefaultPlugins, getDefaultOutput, - ].map((fn) => fn.call(this, buildContext)); + ].map((fn) => fn.call(this, buildContext, customConfig)); /** * Base Config @@ -100,6 +107,7 @@ export const createWebpackConfig = (context: Context, customConfig: CustomConfig let baseConfigs: Configuration = { name, entry, + context: buildContext.projectDir, mode, devtool: isDev || sourceMaps ? 'cheap-module-source-map' : false, cache, @@ -108,8 +116,10 @@ export const createWebpackConfig = (context: Context, customConfig: CustomConfig resolve: { extensions: ['.js', '.jsx', '.json'], alias, + symlinks: true, modules: [ MALEO_PROJECT_ROOT_NODE_MODULES, + PROJECT_ROOT_NODE_MODULES, 'node_modules', ...nodePathList, // Support for NODE_PATH environment variable ], @@ -132,16 +142,33 @@ export const createWebpackConfig = (context: Context, customConfig: CustomConfig }; if (isServer) { + // required on server bundle + const whitelist = [ + /webpack[/\\]hot[/\\](signal|poll)/, + /react[/\\]/, + /@airy[/\\]maleo/, + /@babel[/\\]runtime[/\\]/, + /@babel[/\\]runtime-corejs2[/\\]/, + ]; + baseConfigs = { ...baseConfigs, - // Extract all server node_modules from bundles except React + // Extract all server node_modules from maleo's project and client's externals: [ nodeExternals({ - whitelist: [/react/], + modulesFromFile: { + fileName: require.resolve('~/package.json'), + }, + whitelist, + }), + nodeExternals({ + modulesFromFile: { + fileName: path.join(buildContext.projectDir, 'package.json'), + }, + whitelist, }), ], - node: { // Keep __dirname relative to file not root project __dirname: true, @@ -171,28 +198,49 @@ export const createWebpackConfig = (context: Context, customConfig: CustomConfig /** * Setting Up Default Entry */ -export const getDefaultEntry = (context: BuildContext): Configuration['entry'] => { - const { isServer, projectDir } = context; +export const getDefaultEntry = ( + context: BuildContext, + customConfig: CustomConfig, +): Configuration['entry'] => { + const { isServer, projectDir, isDev } = context; + + const { routes, document, wrap, app } = getStaticEntries(context, customConfig); if (isServer) { + const customServerExist = fileExist(projectDir, 'server'); + const serverEntry = customServerExist + ? path.join(projectDir, SERVER_ENTRY_NAME) + : path.resolve(__dirname, '../../../lib/default/_server.js'); + return { - server: path.join(projectDir, SERVER_ENTRY_NAME), + server: [isDev && 'webpack/hot/signal', serverEntry].filter(Boolean) as string[], + routes, + document, + wrap, + app, }; } + const customClientExist = fileExist(projectDir, path.join(projectDir, 'client')); + const clientEntry = customClientExist + ? path.join(projectDir, CLIENT_ENTRY_NAME) + : path.resolve(__dirname, '../../../lib/default/_client.js'); + return { - main: [ - 'webpack-hot-middleware/client?name=client', - 'react-hot-loader/patch', - path.join(projectDir, CLIENT_ENTRY_NAME), - ], + main: [clientEntry].filter(Boolean) as string[], + routes, + wrap: `maleo-register-loader?page=wrap&absolutePagePath=${wrap}!`, + app: `maleo-register-loader?page=app&absolutePagePath=${app}!`, }; }; /** * Setting Up Default Optimizations */ -export const getDefaultOptimizations = (context: BuildContext): Configuration['optimization'] => { +export const getDefaultOptimizations = ( + context: BuildContext, + customConfig: CustomConfig, +): Configuration['optimization'] => { const { isServer, isDev } = context; const commonOptimizations: Configuration['optimization'] = { @@ -289,20 +337,36 @@ export const getDefaultOptimizations = (context: BuildContext): Configuration['o /** * Setting Up Default Rules */ -export const getDefaultRules = (context: BuildContext): RuleSetRule[] => { +export const getDefaultRules = ( + context: BuildContext, + customConfig: CustomConfig, +): RuleSetRule[] => { + const { isServer, projectDir } = context; + return [ { - test: /\.jsx?/, + test: /\.jsx?$/, exclude: /node_modules/, use: ['maleo-babel-loader'], }, + // to disable webpack's default json-loader + // so we need to define our own rules for routes.json + { + type: 'javascript/auto', + test: path.resolve(projectDir, ROUTES_ENTRY_NAME), + exclude: /node_modules/, + use: `maleo-routes-split?server=${JSON.stringify(!!isServer)}`, + }, ]; }; /** * Setting up Default Plugins */ -export const getDefaultPlugins = (context: BuildContext): Configuration['plugins'] => { +export const getDefaultPlugins = ( + context: BuildContext, + customConfig: CustomConfig, +): Configuration['plugins'] => { const { isDev, projectDir, publicPath, env, isServer, analyzeBundle, name } = context; const commonPlugins: Configuration['plugins'] = @@ -311,6 +375,7 @@ export const getDefaultPlugins = (context: BuildContext): Configuration['plugins // Common Plugins new HashedModuleIdsPlugin(), + isDev && new FriendlyError(), new WebpackBar({ name, }), @@ -319,29 +384,49 @@ export const getDefaultPlugins = (context: BuildContext): Configuration['plugins WEBPACK_PUBLIC_PATH: JSON.stringify(publicPath), __DEV__: isDev, __ENV__: JSON.stringify(env), + __IS_SERVER__: isServer, }), - // Setting up Development Plugins // Commented due to issue with WDM and WHM // details: https://github.com/mzgoddard/hard-source-webpack-plugin/issues/416 - isDev && - new HardSourcePlugin({ - cacheDirectory: path.join( - projectDir, - `node_modules/.cache/hard-source/${isServer ? 'server' : 'client'}/[confighash]`, - ), - - environmentHash: { - root: projectDir, - directories: [], - files: ['package-lock.json', 'yarn.lock'], - }, + // new HardSourcePlugin({ + // cacheDirectory: path.join( + // projectDir, + // `node_modules/.cache/hard-source/${isServer ? 'server' : 'client'}/[confighash]`, + // ), + // cachePrune: { + // // Caches younger than `maxAge` are not considered for deletion. They must + // // be at least this (default: 2 days) old in milliseconds. + // maxAge: 2 * 24 * 60 * 60 * 1000, + // // All caches together must be larger than `sizeThreshold` before any + // // caches will be deleted. Together they must be at least this + // // (default: 50 MB) big in bytes. + // sizeThreshold: 50 * 1024 * 1024, + // }, + + // environmentHash: { + // root: projectDir, + // directories: [], + // files: ['package-lock.json', 'yarn.lock'], + // }, + + // info: { + // mode: env, + // level: 'log', + // }, + // }), - info: { - mode: 'none', - level: 'log', - }, - }), + // Setting up Development Plugins + isDev && new RequireCacheHMR(), + + // HMR + isDev && new HotModuleReplacementPlugin(), + isDev && new NoEmitOnErrorsPlugin(), + + // Bundled Stats + new StatsWriterPlugin({ + isDev, + }), ].filter(Boolean) as Configuration['plugins']) || []; // Setting Up Server Plugins @@ -356,7 +441,33 @@ export const getDefaultPlugins = (context: BuildContext): Configuration['plugins entryOnly: false, }), - // new RequireCacheHMR(), + isDev && + new HardSourcePlugin({ + cacheDirectory: path.join( + projectDir, + `node_modules/.cache/hard-source/server/[confighash]`, + ), + cachePrune: { + // Caches younger than `maxAge` are not considered for deletion. They must + // be at least this (default: 2 days) old in milliseconds. + maxAge: 2 * 24 * 60 * 60 * 1000, + // All caches together must be larger than `sizeThreshold` before any + // caches will be deleted. Together they must be at least this + // (default: 50 MB) big in bytes. + sizeThreshold: 50 * 1024 * 1024, + }, + + environmentHash: { + root: projectDir, + directories: [], + files: ['package-lock.json', 'yarn.lock'], + }, + + info: { + mode: env, + level: 'log', + }, + }), ].filter(Boolean) as Configuration['plugins']) || []; return [...commonPlugins, ...serverPlugins]; @@ -369,14 +480,6 @@ export const getDefaultPlugins = (context: BuildContext): Configuration['plugins }), analyzeBundle && new BundleAnalyzerPlugin(), - - // Bundled Stats - new StatsWriterPlugin({ - isDev, - }), - - new HotModuleReplacementPlugin(), - new NoEmitOnErrorsPlugin(), ].filter(Boolean) as Configuration['plugins']) || []; return [...clientPlugins, ...commonPlugins]; @@ -385,13 +488,26 @@ export const getDefaultPlugins = (context: BuildContext): Configuration['plugins /** * Setting Up Default Output */ -export const getDefaultOutput = (context: BuildContext): Configuration['output'] => { +export const getDefaultOutput = ( + context: BuildContext, + customConfig: CustomConfig, +): Configuration['output'] => { const { isServer, projectDir, buildDirectory, publicPath, isDev } = context; + const hmr = { + hotUpdateChunkFilename: 'hot/hot-update.js', + hotUpdateMainFilename: 'hot/hot-update.json', + }; + if (isServer) { return { filename: '[name].js', + chunkFilename: '[name].js', path: path.resolve(projectDir, buildDirectory), + + library: '[name]', + libraryTarget: 'commonjs2', + ...hmr, }; } @@ -400,26 +516,24 @@ export const getDefaultOutput = (context: BuildContext): Configuration['output'] publicPath, chunkFilename: isDev ? '[name].js' : '[name]-[hash].js', - filename: isDev ? '[name]' : '[name]-[hash]', + filename: isDev ? '[name].js' : '[name]-[hash].js', library: '[name]', - - // hotUpdateChunkFilename: 'hot/hot-update.js', - // hotUpdateMainFilename: 'hot/hot-update.json', + ...hmr, }; }; /** * Load User Config with file name USER_CUSTOM_CONFIG (maleo.config.js) */ -export const loadUserConfig = (dir: string): CustomConfig => { +export const loadUserConfig = (dir: string, quiet?: boolean): CustomConfig => { const cwd: string = path.resolve(dir); const userConfigPath: string = path.resolve(cwd, USER_CUSTOM_CONFIG); try { const userConfig = requireRuntime(userConfigPath); if (userConfig !== undefined) { - // tslint:disable-next-line:quotemark - console.log("[Webpack] Using user's config"); + // tslint:disable-next-line:no-unused-expression quotemark + !quiet && console.log("[Webpack] Using user's config"); return { ...defaultUserConfig, ...userConfig, @@ -429,8 +543,49 @@ export const loadUserConfig = (dir: string): CustomConfig => { return defaultUserConfig; } catch (err) { if (err.code !== 'MODULE_NOT_FOUND') { - console.log('[Webpack] Using Default Config'); + // tslint:disable-next-line:no-unused-expression + !quiet && console.log('[Webpack] Using Default Config'); } return defaultUserConfig; } }; + +/** + * Get Static Entries + * Read from maleo.config.js, if the config not found Maleo will try to search user's from default directory + * If file not found then Maleo will use it's default statics + */ +const getStaticEntries = (context: BuildContext, config: CustomConfig) => { + const { projectDir } = context; + const { customDocument, customWrap, customApp, routes: customRoutes } = config; + + const defaultDocument = path.resolve(__dirname, '../../../lib/render/_document.js'); + const defaultWrap = path.resolve(__dirname, '../../../lib/render/_wrap.js'); + const defaultApp = path.resolve(__dirname, '../../../lib/render/_app.js'); + + const defaultUserRoutes = path.join(projectDir, ROUTES_ENTRY_NAME); + const defaultUserDocument = path.join(projectDir, DOCUMENT_ENTRY_NAME); + const defaultUserWrap = path.join(projectDir, WRAP_ENTRY_NAME); + const defaultUserApp = path.join(projectDir, APP_ENTRY_NAME); + + const cRoutes = customRoutes && path.join(projectDir, customRoutes as string); + const cDoc = customDocument && path.join(projectDir, customDocument as string); + const cWrap = customWrap && path.join(projectDir, customWrap as string); + const cApp = customApp && path.join(projectDir, customApp as string); + + const routes = cRoutes || defaultUserRoutes; + + const document = + cDoc || fileExist(projectDir, '_document') ? defaultUserDocument : defaultDocument; + + const wrap = cWrap || fileExist(projectDir, '_wrap') ? defaultUserWrap : defaultWrap; + + const app = cApp || fileExist(projectDir, '_app') ? defaultUserApp : defaultApp; + + return { + routes, + document, + wrap, + app, + }; +}; diff --git a/packages/Maleo.js/src/client/Client.tsx b/packages/Maleo.js/src/client/client.tsx similarity index 51% rename from packages/Maleo.js/src/client/Client.tsx rename to packages/Maleo.js/src/client/client.tsx index dcbf0846..412acddd 100644 --- a/packages/Maleo.js/src/client/Client.tsx +++ b/packages/Maleo.js/src/client/client.tsx @@ -1,28 +1,37 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Loadable from 'react-loadable'; -import { hot } from 'react-hot-loader'; import { loadInitialProps, loadComponentProps } from '@server/loadInitialProps'; -import { App as DefaultApp } from '@render/_app'; -import { _Wrap as DefaultWrap } from '@render/_wrap'; import { InitialProps } from '@interfaces/render/IRender'; -import { SERVER_INITIAL_DATA, DIV_MALEO_ID } from '@src/constants'; +import { SERVER_INITIAL_DATA, DIV_MALEO_ID } from '@constants/index'; import { matchingRoutes } from '@server/routeHandler'; +import { RegisterEntry } from './registerEntry'; -export const init = async (routes, mod, { Wrap = DefaultWrap, App = DefaultApp }) => { - const { data } = await ensureReady(routes, location.pathname, {}); +export const init = async () => { + try { + const RE = new RegisterEntry(); - const wrapProps = await loadComponentProps(Wrap); - const appProps = await loadComponentProps(App); + const routes = RE.findRegister('routes'); + const Wrap = RE.findRegister('wrap'); + const App = RE.findRegister('app'); - const RenderApp = hot(mod)(() => ( - - - - )); + const { data } = await ensureReady(routes, location.pathname, {}); - hydrate(RenderApp); + const wrapProps = await loadComponentProps(Wrap); + const appProps = await loadComponentProps(App); + + const RenderApp = () => ( + + + + ); + + hydrate(RenderApp); + } catch (err) { + // tslint:disable-next-line:no-console + console.error(err); + } }; export const hydrate = (App: () => React.ReactElement): void => { diff --git a/packages/Maleo.js/src/client/hmr/client-hmr.ts b/packages/Maleo.js/src/client/hmr/client-hmr.ts new file mode 100644 index 00000000..4c9eda2b --- /dev/null +++ b/packages/Maleo.js/src/client/hmr/client-hmr.ts @@ -0,0 +1,189 @@ +// tslint:disable:no-console + +// Highly inspired from // Taken from https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/webpackHotDevClient.js +/** + * 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 + +// I think HMR is a bit of an issue itself, we may just support a browser reload for every changes made +// but I will find other way to work this thing out +// more details: https://github.com/gaearon/react-hot-boilerplate/issues/97#issuecomment-249862775 + +// pollyfill for older browser +export function clientHMR(option?) { + window['EventSource'] = window['EventSource'] || require('eventsource'); + + const url = '/__webpack_hmr'; + + connect(url); + + return function(initialize) { + if (typeof initialize === 'function') { + initialize(); + } + }; +} + +// Remember some state related to hot module replacement. +let isFirstCompilation = true; +let mostRecentCompilationHash = null; +let hasCompileErrors = false; +const hadRuntimeError = false; + +function connect(sourceURL: string) { + console.info('[HMR] Connecting...'); + const newES = new window['EventSource'](sourceURL); + attachListener(newES); +} + +function attachListener(eventSource: EventSource) { + eventSource.onopen = function(event) { + console.info('[HMR] Connected'); + }; + + eventSource.onerror = function(event) { + console.error('[HMR] Disconnected'); + eventSource.close(); + + console.info('[HMR] Trying to reconnect in 10 seconds, or refresh to reconnect immediately'); + setTimeout(() => connect(eventSource.url), 10000); + }; + + eventSource.onmessage = function(event) { + // event.data will be a JSON string containing the message event. + try { + hmrMessageHandler(JSON.parse(event.data)); + } catch (error) { + return; + } + }; +} + +function hmrMessageHandler(data) { + const { action, name, hash } = data; + + switch (action) { + case 'building': + console.info('[HMR] bundle ' + (name ? `'${name}\' ` : '') + 'rebuilding'); + isFirstCompilation = false; + break; + case 'built': + case 'sync': + clearOutdatedErrors(); + + handleAvailableHash(hash); + handleSuccess(); + break; + + default: + } +} + +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(); + + const isHotUpdate = !isFirstCompilation; + isFirstCompilation = false; + hasCompileErrors = false; + + if (isHotUpdate) { + // apply updates + console.log('[HMR] Applying update'); + 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(); + // console.log('[HMR] Modules updated'); + window.location.reload(); + }); + } +} + +// 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'; +} + +// There is a newer version of the code available. +function handleAvailableHash(hash) { + // Update last known compilation hash. + mostRecentCompilationHash = hash; +} + +// Attempt to update code on the fly, fall back to a hard reload. +function tryApplyUpdates(onHotUpdateSuccess?) { + // HotModuleReplacementPlugin is not in Webpack configuration. + // do hard reload if HMR not available + if (!module.hot) { + window.location.reload(); + return; + } + + if (!isUpdateAvailable() || !canApplyUpdates()) { + return; + } + + // https://webpack.github.io/docs/hot-module-replacement.html#check + const 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); + }, + ); + } + + 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(onHotUpdateSuccess); + } + } +} diff --git a/packages/Maleo.js/src/client/registerEntry.ts b/packages/Maleo.js/src/client/registerEntry.ts new file mode 100644 index 00000000..2bccebd8 --- /dev/null +++ b/packages/Maleo.js/src/client/registerEntry.ts @@ -0,0 +1,24 @@ +import { REGISTERS } from '@constants/index'; + +export class RegisterEntry { + registerCache = {}; + + constructor() { + if (window[REGISTERS.WINDOW_VAR_NAME]) { + window[REGISTERS.WINDOW_VAR_NAME].map(this.register); + } + + window[REGISTERS.WINDOW_VAR_NAME] = []; + window[REGISTERS.WINDOW_VAR_NAME].push = this.register; + } + + register = ([entry, fn]) => { + this.registerCache[entry] = fn(); + }; + + findRegister = (key: string) => { + return this.registerCache[key]; + }; +} + +export default RegisterEntry; diff --git a/packages/Maleo.js/src/constants/index.ts b/packages/Maleo.js/src/constants/index.ts index d0cf1c0f..20534819 100644 --- a/packages/Maleo.js/src/constants/index.ts +++ b/packages/Maleo.js/src/constants/index.ts @@ -8,6 +8,11 @@ export const SERVER_ENTRY_NAME: string = 'server.js'; export const CLIENT_ENTRY_NAME: string = 'client.js'; +export const ROUTES_ENTRY_NAME: string = 'routes.json'; +export const DOCUMENT_ENTRY_NAME: string = '_document.jsx'; +export const WRAP_ENTRY_NAME: string = '_wrap.jsx'; +export const APP_ENTRY_NAME: string = '_app.jsx'; + export const BUILD_DIR: string = '.maleo'; export const RUNTIME_CHUNK_FILE: string = 'static/runtime/webpack.js'; @@ -25,4 +30,19 @@ export const MALEO_PROJECT_ROOT_NODE_MODULES: string = path.join( 'node_modules', ); +export const PROJECT_ROOT_NODE_MODULES: string = path.join(process.cwd(), 'node_modules'); + export const AUTODLL_PATH: string = './static/dll'; + +export const CUSTOM_DOCUMENT_PATH: string = path.join(process.cwd(), 'document.js'); + +export const CUSTOM_WRAP_PATH: string = path.join(process.cwd(), 'wrap.js'); + +export const REGISTERS = { + WINDOW_VAR_NAME: '__REGISTERS__', + ROUTES: '__MALEO__ROUTES__', + WRAP: '__MALEO__WRAP__', + APP: '__MALE_APP__', +}; + +export const STATIC_ENTRIES = ['document', 'wrap', 'app', 'routes']; diff --git a/packages/Maleo.js/src/default/_client.ts b/packages/Maleo.js/src/default/_client.ts new file mode 100644 index 00000000..2f7c4fba --- /dev/null +++ b/packages/Maleo.js/src/default/_client.ts @@ -0,0 +1,8 @@ +import { init } from '~/src/client/client'; + +if (__DEV__) { + const { clientHMR } = require('~/src/client/hmr/client-hmr'); + clientHMR({})(init); +} else { + init(); +} diff --git a/packages/Maleo.js/src/default/_server.ts b/packages/Maleo.js/src/default/_server.ts new file mode 100644 index 00000000..f5b6dcf3 --- /dev/null +++ b/packages/Maleo.js/src/default/_server.ts @@ -0,0 +1,12 @@ +import { Server } from '~/src/server/server'; + +const PORT = process.env.PORT || 3000; + +const defaultServer = Server.init({ + port: PORT, +}); + +defaultServer.run(() => { + // tslint:disable-next-line:no-console + console.log('Server running on port :' + PORT); +}); diff --git a/packages/Maleo.js/src/interfaces/build/IWebpackInterfaces.ts b/packages/Maleo.js/src/interfaces/build/IWebpackInterfaces.ts index 6faf3ccd..c3024e18 100644 --- a/packages/Maleo.js/src/interfaces/build/IWebpackInterfaces.ts +++ b/packages/Maleo.js/src/interfaces/build/IWebpackInterfaces.ts @@ -19,6 +19,11 @@ export interface CustomConfig { context: Context, next: WebpackCustomConfigCallback, ) => Configuration; + + customWrap?: string; + customApp?: string; + customDocument?: string; + routes?: string; } export type WebpackCustomConfigCallback = (customConfig: CustomConfig) => Configuration; diff --git a/packages/Maleo.js/src/interfaces/render/IRender.ts b/packages/Maleo.js/src/interfaces/render/IRender.ts index 011e0205..aa14ff06 100644 --- a/packages/Maleo.js/src/interfaces/render/IRender.ts +++ b/packages/Maleo.js/src/interfaces/render/IRender.ts @@ -102,13 +102,7 @@ export interface RenderParam { req: Request; res: Response; dir: string; - routes: AsyncRouteProps[]; - // Document?: React.ReactElement; - // App?: React.ReactElement; - // Wrap?: React.ReactElement; - Document?: typeof React.Component; - App?: typeof React.Component; - Wrap?: typeof React.Component; + renderPage?: ( param: RenderPageParams, ) => (fn?: ModPageFn) => Promise<{ html: string; bundles: LoadableBundles[] }>; @@ -131,3 +125,10 @@ export interface RenderPageParams { export type ModPageFn = ( Page: React.ComponentType, ) => (props: Props) => React.ReactElement; + +export interface ServerAssets { + routes: AsyncRouteProps[]; + document: typeof React.Component; + app: typeof React.Component; + wrap: typeof React.Component; +} diff --git a/packages/Maleo.js/src/interfaces/server/IOptions.ts b/packages/Maleo.js/src/interfaces/server/IOptions.ts index c3035368..b54ca86a 100644 --- a/packages/Maleo.js/src/interfaces/server/IOptions.ts +++ b/packages/Maleo.js/src/interfaces/server/IOptions.ts @@ -1,11 +1,10 @@ import { DocumentProps, AppProps } from '../render/IRender'; export interface IOptions { - port: number; + port: number | string; - assetDir: string; - distDir: string; - routes: []; + assetDir?: string; + routes?: []; _document?: React.ReactElement; _app?: React.ReactElement; diff --git a/packages/Maleo.js/src/render/_app.tsx b/packages/Maleo.js/src/render/_app.tsx index 5d4db9ef..0a41e432 100644 --- a/packages/Maleo.js/src/render/_app.tsx +++ b/packages/Maleo.js/src/render/_app.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import { Switch, Route, withRouter } from 'react-router-dom'; +import { withRouter } from 'react-router-dom'; import { AppProps, InitialProps } from '@interfaces/render/IRender'; -import { loadInitialProps } from '@server/loadInitialProps'; -import { renderRoutes } from '@utils/routes'; +// import { loadInitialProps } from '@server/loadInitialProps'; +import { renderRoutes } from '@routes/routes'; export interface AppState { data?: InitialProps['data']; previousLocation: Location | null; } -export class _App extends React.PureComponent { +class _App extends React.PureComponent { prefetchCache = {}; state = { diff --git a/packages/Maleo.js/src/render/_document.tsx b/packages/Maleo.js/src/render/_document.tsx index 34182cb0..a5f3228e 100644 --- a/packages/Maleo.js/src/render/_document.tsx +++ b/packages/Maleo.js/src/render/_document.tsx @@ -2,10 +2,10 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { HeaderProps, DocumentProps, DocumentContext } from '@interfaces/render/IRender'; -import { SERVER_INITIAL_DATA, DIV_MALEO_ID } from '@src/constants'; +import { SERVER_INITIAL_DATA, DIV_MALEO_ID } from '@constants/index'; // Extendable document -export class Document extends React.Component { +export default class Document extends React.Component { // export class Document extends React.Component implements IDocument { // export const Document: IDocument = class extends React.Component { static getInitialProps = async ({ @@ -106,7 +106,7 @@ export class Scripts extends React.Component { }} /> {preloadScripts.map((p, i) => ( -