diff --git a/README.md b/README.md index 487fe64..5c7c739 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,108 @@ -oc-template-react -================= +# oc-template-typescript-react -react-templates & utilities for the [OpenComponents](https://github.com/opentable/oc) template system +React-templates with TypeScript support & utilities for the [OpenComponents](https://github.com/opentable/oc) template system -*** +Based on Nick Balestra's work on [oc-template-react](https://github.com/opencomponents/oc-template-react) -Module for handling React templates in OC - -[![codecov](https://codecov.io/gh/opencomponents/oc-template-react/branch/master/graph/badge.svg)](https://codecov.io/gh/opencomponents/oc-template-react) -[![Known Vulnerabilities](https://snyk.io/test/github/opencomponents/oc-template-react/badge.svg)](https://snyk.io/test/github/opencomponents/oc-template-react) -[![npm version](https://badge.fury.io/js/oc-template-react.svg)](http://badge.fury.io/js/oc-template-react) - -| Node8 | Node9 | Node10 | -|-------------------|-------------------|-------------------| -| [![Node8][1]][4] | [![Node9][2]][4] | [![Node10][3]][4] | - -[1]: https://travis-matrix-badges.herokuapp.com/repos/opencomponents/oc-template-react/branches/master/1 -[2]: https://travis-matrix-badges.herokuapp.com/repos/opencomponents/oc-template-react/branches/master/2 -[3]: https://travis-matrix-badges.herokuapp.com/repos/opencomponents/oc-template-react/branches/master/3 -[4]: https://travis-ci.org/opencomponents/oc-template-react +--- ## Packages included in this repo -| Name | Version | -|--------|-------| -| [`oc-template-react`](/packages/oc-template-react) | [![npm version](https://badge.fury.io/js/oc-template-react.svg)](http://badge.fury.io/js/oc-template-react) | -| [`oc-template-react-compiler`](/packages/oc-template-react-compiler) | [![npm version](https://badge.fury.io/js/oc-template-react-compiler.svg)](http://badge.fury.io/js/oc-template-react-compiler) | -| [`oc-react-component-wrapper`](/packages/oc-react-component-wrapper) | [![npm version](https://badge.fury.io/js/oc-react-component-wrapper.svg)](http://badge.fury.io/js/oc-react-component-wrapper) | - +| Name | Version | +| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`oc-template-typescript-react`](/packages/oc-template-typescript-react) | [![npm version](https://badge.fury.io/js/oc-template-typescript-react.svg)](http://badge.fury.io/js/oc-template-typescript-react) | +| [`oc-template-typescript-react-compiler`](/packages/oc-template-typescript-react-compiler) | [![npm version](https://badge.fury.io/js/oc-template-typescript-react-compiler.svg)](http://badge.fury.io/js/oc-template-typescript-react-compiler) | ## Usage: Initialize a component with the oc-template-react and follow the CLI instructions ``` -$ oc init oc-template-react +$ npx oc init my-awesome-oc oc-template-typescript-react +$ cd my-awesome-oc +$ npm install ``` ## Extra info: + +### Linting + +Like in [Create React App](https://create-react-app.dev/docs/setting-up-your-editor/#displaying-lint-output-in-the-editor), linting will be done during the build, and you can extend it from .eslintrc.json, by setting the EXTEND_ESLINT environment variable to true. + +It can also be disabled completely by setting the `DISABLE_ESLINT_PLUGIN` environment variable to `true`. + ### package.json requirements -- `template.src` - the react App entry point - should export a react component as `default` -- `template.type` - `oc-template-react` -- required in `devDependencies` - `oc-template-react-compiler` -### package.json optional -- `template.externals` - override unpkg.com externals -``` - "externals": [ - { - "name": "Object.assign", - "global": [ - "Object", - "assign" - ], - "url": "https://mydomain.com/es6-object-assign@1.1.0/dist/object-assign-auto.min.js" - }, - { - "name": "prop-types", - "global": "PropTypes", - "url": "https://mydomain.com/prop-types@15.7.2/prop-types.min.js" - }, - { - "name": "react", - "global": "React", - "url": "https://mydomain.com/react@16.9.0/umd/react.production.min.js" - }, - { - "name": "react-dom", - "global": "ReactDOM", - "url": "https://mydomain.com/react-dom@16.9.0/umd/react-dom.production.min.js" - } - ] -``` + +- `template.src` - the react App entry point - should export a react component as `default` +- `template.type` - `oc-template-typescript-react` +- required in `devDependencies` - `oc-template-react-compiler`, `react`, `react-dom`, `@types/react` ### conventions + - `props = server()` - the viewModel generated by the server is automatically passed to the react application as props - The oc-client-browser is extended and will now expose all the available react-component at `oc.reactComponents[bundleHash]` - You can register an event handler within the [oc.events](https://github.com/opentable/oc/wiki/Browser-client#oceventsoneventname-callback) system for the the `oc:componentDidMount` event. The event will be fired immediately after the react app is mounted. - You can register an event handler within the [oc.events](https://github.com/opentable/oc/wiki/Browser-client#oceventsoneventname-callback) system for the the `oc:cssDidMount` event. The event will be fired immediately after the style tag will be added to the active DOM tree. ### externals + Externals are not bundled when packaging and publishing the component, for better size taking advantage of externals caching. OC will make sure to provide them at Server-Side & Client-Side rendering time for you. + - React - ReactDOM -- PropTypes - + ### features + - `Server Side Rendering` = server side rendering should work as for any other OpenComponent - `css-modules` are supported. +- `sass-modules` are supported (you need to install the `node-sass` dependency). - `post-css` is supported with the following plugins: - [postcss-import](https://github.com/postcss/postcss-import) - [postcss-extend](https://github.com/travco/postcss-extend) - [postcss-icss-values](https://github.com/css-modules/postcss-icss-values) - [autoprefixer](https://github.com/postcss/autoprefixer) -- `White list dependencies` to be inlcuded in the build process done by the compiler. To whitelist dependencies installed in the node_modules folder, add in the package.json of the component a `buildIncludes` list: - ```json - ... - oc : { - files: { - template: { - ... - buildIncludes: ['react-components-to-build'] - } - } - } - ``` - - ## Utils The compiler exposes some utilities that could be used by your React application: -### HOCs +### Hooks -#### withDataProvider +#### useData -An Higher order component that will make a `getData` function available via props. +A hook that will make a `getData` function available via props, plus +the initial data passed from the server to the component. ##### Usage: ```javascript -import { withDataProvider } from 'oc-template-react-compiler/utils'; - -const yourApp = props => { - // you can use props.getData here +import { useData } from 'oc-template-typescript-react-compiler/utils/useData'; + +const App = (props) => { + // getData and getSetting are always available + const { id, getData, getSetting } = useData<{ id: number }>(); + const staticPath = getSetting('staticPath'); + + return ( +
+

Id: {id}

