├── .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