├── .gitignore
├── spin.js
├── spin.config.js
├── webpack.config.js
├── .travis.yml
├── src
├── ConfigPlugin.ts
├── plugins
│ ├── shared
│ │ ├── identity-loader.ts
│ │ ├── UPFinder.ts
│ │ ├── __tests__
│ │ │ └── JSRuleFinder.test.ts
│ │ ├── parallelLoader.ts
│ │ ├── resolveModule.ts
│ │ └── JSRuleFinder.ts
│ ├── webpack
│ │ ├── virtualModuleLoader.ts
│ │ └── SwaggerWebpackPlugin.ts
│ ├── angular
│ │ └── angular-polyfill.ts
│ ├── RestPlugin.ts
│ ├── I18NextPlugin.ts
│ ├── TCombPlugin.ts
│ ├── VuePlugin.ts
│ ├── FlowRuntimePlugin.ts
│ ├── react-native
│ │ ├── polyfills
│ │ │ ├── react-native-polyfill-16.ts
│ │ │ └── react-native-polyfill-15.ts
│ │ ├── liveReloadMiddleware.ts
│ │ ├── symbolicateMiddleware.ts
│ │ └── assetLoader.ts
│ ├── ReactNativeWebPlugin.ts
│ ├── StyledComponentsPlugin.ts
│ ├── ReactHotLoaderPlugin.ts
│ ├── WebAssetsPlugin.ts
│ ├── ReactPlugin.ts
│ ├── BabelPlugin.ts
│ ├── ApolloPlugin.ts
│ ├── AngularPlugin.ts
│ ├── TypeScriptPlugin.ts
│ ├── ReactNativePlugin.ts
│ ├── CssProcessorPlugin.ts
│ └── WebpackPlugin.ts
├── index.ts
├── EnhancedError.ts
├── Builder.ts
├── webpack.config.ts
├── upDirs.ts
├── webpackHooks.ts
├── createWebpackConfig.ts
├── getDeps.ts
├── createConfig.ts
├── createRequire.ts
├── Spin.ts
├── Stack.ts
├── BuilderDiscoverer.ts
├── cli.ts
├── inferConfig.ts
├── ConfigReader.ts
├── createBuilders.ts
└── executor.ts
├── html-plugin-template.ejs
├── docs
├── programmatic.md
├── scripts.md
├── concepts.md
├── howSpinWorks.md
└── configuration.md
├── tsconfig.json
├── LICENSE
├── .all-contributorsrc
├── tslint.json
├── package.json
├── logo.svg
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /.history
3 | /.npmrc
4 | node_modules
5 | lib
6 | package-lock.json
7 | .idea
8 |
--------------------------------------------------------------------------------
/spin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | require('source-map-support').install();
3 |
4 | require('./lib/cli');
5 |
--------------------------------------------------------------------------------
/spin.config.js:
--------------------------------------------------------------------------------
1 | require('source-map-support').install();
2 |
3 | module.exports = require('./lib/spin.config');
4 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('source-map-support').install();
2 |
3 | module.exports = require('./lib/webpack.config');
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | dist: trusty
5 | os:
6 | - linux
7 | script:
8 | - yarn test
9 |
--------------------------------------------------------------------------------
/src/ConfigPlugin.ts:
--------------------------------------------------------------------------------
1 | import Spin from './Spin';
2 |
3 | export interface ConfigPlugin {
4 | configure(builder, spin: Spin);
5 | }
6 |
--------------------------------------------------------------------------------
/src/plugins/shared/identity-loader.ts:
--------------------------------------------------------------------------------
1 | module.exports = function(content, map, meta) {
2 | this.callback(null, content, map, meta);
3 | return;
4 | };
5 |
--------------------------------------------------------------------------------
/src/plugins/webpack/virtualModuleLoader.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable
2 | module.exports = function() {
3 | this.callback(this.query.error, JSON.stringify(this.query.result));
4 | };
5 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import 'source-map-support/register';
2 | export { default as createWebpackConfig } from './createWebpackConfig';
3 | export { default as createConfig } from './createConfig';
4 |
--------------------------------------------------------------------------------
/src/EnhancedError.ts:
--------------------------------------------------------------------------------
1 | export default class EnhancedError extends Error {
2 | private cause: Error;
3 |
4 | constructor(message: string, cause?: Error) {
5 | super(message);
6 | this.cause = cause;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/html-plugin-template.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= htmlWebpackPlugin.options.title %>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/plugins/angular/angular-polyfill.ts:
--------------------------------------------------------------------------------
1 | import 'core-js/es6';
2 | import 'core-js/es7/reflect';
3 | import 'zone.js/dist/zone';
4 |
5 | if (process.env.ENV === 'production') {
6 | // Production
7 | } else {
8 | // Development and test
9 | Error.stackTraceLimit = Infinity;
10 | /* tslint:disable:no-var-requires */
11 | require('zone.js/dist/long-stack-trace-zone');
12 | }
13 |
--------------------------------------------------------------------------------
/src/Builder.ts:
--------------------------------------------------------------------------------
1 | import { RequireFunction } from './createRequire';
2 | import Stack from './Stack';
3 |
4 | export interface Builder {
5 | name: string;
6 | require: RequireFunction;
7 | enabled: boolean;
8 | stack: Stack;
9 | roles: string[];
10 | parent?: Builder;
11 | child?: Builder;
12 | config?: any;
13 | [x: string]: any;
14 | }
15 |
16 | export interface Builders {
17 | [id: string]: Builder;
18 | }
19 |
--------------------------------------------------------------------------------
/src/webpack.config.ts:
--------------------------------------------------------------------------------
1 | // TODO: remove in 0.5.x
2 | import createBuilders from './createBuilders';
3 |
4 | const builders = createBuilders({
5 | cwd: process.env.SPIN_CWD || process.cwd(),
6 | cmd: 'test',
7 | argv: { c: process.env.SPIN_CONFIG }
8 | }).builders;
9 | const testConfig = builders[Object.keys(builders)[0]].config;
10 |
11 | // console.log("test config:", require('util').inspect(testConfig, false, null));
12 | export default testConfig;
13 |
--------------------------------------------------------------------------------
/src/upDirs.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | export default (rootPath: string, relPath: string = '.'): string[] => {
4 | const paths = [];
5 | let curDir = rootPath;
6 | while (true) {
7 | const lastIdx = curDir.lastIndexOf(path.sep, curDir.length - 1);
8 | paths.push(path.join(curDir + (lastIdx < 0 ? path.sep : ''), relPath));
9 | if (lastIdx < 0) {
10 | break;
11 | }
12 | curDir = curDir.substring(0, lastIdx);
13 | }
14 |
15 | return paths;
16 | };
17 |
--------------------------------------------------------------------------------
/docs/programmatic.md:
--------------------------------------------------------------------------------
1 | # SpinJS Programmatic Usage
2 |
3 | ## Storybook Full Control integration example
4 |
5 | `.storybook/webpack.config.js`:
6 | ``` js
7 | const createConfig = require('spinjs').createConfig;
8 |
9 | module.exports = function (baseConfig, configType) {
10 | return createConfig({
11 | cmd: configType === 'DEVELOPMENT' ? 'watch' : 'build',
12 | builderOverrides: { stack: ['storybook'] },
13 | genConfigOverrides: Object.assign({ merge: { entry: 'replace', output: 'replace' } }, baseConfig)
14 | });
15 | };
16 | ```
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "lib": [
6 | "es2016"
7 | ],
8 | "moduleResolution": "node",
9 | "sourceMap": true,
10 | "declaration": true,
11 | "noImplicitAny": false,
12 | "rootDir": "src",
13 | "outDir": "lib",
14 | "allowSyntheticDefaultImports": true,
15 | "experimentalDecorators": true,
16 | "pretty": true,
17 | "removeComments": true
18 | },
19 | "include": [
20 | "**/*.ts"
21 | ],
22 | "exclude": [
23 | "node_modules",
24 | "dist",
25 | "lib"
26 | ]
27 | }
--------------------------------------------------------------------------------
/src/plugins/RestPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 | import SwaggerWebpackPlugin from './webpack/SwaggerWebpackPlugin';
5 |
6 | export default class VuePlugin implements ConfigPlugin {
7 | public configure(builder: Builder, spin: Spin) {
8 | const stack = builder.stack;
9 |
10 | if (stack.hasAll(['webpack', 'server', 'rest'])) {
11 | if (builder.require.probe('swagger-jsdoc') && builder.require.probe('webpack-virtual-modules')) {
12 | builder.config = spin.merge(builder.config, {
13 | plugins: [new SwaggerWebpackPlugin(builder, spin)]
14 | });
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/plugins/I18NextPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 |
5 | export default class I18NextPlugin implements ConfigPlugin {
6 | public configure(builder: Builder, spin: Spin) {
7 | const stack = builder.stack;
8 |
9 | if (stack.hasAll(['i18next', 'webpack'])) {
10 | const webpack = builder.require('webpack');
11 |
12 | builder.config = spin.merge(builder.config, {
13 | module: {
14 | rules: [
15 | {
16 | test: /locales/,
17 | use: { loader: '@alienfast/i18next-loader', options: spin.createConfig(builder, 'i18next', {}) }
18 | }
19 | ]
20 | }
21 | });
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/plugins/TCombPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 | import JSRuleFinder from './shared/JSRuleFinder';
5 |
6 | export default class TCombPlugin implements ConfigPlugin {
7 | public configure(builder: Builder, spin: Spin) {
8 | const stack = builder.stack;
9 |
10 | if (stack.hasAll(['tcomb', 'webpack']) && !stack.hasAny('dll')) {
11 | const jsRuleFinder = new JSRuleFinder(builder);
12 | const jsRule = jsRuleFinder.findJSRule();
13 | if (jsRule && !jsRule.use.options.babelrc) {
14 | jsRule.use = spin.merge(jsRule.use, {
15 | options: {
16 | plugins: [['babel-plugin-tcomb']]
17 | }
18 | });
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/webpackHooks.ts:
--------------------------------------------------------------------------------
1 | import { camelize } from 'humps';
2 |
3 | const webpackHook = (hookType: string, compiler: any, hookName: string, hookFunc: (...args: any[]) => any): void => {
4 | if (compiler.hooks) {
5 | const hook = compiler.hooks[camelize(hookName)];
6 | if (hookType === 'async') {
7 | hook.tapAsync('SpinJS', hookFunc);
8 | } else {
9 | hook.tap('SpinJS', hookFunc);
10 | }
11 | } else {
12 | compiler.plugin(hookName, hookFunc);
13 | }
14 | };
15 |
16 | export const hookSync = (compiler: any, hookName: string, hookFunc: (...args: any[]) => any): void =>
17 | webpackHook('sync', compiler, hookName, hookFunc);
18 |
19 | export const hookAsync = (compiler: any, hookName: string, hookFunc: (...args: any[]) => any): void =>
20 | webpackHook('async', compiler, hookName, hookFunc);
21 |
--------------------------------------------------------------------------------
/src/plugins/shared/UPFinder.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 |
4 | import { Builder } from '../../Builder';
5 | import upDirs from '../../upDirs';
6 |
7 | export default class {
8 | private cwd: string;
9 |
10 | constructor(builder: Builder) {
11 | this.cwd = builder.require.cwd;
12 | }
13 |
14 | public find(candidates: string[]): string {
15 | let foundPath: string;
16 | const paths = upDirs(this.cwd);
17 | for (const dir of paths) {
18 | for (const candidate of candidates) {
19 | const candidatePath = path.join(dir, candidate);
20 | if (fs.existsSync(candidatePath)) {
21 | foundPath = candidatePath;
22 | break;
23 | }
24 | }
25 | if (foundPath) {
26 | break;
27 | }
28 | }
29 | return foundPath;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/createWebpackConfig.ts:
--------------------------------------------------------------------------------
1 | import * as minilog from 'minilog';
2 |
3 | import createBuilders from './createBuilders';
4 |
5 | minilog.enable();
6 | const logger = minilog('spin');
7 |
8 | export default (cwd, configPath, builderName) => {
9 | let builder;
10 | try {
11 | const builders = createBuilders({ cwd, cmd: 'watch', argv: { c: configPath }, builderName }).builders;
12 | for (const builderId of Object.keys(builders)) {
13 | if (builders[builderId].name === builderName) {
14 | builder = builders[builderId];
15 | break;
16 | }
17 | }
18 | } catch (e) {
19 | if (e.cause) {
20 | logger.error(e);
21 | }
22 | throw e;
23 | }
24 | if (!builder) {
25 | throw new Error(`Builder ${builderName} not found, cwd: ${cwd}, config path: ${configPath}`);
26 | }
27 | return builder.config;
28 | };
29 |
--------------------------------------------------------------------------------
/src/getDeps.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { RequireFunction } from './createRequire';
3 |
4 | export interface Dependencies {
5 | [x: string]: string;
6 | }
7 |
8 | const getDeps = (packageJsonPath: string, requireDep: RequireFunction, deps: Dependencies): Dependencies => {
9 | const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
10 | const pkgDeps: any = Object.keys(pkg.dependencies || {});
11 | let result = { ...deps };
12 | for (const dep of pkgDeps) {
13 | if (!dep.startsWith('.') && !result[dep]) {
14 | let depPkg;
15 | try {
16 | depPkg = requireDep.resolve(dep + '/package.json');
17 | } catch (e) {}
18 | if (depPkg) {
19 | result[dep] = depPkg;
20 | const subDeps = getDeps(depPkg, requireDep, result);
21 | result = { ...result, ...subDeps };
22 | }
23 | }
24 | }
25 | return result;
26 | };
27 |
28 | export default getDeps;
29 |
--------------------------------------------------------------------------------
/src/plugins/shared/__tests__/JSRuleFinder.test.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../../../Builder';
2 | import JSRuleFinder from '../JSRuleFinder';
3 |
4 | describe('JSRuleFinder', () => {
5 | it('should create js rule if it does not exist', () => {
6 | // tslint:disable-next-line
7 | const builder: Builder = {} as Builder;
8 | builder.config = {
9 | module: { rules: [] }
10 | };
11 | const rule = new JSRuleFinder(builder).findAndCreateJSRule();
12 | expect(rule).toHaveProperty('test');
13 | });
14 |
15 | it('should find js rule if it exists', () => {
16 | // tslint:disable-next-line
17 | const builder: Builder = {} as Builder;
18 | const regex = /\.js$/;
19 | builder.config = {
20 | module: { rules: [{ test: /abc/ }, { test: regex }, { test: /def/ }] }
21 | };
22 | const rule = new JSRuleFinder(builder).findAndCreateJSRule();
23 | expect(rule).toHaveProperty('test');
24 | expect(rule.test).toEqual(regex);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/plugins/VuePlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 |
5 | export default class VuePlugin implements ConfigPlugin {
6 | public configure(builder: Builder, spin: Spin) {
7 | const stack = builder.stack;
8 |
9 | if (stack.hasAll(['vue', 'webpack'])) {
10 | const webpack = builder.require('webpack');
11 | const VueLoaderPlugin = builder.require('vue-loader/lib/plugin');
12 |
13 | builder.config = spin.merge(builder.config, {
14 | module: {
15 | rules: [
16 | {
17 | test: /\.vue$/,
18 | use: { loader: 'vue-loader', options: spin.createConfig(builder, 'vue', {}) }
19 | }
20 | ]
21 | },
22 | resolve: {
23 | alias: {
24 | vue$: 'vue/dist/vue.esm.js'
25 | }
26 | },
27 | plugins: [new VueLoaderPlugin()]
28 | });
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/plugins/FlowRuntimePlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 | import JSRuleFinder from './shared/JSRuleFinder';
5 |
6 | export default class FlowRuntimePLugin implements ConfigPlugin {
7 | public configure(builder: Builder, spin: Spin) {
8 | const stack = builder.stack;
9 |
10 | if (stack.hasAll(['flow-runtime', 'webpack']) && !stack.hasAny('dll')) {
11 | const jsRuleFinder = new JSRuleFinder(builder);
12 | const jsRule = jsRuleFinder.findAndCreateJSRule();
13 | if (jsRule && !jsRule.use.options.babelrc) {
14 | jsRule.use = spin.merge(jsRule.use, {
15 | options: {
16 | plugins: [
17 | [
18 | 'babel-plugin-flow-runtime',
19 | {
20 | assert: true,
21 | annotate: true
22 | }
23 | ]
24 | ]
25 | }
26 | });
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/plugins/react-native/polyfills/react-native-polyfill-16.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-var-requires no-implicit-dependencies
2 | require('react-native/Libraries/polyfills/Object.es6.js');
3 | require('react-native/Libraries/polyfills/console.js');
4 | require('react-native/Libraries/polyfills/error-guard.js');
5 | require('react-native/Libraries/polyfills/Number.es6.js');
6 | require('react-native/Libraries/polyfills/String.prototype.es6.js');
7 | require('react-native/Libraries/polyfills/Array.prototype.es6.js');
8 | require('react-native/Libraries/polyfills/Array.es6.js');
9 | require('react-native/Libraries/polyfills/Object.es7.js');
10 | require('react-native/Libraries/polyfills/babelHelpers.js');
11 |
12 | declare var __DEV__;
13 |
14 | (global as any).__DEV__ = __DEV__;
15 | (global as any).__BUNDLE_START_TIME__ = (global as any).nativePerformanceNow
16 | ? (global as any).nativePerformanceNow()
17 | : Date.now();
18 |
19 | if (!(global as any).self) {
20 | (global as any).self = global;
21 | }
22 | require('react-native/Libraries/Core/InitializeCore.js');
23 |
--------------------------------------------------------------------------------
/src/plugins/react-native/polyfills/react-native-polyfill-15.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-var-requires no-implicit-dependencies
2 | require('react-native/packager/src/Resolver/polyfills/polyfills.js');
3 | require('react-native/packager/src/Resolver/polyfills/console.js');
4 | require('react-native/packager/src/Resolver/polyfills/error-guard.js');
5 | require('react-native/packager/src/Resolver/polyfills/Number.es6.js');
6 | require('react-native/packager/src/Resolver/polyfills/String.prototype.es6.js');
7 | require('react-native/packager/src/Resolver/polyfills/Array.prototype.es6.js');
8 | require('react-native/packager/src/Resolver/polyfills/Array.es6.js');
9 | require('react-native/packager/src/Resolver/polyfills/Object.es7.js');
10 | require('react-native/packager/src/Resolver/polyfills/babelHelpers.js');
11 |
12 | declare var __DEV__;
13 |
14 | (global as any).__DEV__ = __DEV__;
15 | (global as any).__BUNDLE_START_TIME__ = Date.now();
16 |
17 | if (!(global as any).self) {
18 | (global as any).self = global;
19 | }
20 | require('react-native/Libraries/Core/InitializeCore.js');
21 |
--------------------------------------------------------------------------------
/src/plugins/ReactNativeWebPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 |
5 | export default class ReactNativeWebPlugin implements ConfigPlugin {
6 | public configure(builder: Builder, spin: Spin) {
7 | const stack = builder.stack;
8 |
9 | if (stack.hasAll(['react-native-web', 'webpack']) && stack.hasAny(['server', 'web'])) {
10 | builder.config = spin.merge(builder.config, {
11 | resolve: {
12 | alias: {
13 | 'react-native': 'react-native-web'
14 | }
15 | }
16 | });
17 |
18 | if (stack.hasAny('server')) {
19 | const originalExternals = builder.config.externals;
20 | builder.config.externals = (context, request, callback) => {
21 | if (request.indexOf('react-native') >= 0) {
22 | return callback(null, 'commonjs ' + request + '-web');
23 | } else {
24 | return originalExternals(context, request, callback);
25 | }
26 | };
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/createConfig.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 |
3 | import createBuilders from './createBuilders';
4 |
5 | interface CreateConfigOptions {
6 | cwd?: string;
7 | cmd?: string;
8 | builderName?: string;
9 | builderOverrides?: any;
10 | genConfigOverrides?: any;
11 | }
12 |
13 | export default (options?: CreateConfigOptions): any => {
14 | const defaultOptions: CreateConfigOptions = {};
15 | const { cwd, cmd, builderName, builderOverrides, genConfigOverrides } = options || defaultOptions;
16 | const dir = path.resolve(cwd || '.');
17 | const { builders } = createBuilders({
18 | cwd: dir,
19 | cmd: cmd || 'watch',
20 | argv: {},
21 | builderOverrides,
22 | genConfigOverrides
23 | });
24 | let builder;
25 | if (builderName) {
26 | builder = builders[builder];
27 | } else {
28 | const builderNames = Object.keys(builders || {}).filter(name => !builders[name].parent);
29 | if (builderNames.length > 1) {
30 | throw new Error('Too many matching builders declared you must pick the right one');
31 | } else {
32 | builder = builders[builderNames[0]];
33 | }
34 | }
35 | return builder.config;
36 | };
37 |
--------------------------------------------------------------------------------
/src/plugins/StyledComponentsPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 | import JSRuleFinder from './shared/JSRuleFinder';
5 |
6 | export default class StyledComponentsPlugin implements ConfigPlugin {
7 | public configure(builder: Builder, spin: Spin) {
8 | const stack = builder.stack;
9 |
10 | if (
11 | stack.hasAll(['styled-components', 'webpack']) &&
12 | (stack.hasAny('web') || (stack.hasAny('server') && builder.ssr))
13 | ) {
14 | const jsRuleFinder = new JSRuleFinder(builder);
15 | const jsRule = jsRuleFinder.findJSRule();
16 | if (jsRule && jsRule.use) {
17 | for (let idx = 0; idx < jsRule.use.length; idx++) {
18 | const rule = jsRule.use[idx];
19 | if (rule.loader.indexOf('babel') >= 0 && !rule.options.babelrc) {
20 | jsRule.use[idx] = spin.merge(jsRule.use[idx], {
21 | options: {
22 | plugins: [['babel-plugin-styled-components', { ssr: builder.ssr }]]
23 | }
24 | });
25 | }
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2017 SysGears INC
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 |
--------------------------------------------------------------------------------
/src/plugins/ReactHotLoaderPlugin.ts:
--------------------------------------------------------------------------------
1 | import { Builder } from '../Builder';
2 | import { ConfigPlugin } from '../ConfigPlugin';
3 | import Spin from '../Spin';
4 | import JSRuleFinder from './shared/JSRuleFinder';
5 |
6 | export default class ReactHotLoaderPlugin implements ConfigPlugin {
7 | public configure(builder: Builder, spin: Spin) {
8 | const stack = builder.stack;
9 |
10 | if (stack.hasAll(['react-hot-loader', 'webpack']) && spin.dev && !spin.test && !stack.hasAny('dll')) {
11 | builder.config = spin.mergeWithStrategy(
12 | {
13 | entry: 'prepend'
14 | },
15 | builder.config,
16 | {
17 | entry: {
18 | index: ['react-hot-loader/patch']
19 | }
20 | }
21 | );
22 | const jsRuleFinder = new JSRuleFinder(builder);
23 | const jsRule = jsRuleFinder.findAndCreateJSRule();
24 | const isBabelUsed = jsRule.use.loader && jsRule.use.loader.indexOf('babel') >= 0;
25 | jsRule.use = spin.merge(jsRule.use, {
26 | options: {
27 | plugins: [isBabelUsed ? 'react-hot-loader/babel' : 'react-hot-loader/webpack']
28 | }
29 | });
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/plugins/shared/parallelLoader.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'os';
2 | import * as path from 'path';
3 |
4 | import { Builder } from '../../Builder';
5 | import Spin from '../../Spin';
6 |
7 | export const hasParallelLoalder = (builder: Builder) => {
8 | return !!builder.require.probe('thread-loader');
9 | };
10 |
11 | export const addParalleLoaders = (builder: Builder, spin: Spin, compilerRules) => {
12 | const cacheLoader = builder.require.probe('cache-loader');
13 | const threadLoader = builder.require.probe('thread-loader');
14 | const result = compilerRules.slice(0);
15 | if (threadLoader) {
16 | result.unshift({
17 | loader: 'thread-loader',
18 | options: spin.createConfig(builder, 'threadLoader', {
19 | workers: os.cpus().length - 1
20 | })
21 | });
22 | }
23 | if (cacheLoader && !!builder.cache) {
24 | result.unshift({
25 | loader: 'cache-loader',
26 | options: spin.createConfig(builder, 'cacheLoader', {
27 | cacheDirectory: path.join(
28 | typeof builder.cache === 'string' && builder.cache !== 'auto' ? builder.cache : '.cache',
29 | 'cache-loader'
30 | )
31 | })
32 | });
33 | }
34 | return result;
35 | };
36 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "spin.js",
3 | "projectOwner": "sysgears",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": true,
9 | "contributors": [
10 | {
11 | "login": "vlasenko",
12 | "name": "Victor Vlasenko",
13 | "avatar_url": "https://avatars1.githubusercontent.com/u/1259926?v=3",
14 | "profile": "https://ua.linkedin.com/in/victorvlasenko",
15 | "contributions": [
16 | "code",
17 | "tool",
18 | "doc",
19 | "test",
20 | "question",
21 | "review"
22 | ]
23 | },
24 | {
25 | "login": "mairh",
26 | "name": "Ujjwal",
27 | "avatar_url": "https://avatars0.githubusercontent.com/u/4072250?v=3",
28 | "profile": "https://github.com/mairh",
29 | "contributions": [
30 | "code",
31 | "tool",
32 | "doc",
33 | "test",
34 | "question",
35 | "review"
36 | ]
37 | },
38 | {
39 | "login": "cdmbase",
40 | "name": "cdmbase",
41 | "avatar_url": "https://avatars1.githubusercontent.com/u/20957416?v=4",
42 | "profile": "https://github.com/cdmbase",
43 | "contributions": [
44 | "code"
45 | ]
46 | }
47 | ]
48 | }
49 |
--------------------------------------------------------------------------------
/src/createRequire.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as requireRelative from 'require-relative';
3 |
4 | export interface RequireFunction {
5 | cwd: string;
6 |
7 | (name, relativeTo?): any;
8 | resolve(name, relativeTo?): string;
9 | probe(name, relativeTo?): string;
10 | }
11 |
12 | export default (cwd: string): RequireFunction => {
13 | const require: any = (name, relativeTo?): any => requireModule(name, relativeTo || cwd);
14 | require.resolve = (name, relativeTo?): string => requireModule.resolve(name, relativeTo || cwd);
15 | require.probe = (name, relativeTo?): string => requireModule.probe(name, relativeTo || cwd);
16 | require.cwd = cwd;
17 | return require;
18 | };
19 |
20 | const requireModule: any = (name, relativeTo): any => {
21 | return name.indexOf('.') !== 0 ? requireRelative(name, relativeTo) : require(path.join(relativeTo, name));
22 | };
23 |
24 | requireModule.resolve = (name, relativeTo): string => {
25 | return name.indexOf('.') !== 0
26 | ? requireRelative.resolve(name, relativeTo)
27 | : require.resolve(path.join(relativeTo, name));
28 | };
29 |
30 | requireModule.probe = (name, relativeTo): string => {
31 | try {
32 | return requireModule.resolve(name, relativeTo);
33 | } catch (e) {
34 | return null;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/plugins/react-native/liveReloadMiddleware.ts:
--------------------------------------------------------------------------------
1 | import { hookSync } from '../../webpackHooks';
2 |
3 | function notifyWatcher(watcher) {
4 | const headers = {
5 | 'Content-Type': 'application/json; charset=UTF-8'
6 | };
7 |
8 | watcher.res.writeHead(205, headers);
9 | watcher.res.end(JSON.stringify({ changed: true }));
10 | }
11 |
12 | export default function liveReloadMiddleware(compiler) {
13 | let watchers = [];
14 | let notify = false;
15 |
16 | hookSync(compiler, 'done', () => {
17 | watchers.forEach(watcher => {
18 | notifyWatcher(watcher);
19 | });
20 | if (!watchers.length) {
21 | notify = true;
22 | }
23 |
24 | watchers = [];
25 | });
26 |
27 | return (req, res, next) => {
28 | if (req.path === '/onchange') {
29 | const watcher = { req, res };
30 |
31 | if (notify) {
32 | notifyWatcher(watcher);
33 | notify = false;
34 | } else {
35 | watchers.push(watcher);
36 |
37 | req.on('close', () => {
38 | for (let i = 0; i < watchers.length; i++) {
39 | if (watchers[i] && watchers[i].req === req) {
40 | watchers.splice(i, 1);
41 | break;
42 | }
43 | }
44 | });
45 | }
46 | } else {
47 | next();
48 | }
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/Spin.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable-next-line
2 | import { Configuration } from 'webpack';
3 | import * as merge from 'webpack-merge';
4 |
5 | import { Builder } from './Builder';
6 |
7 | export default class Spin {
8 | public dev: boolean;
9 | public test: boolean;
10 | public watch: boolean;
11 | public cmd: string;
12 | public cwd: string;
13 | public options: any;
14 |
15 | constructor(cwd, cmd) {
16 | this.cmd = cmd;
17 | this.cwd = cwd;
18 | this.dev = ['watch', 'start', 'test'].indexOf(this.cmd) >= 0;
19 | this.test = this.cmd === 'test';
20 | this.watch = ['watch', 'start'].indexOf(this.cmd) >= 0;
21 | }
22 |
23 | public createConfig(builder: Builder, tool: string, config): Configuration {
24 | const { merge: mergeStrategy, ...configOverrides } = builder[tool + 'Config'] || { merge: {} };
25 | return this.mergeWithStrategy(mergeStrategy, config, configOverrides);
26 | }
27 |
28 | public merge(config: any, overrides: any): Configuration {
29 | return merge.smart(config, overrides);
30 | }
31 |
32 | public mergeWithInStrategy(config: any, overrides: any): any {
33 | const { merge: mergeStrategy, ...configOverrides } = overrides || { merge: {} };
34 | return this.mergeWithStrategy(mergeStrategy, config, configOverrides);
35 | }
36 |
37 | public mergeWithStrategy(strategy: any, config: any, overrides: any): Configuration {
38 | return merge.smartStrategy(strategy)(config, overrides);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Stack.ts:
--------------------------------------------------------------------------------
1 | export default class Stack {
2 | public technologies: string[];
3 | public platform: string;
4 |
5 | constructor(name: string, ...stack: string[]) {
6 | this.technologies = stack
7 | .reduce((acc, tech) => {
8 | if (!tech) {
9 | return acc;
10 | } else if (tech.constructor === Array) {
11 | return acc.concat(tech);
12 | } else {
13 | return acc.concat(tech.split(':'));
14 | }
15 | }, [])
16 | .filter((v, i, a) => a.indexOf(v) === i);
17 | if (this.hasAny('server')) {
18 | this.platform = 'server';
19 | } else if (this.hasAny('web')) {
20 | this.platform = 'web';
21 | } else if (this.hasAny('android')) {
22 | this.platform = 'android';
23 | } else if (this.hasAny('ios')) {
24 | this.platform = 'ios';
25 | } else {
26 | throw new Error(
27 | `stack should include one of 'server', 'web', 'android', 'ios', stack: ${this.technologies} for builder ${name}`
28 | );
29 | }
30 | }
31 |
32 | public hasAny(technologies): boolean {
33 | const array = technologies.constructor === Array ? technologies : [technologies];
34 | for (const feature of array) {
35 | if (this.technologies.indexOf(feature) >= 0) {
36 | return true;
37 | }
38 | }
39 | return false;
40 | }
41 |
42 | public hasAll(technologies): boolean {
43 | const array = technologies.constructor === Array ? technologies : [technologies];
44 | for (const feature of array) {
45 | if (this.technologies.indexOf(feature) < 0) {
46 | return false;
47 | }
48 | }
49 | return true;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/BuilderDiscoverer.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import { glob } from 'glob';
3 | import * as _ from 'lodash';
4 | import * as path from 'path';
5 |
6 | import { Builders } from './Builder';
7 | import { ConfigPlugin } from './ConfigPlugin';
8 | import ConfigReader from './ConfigReader';
9 | import Spin from './Spin';
10 |
11 | export default class BuilderDiscoverer {
12 | private configReader: ConfigReader;
13 | private cwd: string;
14 | private argv: any;
15 |
16 | constructor(spin: Spin, plugins: ConfigPlugin[], argv: any) {
17 | this.configReader = new ConfigReader(spin, plugins);
18 | this.cwd = spin.cwd;
19 | this.argv = argv;
20 | }
21 |
22 | public discover(builderOverrides: any): Builders {
23 | const packageRootPaths = this._detectRootPaths();
24 | return packageRootPaths.reduce((res: any, pathName: string) => {
25 | return { ...res, ...this._discoverRecursively(pathName, builderOverrides) };
26 | }, {});
27 | }
28 |
29 | private _discoverRecursively(dir: string, builderOverrides: any): Builders {
30 | if (path.basename(dir) === '.expo') {
31 | return undefined;
32 | }
33 |
34 | let builders = this.configReader.readConfig({
35 | filePath: this.argv.c ? path.join(dir, this.argv.c) : dir,
36 | builderOverrides
37 | });
38 |
39 | const files = fs.readdirSync(dir);
40 | for (const name of files) {
41 | const dirPath = path.join(dir, name);
42 | if (name !== 'node_modules' && fs.statSync(dirPath).isDirectory()) {
43 | builders = { ...builders, ...this._discoverRecursively(dirPath, builderOverrides) };
44 | }
45 | }
46 |
47 | return builders;
48 | }
49 |
50 | private _detectRootPaths(): string[] {
51 | const rootConfig = JSON.parse(fs.readFileSync(`${this.cwd}/package.json`, 'utf8'));
52 | return rootConfig.workspaces && rootConfig.workspaces.length
53 | ? _.flatten(rootConfig.workspaces.map((ws: string) => glob.sync(ws))).map((ws: string) => path.join(this.cwd, ws))
54 | : [this.cwd];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/scripts.md:
--------------------------------------------------------------------------------
1 | # SpinJS Scripts
2 |
3 | SpinJS provides four commands and a few [options](#running-spinjs-commands-with-options) you can run the commands with.
4 |
5 | ## `spin watch`
6 |
7 | Runs the project in development mode with live code reload for all the platforms:
8 |
9 | ```bash
10 | spin watch
11 | ```
12 |
13 | ## `spin build`
14 |
15 | Builds your project for production: the build code is minified.
16 |
17 | Note that the project won't run, SpinJS will only generate and minify the build, and also save the generated files under
18 | the `build` directory. The `build` directory is automatically created under the **current working directory**. To change
19 | the directory where SpinJS will store the built production code, consult the [configuration] guide.
20 |
21 | ```bash
22 | spin build
23 | ```
24 |
25 | ## `spin start`
26 |
27 | Builds your project for production and runs the project in the browser. The code is minified.
28 |
29 | ```bash
30 | spin start
31 | ```
32 |
33 | ## `spin test`
34 |
35 | Runs all the tests using `mocha-webpack`.
36 |
37 | ```bash
38 | spin test
39 | ```
40 |
41 | ## Running SpinJS commands with options
42 |
43 | You can run SpinJS commands using the following options:
44 |
45 | * `-n`, shows the list of builders
46 | * `-d`, disables builders
47 | * `-e`, enables builders
48 | * `-v`, shows generated configurations in the console
49 |
50 | You can specify the option after the SpinJS command:
51 |
52 | ```bash
53 | spin