+ Logo +
+ ) }; yourEnhancedApp = withDataProvider(yourApp); ``` -`getData` accept two arguments: `(params, callback) => callback(err, result)`. It will perform a post back request to the component endpoint with the specified query perams invoking the callback with the results. - -For more details, check the [`example app`](/acceptance-components/react-app/app.js) - -#### withSettingProvider - -An Higher order component that will make a `getSetting` function available via props. - -##### Usage: - -```javascript -import { withSettingProvider } from 'oc-template-react-compiler/utils'; - -const yourApp = props => { - // you can use props.getSetting here -}; - -yourEnhancedApp = withSettingProvider(yourApp); -``` +`getData` accept one argument: `(params) => Promise`. It will perform a post back request to the component endpoint with the specified query perams invoking the callback with the results. `getSetting` accept one argument: `settingName => settingValue`. It will return the value for the requested setting. Settings available at the moment: + - `getSetting('name')` : return the name of the OC component - `getSetting('version')` : return the version of the OC component - `getSetting('baseUrl')` : return the [baseUrl of the oc-registry](https://github.com/opentable/oc/wiki/The-server.js#context-properties) - `getSetting('staticPath')` : return the path to the [static assets](https://github.com/opentable/oc/wiki/The-server.js#add-static-resource-to-the-component) of the OC component + +For more details, check the [`example app`](/acceptance-components/react-app/app.js) diff --git a/packages/oc-template-react-compiler/.prettierrc.js b/packages/oc-template-react-compiler/.prettierrc.js new file mode 100644 index 0000000..7901b6a --- /dev/null +++ b/packages/oc-template-react-compiler/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('prettier-config-guestline'); diff --git a/packages/oc-template-react-compiler/globals.d.ts b/packages/oc-template-react-compiler/globals.d.ts new file mode 100644 index 0000000..997b19d --- /dev/null +++ b/packages/oc-template-react-compiler/globals.d.ts @@ -0,0 +1,47 @@ +declare module 'oc-generic-template-compiler' { + type Callback = (err: Error | null, data?: T) => void; + type CompiledViewInfo = any; + + type OcOptions = { + files: { + data: string; + template: { + src: string; + type: string; + }; + static: string[]; + }; + }; + + type Options = { + publishPath: string; + componentPath: string; + componentPackage: { + name: string; + version: string; + dependencies: Record; + oc: OcOptions; + }; + ocPackage: any; + minify: boolean; + verbose: boolean; + production: boolean; + }; + + type CompileServer = ( + options: Options & { compiledViewInfo: CompiledViewInfo }, + cb: Callback + ) => void; + type CompileView = (options: Options, cb: Callback) => void; + type CompileStatics = (options: Options, cb: Callback<'ok'>) => void; + type GetInfo = () => { + version: string; + }; + + export function createCompile(parameters: { + compileView: CompileView; + compileServer: CompileServer; + compileStatics: CompileStatics; + getInfo: GetInfo; + }): (options: Options, cb: Callback) => void; +} diff --git a/packages/oc-template-react-compiler/lib/compile.js b/packages/oc-template-react-compiler/lib/compile.js index d56a291..dbd210d 100644 --- a/packages/oc-template-react-compiler/lib/compile.js +++ b/packages/oc-template-react-compiler/lib/compile.js @@ -1,11 +1,21 @@ -"use strict"; +'use strict'; -const createCompile = require("oc-generic-template-compiler").createCompile; -const compileStatics = require("oc-statics-compiler"); -const getInfo = require("oc-template-react").getInfo; +const createCompile = require('oc-generic-template-compiler').createCompile; +const compileStatics = require('oc-statics-compiler'); +const getInfo = require('oc-template-typescript-react').getInfo; -const compileServer = require("./compileServer"); -const compileView = require("./compileView"); +const compileServer = require('./compileServer'); +const compileView = require('./compileView'); +const verifyTypeScriptSetup = require('./verifyConfig'); + +const compiler = createCompile({ + compileServer, + compileStatics, + compileView, + getInfo +}); + +const hasTsExtension = (file) => !!file.match(/\.tsx?$/); // OPTIONS // ======= @@ -18,9 +28,14 @@ const compileView = require("./compileView"); // verbose, // watch, // production -module.exports = createCompile({ - compileServer, - compileStatics, - compileView, - getInfo -}); +module.exports = function compile(options, callback) { + const viewFileName = options.componentPackage.oc.files.template.src; + const serverFileName = options.componentPackage.oc.files.data; + const usingTypescript = hasTsExtension(viewFileName) || hasTsExtension(serverFileName); + + if (usingTypescript) { + verifyTypeScriptSetup(options.componentPath); + } + + return compiler(options, callback); +}; diff --git a/packages/oc-template-react-compiler/lib/compileServer.js b/packages/oc-template-react-compiler/lib/compileServer.js index e103a05..5ebb97f 100644 --- a/packages/oc-template-react-compiler/lib/compileServer.js +++ b/packages/oc-template-react-compiler/lib/compileServer.js @@ -1,100 +1,75 @@ -"use strict"; +const vite = require('vite'); +const fs = require('fs-extra'); +const path = require('path'); +const { callbackify } = require('util'); +const coreModules = require('builtin-modules'); +const hashBuilder = require('oc-hash-builder'); +const higherOrderServerTemplate = require('./higherOrderServerTemplate'); -const async = require("async"); -const fs = require("fs-extra"); -const hashBuilder = require("oc-hash-builder"); -const MemoryFS = require("memory-fs"); -const path = require("path"); -const reactComponentWrapper = require("oc-react-component-wrapper"); +const nodeModuleMatcher = /^[a-z@][a-z\-/0-9.]+$/i; -const { - compiler, - configurator: { server: webpackConfigurator } -} = require("./to-abstract-base-template-utils/oc-webpack"); -const higherOrderServerTemplate = require("./higherOrderServerTemplate"); - -module.exports = (options, callback) => { +async function compileServer(options) { + const componentPath = options.componentPath; const serverFileName = options.componentPackage.oc.files.data; let serverPath = path.join(options.componentPath, serverFileName); - if (process.platform === "win32") { - serverPath = serverPath.split("\\").join("\\\\"); + if (process.platform === 'win32') { + serverPath = serverPath.split('\\').join('\\\\'); } - const publishFileName = options.publishFileName || "server.js"; + const publishFileName = options.publishFileName || 'server.js'; const publishPath = options.publishPath; - const stats = options.verbose ? "verbose" : "errors-only"; const dependencies = options.componentPackage.dependencies || {}; const componentName = options.componentPackage.name; const componentVersion = options.componentPackage.version; - const production = options.production; + const production = !!options.production; const higherOrderServerContent = higherOrderServerTemplate({ serverPath, componentName, - componentVersion, - bundleHashKey: options.compiledViewInfo.bundle.hashKey - }); - const tempFolder = path.join(serverPath, "../temp"); - const higherOrderServerPath = path.join( - tempFolder, - "__oc_higherOrderServer.js" - ); - - const config = webpackConfigurator({ - serverPath: higherOrderServerPath, - publishFileName, - dependencies, - stats, - production + componentVersion }); + const tempFolder = path.join(publishPath, 'temp'); + const higherOrderServerPath = path.join(tempFolder, '__oc_higherOrderServer.ts'); + const externals = [...Object.keys(dependencies), ...coreModules]; - async.waterfall( - [ - next => - fs.outputFile(higherOrderServerPath, higherOrderServerContent, next), - next => compiler(config, next), - (data, next) => { - const basePath = path.join(serverPath, "../temp/build"); - const memory = new MemoryFS(data); - const getCompiled = fileName => - memory.readFileSync(`${basePath}/${fileName}`, "UTF8"); - - return fs.ensureDir(publishPath, err => { - if (err) return next(err); - const result = { "server.js": getCompiled(config.output.filename) }; + try { + await fs.outputFile(higherOrderServerPath, higherOrderServerContent); - if (!production) { - try { - result["server.js.map"] = getCompiled( - `${config.output.filename}.map` - ); - } catch (e) { - // skip sourcemap if it doesn't exist + const result = await vite.build({ + appType: 'custom', + root: componentPath, + mode: production ? 'production' : 'development', + logLevel: options.verbose ? 'info' : 'silent', + build: { + lib: { entry: higherOrderServerPath, formats: ['cjs'] }, + write: false, + minify: production, + rollupOptions: { + external: (id) => { + if (nodeModuleMatcher.test(id)) { + if (!externals.includes(id)) { + throw new Error(`Missing dependencies from package.json => ${id}`); + } + return true; } + return false; } + } + } + }); + const out = Array.isArray(result) ? result[0] : result; + const bundle = out.output[0].code; + + await fs.ensureDir(publishPath); + await fs.writeFile(path.join(publishPath, publishFileName), bundle); + + return { + type: 'node.js', + hashKey: hashBuilder.fromString(bundle), + src: publishFileName + }; + } finally { + await fs.remove(tempFolder); + } +} - next(null, result); - }); - }, - (compiledFiles, next) => - async.eachOf( - compiledFiles, - (fileContent, fileName, next) => - fs.writeFile(path.join(publishPath, fileName), fileContent, next), - err => - next( - err, - err - ? null - : { - type: "node.js", - hashKey: hashBuilder.fromString( - compiledFiles[publishFileName] - ), - src: publishFileName - } - ) - ) - ], - (err, data) => fs.remove(tempFolder, err2 => callback(err, data)) - ); -}; +module.exports = callbackify(compileServer); diff --git a/packages/oc-template-react-compiler/lib/compileView.js b/packages/oc-template-react-compiler/lib/compileView.js index 35c21fa..104cacf 100644 --- a/packages/oc-template-react-compiler/lib/compileView.js +++ b/packages/oc-template-react-compiler/lib/compileView.js @@ -1,135 +1,134 @@ -"use strict"; +const fs = require('fs-extra'); +const vite = require('vite'); +const react = require('@vitejs/plugin-react'); +const path = require('path'); +const EnvironmentPlugin = require('vite-plugin-environment').default; +const hashBuilder = require('oc-hash-builder'); +const ocViewWrapper = require('oc-view-wrapper'); +const { callbackify } = require('util'); +const viewTemplate = require('./viewTemplate'); +const reactOCProviderTemplate = require('./reactOCProviderTemplate'); +const cssModules = require('./cssModulesPlugin'); -const async = require("async"); -const fs = require("fs-extra"); -const hashBuilder = require("oc-hash-builder"); -const MemoryFS = require("memory-fs"); -const minifyFile = require("oc-minify-file"); -const ocViewWrapper = require("oc-view-wrapper"); -const path = require("path"); -const reactComponentWrapper = require("oc-react-component-wrapper"); -const strings = require("oc-templates-messages"); +const clientName = 'clientBundle'; -const { - compiler, - configurator: { client: webpackConfigurator } -} = require("./to-abstract-base-template-utils/oc-webpack"); +const partition = (array, predicate) => { + const matches = []; + const rest = []; + for (const element of array) { + if (predicate(element)) { + matches.push(element); + } else { + rest.push(element); + } + } + return [matches, rest]; +}; -const fontFamilyUnicodeParser = require("./to-abstract-base-template-utils/font-family-unicode-parser"); -const reactOCProviderTemplate = require("./reactOCProviderTemplate"); -const viewTemplate = require("./viewTemplate"); +async function compileView(options) { + function processRelativePath(relativePath) { + let pathStr = path.join(options.componentPath, relativePath); + if (process.platform === 'win32') { + return pathStr.split('\\').join('\\\\'); + } + return pathStr; + } -module.exports = (options, callback) => { + const staticFiles = options.componentPackage.oc.files.static; + const staticFolder = Array.isArray(staticFiles) ? staticFiles[0] : staticFiles; const viewFileName = options.componentPackage.oc.files.template.src; - let viewPath = path.join(options.componentPath, viewFileName); - if (process.platform === "win32") { - viewPath = viewPath.split("\\").join("\\\\"); - } + const componentPath = options.componentPath; + const viewPath = processRelativePath(viewFileName); + const publishPath = options.publishPath; - const publishFileName = options.publishFileName || "template.js"; + const tempPath = path.join(publishPath, 'temp'); + const publishFileName = options.publishFileName || 'template.js'; const componentPackage = options.componentPackage; - const { getInfo } = require("../index"); + const { getInfo } = require('../index'); const externals = componentPackage.oc.files.template.externals || getInfo().externals; - const production = options.production; + const production = !!options.production; const reactOCProviderContent = reactOCProviderTemplate({ viewPath }); - const reactOCProviderName = "reactOCProvider.js"; - const reactOCProviderPath = path.join( - publishPath, - "temp", - reactOCProviderName - ); + const reactOCProviderName = 'reactOCProvider.tsx'; + const reactOCProviderPath = path.join(tempPath, reactOCProviderName); - const compile = (options, cb) => { - const config = webpackConfigurator({ - viewPath: options.viewPath, - externals: externals.reduce((externals, dep) => { - externals[dep.name] = dep.global; - return externals; - }, {}), - publishFileName, - production, - buildIncludes: componentPackage.oc.files.template.buildIncludes || [] - }); - compiler(config, (err, data) => { - if (err) { - return cb(err); - } + await fs.outputFile(reactOCProviderPath, reactOCProviderContent); - const memoryFs = new MemoryFS(data); - const bundle = memoryFs.readFileSync( - `/build/${config.output.filename}`, - "UTF8" - ); + const globals = externals.reduce((externals, dep) => { + externals[dep.name] = dep.global; + return externals; + }, {}); - const bundleHash = hashBuilder.fromString(bundle); - const bundleName = "react-component"; - const bundlePath = path.join(publishPath, `${bundleName}.js`); - const wrappedBundle = reactComponentWrapper(bundleHash, bundle); - fs.outputFileSync(bundlePath, wrappedBundle); + const result = await vite.build({ + appType: 'custom', + root: componentPath, + mode: production ? 'production' : 'development', + plugins: [react(), EnvironmentPlugin(['NODE_ENV']), cssModules()], + logLevel: 'silent', + build: { + sourcemap: !production, + lib: { entry: reactOCProviderPath, formats: ['iife'], name: clientName }, + write: false, + minify: production, + rollupOptions: { + external: Object.keys(globals), + output: { + globals + } + } + } + }); + const out = Array.isArray(result) ? result[0] : result; + const bundle = out.output.find((x) => x.facadeModuleId.endsWith(reactOCProviderName)).code; + const [cssAssets, otherAssets] = partition( + out.output.filter((x) => x.type === 'asset'), + (x) => x.fileName.endsWith('.css') + ); + const cssStyles = cssAssets.map((x) => x.source.replace(/\n/g, '') ?? '').join(' '); + const bundleHash = hashBuilder.fromString(bundle); + const wrappedBundle = `(function() { + ${bundle} - let css = null; - if (data.build["main.css"]) { - // This is an awesome hack by KimTaro that will blow your mind. - // Remove it once this get merged: https://github.com/webpack-contrib/css-loader/pull/523 - css = fontFamilyUnicodeParser( - memoryFs.readFileSync(`/build/main.css`, "UTF8") - ); + return ${clientName}; + })()`; - // We convert single quotes to double quotes in order to - // support the viewTemplate's string interpolation - css = minifyFile(".css", css).replace(/\'/g, '"'); - const cssPath = path.join(publishPath, `styles.css`); - fs.outputFileSync(cssPath, css); - } + const reactRoot = `oc-reactRoot-${componentPackage.name}`; + const templateString = viewTemplate({ + reactRoot, + css: cssStyles, + externals, + wrappedBundle, + hash: bundleHash + }); + const templateStringCompressed = production + ? templateString.replace(/\s+/g, ' ') + : templateString; + const hash = hashBuilder.fromString(templateStringCompressed); + const view = ocViewWrapper(hash, templateStringCompressed); - const reactRoot = `oc-reactRoot-${componentPackage.name}`; - const templateString = viewTemplate({ - reactRoot, - css, - externals, - bundleHash, - bundleName - }); + await fs.unlink(reactOCProviderPath); + await fs.mkdir(publishPath, { recursive: true }); + await fs.writeFile(path.join(publishPath, publishFileName), view); + if (staticFolder) { + for (const asset of otherAssets) { + // asset.fileName could have paths like assets/file.js + // so we need to create those extra directories + await fs.ensureFile(path.join(publishPath, staticFolder, asset.fileName)); + await fs.writeFile( + path.join(publishPath, staticFolder, asset.fileName), + asset.source, + 'utf-8' + ); + } + } - const templateStringCompressed = production - ? templateString.replace(/\s+/g, " ") - : templateString; - const hash = hashBuilder.fromString(templateStringCompressed); - const view = ocViewWrapper(hash, templateStringCompressed); - return cb(null, { - template: { view, hash }, - bundle: { hash: bundleHash } - }); - }); + return { + template: { + type: options.componentPackage.oc.files.template.type, + hashKey: hash, + src: publishFileName + } }; +} - async.waterfall( - [ - next => fs.outputFile(reactOCProviderPath, reactOCProviderContent, next), - next => compile({ viewPath: reactOCProviderPath }, next), - (compiled, next) => - fs.remove(reactOCProviderPath, err => next(err, compiled)), - (compiled, next) => fs.ensureDir(publishPath, err => next(err, compiled)), - (compiled, next) => - fs.writeFile( - path.join(publishPath, publishFileName), - compiled.template.view, - err => next(err, compiled) - ) - ], - (err, compiled) => { - if (err) { - return callback(strings.errors.compilationFailed(viewFileName, err)); - } - callback(null, { - template: { - type: options.componentPackage.oc.files.template.type, - hashKey: compiled.template.hash, - src: publishFileName - }, - bundle: { hashKey: compiled.bundle.hash } - }); - } - ); -}; +module.exports = callbackify(compileView); diff --git a/packages/oc-template-react-compiler/lib/cssModulesPlugin.js b/packages/oc-template-react-compiler/lib/cssModulesPlugin.js new file mode 100644 index 0000000..85f8614 --- /dev/null +++ b/packages/oc-template-react-compiler/lib/cssModulesPlugin.js @@ -0,0 +1,81 @@ +const { transformSync } = require('@babel/core'); + +const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)`; +const importRE = + /import\s+([\S]+)\s+from\s+('|")([\S]+)\.(css|less|sass|scss|styl|stylus|postcss)(\?[\S]*)?('|")/; +const jsRE = /\.(js|mjs|ts|jsx|tsx)/; +const cssLangRE = new RegExp(cssLangs); +const cssModuleRE = new RegExp(`\\.module${cssLangs}`); + +function autoCSSModulePlugin() { + return () => { + return { + visitor: { + ImportDeclaration: (path) => { + const { node } = path; + if (!node) { + return; + } + // 如果不是module css的那么就通过 转化为 module.styl来模块化css + if ( + node.specifiers && + node.specifiers.length > 0 && + cssLangRE.test(node.source.value) && + !cssModuleRE.test(node.source.value) + ) { + const cssFile = node.source.value; + const extension = cssFile.split('.').pop() ?? 'css'; + node.source.value = + cssFile + (cssFile.indexOf('?') > -1 ? '&' : '?') + `.module.${extension}`; + } + } + } + }; + }; +} + +function transform(code, { sourceMap, file }) { + const parsePlugins = ['jsx']; + if (/\.tsx?$/.test(file)) { + parsePlugins.push('typescript'); + } + + const result = transformSync(code, { + configFile: false, + parserOpts: { + sourceType: 'module', + allowAwaitOutsideFunction: true, + plugins: parsePlugins + }, + sourceMaps: true, + sourceFileName: file, + inputSourceMap: sourceMap, + plugins: [autoCSSModulePlugin()] + }); + + return { + code: result.code, + map: result.map + }; +} + +function viteTransformCSSModulesPlugin() { + const name = 'vite-plugin-transform-css-modules'; + return { + name, + transform(code, id) { + if (jsRE.test(id) && importRE.test(code)) { + const result = transform(code, { + file: id, + sourceMap: this.getCombinedSourcemap() + }); + if (result) { + return result; + } + } + return undefined; + } + }; +} + +module.exports = viteTransformCSSModulesPlugin; diff --git a/packages/oc-template-react-compiler/lib/higherOrderServerTemplate.js b/packages/oc-template-react-compiler/lib/higherOrderServerTemplate.js index ea227c0..a9a20ce 100644 --- a/packages/oc-template-react-compiler/lib/higherOrderServerTemplate.js +++ b/packages/oc-template-react-compiler/lib/higherOrderServerTemplate.js @@ -1,12 +1,10 @@ -const higherOrderServerTemplate = ({ - serverPath, - bundleHashKey, - componentName, - componentVersion -}) => ` -import { data as dataProvider } from '${serverPath}'; -export const data = (context, callback) => { - dataProvider(context, (error, model) => { +const removeExtension = (path) => path.replace(/\.(j|t)sx?$/, ''); + +const higherOrderServerTemplate = ({ serverPath, componentName, componentVersion }) => ` +import { data as dataProvider } from '${removeExtension(serverPath)}'; + +export const data = (context : any, callback : (error: any, data?: any) => void) => { + dataProvider(context, (error: any, model: any) => { if (error) { return callback(error); } @@ -20,8 +18,6 @@ export const data = (context, callback) => { const srcPath = srcPathHasProtocol ? context.staticPath : ("https:" + context.staticPath); return callback(null, Object.assign({}, { reactComponent: { - key: "${bundleHashKey}", - src: srcPath + "react-component.js", props } })); diff --git a/packages/oc-template-react-compiler/lib/oc-app.d.ts b/packages/oc-template-react-compiler/lib/oc-app.d.ts new file mode 100644 index 0000000..56ded94 --- /dev/null +++ b/packages/oc-template-react-compiler/lib/oc-app.d.ts @@ -0,0 +1,72 @@ +interface OC { + conf: { + templates: Array<{ + type: string; + externals: string[]; + }>; + }; + cmd: { + push: (cb: (oc: OC) => void) => void; + }; + events: { + on: (eventName: string, fn: (...data: any[]) => void) => void; + off: (eventName: string, fn?: (...data: any[]) => void) => void; + fire: (eventName: string, data?: any) => void; + reset: () => void; + }; + renderNestedComponent: (ocElement: HTMLElement, cb: () => void) => void; +} + +declare global { + interface Window { + oc: OC; + } + + namespace JSX { + interface IntrinsicElements { + 'oc-component': React.DetailedHTMLProps< + React.HTMLAttributes & { href: string }, + HTMLElement + >; + } + } +} + +export interface AcceptLanguage { + code: string; + script?: any; + region: string; + quality: number; +} + +export interface Env { + name: string; +} + +export interface Plugins {} + +export interface External { + global: string; + url: string; + name: string; +} + +export interface Template { + type: string; + version: string; + externals: External[]; +} + +export interface Context { + acceptLanguage: AcceptLanguage[]; + baseUrl: string; + env: E; + params: T; + plugins: Plugins; + requestHeaders: Record; + requestIp: string; + setEmptyResponse: () => void; + setHeader: (header: string, value: string) => void; + staticPath: string; + templates: Template[]; +} diff --git a/packages/oc-template-react-compiler/lib/reactOCProviderTemplate.js b/packages/oc-template-react-compiler/lib/reactOCProviderTemplate.js index d449e9f..9a5402a 100644 --- a/packages/oc-template-react-compiler/lib/reactOCProviderTemplate.js +++ b/packages/oc-template-react-compiler/lib/reactOCProviderTemplate.js @@ -1,53 +1,53 @@ +const removeExtension = (path) => path.replace(/\.(t|j)sx?$/, ''); + const reactOCProviderTemplate = ({ viewPath }) => ` - import PropTypes from 'prop-types'; import React from 'react'; - import View from '${viewPath}'; + import View from '${removeExtension(viewPath)}'; + import { DataProvider } from 'oc-template-typescript-react-compiler/utils/useData' class OCProvider extends React.Component { componentDidMount(){ - const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = this.props; - window.oc.events.fire('oc:componentDidMount', rest); + const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = (this.props as any); + (window as any).oc.events.fire('oc:componentDidMount', rest); } - getChildContext() { - const getData = (parameters, cb) => { - return window.oc.getData({ - name: this.props._componentName, - version: this.props._componentVersion, - baseUrl: this.props._baseUrl, - parameters - }, (err, data) => { - if (err) { - return cb(err); - } - const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = data.reactComponent.props; - cb(null, rest, data.reactComponent.props); - }); - }; - const getSetting = setting => { - const settingHash = { - name: this.props._componentName, - version: this.props._componentVersion, - baseUrl: this.props._baseUrl, - staticPath: this.props._staticPath - }; - return settingHash[setting]; + getData(providerProps: any, parameters: any, cb: (error: any, parameters?: any, props?: any) => void) { + return (window as any).oc.getData({ + name: providerProps._componentName, + version: providerProps._componentVersion, + baseUrl: providerProps._baseUrl, + parameters + }, (err: any, data: any) => { + if (err) { + return cb(err); + } + const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = (data.reactComponent.props as any); + cb(null, rest, data.reactComponent.props); + }); + } + + getSetting(providerProps: any, setting: string) { + const settingHash = { + name: providerProps._componentName, + version: providerProps._componentVersion, + baseUrl: providerProps._baseUrl, + staticPath: providerProps._staticPath }; - return { getData, getSetting }; + return (settingHash as any)[setting]; } render() { - const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = this.props; + const { _staticPath, _baseUrl, _componentName, _componentVersion, ...rest } = (this.props as any); + (rest as any).getData = (parameters: any, cb: (error: any, parameters?: any, props?: any) => void) => this.getData(this.props, parameters, cb); + (rest as any).getSetting = (setting: string) => this.getSetting(this.props, setting); return ( - + + + ); } } - OCProvider.childContextTypes = { - getData: PropTypes.func, - getSetting: PropTypes.func - }; export default OCProvider `; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/font-family-unicode-parser.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/font-family-unicode-parser.js deleted file mode 100644 index 948ede5..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/font-family-unicode-parser.js +++ /dev/null @@ -1,25 +0,0 @@ -const RE_FONT_FAMILY = /font-family\s*:\s*([^;\n]+)[;\n]/g; -const RE_FONT_NAMES = /(?:(?:'|(?:\\"))?([(?:\\\\)\w\d _-]+)(?:'|(?:\\"))?(,\s*)?)/g; -const RE_DOUBLE_COUTE = /"/g; -const RE_SPACE = /[ ]+/g; -const RE_UNICODE = /\\([\d\w]{4})/g; - -module.exports = function(source) { - source = source.replace(RE_FONT_FAMILY, (str, fontNames) => { - const strFontNames = fontNames - .replace(RE_DOUBLE_COUTE, "'") - .replace(RE_FONT_NAMES, (match, fontName, sep) => { - if (match.includes("'")) { - const replacement = fontName - .replace(RE_SPACE, " ") - .replace(RE_UNICODE, (m, unicode) => - String.fromCharCode(parseInt(unicode, 16)) - ); - return `'${replacement}'${sep || ""}`; - } - return match; - }); - return `font-family: ${strFontNames};`; - }); - return source; -}; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/index.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/index.js deleted file mode 100644 index eabd14c..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/index.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -const compiler = require("./lib/compiler"); -const configurator = require("./lib/configurator"); - -module.exports = { - compiler, - configurator -}; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/compiler/index.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/compiler/index.js deleted file mode 100644 index 449050b..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/compiler/index.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; - -const MemoryFS = require("memory-fs"); -const webpack = require("webpack"); - -const memoryFs = new MemoryFS(); - -module.exports = function compiler(config, callback) { - const logger = config.logger; - delete config.logger; - - const compiler = webpack(config); - compiler.outputFileSystem = memoryFs; - - compiler.run((error, stats) => { - let softError; - let warning; - - // handleFatalError - if (error) { - return callback(error); - } - - const info = stats.toJson(); - // handleSoftErrors - if (stats.hasErrors()) { - softError = info.errors.toString(); - return callback(softError); - } - // handleWarnings - if (stats.hasWarnings()) { - warning = info.warnings.toString(); - } - - const log = stats.toString(config.stats || "errors-only"); - - if (log) { - logger.log(log); - } - callback(null, memoryFs.data); - }); -}; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/client/index.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/client/index.js deleted file mode 100644 index 9f22c9c..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/client/index.js +++ /dev/null @@ -1,120 +0,0 @@ -"use strict"; - -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const MinifyPlugin = require("babel-minify-webpack-plugin"); -const path = require("path"); -const webpack = require("webpack"); -const _ = require("lodash"); - -const createExcludeRegex = require("../createExcludeRegex"); - -module.exports = options => { - const buildPath = options.buildPath || "/build"; - const production = options.production; - const buildIncludes = options.buildIncludes.concat( - "oc-template-react-compiler/utils" - ); - const excludeRegex = createExcludeRegex(buildIncludes); - const localIdentName = !production - ? "oc__[path][name]-[ext]__[local]__[hash:base64:8]" - : "[local]__[hash:base64:8]"; - - const cssLoader = { - test: /\.css$/, - use: [ - MiniCssExtractPlugin.loader, - { - loader: require.resolve("css-loader"), - options: { - importLoaders: 1, - modules: true, - localIdentName, - camelCase: true - } - }, - { - loader: require.resolve("postcss-loader"), - options: { - ident: "postcss", - plugins: [ - require("postcss-import"), - require("postcss-extend"), - require("postcss-icss-values"), - require("autoprefixer") - ] - } - } - ] - }; - - let plugins = [ - new MiniCssExtractPlugin({ - filename: "[name].css", - allChunks: true, - ignoreOrder: true - }), - new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify( - production ? "production" : "development" - ) - }) - ]; - if (production) { - plugins = plugins.concat(new MinifyPlugin()); - } - - const cacheDirectory = !production; - const polyfills = ["Object.assign"]; - - return { - mode: production ? "production" : "development", - optimization: { - // https://webpack.js.org/configuration/optimization/ - // Override production mode optimization for minification - // As it currently breakes the build, still rely on babel-minify-webpack-plugin instead - minimize: false - }, - entry: options.viewPath, - output: { - path: buildPath, - filename: options.publishFileName - }, - externals: _.omit(options.externals, polyfills), - module: { - rules: [ - cssLoader, - { - test: /\.jsx?$/, - exclude: excludeRegex, - use: [ - { - loader: require.resolve("babel-loader"), - options: { - cacheDirectory, - babelrc: false, - presets: [ - [ - require.resolve("babel-preset-env"), - { modules: false, loose: true } - ], - [require.resolve("babel-preset-react")] - ], - plugins: [ - [require.resolve("babel-plugin-transform-object-rest-spread")] - ] - } - } - ] - } - ] - }, - plugins, - resolve: { - alias: { - react: path.join(__dirname, "../../node_modules/react"), - "react-dom": path.join(__dirname, "../../node_modules/react-dom"), - "prop-types": path.join(__dirname, "../../node_modules/prop-types") - } - } - }; -}; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/createExcludeRegex.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/createExcludeRegex.js deleted file mode 100644 index c8a00da..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/createExcludeRegex.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; - -const createExcludeRegex = buildIncludes => - new RegExp(`node_modules\/(?!(${buildIncludes.join("|")}))`); - -module.exports = createExcludeRegex; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/index.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/index.js deleted file mode 100644 index bba8224..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/index.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -const client = require("./client"); -const server = require("./server"); - -module.exports = { - client, - server -}; diff --git a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/server/index.js b/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/server/index.js deleted file mode 100644 index 1d928e6..0000000 --- a/packages/oc-template-react-compiler/lib/to-abstract-base-template-utils/oc-webpack/lib/configurator/server/index.js +++ /dev/null @@ -1,93 +0,0 @@ -/*jshint camelcase:false */ -"use strict"; - -const MinifyPlugin = require("babel-minify-webpack-plugin"); -const externalDependenciesHandlers = require("oc-external-dependencies-handler"); -const path = require("path"); -const webpack = require("webpack"); - -module.exports = function webpackConfigGenerator(options) { - const production = - options.production !== undefined ? options.production : "true"; - - const sourceMaps = !production; - const devtool = sourceMaps ? "#source-map" : ""; - - const jsLoaders = [ - { - loader: require.resolve("babel-loader"), - options: { - cacheDirectory: true, - retainLines: true, - sourceMaps, - sourceRoot: path.join(options.serverPath, ".."), - babelrc: false, - presets: [ - [ - require.resolve("babel-preset-env"), - { - modules: false, - targets: { - node: 6 - } - } - ], - [ - require.resolve("babel-preset-react") - ] - ], - plugins: [ - [require.resolve("babel-plugin-transform-object-rest-spread")] - ] - } - } - ]; - - const plugins = [ - new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify( - production ? "production" : "development" - ) - }) - ]; - - if (production) { - jsLoaders.unshift({ - loader: require.resolve("infinite-loop-loader") - }); - plugins.unshift(new MinifyPlugin()); - } - - return { - mode: production ? "production" : "development", - optimization: { - // https://webpack.js.org/configuration/optimization/ - // Override production mode optimization for minification - // As it currently breakes the build, still rely on babel-minify-webpack-plugin instead - minimize: false - }, - devtool, - entry: options.serverPath, - target: "node", - output: { - path: path.join(options.serverPath, "../build"), - filename: options.publishFileName, - libraryTarget: "commonjs2", - devtoolModuleFilenameTemplate: "[absolute-resource-path]", - devtoolFallbackModuleFilenameTemplate: "[absolute-resource-path]?[hash]" - }, - externals: externalDependenciesHandlers(options.dependencies), - module: { - rules: [ - { - test: /\.js$/, - exclude: /node_modules/, - use: jsLoaders - } - ] - }, - plugins, - logger: options.logger || console, - stats: options.stats - }; -}; diff --git a/packages/oc-template-react-compiler/lib/verifyConfig.js b/packages/oc-template-react-compiler/lib/verifyConfig.js new file mode 100644 index 0000000..c99218b --- /dev/null +++ b/packages/oc-template-react-compiler/lib/verifyConfig.js @@ -0,0 +1,212 @@ +const chalk = require('chalk'); +const fs = require('fs'); +const resolve = require('resolve'); +const path = require('path'); +const os = require('os'); +const immer = require('immer').produce; + +function writeJson(fileName, object) { + fs.writeFileSync(fileName, JSON.stringify(object, null, 2).replace(/\n/g, os.EOL) + os.EOL); +} + +function verifyTypeScriptSetup(componentPath) { + const paths = { + appTsConfig: path.resolve(componentPath, 'tsconfig.json'), + yarnLockFile: path.resolve(componentPath, 'yarn.lock'), + appNodeModules: path.resolve(componentPath, 'node_modules'), + appTypeDeclarations: path.resolve(componentPath, 'src', 'oc-app.d.ts') + }; + let firstTimeSetup = false; + + if (!fs.existsSync(paths.appTsConfig)) { + writeJson(paths.appTsConfig, {}); + firstTimeSetup = true; + } + + const isYarn = fs.existsSync(paths.yarnLockFile); + + // Ensure typescript is installed + let ts; + try { + ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules + })); + } catch (_) { + console.error( + chalk.bold.red( + `It looks like you're trying to use TypeScript but do not have ${chalk.bold( + 'typescript' + )} installed.` + ) + ); + console.error( + chalk.bold( + 'Please install', + chalk.cyan.bold('typescript'), + 'by running', + chalk.cyan.bold(isYarn ? 'yarn add typescript' : 'npm install typescript') + '.' + ) + ); + console.error(); + process.exit(1); + } + + const compilerOptions = { + // These are suggested values and will be set when not present in the + // tsconfig.json + // 'parsedValue' matches the output value from ts.parseJsonConfigFileContent() + target: { + parsedValue: ts.ScriptTarget.ES5, + suggested: 'es5' + }, + lib: { suggested: ['dom', 'dom.iterable', 'esnext'] }, + allowJs: { suggested: true }, + skipLibCheck: { suggested: true }, + esModuleInterop: { suggested: true }, + allowSyntheticDefaultImports: { suggested: true }, + strict: { suggested: true }, + forceConsistentCasingInFileNames: { suggested: true }, + // TODO: Enable for v4.0 (#6936) + // noFallthroughCasesInSwitch: { suggested: true }, + + // These values are required and cannot be changed by the user + // Keep this in sync with the rollup config + module: { + parsedValue: ts.ModuleKind.ESNext, + value: 'esnext', + reason: 'for import() and import/export' + }, + moduleResolution: { + parsedValue: ts.ModuleResolutionKind.NodeJs, + value: 'node', + reason: 'to match rollup resolution' + }, + resolveJsonModule: { value: true, reason: 'to match rollup loader' }, + isolatedModules: { value: true, reason: 'implementation limitation' }, + jsx: { + parsedValue: ts.JsxEmit.ReactJSX, + suggested: 'react-jsx' + }, + paths: { value: undefined, reason: 'aliased imports are not supported' } + }; + + const formatDiagnosticHost = { + getCanonicalFileName: (fileName) => fileName, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => os.EOL + }; + + const messages = []; + let appTsConfig; + let parsedTsConfig; + let parsedCompilerOptions; + try { + const { config: readTsConfig, error } = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile); + + if (error) { + throw new Error(ts.formatDiagnostic(error, formatDiagnosticHost)); + } + + appTsConfig = readTsConfig; + + // Get TS to parse and resolve any "extends" + // Calling this function also mutates the tsconfig above, + // adding in "include" and "exclude", but the compilerOptions remain untouched + let result; + parsedTsConfig = immer(readTsConfig, (config) => { + result = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(paths.appTsConfig)); + }); + + if (result.errors && result.errors.length) { + throw new Error(ts.formatDiagnostic(result.errors[0], formatDiagnosticHost)); + } + + parsedCompilerOptions = result.options; + } catch (e) { + if (e && e.name === 'SyntaxError') { + console.error( + chalk.red.bold( + 'Could not parse', + chalk.cyan('tsconfig.json') + '.', + 'Please make sure it contains syntactically correct JSON.' + ) + ); + } + + console.log(e && e.message ? `${e.message}` : ''); + process.exit(1); + } + + if (appTsConfig.compilerOptions == null) { + appTsConfig.compilerOptions = {}; + firstTimeSetup = true; + } + + for (const option of Object.keys(compilerOptions)) { + const { parsedValue, value, suggested, reason } = compilerOptions[option]; + + const valueToCheck = parsedValue === undefined ? value : parsedValue; + const coloredOption = chalk.cyan('compilerOptions.' + option); + + if (suggested != null) { + if (parsedCompilerOptions[option] === undefined) { + appTsConfig.compilerOptions[option] = suggested; + messages.push( + `${coloredOption} to be ${chalk.bold('suggested')} value: ${chalk.cyan.bold( + suggested + )} (this can be changed)` + ); + } + } else if (parsedCompilerOptions[option] !== valueToCheck) { + appTsConfig.compilerOptions[option] = value; + messages.push( + `${coloredOption} ${chalk.bold(valueToCheck == null ? 'must not' : 'must')} be ${ + valueToCheck == null ? 'set' : chalk.cyan.bold(value) + }` + (reason != null ? ` (${reason})` : '') + ); + } + } + + // tsconfig will have the merged "include" and "exclude" by this point + if (parsedTsConfig.include == null) { + appTsConfig.include = ['src']; + messages.push(`${chalk.cyan('include')} should be ${chalk.cyan.bold('src')}`); + } + + if (parsedTsConfig.exclude == null) { + appTsConfig.exclude = ['src/_package']; + messages.push(`${chalk.cyan('exclude')} should have ${chalk.cyan.bold('src/_package')}`); + } + + if (messages.length > 0) { + if (firstTimeSetup) { + console.log( + chalk.bold('Your', chalk.cyan('tsconfig.json'), 'has been populated with default values.') + ); + console.log(); + } else { + console.warn( + chalk.bold( + 'The following changes are being made to your', + chalk.cyan('tsconfig.json'), + 'file:' + ) + ); + messages.forEach((message) => { + console.warn(' - ' + message); + }); + console.warn(); + } + writeJson(paths.appTsConfig, appTsConfig); + } + + // Reference `oc-template-typescript-react-compiler` types + if (!fs.existsSync(paths.appTypeDeclarations)) { + fs.writeFileSync( + paths.appTypeDeclarations, + `/// ${os.EOL}` + ); + } +} + +module.exports = verifyTypeScriptSetup; diff --git a/packages/oc-template-react-compiler/lib/viewTemplate.js b/packages/oc-template-react-compiler/lib/viewTemplate.js index 837bca5..5a64bf2 100644 --- a/packages/oc-template-react-compiler/lib/viewTemplate.js +++ b/packages/oc-template-react-compiler/lib/viewTemplate.js @@ -1,32 +1,31 @@ -const viewTemplate = ({ - reactRoot, - css, - externals, - bundleHash, - bundleName -}) => `function(model){ +const viewTemplate = ({ reactRoot, css, externals, wrappedBundle, hash }) => `function(model){ var modelHTML = model.__html ? model.__html : ''; var staticPath = model.reactComponent.props._staticPath; var props = JSON.stringify(model.reactComponent.props); - var randomId = Math.random() * 10000000; - var reactRootId = "${reactRoot}-" + randomId; - return '${css ? "" : ""}' + - '
' + modelHTML + '
' + + window.oc = window.oc || {}; + window.oc.__typescriptReactTemplate = window.oc.__typescriptReactTemplate || { count: 0 }; + oc.reactComponents = oc.reactComponents || {}; + oc.reactComponents['${hash}'] = oc.reactComponents['${hash}'] || (${wrappedBundle}); + var count = window.oc.__typescriptReactTemplate.count; + var templateId = "${reactRoot}-" + count; + window.oc.__typescriptReactTemplate.count++; + return '
' + modelHTML + '
' + + '${css ? '' : ''}' + '' diff --git a/packages/oc-template-react-compiler/package.json b/packages/oc-template-react-compiler/package.json index 6aa3081..66445a1 100644 --- a/packages/oc-template-react-compiler/package.json +++ b/packages/oc-template-react-compiler/package.json @@ -22,38 +22,27 @@ "email": "nick@balestra.ch" }, "license": "MIT", + "types": "./lib/oc-app.d.ts", "dependencies": { - "async": "2.6.3", - "autoprefixer": "9.6.2", - "babel-loader": "7.1.5", - "babel-minify-webpack-plugin": "0.3.1", - "babel-plugin-transform-object-rest-spread": "6.26.0", - "babel-preset-env": "1.7.0", - "babel-preset-react": "6.24.1", - "css-loader": "1.0.1", - "fs-extra": "8.1.0", - "infinite-loop-loader": "1.0.6", - "lodash": "4.17.19", - "memory-fs": "0.4.1", - "mini-css-extract-plugin": "0.8.0", - "node-dir": "0.1.17", + "@vitejs/plugin-react": "^3.1.0", + "chalk": "^3.0.0", + "fs-extra": "10.0.0", + "immer": "^9.0.6", "oc-external-dependencies-handler": "1.0.8", "oc-generic-template-compiler": "2.0.5", - "oc-get-unix-utc-timestamp": "1.0.2", "oc-hash-builder": "1.0.2", - "oc-minify-file": "1.0.15", - "oc-react-component-wrapper": "1.0.2", "oc-statics-compiler": "2.0.11", "oc-template-react": "2.1.0", - "oc-templates-messages": "1.0.2", "oc-view-wrapper": "1.0.3", - "postcss-extend": "1.0.5", - "postcss-icss-values": "2.0.2", - "postcss-import": "12.0.1", - "postcss-loader": "3.0.0", - "prop-types": "15.7.2", - "react": "16.9.0", - "webpack": "4.41.0" + "react": "18.2.0", + "react-dom": "18.2.0", + "resolve": "^1.20.0", + "vite": "^4.2.1", + "vite-plugin-environment": "^1.1.3" + }, + "devDependencies": { + "lodash": "4.17.19", + "node-dir": "0.1.17" }, "files": [ "index.js", @@ -63,4 +52,4 @@ "README.md", "LICENSE" ] -} +} \ No newline at end of file diff --git a/packages/oc-template-react-compiler/scaffold/src/.eslintrc.json b/packages/oc-template-react-compiler/scaffold/src/.eslintrc.json new file mode 100644 index 0000000..fcf5ef3 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["react-app"] +} diff --git a/packages/oc-template-react-compiler/scaffold/src/app.js b/packages/oc-template-react-compiler/scaffold/src/app.js deleted file mode 100644 index 34bbfe3..0000000 --- a/packages/oc-template-react-compiler/scaffold/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import styles from "./styles.css"; - -const App = props => -
-

- Hello {props.name} -

-
; - -export default App; diff --git a/packages/oc-template-react-compiler/scaffold/src/package.json b/packages/oc-template-react-compiler/scaffold/src/package.json index 025e976..fb538f3 100644 --- a/packages/oc-template-react-compiler/scaffold/src/package.json +++ b/packages/oc-template-react-compiler/scaffold/src/package.json @@ -2,25 +2,43 @@ "name": "base-component-react", "description": "", "version": "1.0.0", + "scripts": { + "lint": "eslint --max-warnings 0 --ext .js,.jsx,.ts,.tsx src", + "build": "tsc --noEmit && oc package .", + "test": "vitest" + }, "oc": { "files": { - "data": "server.js", + "data": "src/server.ts", "template": { - "src": "app.js", - "type": "oc-template-react" - } + "src": "src/App.tsx", + "type": "oc-template-typescript-react" + }, + "static": [ + "public" + ] }, "parameters": { - "name": { - "default": "World", - "description": "Your name", - "example": "Jane Doe", - "mandatory": false, - "type": "string" + "userId": { + "default": 0, + "description": "The user id from the user database", + "example": 0, + "mandatory": true, + "type": "number" } } }, + "dependencies": {}, "devDependencies": { - "oc-template-react-compiler": "*" + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.4.3", + "@types/react": "^18.0.28", + "jsdom": "^21.1.1", + "oc-template-typescript-react": "4.0.0", + "oc-template-typescript-react-compiler": "4.1.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "typescript": "5.0.2", + "vitest": "^0.29.7" } } diff --git a/packages/oc-template-react-compiler/scaffold/src/public/logo.png b/packages/oc-template-react-compiler/scaffold/src/public/logo.png new file mode 100644 index 0000000..5164af8 Binary files /dev/null and b/packages/oc-template-react-compiler/scaffold/src/public/logo.png differ diff --git a/packages/oc-template-react-compiler/scaffold/src/server.js b/packages/oc-template-react-compiler/scaffold/src/server.js deleted file mode 100644 index 0916566..0000000 --- a/packages/oc-template-react-compiler/scaffold/src/server.js +++ /dev/null @@ -1,4 +0,0 @@ -export const data = (context, callback) => { - const { name } = context.params; - return callback(null, { name }); -}; diff --git a/packages/oc-template-react-compiler/scaffold/src/src/App.test.tsx b/packages/oc-template-react-compiler/scaffold/src/src/App.test.tsx new file mode 100644 index 0000000..529aec0 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/App.test.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { describe, it, afterEach, beforeEach, expect, vi } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { DataProvider } from 'oc-template-typescript-react-compiler/utils/useData'; + +import App from './App'; + +const getData = vi.fn(); + +describe('App - Page', () => { + beforeEach(() => { + window.oc = { events: { on: vi.fn(), fire: vi.fn() } } as any; + }); + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('Gets more data when clicking the button', () => { + getData.mockImplementationOnce(() => Promise.resolve({})); + render( + + + + ); + + const extraInfoButton = screen.getByText(/Get extra information/i); + expect(extraInfoButton).toBeTruthy(); + }); +}); diff --git a/packages/oc-template-react-compiler/scaffold/src/src/App.tsx b/packages/oc-template-react-compiler/scaffold/src/src/App.tsx new file mode 100644 index 0000000..7bdda6b --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/App.tsx @@ -0,0 +1,51 @@ +import { useState } from 'react'; +import { useData } from 'oc-template-typescript-react-compiler/utils/useData'; +import styles from './styles.css'; +import type { AdditionalData, ClientProps } from './types'; + +interface AppProps extends ClientProps { + getMoreData?: boolean; +} + +const App: React.FC = () => { + const { firstName, lastName, userId, getData, getSetting } = useData(); + const staticPath = getSetting('staticPath'); + const [additionalData, setAdditionalData] = useState(null); + const [error, setError] = useState(''); + + const fetchMoreData = async () => { + setError(''); + try { + const data = await getData({ userId, getMoreData: true }); + setAdditionalData(data); + } catch (err) { + setError(String(err)); + } + }; + + if (error) { + return
Something wrong happened!
; + } + + return ( +
+ Logo +

+ Hello, {firstName} {lastName} +

+ {additionalData && ( +
+
Age: {additionalData.age}
+
+ Hobbies: {additionalData.hobbies.map((x) => x.toLowerCase()).join(', ')} +
+
+ )} + +
+ ); +}; + +export default App; diff --git a/packages/oc-template-react-compiler/scaffold/src/src/oc-app.d.ts b/packages/oc-template-react-compiler/scaffold/src/src/oc-app.d.ts new file mode 100644 index 0000000..f5feb5e --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/oc-app.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/packages/oc-template-react-compiler/scaffold/src/src/server.ts b/packages/oc-template-react-compiler/scaffold/src/src/server.ts new file mode 100644 index 0000000..3678ee2 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/server.ts @@ -0,0 +1,34 @@ +import { Context } from 'oc-template-typescript-react-compiler'; +import { AdditionalData, ClientProps, OcParameters } from './types'; + +const database = [ + { name: 'John Doe', age: 34, hobbies: ['Swimming', 'Basketball'] }, + { name: 'Jane Doe', age: 35, hobbies: ['Running', 'Rugby'] } +]; + +async function getUser(userId: number) { + return database[userId]; +} + +export async function data( + context: Context, + callback: (error: any, data: ClientProps | AdditionalData) => void +) { + const { userId } = context.params; + const user = await getUser(userId); + const shouldGetMoreData = context.params.getMoreData; + const [firstName, lastName] = user.name.split(/\s+/); + + if (shouldGetMoreData) { + return callback(null, { + age: user.age, + hobbies: user.hobbies + }); + } + + return callback(null, { + userId, + firstName, + lastName + }); +} diff --git a/packages/oc-template-react-compiler/scaffold/src/src/setupTests.js b/packages/oc-template-react-compiler/scaffold/src/src/setupTests.js new file mode 100644 index 0000000..7b0828b --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/setupTests.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/oc-template-react-compiler/scaffold/src/src/styles.css b/packages/oc-template-react-compiler/scaffold/src/src/styles.css new file mode 100644 index 0000000..a50ed9d --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/styles.css @@ -0,0 +1,30 @@ +.container { + background-color: #3b246c; + color: #fff; + font-family: sans-serif; + padding: 40px; +} + +.button { + background-color: #a97613; + border: none; + padding: 15px 32px; + text-align: center; + font-size: 16px; + text-decoration: none; + display: inline-block; + color: inherit; + cursor: pointer; +} + +.info { + margin-bottom: 20px; +} + +.block { + margin: 6px 0; +} + +.button:hover { + background-color: #c79535; +} diff --git a/packages/oc-template-react-compiler/scaffold/src/src/types.ts b/packages/oc-template-react-compiler/scaffold/src/src/types.ts new file mode 100644 index 0000000..ac66271 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/src/types.ts @@ -0,0 +1,15 @@ +export interface OcParameters { + userId: number; + getMoreData?: boolean; +} + +export interface AdditionalData { + hobbies: string[]; + age: number; +} + +export interface ClientProps extends Partial { + userId: number; + firstName: string; + lastName: string; +} diff --git a/packages/oc-template-react-compiler/scaffold/src/styles.css b/packages/oc-template-react-compiler/scaffold/src/styles.css deleted file mode 100644 index 2ece34e..0000000 --- a/packages/oc-template-react-compiler/scaffold/src/styles.css +++ /dev/null @@ -1,4 +0,0 @@ -.special { - background: palevioletred; - color: white; -} \ No newline at end of file diff --git a/packages/oc-template-react-compiler/scaffold/src/tsconfig.json b/packages/oc-template-react-compiler/scaffold/src/tsconfig.json new file mode 100644 index 0000000..bf369f9 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + // These are suggested values and will be set when not present in the + // tsconfig.json + // 'parsedValue' matches the output value from ts.parseJsonConfigFileContent() + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + + // These values are required and cannot be changed by the user + // Keep this in sync with the rollup config + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["src/_package"] +} diff --git a/packages/oc-template-react-compiler/scaffold/src/vite.config.ts b/packages/oc-template-react-compiler/scaffold/src/vite.config.ts new file mode 100644 index 0000000..69008b4 --- /dev/null +++ b/packages/oc-template-react-compiler/scaffold/src/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + // @ts-ignore Missing test property + test: { + environment: 'jsdom' + } +}); diff --git a/packages/oc-template-react-compiler/utils/.eslintrc.js b/packages/oc-template-react-compiler/utils/.eslintrc.js new file mode 100644 index 0000000..902fe70 --- /dev/null +++ b/packages/oc-template-react-compiler/utils/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + env: { + commonjs: false + }, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + extends: '../../../.eslintrc.js' +}; diff --git a/packages/oc-template-react-compiler/utils/index.js b/packages/oc-template-react-compiler/utils/index.js deleted file mode 100644 index 2615bd2..0000000 --- a/packages/oc-template-react-compiler/utils/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import withDataProvider from "./withDataProvider"; -import withSettingProvider from "./withSettingProvider"; - -export { withDataProvider, withSettingProvider }; diff --git a/packages/oc-template-react-compiler/utils/useData.tsx b/packages/oc-template-react-compiler/utils/useData.tsx new file mode 100644 index 0000000..44b1e57 --- /dev/null +++ b/packages/oc-template-react-compiler/utils/useData.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +const DataContext = React.createContext({}); + +type Data = T & { + getData(parameters: T, cb: (err: Error | null, data: any) => void): void; + getSetting(setting: 'name' | 'version' | 'baseUrl' | 'staticPath'): string; +}; + +type PromiseData = T & { + getData(parameters?: Partial): Promise; + getSetting(setting: 'name' | 'version' | 'baseUrl' | 'staticPath'): string; +}; + +export const DataProvider = ({ children, ...props }: any) => { + return {children}; +}; + +export function useData(): PromiseData { + const { + value: { getData, ...rest } + }: { value: Data } = React.useContext(DataContext); + const asyncGetData = React.useCallback((data: I) => { + return new Promise((resolve, reject) => { + // @ts-ignore + getData({ ...rest, ...data }, (err, newData) => { + if (err) { + reject(err); + } else { + resolve(newData); + } + }); + }); + }, []); + + // @ts-ignore + return { getData: asyncGetData, ...rest }; +} diff --git a/packages/oc-template-react-compiler/utils/withDataProvider.js b/packages/oc-template-react-compiler/utils/withDataProvider.js deleted file mode 100644 index b53bbae..0000000 --- a/packages/oc-template-react-compiler/utils/withDataProvider.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -const withDataProvider = BaseComponent => { - const Enhanced = (props, context) => { - const propsWithGetData = { - ...props, - getData: context.getData - }; - - return ; - }; - - Enhanced.contextTypes = { - getData: PropTypes.func - }; - - return Enhanced; -}; - -export default withDataProvider; diff --git a/packages/oc-template-react-compiler/utils/withSettingProvider.js b/packages/oc-template-react-compiler/utils/withSettingProvider.js deleted file mode 100644 index b4fda43..0000000 --- a/packages/oc-template-react-compiler/utils/withSettingProvider.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -const withSettingProvider = BaseComponent => { - const Enhanced = (props, context) => { - const propsWithGetSetting = { - ...props, - getSetting: context.getSetting - }; - - return ; - }; - - Enhanced.contextTypes = { - getSetting: PropTypes.func - }; - - return Enhanced; -}; - -export default withSettingProvider;