├── .npmignore
├── .gitignore
├── .travis.yml
├── component-playground
├── global-reset.css
├── lib
│ ├── parse-fixture-path.js
│ ├── get-title-for-fixture.js
│ └── get-component-fixture-tree.js
├── public
│ └── index.html
├── entry.js
└── config.js
├── .jscsrc
├── bin
└── component-playground.js
├── karma.conf.js
├── tests
├── component-playground
│ └── lib
│ │ ├── parse-fixture-path.js
│ │ └── get-title-for-fixture.js
└── bind-polyfill.js
├── LICENSE
├── package.json
├── CONTRIBUTING.md
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tests
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | tests/coverage
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 | after_success:
5 | - npm run coveralls
6 |
--------------------------------------------------------------------------------
/component-playground/global-reset.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | margin: 0;
3 | padding: 0;
4 | height: 100%;
5 | }
6 |
--------------------------------------------------------------------------------
/component-playground/lib/parse-fixture-path.js:
--------------------------------------------------------------------------------
1 | module.exports = function(fixturePath) {
2 | // './my-component/my-state.js' => ('my-component', 'my-state')
3 | return fixturePath.match(/^\.\/(.+)\/(.+)\.js$/);
4 | };
5 |
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "excludeFiles": [
3 | ".git/**",
4 | "node_modules/**",
5 | "tests/coverage/**"
6 | ],
7 | "fileExtensions": [".js", ".jsx"],
8 | "preset": "google",
9 |
10 | "disallowMultipleVarDecl": null
11 | }
12 |
--------------------------------------------------------------------------------
/component-playground/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | React Component Playground
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/component-playground/lib/get-title-for-fixture.js:
--------------------------------------------------------------------------------
1 | module.exports = function(params) {
2 | var title = 'React Component Playground';
3 |
4 | // Set document title to the name of the selected fixture
5 | if (params.component && params.fixture) {
6 | title = params.component + ':' + params.fixture + ' – ' + title;
7 | }
8 |
9 | return title;
10 | };
11 |
--------------------------------------------------------------------------------
/bin/component-playground.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path'),
4 | WebpackDevServer = require('webpack-dev-server'),
5 | webpack = require('webpack');
6 |
7 | var rootPath = path.join(__dirname, '..'),
8 | playgroundPath = path.join(rootPath, 'component-playground');
9 |
10 | var config = require(path.join(playgroundPath, 'config'));
11 | var compiler = webpack(config.webpack);
12 |
13 | var server = new WebpackDevServer(compiler, {
14 | contentBase: path.join(playgroundPath, 'public'),
15 | publicPath: '/build/',
16 | hot: true
17 | });
18 |
19 | server.listen(config.server.port, config.server.hostname, function() {});
20 |
--------------------------------------------------------------------------------
/component-playground/entry.js:
--------------------------------------------------------------------------------
1 | require('./global-reset.css');
2 |
3 | var ReactQuerystringRouter = require('react-querystring-router'),
4 | ComponentPlayground = require('react-component-playground'),
5 | getComponentFixtureTree = require('./lib/get-component-fixture-tree.js'),
6 | getTitleForFixture = require('./lib/get-title-for-fixture.js');
7 |
8 | module.exports = new ReactQuerystringRouter.Router({
9 | container: document.getElementById('component-playground'),
10 | defaultProps: {
11 | components: getComponentFixtureTree()
12 | },
13 | getComponentClass: function() {
14 | return ComponentPlayground;
15 | },
16 | onChange: function(params) {
17 | document.title = getTitleForFixture(params);
18 | }
19 | });
20 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | basePath: 'tests/',
4 | browsers: ['PhantomJS'],
5 | coverageReporter: {
6 | type: 'lcov',
7 | dir: 'coverage/'
8 | },
9 | files: [
10 | '**/*.js'
11 | ],
12 | frameworks: ['mocha', 'chai', 'sinon-chai'],
13 | preprocessors: {
14 | '**/*.js': ['webpack']
15 | },
16 | reporters: ['mocha', 'coverage'],
17 | webpack: {
18 | module: {
19 | postLoaders: [{
20 | test: /\.js$/,
21 | exclude: /(node_modules|tests)\//,
22 | loader: 'istanbul-instrumenter'
23 | }]
24 | }
25 | },
26 | webpackMiddleware: {
27 | noInfo: true
28 | }
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/tests/component-playground/lib/parse-fixture-path.js:
--------------------------------------------------------------------------------
1 | var parseFixturePath =
2 | require('../../../component-playground/lib/parse-fixture-path.js');
3 |
4 | describe('Parse fixture path', function() {
5 | it('should extract component and fixture name', function() {
6 | var parts = parseFixturePath('./my-component/my-state.js');
7 |
8 | expect(parts[1]).to.equal('my-component');
9 | expect(parts[2]).to.equal('my-state');
10 | });
11 |
12 | it('should extract nested component and fixture name', function() {
13 | var parts = parseFixturePath('./component-group/my-component/my-state.js');
14 |
15 | expect(parts[1]).to.equal('component-group/my-component');
16 | expect(parts[2]).to.equal('my-state');
17 | });
18 | })
19 |
--------------------------------------------------------------------------------
/tests/component-playground/lib/get-title-for-fixture.js:
--------------------------------------------------------------------------------
1 | var getTitleForFixture =
2 | require('../../../component-playground/lib/get-title-for-fixture.js');
3 |
4 | describe('Get title for fixture', function() {
5 | it('should return "React Component Playground" ' +
6 | 'when no fixture is selected', function() {
7 | var title = getTitleForFixture({});
8 |
9 | expect(title).to.equal('React Component Playground');
10 | });
11 |
12 | it('should include component and fixture name in title ' +
13 | 'when no fixture is selected', function() {
14 | var title = getTitleForFixture({
15 | component: 'FooComponent',
16 | fixture: 'bar-fixture'
17 | });
18 |
19 | expect(title)
20 | .to.equal('FooComponent:bar-fixture – React Component Playground');
21 | });
22 | })
23 |
--------------------------------------------------------------------------------
/component-playground/lib/get-component-fixture-tree.js:
--------------------------------------------------------------------------------
1 | var parseFixturePath = require('./parse-fixture-path.js');
2 |
3 | module.exports = function() {
4 | var requireFixture = require.context('COSMOS_FIXTURES', true, /\.js$/),
5 | fixtures = {};
6 |
7 | requireFixture.keys().forEach(function(fixturePath) {
8 | var pathParts = parseFixturePath(fixturePath),
9 | componentName = pathParts[1],
10 | fixtureName = pathParts[2];
11 |
12 | // Fixtures are grouped per component
13 | if (!fixtures[componentName]) {
14 | fixtures[componentName] = {
15 | class: require('COSMOS_COMPONENTS/' + componentName),
16 | fixtures: {}
17 | };
18 | }
19 |
20 | fixtures[componentName].fixtures[fixtureName] = requireFixture(fixturePath);
21 | });
22 |
23 | return fixtures;
24 | };
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ovidiu Cherecheș
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/tests/bind-polyfill.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PhantomJS doesn't support Function.prototype.bind natively, so
3 | * polyfill it whenever this module is required.
4 | */
5 |
6 | (function() {
7 | var Ap = Array.prototype;
8 | var slice = Ap.slice;
9 | var Fp = Function.prototype;
10 |
11 | if (!Fp.bind) {
12 | Fp.bind = function(context) {
13 | var func = this;
14 | var args = slice.call(arguments, 1);
15 |
16 | function bound() {
17 | var invokedAsConstructor = func.prototype && (this instanceof func);
18 | return func.apply(
19 | // Ignore the context parameter when invoking the bound function
20 | // as a constructor. Note that this includes not only constructor
21 | // invocations using the new keyword but also calls to base class
22 | // constructors such as BaseClass.call(this, ...) or super(...).
23 | !invokedAsConstructor && context || this,
24 | args.concat(slice.call(arguments))
25 | );
26 | }
27 |
28 | // The bound function must share the .prototype of the unbound
29 | // function so that any object created by one constructor will count
30 | // as an instance of both constructors.
31 | bound.prototype = func.prototype;
32 |
33 | return bound;
34 | };
35 | }
36 | })();
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cosmos-js",
3 | "version": "0.6.8",
4 | "description": "A development tool for designing truly encapsulated React components",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/skidding/cosmos.git"
8 | },
9 | "dependencies": {
10 | "babel-core": "^5.2.17",
11 | "babel-loader": "^5.0.0",
12 | "css-loader": "^0.15.6",
13 | "less": "^2.5.1",
14 | "less-loader": "^2.2.0",
15 | "lodash": "^3.5.0",
16 | "react-component-playground": "^0.3.0",
17 | "react-hot-loader": "^1.2.7",
18 | "react-querystring-router": "^0.2.0",
19 | "style-loader": "^0.12.3",
20 | "webpack": "^1.7.3",
21 | "webpack-dev-server": "^1.8.0",
22 | "yargs": "^3.6.0"
23 | },
24 | "devDependencies": {
25 | "chai": "^2.2.0",
26 | "coveralls": "^2.11.2",
27 | "esprima-fb": "^12001.1.0-dev-harmony-fb",
28 | "istanbul": "^0.3.13",
29 | "istanbul-instrumenter-loader": "^0.1.2",
30 | "jscs": "^1.12.0",
31 | "karma": "^0.13.2",
32 | "karma-chai": "^0.1.0",
33 | "karma-cli": "0.0.4",
34 | "karma-coverage": "^0.2.7",
35 | "karma-mocha": "^0.1.10",
36 | "karma-mocha-reporter": "^1.0.2",
37 | "karma-phantomjs-launcher": "^0.1.4",
38 | "karma-sinon-chai": "^0.3.0",
39 | "karma-webpack": "^1.7.0",
40 | "mocha": "^2.2.4",
41 | "sinon": "^1.14.1",
42 | "sinon-chai": "^2.7.0"
43 | },
44 | "peerDependencies": {
45 | "react": "^0.13.1"
46 | },
47 | "scripts": {
48 | "pretest": "jscs --esprima=esprima-fb ./",
49 | "test": "karma start --single-run",
50 | "coveralls": "cat tests/coverage/*/lcov.info | node_modules/coveralls/bin/coveralls.js"
51 | },
52 | "bin": {
53 | "component-playground": "./bin/component-playground.js"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/component-playground/config.js:
--------------------------------------------------------------------------------
1 | var path = require('path'),
2 | argv = require('yargs').argv,
3 | _ = require('lodash'),
4 | webpack = require('webpack');
5 |
6 | var playgroundPath = __dirname,
7 | rootPath = path.join(playgroundPath, '..'),
8 | modulesPath = path.join(rootPath, 'node_modules'),
9 | cwd = process.cwd();
10 |
11 | var resolvePath = function(userPath) {
12 | return path.resolve(cwd, userPath);
13 | };
14 |
15 | var config = {},
16 | userConfig;
17 |
18 | try {
19 | userConfig = require(
20 | resolvePath(argv.configPath || 'component-playground.config'));
21 | } catch (e) {
22 | if (e instanceof Error && e.code === 'MODULE_NOT_FOUND') {
23 | userConfig = {};
24 | } else {
25 | throw e;
26 | }
27 | }
28 |
29 | config.server = _.extend({
30 | port: 8989,
31 | hostname: 'localhost'
32 | }, userConfig.server);
33 |
34 | config.webpack = {
35 | context: playgroundPath,
36 | entry: [
37 | 'webpack-dev-server/client?http://' + config.server.hostname + ':' +
38 | config.server.port,
39 | 'webpack/hot/dev-server',
40 | './entry'
41 | ],
42 | resolve: {
43 | alias: {
44 | COSMOS_COMPONENTS: resolvePath(argv.componentsPath || 'components'),
45 | COSMOS_FIXTURES: resolvePath(argv.fixturesPath || 'fixtures')
46 | },
47 | extensions: ['', '.js', '.jsx']
48 | },
49 | resolveLoader: {
50 | root: modulesPath
51 | },
52 | module: {
53 | loaders: [{
54 | test: /\.jsx?$/,
55 | exclude: /node_modules/,
56 | loaders: ['react-hot-loader', 'babel-loader']
57 | }, {
58 | test: /\.css$/,
59 | loader: 'style-loader!css-loader'
60 | }, {
61 | test: /\.less$/,
62 | loader: 'style-loader!css-loader!less-loader'
63 | }]
64 | },
65 | output: {
66 | libraryTarget: 'umd',
67 | library: 'cosmosRouter',
68 | path: path.join(playgroundPath, 'public', 'build'),
69 | filename: 'bundle.js',
70 | publicPath: '/build/'
71 | },
72 | plugins: [
73 | new webpack.HotModuleReplacementPlugin(),
74 | new webpack.NoErrorsPlugin()
75 | ]
76 | };
77 |
78 | if (userConfig.webpack) {
79 | config.webpack = userConfig.webpack(config.webpack);
80 | }
81 |
82 | module.exports = config;
83 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Mission
2 |
3 | **How to design components that are truly encapsulated?**
4 |
5 | [Read more.](https://github.com/skidding/cosmos/wiki/Problem)
6 |
7 | ## Manifesto
8 |
9 | _cos·mos1 `/ˈkäzməs,-ˌmōs,-ˌmäs/` noun — 1. The universe seen as
10 | a well-ordered whole._
11 |
12 | - Zero bootstrap
13 | - Can be plugged into any other framework
14 | - Everything is a Component
15 | - Components are oblivious of ancestors
16 | - Any component can be represented by a URI
17 | - The state of the entire UI can be serialized at any given point in time
18 | - Components can implement any data sync mechanism
19 |
20 | These set of principles were defined before any code was written and have guided the evolution of Cosmos to present.
21 |
22 | ## Packaging
23 |
24 | Cosmos is split into smaller repositories for modularity and ease of development. This contributing guide applies to all included packages.
25 |
26 | - [ReactComponentTree](https://github.com/skidding/react-component-tree) – Serialize and reproduce the state of an entire tree of React components
27 | - [ReactComponentPlayground](https://github.com/skidding/react-component-playground) – Minimal frame for loading and testing React components in isolation
28 | - [ReactQuerystringRouter](https://github.com/skidding/react-querystring-router) – Bare router for React components, using query string as props
29 |
30 | The `cosmos-js` package is the top layer, responsible for offering a minimum configuration ComponentPlayground setup for projects powered by React components.
31 |
32 | ## Design
33 |
34 | [The Best Code is No Code At All.](http://blog.codinghorror.com/the-best-code-is-no-code-at-all/) Start with this in mind before adding any new line of code.
35 |
36 | 1. When thinking of adding or changing something, make sure it is in line with the project [mission.](#mission)
37 | 2. Once the value added is clear, strive for the simplest, most obvious solution.
38 |
39 | ## Style
40 |
41 | Unless explicitly overridden, all rules from the [Google JavaScript Style Guide](https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml) apply. The [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) is also a good reference.
42 |
43 | ## Tests
44 |
45 | Unit tests both keep the project sane and define its specs. Providing test coverage for every contribution is mandatory.
46 |
47 | Repo | Build | Status
48 | --- | --- | ---
49 | [Cosmos](https://github.com/skidding/cosmos/tree/master/tests) | [](https://travis-ci.org/skidding/cosmos) | [](https://coveralls.io/r/skidding/cosmos?branch=master)
50 | [ReactComponentTree](https://github.com/skidding/react-component-tree/tree/master/tests) | [](https://travis-ci.org/skidding/react-component-tree) | [](https://coveralls.io/r/skidding/react-component-tree?branch=master)
51 | [ReactComponentPlayground](https://github.com/skidding/react-component-playground/tree/master/tests) | [](https://travis-ci.org/skidding/react-component-playground) | [](https://coveralls.io/r/skidding/react-component-playground?branch=master)
52 | [ReactQuerystringRouter](https://github.com/skidding/react-querystring-router/tree/master/tests) | [](https://travis-ci.org/skidding/react-querystring-router) | [](https://coveralls.io/r/skidding/react-querystring-router?branch=master)
53 |
54 | The only untested sections are the [webpack config](component-playground/config.js) and the [dev runner](bin/component-playground.js), which are also the parts most likely to change in the future.
55 |
56 | ## Git flow
57 |
58 | Make sure you're familiar with the [Github flow.](https://guides.github.com/introduction/flow/)
59 |
60 | ### Branches
61 |
62 | Prefix branch names with the corresponding issue number. E.g. `137-contributing-guide`
63 |
64 | ### Commits
65 |
66 | Use the imperative mood to express verbs and add a hashtag with the corresponding issue number at the end of each commit message. The Github UI will generate links to the referenced issues.
67 |
68 | > Adapt fixture mapping to new ComponentPlayground format [#115](https://github.com/skidding/cosmos/issues/115)
69 |
70 | ### Review
71 |
72 | Ask for review *before* writing the code. Saves everybody time and effort.
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cosmos [](https://travis-ci.org/skidding/cosmos) [](https://coveralls.io/r/skidding/cosmos?branch=master)
2 |
3 | Cosmos is a JavaScript DX* tool for designing truly encapsulated
4 | [React](http://facebook.github.io/react/) components.
5 |
6 | It scans your project for React component
7 | [_fixtures_](http://en.wikipedia.org/wiki/Test_fixture) and loads them inside [ComponentPlayground](https://github.com/skidding/react-component-playground),
8 | enabling you to:
9 |
10 | 1. Render your components under any combination of props and state
11 | 2. See component states evolve in real-time as you interact with running
12 | instances
13 |
14 | > Working with ComponentPlayground improves the component design because it
15 | surfaces any implicit dependencies. It also forces you to define sane inputs
16 | for every component, making them more predictable and easier to debug down the
17 | road.
18 |
19 | 
20 |
21 | _\*DX stands for Developer Experience, the counterpart of UX in building a product, system or service._
22 |
23 | ### Requirements
24 |
25 | - [x] You should already be using CommonJS modules to structure your code and
26 | [webpack](http://webpack.github.io/) to bundle your modules for the browser
27 | - [x] Your React components should be fully encapsulated. They should have no
28 | global dependencies and rely exclusively on _props_ for input. Including styles,
29 | which means you need to be using
30 | [style-loader](https://github.com/webpack/style-loader).
31 | - [x] You must create component fixtures for ComponentPlayground to load. The
32 | component and fixture files should be nested as in the folder structure below.
33 | See the [example repo](https://github.com/skidding/cosmos-example) for a better
34 | picture.
35 |
36 | ### Installing
37 |
38 | - Install the Cosmos package through npm `npm install cosmos-js`
39 | - Run the ComponentPlayground executable `node_modules/.bin/component-playground`
40 | - Open [localhost:8989](http://localhost:8989)
41 |
42 | ### Under the hood
43 |
44 | Running the ComponentPlayground executable will:
45 |
46 | 1. Start a [webpack dev server](http://webpack.github.io/docs/webpack-dev-server.html),
47 | serving an instance of ComponentPlayground at `localhost:8989`
48 | 2. Scan the current folder for components and fixtures and feed them to
49 | ComponentPlayground
50 |
51 | #### File structure
52 |
53 | This is the file structure Cosmos expects:
54 | ```
55 | |
56 | +-- components
57 | | +-- FooComponent.jsx
58 | | +-- namespace
59 | | | +-- BarComponent.jsx
60 | +-- fixtures
61 | | +-- FooComponent
62 | | | +-- default.js
63 | | | +-- active.js
64 | | +-- namespace
65 | | | +-- BarComponent
66 | | | +-- default.js
67 | | | +-- paused.js
68 | ```
69 |
70 | If the _components_ and _fixtures_ folders are not siblings, their paths can be
71 | specified via cli args:
72 |
73 | ```bash
74 | node_modules/.bin/component-playground --components-path src/components --fixtures-path tests/fixtures
75 | ```
76 |
77 | #### webpack config
78 |
79 | The webpack build bundles modules from both the current folder and the Cosmos
80 | package. It is compatible with React classes, ES6 classes, JSX and CSS/LESS
81 | modules [out of the box](component-playground/config.js#L34-L76), but you can
82 | customize the webpack config to support additional loaders and settings by
83 | creating a `component-playground.config.js` file in the project root. E.g.
84 |
85 | ```js
86 | module.exports.webpack = function(config) {
87 | config.module.loaders.push(/*...*/);
88 | return config;
89 | };
90 | ```
91 |
92 | You can use this functionality to inject external styles or scripts if your
93 | components need e.g. Bootstrap to work.
94 | ```js
95 | config.entry.push(path.join(process.cwd(), 'injectBootstrapTags.js'));
96 | ```
97 |
98 | #### Hot loading
99 |
100 | Cosmos includes [React Hot Loader](http://gaearon.github.io/react-hot-loader/)
101 | and has webpack's [hot module replacement](http://webpack.github.io/docs/hot-module-replacement.html)
102 | enabled so you can tweak the components and their styles without refreshing the
103 | browser:
104 |
105 | 
106 |
107 | ### Thank you for your interest!
108 |
109 | [](https://gitter.im/skidding/cosmos?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
110 |
111 | Explore the [Contributing Guide](CONTRIBUTING.md) for more information.
112 |
--------------------------------------------------------------------------------