Skip to content

Commit

Permalink
v7
Browse files Browse the repository at this point in the history
  • Loading branch information
ndreckshage committed Sep 15, 2017
1 parent 019a6ba commit fd2ec18
Show file tree
Hide file tree
Showing 21 changed files with 396 additions and 245 deletions.
88 changes: 28 additions & 60 deletions README.md
@@ -1,105 +1,73 @@
# sambell
sambell is a minimal framework for server-rendered React applications, ideal for universal react-router projects.
create performant server-rendered React applications with no build configuration; ideal for universal react-router projects.

- similar to [create-react-app](https://github.com/facebookincubator/create-react-app), but server side rendering.
- similar to [next.js](https://github.com/zeit/next.js), but with [react-router](https://github.com/ReactTraining/react-router).
- `sambell` came first! commit history proof :stuck_out_tongue_closed_eyes:

Both [create-react-app](https://github.com/facebookincubator/create-react-app) and [next.js](https://github.com/zeit/next.js) are great projects, try them! I like aspects of both. But you don't get a **universal react-router** application out of the box.

###### Why not Next?

I think [next.js](https://github.com/zeit/next.js) is great, with one big flaw: you don't get a layout file. The entire application re-renders on route changes. This makes things like animated transitions impossible. But even something simple like a **material design ink button** link won't work, because the re-render kills the button animation. I think [react-router](https://github.com/ReactTraining/react-router) is fundamentally more powerful, and opt for that.

## What will my app look like?!?

Check out [the template](template)!

**3 files**. **<100 loc** total for a universal single page app. (5 files to show async loading)

## Install

```
yarn global add sambell
sambell new my-app
cd my-app
sambell new app
cd app
yarn start
```

## Features

**Performant**

- React **16**
- Server side rendering. Universal.
- Critical styles with [styled-jsx](https://github.com/zeit/styled-jsx)
- Async loading of routes with `react-loadable` (forked version `@humblespark/react-loadable`)
- Webpack build optimized for production.

**Webpack**

- Webpack 2 (code splitting, tree shaking, etc).
- Webpack runs for **both** client and server code.
- Minimal loaders (only a JS loader). But it is configurable if you want to add more.
- **absolute path** requires from your project root. `import App from 'App'`
- Sourcemaps for client & server

**Babel**

- Presets: es2015, stage-1, react
- Plugins: [styled-jsx](https://github.com/zeit/styled-jsx)

**Router**
**Dev experience**

- Everything you (or at least, I) want without setting anything up!
- Client side SPA with [react-router](https://github.com/ReactTraining/react-router) **version 4**.
- Easy to add non-v4 react-router if you prefer that. But v4 is really nice if starting a new project.

**CSS**

- [styled-jsx](https://github.com/zeit/styled-jsx) is a great feature of Next.js that I bring in here. I find it to be more pleasant than `css-modules`, and eaiser to work with for a universal application (critical styles, etc).

**sambell/env**
**Performant**

- `import { WEBPACK_MANIFEST, CLIENT_ENTRY, CLIENT_CHUNKS, CLIENT_OUTPUT_DIR, WEBPACK_PUBLIC_PATH, waitForChunks } from 'sambell/env';` to make creating your html template easy in `server.js`.
- React **16**
- Server side rendering. Universal.
- Critical styles with [styled-jsx](https://github.com/zeit/styled-jsx).
- Async loading of routes with `react-loadable` (forked version `@humblespark/react-loadable`).
- Async (`<script async />`) loading of all webpack scripts.
- Webpack build optimized for production.

**Async components**

- Full client & server side support for async loading components, with `react-loadable`
- Forked version (`@humblespark/react-loadable`) to work with server side webpack build & a fix for checksum mismatch.
- `import ready from 'sambell/ready';` `ready(() => renderApp())` for async webpack loading of chunks.

```
const MyAsyncComponent = Loadable({
loader: () => import(/* webpackChunkName: "components/MyAsyncComponent" */'components/MyAsyncComponent'),
webpackRequireWeakId: () => require.resolveWeak('components/MyAsyncComponent'),
chunkName: 'components/MyAsyncComponent',
});
const Moon = Loadable(() => import(/* webpackChunkName: "components/Moon" */'components/Moon'));
```

**Other**
**Webpack / Babel**

- Webpack 2 (code splitting, tree shaking, etc).
- Webpack runs for **both** client and server code.
- Minimal loaders (only a JS loader). But it is configurable if you want to add more.
- **absolute path** requires from your project root. `import App from 'App'`.
- Sourcemaps for client & server.
- Babel Presets: es2015, stage-1, react
- Babel Plugins: [styled-jsx](https://github.com/zeit/styled-jsx)
- Polyfills: `isomorphic-fetch`, `babel-polyfill`

**Configurable**

- You get a `server.js` file. You have full control over client and server.
- Add a `gerty.js` file to your project root.
- Currently, webpack only function that it called. called with:
- `config` (base config from sambell). for you to overrides
- `settings` (`{ dev, node }`) to conditionally alter config
- `webpack` webpack instance for you to use if needed.

\**gerty.js*
\**gerty.js* (basic configuration to control where stuff goes)

```javascript
// @NOTE optional!
// @NOTE this file is not compiled, use only whats available in your node version!

module.exports = {
// full webpack overrides
webpack: (config, { dev, node }, webpack) => {
config.devtool = 'eval';
return config;
},
clientEntry: 'client',
serverEntry: 'server',
clientOutputDirectory: '.sambell/client',
serverOutputDirectory: '.sambell/server',
publicPath: '/static/webpack/',
webpack: config => config,
};
```
31 changes: 21 additions & 10 deletions bin/sambell.js
Expand Up @@ -11,23 +11,34 @@ const spawn = require('child_process').spawn;
const path = require('path');

if (command === 'run') {
spawn('node', [path.resolve(__dirname, '..', 'scripts/run.js')], { stdio: 'inherit' });
spawn('node', [path.resolve(__dirname, '..', 'scripts/run.js')], {
stdio: 'inherit',
});
} else if (command === 'build') {
spawn('node', [path.resolve(__dirname, '..', 'scripts/build.js')], { stdio: 'inherit' });
spawn('node', [path.resolve(__dirname, '..', 'scripts/build.js')], {
stdio: 'inherit',
});
} else if (command === 'watch') {
spawn('node', [path.resolve(__dirname, '..', 'scripts/watch.js')], {
stdio: 'inherit',
});
} else if (command === 'new') {
console.log(chalk.green('Cloning...'));
const { _: [, dest] } = argv;
const finalDest = path.resolve(process.cwd(), dest);
ncp(path.resolve(__dirname, '..', 'template'), finalDest, function (err) {
if (err) return console.error(err);
try {
fs.renameSync(path.resolve(finalDest, '.npmignore'), path.resolve(finalDest, '.gitignore'));
} catch (e) {} // if no .npmignore, already .gitignore
process.chdir(finalDest);
spawn('yarn', ['install'], { stdio: 'inherit' });
ncp(path.resolve(__dirname, '..', 'template'), finalDest, function(err) {
if (err) return console.error(err);
try {
fs.renameSync(
path.resolve(finalDest, '.npmignore'),
path.resolve(finalDest, '.gitignore'),
);
} catch (e) {} // if no .npmignore, already .gitignore
process.chdir(finalDest);
spawn('yarn', ['install'], { stdio: 'inherit' });
});
} else if (!command && (argv.v || argv.version)) {
console.log(chalk.cyan(`sambell ${packageJson.version}`));
} else {
console.log(chalk.red('Valid commands: run; build; new'));
console.log(chalk.red('Valid commands: run; build; watch; new'));
}
6 changes: 4 additions & 2 deletions webpack/sambell-ready.js → modules/client.js
@@ -1,3 +1,5 @@
// @NOTE ensure all of our async scripts load before loading our app

let ready = false;
let clientCalledReady = false;
let clientCb = () => {};
Expand All @@ -11,11 +13,11 @@ const maybeLoad = () => {
ready = true;
clientCb();
}
}
};

window.__SAMBELL_CHUNK_CB__ = maybeLoad;

export default cb => {
export const scriptsReady = cb => {
clientCalledReady = true;
clientCb = cb;
maybeLoad();
Expand Down
44 changes: 44 additions & 0 deletions modules/server.js
@@ -0,0 +1,44 @@
import React from 'react';

// @NOTE render our script tags.
// take array of async chunkNames if using @humblespark/react-loadable (recommended)

export const renderScripts = (asyncChunks = []) => {
if (typeof window !== 'undefined') {
throw new Error('ONLY AVAILABLE ON SERVER.');
}

const clientChunks = JSON.parse('{{SAMBELL_CLIENT_CHUNKS}}');
const filteredAsyncChunks = asyncChunks.reduce(
(acc, asyncChunk) =>
acc.includes(asyncChunk) || !clientChunks[asyncChunk]
? acc
: [...acc, clientChunks[asyncChunk]],
[],
);

const totalChunks = [
'{{SAMBELL_CLIENT_VENDOR_ENTRY}}',
'{{SAMBELL_CLIENT_ENTRY}}',
...filteredAsyncChunks,
];

const MANIFEST = '{{SAMBELL_CLIENT_WEBPACK_MANIFEST}}';
const WAIT_FOR = `window.__SAMBELL_WAIT_FOR_CHUNKS__=${totalChunks.length};`;

return [
<script
key="__sambell_init__"
type="text/javascript"
dangerouslySetInnerHTML={{ __html: `${MANIFEST}${WAIT_FOR}` }}
/>,
totalChunks.map(chunk => (
<script
key={`__sambell_chunk_${chunk}__`}
type="text/javascript"
src={`{{SAMBELL_WEBPACK_PUBLIC_PATH}}${chunk}`}
async
/>
)),
];
};
3 changes: 1 addition & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "sambell",
"version": "6.0.0",
"version": "7.0.0",
"license": "MIT",
"files": ["bin", "scripts", "template", "webpack"],
"bin": {
Expand All @@ -22,7 +22,6 @@
"isomorphic-fetch": "2.2.1",
"minimist": "1.2.0",
"ncp": "2.0.0",
"react": "16.0.0-rc.2",
"react-dev-utils": "4.0.1",
"source-map-support": "0.4.17",
"styled-jsx": "1.0.10",
Expand Down
36 changes: 23 additions & 13 deletions scripts/build.js
Expand Up @@ -7,34 +7,44 @@ const chalk = require('chalk');
const path = require('path');

var clientEntriesByChunk = '';
const finalize = (serverEntriesByChunk = null, _clientEntriesByChunk = null) => {
const finalize = (
serverEntriesByChunk = null,
_clientEntriesByChunk = null,
) => {
if (_clientEntriesByChunk) clientEntriesByChunk = _clientEntriesByChunk;
if (!serverEntriesByChunk || !clientEntriesByChunk) {
console.log(chalk.red('No client/server entry.'), clientEntry, serverEntry);
console.log(
chalk.red('No client/server entry.'),
clientEntriesByChunk,
serverEntriesByChunk,
);
return;
}

const serverEntry = serverEntriesByChunk.run[0];
const serverPath = path.resolve(webpackServerProdConfig.output.path, serverEntry);
const serverPath = path.resolve(
webpackServerProdConfig.output.path,
serverEntry,
);

replaceEntry(
webpackServerProdConfig,
webpackClientProdConfig,
serverEntriesByChunk,
clientEntriesByChunk,
() => {
console.log(chalk.green(`Production version built. Run... ${chalk.bold(`node ${path.relative(process.cwd(), serverPath)}`)}`));
console.log(
chalk.green(
`Production version built. Run... ${chalk.bold(
`node ${path.relative(process.cwd(), serverPath)}`,
)}`,
),
);
console.log('');
}
},
);
};

webpack([
webpackClientProdConfig,
webpackServerProdConfig,
]).run(
handleWebpackStats(
finalize,
webpackClientProdConfig
)
webpack([webpackClientProdConfig, webpackServerProdConfig]).run(
handleWebpackStats(finalize, webpackClientProdConfig),
);
37 changes: 23 additions & 14 deletions scripts/run.js
Expand Up @@ -9,12 +9,18 @@ const path = require('path');

var server = null;
var clientEntriesByChunk = '';
const refreshServer = (serverEntriesByChunk = null, _clientEntriesByChunk = null) => {
const refreshServer = (
serverEntriesByChunk = null,
_clientEntriesByChunk = null,
) => {
if (_clientEntriesByChunk) clientEntriesByChunk = _clientEntriesByChunk;
if (!serverEntriesByChunk || !clientEntriesByChunk) return;

const serverEntry = serverEntriesByChunk.run[0];
const serverPath = path.resolve(webpackServerDevConfig.output.path, serverEntry);
const serverPath = path.resolve(
webpackServerDevConfig.output.path,
serverEntry,
);

replaceEntry(
webpackServerDevConfig,
Expand All @@ -23,20 +29,23 @@ const refreshServer = (serverEntriesByChunk = null, _clientEntriesByChunk = null
clientEntriesByChunk,
() => {
if (server) server.kill();
console.log(chalk.green(`${server ? 'Restarting' : 'Starting'} sambell...`));
server = spawn('node', [serverPath], { stdio: 'inherit', env: process.env });
console.log(chalk.green(`${chalk.bold('RUN!')} (localhost:${process.env.PORT || 3000})`));
}
console.log(
chalk.green(`${server ? 'Restarting' : 'Starting'} sambell...`),
);
server = spawn('node', [serverPath], {
stdio: 'inherit',
env: process.env,
});
console.log(
chalk.green(
`${chalk.bold('RUN!')} (localhost:${process.env.PORT || 3000})`,
),
);
},
);
};

webpack([
webpackClientDevConfig,
webpackServerDevConfig,
]).watch(
webpack([webpackClientDevConfig, webpackServerDevConfig]).watch(
{},
handleWebpackStats(
refreshServer,
webpackClientDevConfig
)
handleWebpackStats(refreshServer, webpackClientDevConfig),
);

0 comments on commit fd2ec18

Please sign in to comment.