diff --git a/.gitignore b/.gitignore index ab939bf69..961c34e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ es lib stats.html build +dist index.d.ts diff --git a/packages/playgrood/bin/index.js b/packages/playgrodd/bin/index.js similarity index 91% rename from packages/playgrood/bin/index.js rename to packages/playgrodd/bin/index.js index faa8331cf..38b5b24da 100755 --- a/packages/playgrood/bin/index.js +++ b/packages/playgrodd/bin/index.js @@ -1,7 +1,7 @@ #!/usr/bin/env node const yargs = require('yargs') -const { server } = require('../build/main/server') +const { server } = require('../dist/main/server') yargs .command( diff --git a/packages/playgrood/package.json b/packages/playgrodd/package.json similarity index 59% rename from packages/playgrood/package.json rename to packages/playgrodd/package.json index e3083b8b7..1f5e343d8 100644 --- a/packages/playgrood/package.json +++ b/packages/playgrodd/package.json @@ -2,9 +2,9 @@ "name": "playgrodd", "description": "Blazing fast and zero config React components playground", "version": "0.0.1", - "main": "./build/main/index.jsx", - "typings": "./build/main/index.d.ts", - "module": "./build/module/index.jsx", + "main": "./dist/main/index.jsx", + "typings": "./dist/main/index.d.ts", + "module": "./dist/module/index.jsx", "bin": { "playgrodd": "./bin/index.js" }, @@ -16,29 +16,45 @@ "fix:prettier": "prettier \"src/**/*.{ts,tsx}\" --write", "fix:tslint": "tslint --fix --project .", "watch": "run-s clean build:main && run-p \"build:main -- -w\"", - "clean": "trash build" + "clean": "trash dist" }, "dependencies": { + "@babel/core": "7.0.0-beta.42", + "@babel/preset-env": "^7.0.0-beta.42", + "@babel/preset-react": "^7.0.0-beta.42", + "babel-loader": "^8.0.0-beta", "babel-polyfill": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", "babylon": "^6.18.0", - "create-react-context": "^0.2.1", + "connect-history-api-fallback": "^1.5.0", "express": "^4.16.3", "fast-glob": "^2.2.0", + "find-up": "^2.1.0", "history": "^4.7.2", + "html-webpack-plugin": "^3.0.7", + "invariant": "^2.2.4", "mkdirp": "^0.5.1", - "parcel-bundler": "^1.6.2", - "parcel-plugin-bundle-manifest": "^0.1.0", "prop-types": "^15.6.1", "react": "^16.2.0", "react-dom": "^16.2.0", "react-router-dom": "^4.2.2", "trash": "^4.3.0", + "unstated": "^1.1.0", "uuid": "^3.2.1", + "webpack": "^4.1.1", + "webpack-dev-server": "^3.1.1", + "webpack-hot-middleware": "^2.21.2", "yargs": "^11.0.0" }, "devDependencies": { + "@types/babel-traverse": "^6.25.3", "@types/babylon": "^6.16.2", + "@types/connect-history-api-fallback": "^1.3.1", "@types/express": "^4.11.1", + "@types/find-up": "^2.1.1", + "@types/html-webpack-plugin": "^2.30.3", + "@types/invariant": "^2.2.29", "@types/mkdirp": "^0.5.2", "@types/node": "^9.4.7", "@types/react": "^16.0.40", @@ -46,6 +62,9 @@ "@types/react-router-dom": "^4.2.5", "@types/trash": "^4.3.0", "@types/uuid": "^3.4.3", + "@types/webpack": "^4.1.1", + "@types/webpack-dev-middleware": "^2.0.1", + "@types/webpack-hot-middleware": "^2.16.3", "@types/yargs": "^11.0.0", "npm-run-all": "^4.1.2", "prettier": "^1.11.1", diff --git a/packages/playgrodd/src/compiler/config.ts b/packages/playgrodd/src/compiler/config.ts new file mode 100644 index 000000000..3b192eb4b --- /dev/null +++ b/packages/playgrodd/src/compiler/config.ts @@ -0,0 +1,71 @@ +import * as fs from 'fs' +import * as path from 'path' +import findup from 'find-up' +import webpack, { Loader, Configuration } from 'webpack' +import HtmlWebpackPlugin from 'html-webpack-plugin' + +import { IComponentMap } from '../utils/components' +import * as paths from './paths' + +export { config as devServerConfig } from './dev-server' + +const babelLoader = (babelrc: string | null): Loader => ({ + loader: require.resolve('babel-loader'), + options: babelrc + ? JSON.parse(babelrc) + : { + babelrc: false, + cacheDirectory: true, + presets: [ + require.resolve('@babel/preset-env'), + require.resolve('@babel/preset-react'), + ], + }, +}) + +export const config = async ( + components: IComponentMap +): Promise => { + const babelrcPath = await findup('.babelrc') + const babelrc = babelrcPath ? fs.readFileSync(babelrcPath, 'utf-8') : null + + return { + mode: 'development', + context: paths.ROOT, + entry: [ + ...Object.values(components).map(({ filepath: f }) => f), + paths.INDEX_JS, + ], + output: { + pathinfo: true, + path: paths.DIST, + publicPath: '/', + filename: 'static/js/[name].js', + sourceMapFilename: 'static/js/[name].js.map', + crossOriginLoading: 'anonymous', + devtoolModuleFilenameTemplate: (info: any) => + path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), + }, + module: { + rules: [ + { + test: /\.(js|jsx)$/, + exclude: /node_modules/, + include: [paths.ROOT], + use: babelLoader(babelrc), + }, + ], + }, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + }, + plugins: [ + new webpack.DefinePlugin({ + __PLAYGRODD_COMPONENTS__: JSON.stringify(components), + }), + new HtmlWebpackPlugin({ + template: paths.INDEX_HTML, + }), + ], + } +} diff --git a/packages/playgrodd/src/compiler/dev-server.ts b/packages/playgrodd/src/compiler/dev-server.ts new file mode 100644 index 000000000..82fd683f5 --- /dev/null +++ b/packages/playgrodd/src/compiler/dev-server.ts @@ -0,0 +1,26 @@ +import * as paths from './paths' + +const protocol = process.env.HTTPS === 'true' ? 'https' : 'http' +const host = process.env.HOST || '0.0.0.0' + +export const config = (compiler: any) => ({ + compress: true, + clientLogLevel: 'none', + contentBase: paths.DIST, + watchContentBase: true, + publicPath: '/', + hot: true, + quiet: true, + noInfo: true, + https: protocol === 'https', + host: host, + overlay: false, + watchOptions: { + ignored: /node_modules/, + }, + stats: { + colors: true, + chunks: false, + chunkModules: false, + }, +}) diff --git a/packages/playgrodd/src/compiler/generate-files.tsx b/packages/playgrodd/src/compiler/generate-files.tsx new file mode 100644 index 000000000..0a0c23bdb --- /dev/null +++ b/packages/playgrodd/src/compiler/generate-files.tsx @@ -0,0 +1,27 @@ +import * as React from 'react' +import { renderToString } from 'react-dom/server' + +const Html = () => ( + + + Playgrodd + +
+ + + +) + +export const generateHtml = () => renderToString() + +export const generateJs = () => + `import 'babel-polyfill' + + import * as React from 'react' + import { render } from 'react-dom' + import { App } from 'playgrodd-theme-default' + + render( + , + document.querySelector('#root') + )` diff --git a/packages/playgrodd/src/compiler/index.ts b/packages/playgrodd/src/compiler/index.ts new file mode 100644 index 000000000..8233ef66d --- /dev/null +++ b/packages/playgrodd/src/compiler/index.ts @@ -0,0 +1,36 @@ +import fs from 'fs' +import mkdir from 'mkdirp' +import trash from 'trash' +import webpack from 'webpack' + +import * as paths from './paths' +import { config } from './config' +import { IComponentMap } from '../utils/components' +import { generateHtml, generateJs } from './generate-files' + +export { config as devServerConfig } from './dev-server' + +const checkMkdirTheme = (): void => { + try { + fs.lstatSync(paths.THEME) + } catch (err) { + mkdir.sync(paths.THEME) + } +} + +const tempFile = (filepath: string, content: string) => { + checkMkdirTheme() + fs.writeFileSync(filepath, content, 'utf-8') +} + +export const createCompiler = async (components: IComponentMap) => { + const js = generateJs() + const html = generateHtml() + const webpackConfig = await config(components) + + await trash(paths.THEME) + tempFile(paths.INDEX_JS, js) + tempFile(paths.INDEX_HTML, html) + + return webpack(webpackConfig) +} diff --git a/packages/playgrodd/src/compiler/paths.ts b/packages/playgrodd/src/compiler/paths.ts new file mode 100644 index 000000000..aceb35d16 --- /dev/null +++ b/packages/playgrodd/src/compiler/paths.ts @@ -0,0 +1,10 @@ +import * as fs from 'fs' +import * as path from 'path' + +export const ROOT = fs.realpathSync(process.cwd()) +export const PLAYGRODD = path.join(ROOT, '.playgrodd') +export const THEME = path.join(PLAYGRODD, 'theme') +export const INDEX_JS = path.join(THEME, 'index.jsx') +export const INDEX_HTML = path.join(THEME, 'index.html') + +export const DIST = path.join(PLAYGRODD, 'dist') diff --git a/packages/playgrodd/src/components/Html.tsx b/packages/playgrodd/src/components/Html.tsx new file mode 100644 index 000000000..8734223ee --- /dev/null +++ b/packages/playgrodd/src/components/Html.tsx @@ -0,0 +1,12 @@ +import * as React from 'react' + +export const Html = () => ( + + + Playgrodd + +
+ + + +) diff --git a/packages/playgrodd/src/components/Playgrodd.tsx b/packages/playgrodd/src/components/Playgrodd.tsx new file mode 100644 index 000000000..c75f12bd2 --- /dev/null +++ b/packages/playgrodd/src/components/Playgrodd.tsx @@ -0,0 +1,14 @@ +import * as React from 'react' +import { Router } from 'react-router-dom' +import { createBrowserHistory, History } from 'history' +import { Provider } from 'unstated' + +import { container } from '../documents/container' + +export const history: History = createBrowserHistory() + +export const Playgrodd: React.SFC = ({ children }) => ( + + {children} + +) diff --git a/packages/playgrodd/src/components/Preview.tsx b/packages/playgrodd/src/components/Preview.tsx new file mode 100644 index 000000000..823b3156f --- /dev/null +++ b/packages/playgrodd/src/components/Preview.tsx @@ -0,0 +1,35 @@ +import * as React from 'react' +import { Subscribe } from 'unstated' +import { Route } from 'react-router-dom' + +import { Doc } from '../documents' +import { IComponent } from '../utils/components' +import { DocumentsContainer } from '../documents/container' + +const components = __PLAYGRODD_COMPONENTS__ +const loadDocument = (doc: Doc) => { + const { route }: IComponent = components[doc.getName()] + const sections = doc.getSections() + + return ( + + sections.map(({ id, title, render: Component }) => ( + + {title &&

{title}

} + +
+ )) + } + /> + ) +} + +export const Preview: React.SFC = () => ( + + {({ state }) => state.documents.map(loadDocument)} + +) diff --git a/packages/playgrodd/src/documents/container.ts b/packages/playgrodd/src/documents/container.ts new file mode 100644 index 000000000..30d3f2412 --- /dev/null +++ b/packages/playgrodd/src/documents/container.ts @@ -0,0 +1,21 @@ +import { Container } from 'unstated' + +import { Doc } from './' + +export interface DocumentState { + documents: Doc[] +} + +export class DocumentsContainer extends Container { + state = { + documents: [], + } + + public add(document: Doc) { + this.setState({ + documents: [document, ...this.state.documents], + }) + } +} + +export const container = new DocumentsContainer() diff --git a/packages/playgrodd/src/documents/index.ts b/packages/playgrodd/src/documents/index.ts new file mode 100644 index 000000000..1cd21aedf --- /dev/null +++ b/packages/playgrodd/src/documents/index.ts @@ -0,0 +1,79 @@ +import invariant from 'invariant' +import { v4 } from 'uuid' + +export { Preview } from '../components/Preview' +export { Playgrodd } from '../components/Playgrodd' + +import { container } from './container' + +const isFn = (value: any): boolean => typeof value === 'function' + +export interface IRenderMethod { + (): JSX.Element +} + +export interface ISection { + id: string + render: IRenderMethod + title?: string +} + +export class Doc { + private _name: string + private _description: string | null + private _sections: ISection[] + + constructor(name: string) { + this._name = name + this._sections = [] + this._description = null + + container.add(this) + return this + } + + // setters + + description(value: string) { + this._description = value + return this + } + + section(...args: any[]) { + const [title, renderMethod] = args + const render: IRenderMethod = isFn(title) ? title : renderMethod + + invariant( + !isFn(title) || !isFn(renderMethod), + 'You need to set a function that will be render your sectoin' + ) + + this._sections.push({ + render, + id: v4(), + ...(title && !isFn(title) && { title }), + }) + + return this + } + + // getters + + public getName(): string { + return this._name + } + + public getDescription(): string | null { + return this._description + } + + public getSections(): ISection[] { + return this._sections + } +} + +export interface IDoc { + (name: string): Doc +} + +export const doc: IDoc = name => new Doc(name) diff --git a/packages/playgrood/src/index.ts b/packages/playgrodd/src/index.ts similarity index 74% rename from packages/playgrood/src/index.ts rename to packages/playgrodd/src/index.ts index a5d6485fc..a821392d9 100644 --- a/packages/playgrood/src/index.ts +++ b/packages/playgrodd/src/index.ts @@ -1,2 +1,3 @@ export { Preview } from './components/Preview' export { Playgrodd } from './components/Playgrodd' +export { doc } from './documents' diff --git a/packages/playgrodd/src/server.ts b/packages/playgrodd/src/server.ts new file mode 100644 index 000000000..a566d3b2f --- /dev/null +++ b/packages/playgrodd/src/server.ts @@ -0,0 +1,22 @@ +import express from 'express' +import { Arguments } from 'yargs' +import devServerMiddleware from 'webpack-dev-middleware' +import historyApiFallback from 'connect-history-api-fallback' +import hotMiddleware from 'webpack-hot-middleware' + +import { componentsFromPattern } from './utils/components' +import { createCompiler, devServerConfig } from './compiler' + +exports.server = async ({ files: pattern }: Arguments) => { + const app = express() + const components = await componentsFromPattern(pattern) + const compiler = await createCompiler(components) + + app.use(historyApiFallback()) + app.use(hotMiddleware(compiler, { log: false, heartbeat: 2000 })) + app.use(devServerMiddleware(compiler, devServerConfig(compiler))) + + app.listen(3000, () => { + console.log('Example app listening on port 3000!') + }) +} diff --git a/packages/playgrodd/src/types.d.ts b/packages/playgrodd/src/types.d.ts new file mode 100644 index 000000000..afb43f65a --- /dev/null +++ b/packages/playgrodd/src/types.d.ts @@ -0,0 +1 @@ +declare const __PLAYGRODD_COMPONENTS__: any diff --git a/packages/playgrodd/src/utils/components.ts b/packages/playgrodd/src/utils/components.ts new file mode 100644 index 000000000..a125e2bea --- /dev/null +++ b/packages/playgrodd/src/utils/components.ts @@ -0,0 +1,75 @@ +import fs from 'fs' +import path from 'path' +import glob from 'fast-glob' +import { parse } from 'babylon' +import { NodePath } from 'babel-traverse' +import * as t from 'babel-types' + +import { traverseAndAssign } from './traverse' + +export interface IComponent { + readonly id: string + readonly route: string + readonly filepath: string +} + +export interface IComponentMap { + readonly [key: string]: IComponent +} + +const ROOT_PATH = fs.realpathSync(process.cwd()) + +const convertToAst = (entry: string): t.File => + parse(fs.readFileSync(entry, 'utf-8'), { + sourceType: 'module', + plugins: ['jsx'], + }) + +const hasPlaygroddImported = (path: NodePath): boolean => + path.isImportDeclaration() && + path.node && + path.node.source && + path.node.source.value === 'playgrodd' + +const hasDocFn = (path: NodePath): boolean => + path.node.specifiers && + path.node.specifiers.some( + (node: NodePath) => + t.isImportSpecifier(node) && node.imported.name === 'doc' + ) + +const checkIfImportPlaygrodd = traverseAndAssign, boolean>( + path => hasPlaygroddImported(path) && hasDocFn(path), + path => true +) + +const isPlaygroddFile = (entry: string) => + checkIfImportPlaygrodd(convertToAst(entry)) + +const getNameFromDoc = traverseAndAssign( + path => path.isCallExpression() && path.node.callee.name === 'doc', + path => path.node.arguments[0].value +) + +const reduceByName = (obj: any, entry: string): IComponentMap => { + const ast = convertToAst(entry) + const name = getNameFromDoc(ast) + const route = path.join('/', path.parse(entry).dir, name || '') + const filepath = path.join(ROOT_PATH, entry) + + return Object.assign({}, obj, { + [`${name}`]: { + filepath, + route, + }, + }) +} + +export const componentsFromPattern = (pattern: string): IComponentMap => { + const ignoreGlob = '!node_modules' + const entries: string[] = glob.sync( + Array.isArray(pattern) ? [...pattern, ignoreGlob] : [pattern, ignoreGlob] + ) + + return entries.filter(isPlaygroddFile).reduce(reduceByName, {}) +} diff --git a/packages/playgrodd/src/utils/traverse.ts b/packages/playgrodd/src/utils/traverse.ts new file mode 100644 index 000000000..0e86c3d98 --- /dev/null +++ b/packages/playgrodd/src/utils/traverse.ts @@ -0,0 +1,30 @@ +import traverse from 'babel-traverse' +import * as t from 'babel-types' + +export interface IWhenFn

{ + (path: P): boolean +} + +export interface IAssignFn { + (path: P): V +} + +export function traverseAndAssign

( + when: IWhenFn

, + assign: IAssignFn +) { + return (ast: t.File): V | undefined => { + let value + + traverse(ast, { + enter(path: any) { + if (when(path)) { + value = assign(path) + return + } + }, + }) + + return value + } +} diff --git a/packages/playgrood/tsconfig.json b/packages/playgrodd/tsconfig.json similarity index 98% rename from packages/playgrood/tsconfig.json rename to packages/playgrodd/tsconfig.json index c7b53a61b..ec3e6b47c 100644 --- a/packages/playgrood/tsconfig.json +++ b/packages/playgrodd/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2017", - "outDir": "build/main", + "outDir": "dist/main", "rootDir": "src", "moduleResolution": "node", "module": "commonjs", diff --git a/packages/playgrood/tsconfig.module.json b/packages/playgrodd/tsconfig.module.json similarity index 83% rename from packages/playgrood/tsconfig.module.json rename to packages/playgrodd/tsconfig.module.json index dfb74fa3a..cf32f6278 100644 --- a/packages/playgrood/tsconfig.module.json +++ b/packages/playgrodd/tsconfig.module.json @@ -2,7 +2,7 @@ "extends": "./tsconfig", "compilerOptions": { "target": "esnext", - "outDir": "build/module", + "outDir": "dist/module", "module": "esnext" }, "exclude": [ diff --git a/packages/playgrood/tslint.json b/packages/playgrodd/tslint.json similarity index 100% rename from packages/playgrood/tslint.json rename to packages/playgrodd/tslint.json diff --git a/packages/playgrood/src/components/AsyncRoute.tsx b/packages/playgrood/src/components/AsyncRoute.tsx deleted file mode 100644 index 7f35afb6c..000000000 --- a/packages/playgrood/src/components/AsyncRoute.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { Route } from 'react-router-dom' -import { IComponent } from '../utils/components' - -export interface IAsyncRouteProps { - component: IComponent -} - -export interface IAsyncRouteState { - render(): JSX.Element | null -} - -export class AsyncRoute extends React.Component< - IAsyncRouteProps, - IAsyncRouteState -> { - constructor(props: any, ctx: any) { - super(props, ctx) - - this.state = { - render: () => null, - } - } - - async componentDidMount() { - const { name } = this.props.component - const { importFn } = window.__PLAYGRODD_COMPONENTS__[`${name}`] - const { doc: render } = await importFn - - this.setState({ render }) - } - - render() { - return ( - - ) - } -} diff --git a/packages/playgrood/src/components/Html.tsx b/packages/playgrood/src/components/Html.tsx deleted file mode 100644 index efaeac9dd..000000000 --- a/packages/playgrood/src/components/Html.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react' - -import { IComponents } from '../utils/components' - -export interface IHtmlProps { - components: IComponents -} - -export const Html: React.SFC = ({ components }) => { - const stringifiedComps = JSON.stringify(components) - - return ( - - - playgrodd - -

-