diff --git a/packages/apps/doctor/config/babel.config.js b/packages/apps/doctor/config/babel.config.js new file mode 100644 index 00000000..f4aa8efb --- /dev/null +++ b/packages/apps/doctor/config/babel.config.js @@ -0,0 +1 @@ +module.exports = require('babel-preset-enact'); diff --git a/packages/apps/doctor/config/corejs-proxy.js b/packages/apps/doctor/config/corejs-proxy.js new file mode 100644 index 00000000..ac803a5a --- /dev/null +++ b/packages/apps/doctor/config/corejs-proxy.js @@ -0,0 +1,17 @@ +/* + * corejs-proxy.js + * + * For babel-preset-env with "useBuiltin":"entry", it requires that the + * require('core-js') expression be at the module level for it to + * be transpiled into the individual core-js polyfills. This proxy module + * allows for dynamic core-js usage while still using the individual feature + * transforms. + */ + +// Apply stable core-js polyfills +require('core-js/stable'); + +// Manually set global._babelPolyfill as a flag to avoid multiple loading. +// Uses 'babelPolyfill' name for historical meaning and external/backward +// compatibility. +global._babelPolyfill = true; diff --git a/packages/apps/doctor/config/createEnvironmentHash.js b/packages/apps/doctor/config/createEnvironmentHash.js new file mode 100644 index 00000000..d117a2cb --- /dev/null +++ b/packages/apps/doctor/config/createEnvironmentHash.js @@ -0,0 +1,9 @@ +'use strict'; +const {createHash} = require('crypto'); + +module.exports = env => { + const hash = createHash('md5'); + hash.update(JSON.stringify(env)); + + return hash.digest('hex'); +}; diff --git a/packages/apps/doctor/config/custom-skin-template.ejs b/packages/apps/doctor/config/custom-skin-template.ejs new file mode 100644 index 00000000..803d7b26 --- /dev/null +++ b/packages/apps/doctor/config/custom-skin-template.ejs @@ -0,0 +1,14 @@ + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + +
+ + diff --git a/packages/apps/doctor/config/dotenv.js b/packages/apps/doctor/config/dotenv.js new file mode 100644 index 00000000..5482c24c --- /dev/null +++ b/packages/apps/doctor/config/dotenv.js @@ -0,0 +1,29 @@ +/* eslint-env node, es6 */ + +const fs = require('fs'); +const path = require('path'); +const dotenv = require('dotenv'); +const {expand} = require('dotenv-expand'); + +// Loads all required .env files in correct order, for a given mode. +// See https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +module.exports = { + load: function (context) { + const mode = process.env.NODE_ENV || 'development'; + [ + `.env.${mode}.local`, + // Similar to create-react app, don't include `.env.local` for + // `test` environment for test result consistency. + mode !== 'test' && `.env.local`, + `.env.${mode}`, + '.env' + ] + .filter(Boolean) + .map(env => path.join(context, env)) + .forEach(env => { + if (fs.existsSync(env)) { + expand(dotenv.config({path: env})); + } + }); + } +}; diff --git a/packages/apps/doctor/config/html-template.ejs b/packages/apps/doctor/config/html-template.ejs new file mode 100644 index 00000000..4f5a4edc --- /dev/null +++ b/packages/apps/doctor/config/html-template.ejs @@ -0,0 +1,12 @@ + + + + + + + <%= htmlWebpackPlugin.options.title %> + + +
+ + diff --git a/packages/apps/doctor/config/jest/babelTransform.js b/packages/apps/doctor/config/jest/babelTransform.js new file mode 100644 index 00000000..1212a7b6 --- /dev/null +++ b/packages/apps/doctor/config/jest/babelTransform.js @@ -0,0 +1,23 @@ +/** + * Portions of this source code file are from create-react-app, used under the + * following MIT license: + * + * Copyright (c) 2014-present, Facebook, Inc. + * https://github.com/facebook/create-react-app + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const path = require('path'); +const babelJest = require('babel-jest').default; + +module.exports = babelJest.createTransformer({ + extends: path.join(__dirname, '..', 'babel.config.js'), + plugins: [ + require.resolve('@babel/plugin-transform-modules-commonjs'), + require.resolve('babel-plugin-dynamic-import-node') + ], + babelrc: false, + configFile: false +}); diff --git a/packages/apps/doctor/config/jest/cssTransform.js b/packages/apps/doctor/config/jest/cssTransform.js new file mode 100644 index 00000000..0410812c --- /dev/null +++ b/packages/apps/doctor/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + } +}; diff --git a/packages/apps/doctor/config/jest/fileTransform.js b/packages/apps/doctor/config/jest/fileTransform.js new file mode 100644 index 00000000..8f6eeba6 --- /dev/null +++ b/packages/apps/doctor/config/jest/fileTransform.js @@ -0,0 +1,13 @@ +'use strict'; + +const path = require('path'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + return `module.exports = ${assetFilename};`; + } +}; diff --git a/packages/apps/doctor/config/jest/jest.config.js b/packages/apps/doctor/config/jest/jest.config.js new file mode 100644 index 00000000..83cead09 --- /dev/null +++ b/packages/apps/doctor/config/jest/jest.config.js @@ -0,0 +1,101 @@ +/** + * Portions of this source code file are from create-react-app, used under the + * following MIT license: + * + * Copyright (c) 2015-present, Facebook, Inc. + * https://github.com/facebook/create-react-app + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +const fs = require('fs'); +const path = require('path'); +const {optionParser: app} = require('@enact/dev-utils'); + +const rbConst = name => + 'ILIB_' + + path + .basename(name) + .replace(/[-_\s]/g, '_') + .toUpperCase() + + '_PATH'; + +const iLibDirs = ['node_modules/@enact/i18n/ilib', 'node_modules/ilib', 'ilib']; +const globals = { + __DEV__: true, + ILIB_BASE_PATH: iLibDirs.find(f => fs.existsSync(path.join(app.context, f))) || iLibDirs[1], + ILIB_RESOURCES_PATH: 'resources', + ILIB_CACHE_ID: new Date().getTime() + '', + [rbConst(app.name)]: 'resources' +}; + +for (let t = app.theme; t; t = t.theme) { + const themeRB = path.join(t.path, 'resources'); + globals[rbConst(t.name)] = path.relative(app.context, themeRB).replace(/\\/g, '/'); +} + +const ignorePatterns = [ + // Common directories to ignore + '/node_modules/', + '/(.*/)*coverage/', + '/(.*/)*build/', + '/(.*/)*dist/', + '/(.*/)*docs/', + '/(.*/)*samples/', + '/(.*/)*tests/screenshot/', + '/(.*/)*tests/ui/' +]; + +// Setup env var to signify a testing environment +process.env.BABEL_ENV = 'test'; +process.env.NODE_ENV = 'test'; +process.env.PUBLIC_URL = ''; +process.env.BROWSERSLIST = 'current node'; + +// Load applicable .env files into environment variables. +require('../dotenv').load(app.context); + +// Find any applicable user test setup file +const userSetupFile = ['mjs', 'js', 'jsx', 'ts', 'tsx'] + .map(ext => path.join(app.context, 'src', 'setupTests.' + ext)) + .find(file => fs.existsSync(file)); + +module.exports = { + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], + coveragePathIgnorePatterns: ignorePatterns, + setupFiles: [require.resolve('../polyfills')], + setupFilesAfterEnv: [require.resolve('./setupTests'), userSetupFile].filter(Boolean), + testMatch: [ + '/**/__tests__/**/*.{js,jsx,ts,tsx}', + '/**/*.+(spec|test).{js,jsx,ts,tsx}', + '/**/*-specs.{js,jsx,ts,tsx}' + ], + testPathIgnorePatterns: ignorePatterns, + testEnvironment: 'jsdom', + testEnvironmentOptions: {pretendToBeVisual: true}, + testURL: 'http://localhost', + transform: { + '^.+\\.(js|jsx|ts|tsx)$': require.resolve('./babelTransform'), + '^.+\\.(css|less|sass|scss)$': require.resolve('./cssTransform.js'), + '^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|less|sass|scss|json)$)': require.resolve('./fileTransform') + }, + transformIgnorePatterns: [ + '[/\\\\]node_modules[/\\\\](?!@enact).+\\.(js|jsx|mjs|cjs|ts|tsx)$', + '^.+\\.module\\.(css|less|sass|scss)$' + ], + moduleNameMapper: { + '^.+\\.module\\.(css|less|sass|scss)$': require.resolve('identity-obj-proxy'), + '^@testing-library/jest-dom$': require.resolve('@testing-library/jest-dom'), + '^@testing-library/react$': require.resolve('@testing-library/react'), + '^@testing-library/user-event$': require.resolve('@testing-library/user-event'), + '^react$': require.resolve('react'), + // Backward compatibility for new iLib location with old Enact + '^ilib[/](.*)$': path.join(app.context, globals.ILIB_BASE_PATH, '$1'), + // Backward compatibility for old iLib location with new Enact + '^@enact[/]i18n[/]ilib[/](.*)$': path.join(app.context, globals.ILIB_BASE_PATH, '$1') + }, + moduleFileExtensions: ['js', 'jsx', 'json', 'ts', 'tsx'], + globals, + watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'].map(m => require.resolve(m)), + resetMocks: true +}; diff --git a/packages/apps/doctor/config/jest/setupTests.js b/packages/apps/doctor/config/jest/setupTests.js new file mode 100644 index 00000000..227e44cf --- /dev/null +++ b/packages/apps/doctor/config/jest/setupTests.js @@ -0,0 +1,103 @@ +/* eslint-env jest */ +const fs = require('fs'); +const path = require('path'); +const {packageRoot} = require('@enact/dev-utils'); + +const filters = [ + 'Invalid prop', + 'Failed prop type', + 'Unknown prop', + 'non-boolean attribute', + 'Received NaN', + 'Invalid value', + 'React does not recognize', + 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut', + 'Invalid event handler property', + 'Unknown event handler property', + 'Directly setting property `innerHTML` is not permitted', + 'The `aria` attribute is reserved for future use in ', + 'for a string attribute `is`. If this is expected, cast', + 'Invalid DOM property' +]; +const filterExp = new RegExp('(' + filters.join('|') + ')'); + +// Configure proptype & react error checking on the console. + +beforeEach(() => { + jest.spyOn(console, 'warn'); + jest.spyOn(console, 'error'); +}); + +afterEach(() => { + const actual = (console.warn.mock ? console.warn.mock.calls : []) + .concat(console.error.mock ? console.error.mock.calls : []) + .filter(([m]) => filterExp.test(m)); + const expected = 0; + + if (console.warn.mock) { + console.warn.mockRestore(); + } + if (console.error.mock) { + console.error.mockRestore(); + } + + expect(actual).toHaveLength(expected); +}); + +// Set initial resolution to VGA, similar to PhantomJS. +// Will ideally want to use a more modern resolution later. + +global.innerHeight = 640; +global.innerWidth = 480; + +// Support local file sync XHR to support iLib loading. + +const ilibPaths = Object.keys(global).filter(k => /ILIB_[^_]+_PATH/.test(k)); +const pkg = packageRoot(); +const XHR = global.XMLHttpRequest; +class ILibXHR extends XHR { + open(method, url) { + if (ilibPaths.some(p => url.startsWith(global[p]))) { + this.send = () => { + try { + const file = path.join(pkg.path, url.replace(/\//g, path.sep)); + this.fileText = fs.readFileSync(file, {encoding: 'utf8'}); + this.fileStatus = 200; + } catch (e) { + this.fileText = ''; + this.fileStatus = 404; + } + this.dispatchEvent(new global.Event('readystatechange')); + this.dispatchEvent(new global.ProgressEvent('load')); + this.dispatchEvent(new global.ProgressEvent('loadend')); + }; + } else { + return super.open(...arguments); + } + } + get readyState() { + return typeof this.fileStatus !== 'undefined' ? XHR.DONE : super.readyState; + } + get status() { + return typeof this.fileStatus !== 'undefined' ? this.fileStatus : super.status; + } + get responseText() { + return typeof this.fileText !== 'undefined' ? this.fileText : super.responseText; + } +} +global.XMLHttpRequest = ILibXHR; + +beforeEach(() => { + global.Element.prototype.animate = jest.fn().mockImplementation(() => { + const animation = { + onfinish: null, + cancel: () => { + if (animation.onfinish) animation.onfinish(); + }, + finish: () => { + if (animation.onfinish) animation.onfinish(); + } + }; + return animation; + }); +}); diff --git a/packages/apps/doctor/config/polyfills.js b/packages/apps/doctor/config/polyfills.js new file mode 100644 index 00000000..81218ead --- /dev/null +++ b/packages/apps/doctor/config/polyfills.js @@ -0,0 +1,26 @@ +/* eslint no-var: off, no-extend-native: off */ +/* + * polyfills.js + * + * Any polyfills or code required prior to loading the app. + */ + +if (!global.skipPolyfills && !global._babelPolyfill) { + // Temporarily remap [Array].toLocaleString to [Array].toString. + // Fixes an issue with loading the polyfills within the v8 snapshot environment + // where toLocaleString() within the TypedArray polyfills causes snapshot failure. + var origToLocaleString = Array.prototype.toLocaleString, + origTypedToLocaleString; + Array.prototype.toLocaleString = Array.prototype.toString; + if (global.Int8Array && Int8Array.prototype.toLocaleString) { + origTypedToLocaleString = Int8Array.prototype.toLocaleString; + Int8Array.prototype.toLocaleString = Int8Array.prototype.toString; + } + + // Apply core-js polyfills + require('./corejs-proxy'); + + // Restore real [Array].toLocaleString for runtime usage. + if (origToLocaleString) Array.prototype.toLocaleString = origToLocaleString; + if (origTypedToLocaleString) Int8Array.prototype.toLocaleString = origTypedToLocaleString; +} diff --git a/packages/apps/doctor/config/webpack.config.js b/packages/apps/doctor/config/webpack.config.js new file mode 100644 index 00000000..ebca82a9 --- /dev/null +++ b/packages/apps/doctor/config/webpack.config.js @@ -0,0 +1,606 @@ +/* eslint-env node, es6 */ + +const fs = require('fs'); +const path = require('path'); +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); +const ESLintPlugin = require('eslint-webpack-plugin'); +const ForkTsCheckerWebpackPlugin = + process.env.TSC_COMPILE_ON_ERROR === 'true' + ? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin') + : require('react-dev-utils/ForkTsCheckerWebpackPlugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); +const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); +const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); +const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin'); +const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); +const resolve = require('resolve'); +const TerserPlugin = require('terser-webpack-plugin'); +const { DefinePlugin, EnvironmentPlugin } = require('webpack'); +const { + optionParser: app, + cssModuleIdent: getSimpleCSSModuleLocalIdent, + GracefulFsPlugin, + ILibPlugin, + WebOSMetaPlugin, +} = require('@enact/dev-utils'); +const createEnvironmentHash = require('./createEnvironmentHash'); + +// This is the production and development configuration. +// It is focused on developer experience, fast rebuilds, and a minimal bundle. +module.exports = function ( + env, + isomorphic = false, + ilibAdditionalResourcesPath, +) { + process.chdir(app.context); + + // Load applicable .env files into environment variables. + require('./dotenv').load(app.context); + + // Sets the browserslist default fallback set of browsers to the Enact default browser support list. + app.setEnactTargetsAsDefault(); + + // Check if JSX transform is able + const hasJsxRuntime = (() => { + if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { + return false; + } + + try { + require.resolve('react/jsx-runtime'); + return true; + } catch (e) { + return false; + } + })(); + + // Check if TypeScript is setup + const useTypeScript = fs.existsSync('tsconfig.json'); + + // Check if Tailwind config exists + const useTailwind = fs.existsSync( + path.join(app.context, 'tailwind.config.js'), + ); + + process.env.NODE_ENV = env || process.env.NODE_ENV; + const isEnvProduction = process.env.NODE_ENV === 'production'; + + const publicPath = getPublicUrlOrPath( + !isEnvProduction, + app.publicUrl, + process.env.PUBLIC_URL, + ).replace(/^\/$/, ''); + + // Source maps are resource heavy and can cause out of memory issue for large source files. + // By default, sourcemaps will be used in development, however it can universally forced + // on or off by setting the GENERATE_SOURCEMAP environment variable. + const GENERATE_SOURCEMAP = + process.env.GENERATE_SOURCEMAP || (isEnvProduction ? 'false' : 'true'); + const shouldUseSourceMap = GENERATE_SOURCEMAP !== 'false'; + + const getLocalIdent = + process.env.SIMPLE_CSS_IDENT !== 'false' + ? getSimpleCSSModuleLocalIdent + : getCSSModuleLocalIdent; + + // common function to get style loaders + const getStyleLoaders = (cssLoaderOptions = {}, preProcessor) => { + // Multiple styling-support features are used together, bottom-to-top. + // An optonal preprocessor, like "less loader", compiles LESS syntax into CSS. + // "postcss" loader applies autoprefixer to our CSS. + // "css" loader resolves paths in CSS and adds assets as dependencies. + // `MiniCssExtractPlugin` takes the resulting CSS and puts it into an + // external file in our build process. If you use code splitting, any async + // bundles will stilluse the "style" loader inside the async code so CSS + // from them won't be in the main CSS file. + // When INLINE_STYLES env var is set, instead of MiniCssExtractPlugin, uses + // `style` loader to dynamically inline CSS in style tags at runtime. + const loaders = [ + process.env.INLINE_STYLES + ? require.resolve('style-loader') + : MiniCssExtractPlugin.loader, + { + loader: require.resolve('css-loader'), + options: Object.assign( + { sourceMap: shouldUseSourceMap }, + cssLoaderOptions, + { + url: { + filter: url => { + // Don't handle absolute path urls + if (url.startsWith('/')) { + return false; + } + + return true; + }, + }, + }, + ), + }, + { + // Options for PostCSS as we reference these options twice + // Adds vendor prefixing based on your specified browser support in + // package.json + loader: require.resolve('postcss-loader'), + options: { + postcssOptions: { + // Necessary for external CSS imports to work + // https://github.com/facebook/create-react-app/issues/2677 + ident: 'postcss', + plugins: [ + useTailwind && 'tailwindcss', + // Fix and adjust for known flexbox issues + // See https://github.com/philipwalton/flexbugs + 'postcss-flexbugs-fixes', + // Support @global-import syntax to import css in a global context. + 'postcss-global-import', + // Transpile stage-3 CSS standards based on browserslist targets. + // See https://preset-env.cssdb.org/features for supported features. + // Includes support for targetted auto-prefixing. + [ + 'postcss-preset-env', + { + autoprefixer: { + flexbox: 'no-2009', + remove: false, + }, + stage: 3, + features: { 'custom-properties': false }, + }, + ], + // Adds PostCSS Normalize to standardize browser quirks based on + // the browserslist targets. + !useTailwind && require('postcss-normalize'), + // Resolution indepedence support + app.ri !== false && + require('postcss-resolution-independence')(app.ri), + ].filter(Boolean), + }, + sourceMap: shouldUseSourceMap, + }, + }, + ]; + if (preProcessor) { + loaders.push(preProcessor); + } + return loaders; + }; + + const getLessStyleLoaders = cssLoaderOptions => + getStyleLoaders(cssLoaderOptions, { + loader: require.resolve('less-loader'), + options: { + lessOptions: { + modifyVars: Object.assign({ __DEV__: !isEnvProduction }, app.accent), + }, + sourceMap: shouldUseSourceMap, + }, + }); + + const getScssStyleLoaders = cssLoaderOptions => + getStyleLoaders(cssLoaderOptions, { + loader: require.resolve('sass-loader'), + options: { + sourceMap: shouldUseSourceMap, + }, + }); + + const getAdditionalModulePaths = paths => { + if (!paths) return []; + return Array.isArray(paths) ? paths : [paths]; + }; + + return { + mode: isEnvProduction ? 'production' : 'development', + // Don't attempt to continue if there are any errors. + bail: true, + // Webpack noise constrained to errors and warnings + stats: 'errors-warnings', + // Use source maps during development builds or when specified by GENERATE_SOURCEMAP + devtool: + shouldUseSourceMap && + (isEnvProduction ? 'source-map' : 'cheap-module-source-map'), + // These are the "entry points" to our application. + entry: { + main: [ + // Include any polyfills needed for the target browsers. + require.resolve('./polyfills'), + // This is your app's code + app.context, + ], + }, + output: { + // The build output directory. + path: path.resolve('./dist'), + // Generated JS file names (with nested folders). + // There will be one main bundle, and one file per asynchronous chunk. + // We don't currently advertise code splitting but Webpack supports it. + filename: '[name].js', + // There are also additional JS chunk files if you use code splitting. + chunkFilename: 'chunk.[name].js', + assetModuleFilename: '[path][name][ext]', + // Add /* filename */ comments to generated require()s in the output. + pathinfo: !isEnvProduction, + publicPath, + // Improved sourcemap path name mapping for system filepaths + devtoolModuleFilenameTemplate: info => { + let file = isEnvProduction + ? path.relative(app.context, info.absoluteResourcePath) + : path.resolve(info.absoluteResourcePath); + file = file.replace(/\\/g, '/').replace(/\.\./g, '_'); + const loader = info.allLoaders.match(/[^\\/]+-loader/); + if (info.resource.includes('.less') && loader) { + // Temporary special handling for LESS files. The css-loader will + // output absolute-path mapped LESS sourcemaps, unaffected by this + // function, while both css-loader and style-loader pseudo modules + // will get their own sourcemaps. Good to differentiate. + return file + '?' + loader[0]; + } else { + return file; + } + }, + }, + cache: { + type: 'filesystem', + version: createEnvironmentHash(Object.keys(process.env)), + cacheDirectory: path.resolve('./node_modules/.cache'), + store: 'pack', + buildDependencies: { + defaultWebpack: ['webpack/lib/'], + config: [__filename], + tsconfig: useTypeScript ? ['tsconfig.json'] : [], + }, + }, + infrastructureLogging: { + level: 'none', + }, + ignoreWarnings: [ + // We ignore 'Module not found' warnings from SnapshotPlugin + { + module: /SnapshotPlugin/, + message: /Module not found/, + }, + ], + resolve: { + // These are the reasonable defaults supported by the React/ES6 ecosystem. + extensions: ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.json'].filter( + ext => useTypeScript || !ext.includes('ts'), + ), + // Allows us to specify paths to check for module resolving. + modules: [ + path.resolve('./node_modules'), + 'node_modules', + ...getAdditionalModulePaths(app.additionalModulePaths), + ], + // Don't resolve symlinks to their underlying paths + symlinks: false, + // Backward compatibility for apps using new ilib references with old Enact + // and old apps referencing old iLib location with new Enact + alias: fs.existsSync( + path.join(app.context, 'node_modules', '@enact', 'i18n', 'ilib'), + ) + ? Object.assign({ ilib: '@enact/i18n/ilib' }, app.alias) + : Object.assign({ '@enact/i18n/ilib': 'ilib' }, app.alias), + // Optional configuration for redirecting module requests. + fallback: app.resolveFallback, + }, + module: { + rules: [ + shouldUseSourceMap && { + enforce: 'pre', + exclude: /@babel(?:\/|\\{1,2})runtime/, + test: /\.(js|mjs|jsx|ts|tsx|css)$/, + loader: require.resolve('source-map-loader'), + }, + { + // "oneOf" will traverse all following loaders until one will + // match the requirements. When no loader matches it will fall + // back to the "file" loader at the end of the loader list. + oneOf: [ + // Process JS with Babel. + { + test: /\.(js|mjs|jsx|ts|tsx)$/, + exclude: /node_modules.(?!@enact)/, + loader: require.resolve('babel-loader'), + options: { + configFile: path.join(__dirname, 'babel.config.js'), + babelrc: false, + // This is a feature of `babel-loader` for webpack (not Babel itself). + // It enables caching results in ./node_modules/.cache/babel-loader/ + // directory for faster rebuilds. + cacheDirectory: !isEnvProduction, + cacheCompression: false, + compact: isEnvProduction, + }, + }, + // Style-based rules support both LESS and CSS format, with *.module.* extension format + // to designate CSS modular support. + // See comments within `getStyleLoaders` for details on the stylesheet loader chains and + // options used at each level of processing. + { + test: /\.module\.css$/, + use: getStyleLoaders({ + importLoaders: 1, + modules: { + getLocalIdent, + }, + }), + }, + { + test: /\.css$/, + // The `forceCSSModules` Enact build option can be set true to universally apply + // modular CSS support. + use: getStyleLoaders({ + importLoaders: 1, + modules: { + ...(app.forceCSSModules + ? { getLocalIdent } + : { mode: 'icss' }), + }, + }), + // Don't consider CSS imports dead code even if the + // containing package claims to have no side effects. + // Remove this when webpack adds a warning or an error for this. + // See https://github.com/webpack/webpack/issues/6571 + sideEffects: true, + }, + { + test: /\.module\.less$/, + use: getLessStyleLoaders({ + importLoaders: 2, + modules: { + getLocalIdent, + }, + }), + }, + { + test: /\.less$/, + use: getLessStyleLoaders({ + importLoaders: 2, + modules: { + ...(app.forceCSSModules + ? { getLocalIdent } + : { mode: 'icss' }), + }, + }), + sideEffects: true, + }, + // Opt-in support for CSS Modules, but using SASS + // using the extension .module.scss or .module.sass + { + test: /\.module\.(scss|sass)$/, + use: getScssStyleLoaders({ + importLoaders: 3, + modules: { + getLocalIdent, + }, + }), + }, + // Opt-in support for SASS (using .scss or .sass extensions) + { + test: /\.(scss|sass)$/, + use: getScssStyleLoaders({ + importLoaders: 3, + modules: { + ...(app.forceCSSModules + ? { getLocalIdent } + : { mode: 'icss' }), + }, + }), + }, + // "file" loader handles on all files not caught by the above loaders. + // When you `import` an asset, you get its output filename and the file + // is copied during the build process. + { + // Exclude `js` files to keep "css" loader working as it injects + // its runtime that would otherwise be processed through "file" loader. + // Also exclude `html` and `json` extensions so they get processed + // by webpacks internal loaders. + // Exclude `ejs` HTML templating language as that's handled by + // the HtmlWebpackPlugin. + exclude: [ + /^$/, + /\.(js|mjs|jsx|ts|tsx)$/, + /\.html$/, + /\.ejs$/, + /\.json$/, + ], + type: 'asset/resource', + }, + // ** STOP ** Are you adding a new loader? + // Make sure to add the new loader(s) before the "file" loader. + { + test: /\.(js|mjs|jsx|ts|tsx)$/, + use: [ + { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + ], + }, + }, + ], + }, + ], + }, + ].filter(Boolean), + }, + // Target app to build for a specific environment (default 'browserslist') + target: app.environment, + // Optional configuration for polyfilling NodeJS built-ins. + node: app.nodeBuiltins, + performance: false, + optimization: { + minimize: isEnvProduction, + // These are only used in production mode + minimizer: [ + new TerserPlugin({ + terserOptions: { + parse: { + // we want uglify-js to parse ecma 8 code. However, we don't want it + // to apply any minfication steps that turns valid ecma 5 code + // into invalid ecma 5 code. This is why the 'compress' and 'output' + // sections only apply transformations that are ecma 5 safe + // https://github.com/facebook/create-react-app/pull/4234 + ecma: 8, + }, + compress: { + ecma: 5, + warnings: false, + // Disabled because of an issue with Uglify breaking seemingly valid code: + // https://github.com/facebook/create-react-app/issues/2376 + // Pending further investigation: + // https://github.com/mishoo/UglifyJS2/issues/2011 + comparisons: false, + // Disabled because of an issue with Terser breaking valid code: + // https://github.com/facebook/create-react-app/issues/5250 + // Pending futher investigation: + // https://github.com/terser-js/terser/issues/120 + inline: 2, + }, + mangle: { + safari10: true, + }, + output: { + ecma: 5, + comments: false, + // Turned on because emoji and regex is not minified properly using default + // https://github.com/facebook/create-react-app/issues/2488 + ascii_only: true, + }, + }, + // Use multi-process parallel running to improve the build speed + // Default number of concurrent runs: os.cpus().length - 1 + parallel: true, + }), + new CssMinimizerPlugin(), + ], + }, + plugins: [ + // Generates an `index.html` file with the js and css tags injected. + new HtmlWebpackPlugin({ + // Title can be specified in the package.json enact options or will + // be determined automatically from any appinfo.json files discovered. + title: app.title || '', + inject: 'body', + template: app.template || path.join(__dirname, 'html-template.ejs'), + xhtml: true, + minify: isEnvProduction && { + removeComments: true, + collapseWhitespace: false, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + }, + }), + // Make NODE_ENV environment variable available to the JS code, for example: + // if (process.env.NODE_ENV === 'production') { ... }. + // It is absolutely essential that NODE_ENV was set to production here. + // Otherwise React will be compiled in the very slow development mode. + new DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify( + isEnvProduction ? 'production' : 'development', + ), + 'process.env.PUBLIC_URL': JSON.stringify(publicPath), + // Define ENACT_PACK_ISOMORPHIC global variable to determine to use + // `hydrateRoot` for isomorphic build and `createRoot` for non-isomorphic build by app. + ENACT_PACK_ISOMORPHIC: isomorphic, + }), + // Inject prefixed environment variables within code, when used + new EnvironmentPlugin( + Object.keys(process.env).filter(key => + /^(REACT_APP|WDS_SOCKET)/.test(key), + ), + ), + // Note: this won't work without MiniCssExtractPlugin.loader in `loaders`. + !process.env.INLINE_STYLES && + new MiniCssExtractPlugin({ + filename: '[name].css', + chunkFilename: 'chunk.[name].css', + }), + // Webpack5 removed node polyfills but we need this to run screenshot tests + new NodePolyfillPlugin(), + // Provide meaningful information when modules are not found + new ModuleNotFoundPlugin(app.context), + // Ensure correct casing in module filepathes + new CaseSensitivePathsPlugin(), + // Switch the internal NodeOutputFilesystem to use graceful-fs to avoid + // EMFILE errors when hanndling mass amounts of files at once, such as + // what happens when using ilib bundles/resources. + new GracefulFsPlugin(), + // Automatically configure iLib library within @enact/i18n. Additionally, + // ensure the locale data files and the resource files are copied during + // the build to the output directory. + new ILibPlugin({ + publicPath, + symlinks: false, + ilibAdditionalResourcesPath, + }), + // Automatically detect ./appinfo.json and ./webos-meta/appinfo.json files, + // and parses any to copy over any webOS meta assets at build time. + new WebOSMetaPlugin({ htmlPlugin: HtmlWebpackPlugin }), + // TypeScript type checking + useTypeScript && + new ForkTsCheckerWebpackPlugin({ + async: !isEnvProduction, + typescript: { + typescriptPath: resolve.sync('typescript', { + basedir: 'node_modules', + }), + configOverwrite: { + compilerOptions: { + sourceMap: shouldUseSourceMap, + skipLibCheck: true, + inlineSourceMap: false, + declarationMap: false, + noEmit: true, + incremental: true, + tsBuildInfoFile: 'node_modules/.cache/tsconfig.tsbuildinfo', + }, + }, + context: app.context, + diagnosticOptions: { + syntactic: true, + }, + mode: 'write-references', + // profile: true, + }, + issue: { + // prettier-ignore + include: [ + {file: '../**/src/**/*.{ts,tsx}'}, + {file: '**/src/**/*.{ts,tsx}'} + ], + exclude: [ + { file: '**/src/**/__tests__/**' }, + { file: '**/src/**/?(*.){spec|test}.*' }, + { file: '**/src/setupProxy.*' }, + { file: '**/src/setupTests.*' }, + ], + }, + logger: { + infrastructure: 'silent', + }, + }), + new ESLintPlugin({ + // Plugin options + extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'], + formatter: require.resolve('react-dev-utils/eslintFormatter'), + eslintPath: require.resolve('eslint'), + // ESLint class options + resolvePluginsRelativeTo: __dirname, + cache: true, + }), + ].filter(Boolean), + }; +}; diff --git a/packages/apps/doctor/firebase.js b/packages/apps/doctor/firebase.js new file mode 100644 index 00000000..8c537390 --- /dev/null +++ b/packages/apps/doctor/firebase.js @@ -0,0 +1,50 @@ +import dotenv from 'dotenv'; +import { getDownloadURL, getStorage, ref, uploadBytes } from 'firebase/storage'; +import { + getAuth, + signInWithEmailAndPassword, + createUserWithEmailAndPassword, + setPersistence, + browserLocalPersistence, + signOut, +} from 'firebase/auth'; +import { initializeApp } from 'firebase/app'; + +dotenv.config(); + +const firebaseConfig = { + apiKey: process.env.REACT_APP_FB_API_KEY, + authDomain: process.env.REACT_APP_FB_AUTH_DOMAIN, + projectId: process.env.REACT_APP_FB_PROJECT_ID, + storageBucket: process.env.REACT_APP_FB_STORAGE_BUCKET, + messagingSenderId: process.env.REACT_APP_FB_MESSAGING_SENDER_ID, + appId: process.env.REACT_APP_FB_API_ID, +}; + +const app = initializeApp(firebaseConfig); +const storage = getStorage(app); +export const auth = getAuth(app); + +export const uploadBlob = async (blob, uid) => { + const storageRef = ref(storage, `${uid}/profileImg.png`); + await uploadBytes(storageRef, blob); +}; + +export const getUserImage = email => { + getDownloadURL(ref(storage, `${email}/profileImg.png`)) + .then(url => console.log(url)) + .catch(() => null); +}; + +export const fbSignUp = async data => { + const { email, password, ...rest } = data; + const { user } = await createUserWithEmailAndPassword(auth, email, password); + + if (rest?.profileImgBlob) { + await uploadBlob(rest.profileImgBlob, user.uid); + } + + return user.uid; +}; + +export const fbLogOut = async () => signOut(auth); diff --git a/packages/apps/doctor/package.json b/packages/apps/doctor/package.json index b5ad6708..ce68b364 100644 --- a/packages/apps/doctor/package.json +++ b/packages/apps/doctor/package.json @@ -5,15 +5,15 @@ "author": "", "main": "src/index.js", "scripts": { - "serve": "enact serve", - "pack": "enact pack", - "pack-p": "enact pack -p", - "watch": "enact pack --watch", - "clean": "enact clean", - "lint": "enact lint .", - "license": "enact license", - "test": "enact test", - "test-watch": "enact test --watch" + "serve": "node ./scripts/serve.js", + "pack": "node ./scripts/pack.js", + "pack-p": "node ./scripts/pack.js -p", + "watch": "node ./scripts/pack.js --watch", + "clean": "node ./scripts/clean.js", + "lint": "node ./scripts/lint.js .", + "license": "node ./scripts/license.js", + "test": "node ./scripts/test.js", + "test-watch": "node ./scripts/test.js --watch" }, "license": "UNLICENSED", "private": true, @@ -25,12 +25,14 @@ "theme": "sandstone" }, "eslintConfig": { - "extends": "enact-proxy" + "extends": "enact" }, "eslintIgnore": [ "node_modules/*", "build/*", - "dist/*" + "dist/*", + "config/*", + "scripts/*" ], "dependencies": { "@enact/core": "^4.7.1", @@ -39,6 +41,7 @@ "@enact/spotlight": "^4.7.1", "@enact/ui": "^4.7.1", "@enact/webos": "^4.7.1", + "core-js": "3.22.8", "ilib": "npm:ilib-webos@^14.17.0-webos1", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -46,6 +49,70 @@ "web-vitals": "^3.3.2" }, "devDependencies": { - "eslint-config-enact-proxy": "^1.0.5" + "@babel/eslint-plugin": "^7.19.1", + "@enact/dev-utils": "^5.1.2", + "@enact/template-sandstone": "^1.5.2", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", + "@testing-library/dom": "^8.20.0", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "babel-jest": "^27.5.1", + "babel-loader": "^8.2.5", + "babel-plugin-dynamic-import-node": "^2.3.3", + "babel-preset-enact": "^0.1.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "css-loader": "^6.7.3", + "css-minimizer-webpack-plugin": "^3.4.1", + "dotenv": "^16.0.3", + "dotenv-expand": "^8.0.3", + "eslint": "^8.35.0", + "eslint-config-enact": "^4.1.4", + "eslint-config-enact-proxy": "^1.0.5", + "eslint-plugin-enact": "^1.0.2", + "eslint-plugin-jest": "^26.6.0", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-testing-library": "^5.10.2", + "eslint-webpack-plugin": "^3.2.0", + "expose-loader": "^3.1.0", + "file-loader": "^6.2.0", + "filesize": "^8.0.7", + "fs-extra": "^10.1.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^27.5.1", + "jest-watch-typeahead": "0.6.5", + "less": "^4.1.3", + "less-loader": "^8.1.1", + "license-checker": "^25.0.1", + "mini-css-extract-plugin": "^2.7.3", + "minimist": "^1.2.8", + "node-polyfill-webpack-plugin": "^1.1.4", + "postcss": "^8.4.21", + "postcss-flexbugs-fixes": "^5.0.2", + "postcss-global-import": "^1.0.6", + "postcss-loader": "^6.2.1", + "postcss-normalize": "^10.0.1", + "postcss-preset-env": "^7.8.3", + "postcss-resolution-independence": "^1.1.0", + "postcss-safe-parser": "^6.0.0", + "prompts": "^2.4.2", + "react-dev-utils": "^12.0.1", + "react-refresh": "^0.14.0", + "react-test-renderer": "^18.2.0", + "resolution-independence": "^1.0.0", + "resolve": "^1.22.1", + "sass-loader": "^13.2.0", + "source-map-loader": "^3.0.1", + "strip-ansi": "^6.0.1", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.7", + "webpack": "^5.76.0", + "webpack-dev-server": "^4.11.1" } } diff --git a/packages/apps/doctor/package.old.json b/packages/apps/doctor/package.old.json new file mode 100644 index 00000000..b5ad6708 --- /dev/null +++ b/packages/apps/doctor/package.old.json @@ -0,0 +1,51 @@ +{ + "name": "@housepital/doctor", + "version": "1.0.0", + "description": "A general template for an Enact Sandstone application for webOS TVs", + "author": "", + "main": "src/index.js", + "scripts": { + "serve": "enact serve", + "pack": "enact pack", + "pack-p": "enact pack -p", + "watch": "enact pack --watch", + "clean": "enact clean", + "lint": "enact lint .", + "license": "enact license", + "test": "enact test", + "test-watch": "enact test --watch" + }, + "license": "UNLICENSED", + "private": true, + "repository": "", + "engines": { + "npm": ">=6.9.0" + }, + "enact": { + "theme": "sandstone" + }, + "eslintConfig": { + "extends": "enact-proxy" + }, + "eslintIgnore": [ + "node_modules/*", + "build/*", + "dist/*" + ], + "dependencies": { + "@enact/core": "^4.7.1", + "@enact/i18n": "^4.7.1", + "@enact/sandstone": "^2.7.1", + "@enact/spotlight": "^4.7.1", + "@enact/ui": "^4.7.1", + "@enact/webos": "^4.7.1", + "ilib": "npm:ilib-webos@^14.17.0-webos1", + "prop-types": "^15.8.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "web-vitals": "^3.3.2" + }, + "devDependencies": { + "eslint-config-enact-proxy": "^1.0.5" + } +} diff --git a/packages/apps/doctor/scripts/clean.js b/packages/apps/doctor/scripts/clean.js new file mode 100644 index 00000000..3b0849a7 --- /dev/null +++ b/packages/apps/doctor/scripts/clean.js @@ -0,0 +1,67 @@ +/* eslint-env node, es6 */ +const path = require('path'); +const chalk = require('chalk'); +const fs = require('fs-extra'); +const minimist = require('minimist'); +const packageRoot = require('@enact/dev-utils').packageRoot; + +const build = 'build'; +const dist = 'dist'; +const node_modules = 'node_modules'; +const samples = 'samples'; +const ssTests = path.join('tests', 'screenshot'); +const uiTests = path.join('tests', 'ui'); + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact clean'; + + console.log(' Usage'); + console.log(` ${e} [options] [paths...]`); + console.log(); + console.log(' Arguments'); + console.log(' paths Additional path(s) to delete'); + console.log(); + console.log(' Options'); + console.log(' -a, --all Clean all temporary files'); + console.log(' (includes node_modules)'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + process.exit(0); +} + +function api({paths = [], all = false} = {}) { + const known = [build, dist]; + if (all) known.push(node_modules); + if (fs.existsSync(samples)) { + const sampleDirs = fs + .readdirSync(samples) + .map(p => path.join(samples, p)) + .filter(p => fs.existsSync(path.join(p, 'package.json'))); + sampleDirs.forEach(p => { + known.push(path.join(p, build), path.join(p, dist)); + if (all) known.push(path.join(p, node_modules)); + }); + } + if (fs.existsSync(ssTests)) known.push(path.join(ssTests, dist)); + if (fs.existsSync(uiTests)) known.push(path.join(uiTests, dist)); + return Promise.all(paths.concat(known).map(d => fs.remove(d))); +} + +function cli(args) { + const opts = minimist(args, { + boolean: ['help', 'all'], + alias: {h: 'help', a: 'all'} + }); + if (opts.help) displayHelp(); + + process.chdir(packageRoot().path); + api({paths: opts._, all: opts.all}).catch(err => { + console.error(chalk.red('ERROR: ') + 'Failed to clean project.\n' + err.message); + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/info.js b/packages/apps/doctor/scripts/info.js new file mode 100644 index 00000000..7fdbad43 --- /dev/null +++ b/packages/apps/doctor/scripts/info.js @@ -0,0 +1,177 @@ +/* eslint-env node, es6 */ +const path = require('path'); +const fs = require('fs'); +const chalk = require('chalk'); +const spawn = require('cross-spawn'); +const minimist = require('minimist'); +const resolveSync = require('resolve').sync; + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact info'; + + console.log(' Usage'); + console.log(` ${e} [options]`); + console.log(); + console.log(' Options'); + console.log(' --dev Include dev dependencies'); + console.log(' --cli Display CLI information'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + process.exit(0); +} + +function logVersion(pkg, rel = __dirname) { + try { + const jsonPath = resolveSync(pkg + '/package.json', {basedir: rel}); + const meta = require(jsonPath); + const dir = path.dirname(jsonPath); + if (fs.lstatSync(dir).isSymbolicLink()) { + const realDir = fs.realpathSync(dir); + const git = gitInfo(realDir); + console.log(meta.name + ': ' + (git || meta.version)); + console.log(chalk.cyan('\tSymlinked from'), realDir); + } else { + console.log(meta.name + ': ' + meta.version); + } + } catch (e) { + console.log(pkg + ': ' + chalk.red('')); + } +} + +function gitInfo(dir) { + const git = (args = []) => { + try { + const result = spawn.sync('git', args, {encoding: 'utf8', cwd: dir, env: process.env}); + if (!result.error && result.status === 0) return result.stdout.trim(); + } catch (e) { + // do nothing + } + }; + const tag = git(['describe', '--tags', '--exact-match']); + if (tag) { + return chalk.green(`${tag} (git)`); + } else { + const branch = git(['symbolic-ref', '-q', '--short', 'HEAD']) || 'HEAD'; + const commit = git(['rev-parse', '--short', 'HEAD']); + if (commit) return chalk.green(`${branch} @ ${commit} (git)`); + } +} + +function globalModules() { + try { + const result = spawn.sync('npm', ['config', 'get', 'prefix', '-g'], { + cwd: process.cwd(), + env: process.env + }); + if (result.error || result.status !== 0 || !(result.stdout = result.stdout.trim())) { + return require('global-modules'); + } else if (process.platform === 'win32' || ['msys', 'cygwin'].includes(process.env.OSTYPE)) { + return path.resolve(result.stdout, 'node_modules'); + } else { + return path.resolve(result.stdout, 'lib/node_modules'); + } + } catch (e) { + return require('global-modules'); + } +} + +function api({cliInfo = false, dev = false} = {}) { + return new Promise((resolve, reject) => { + try { + if (cliInfo) { + // Display info on CLI itself + const gm = globalModules(); + const gCLI = path.join(gm, '@enact', 'cli'); + const isGlobal = + fs.existsSync(gCLI) && + path.dirname(require.resolve(path.join(gCLI, 'package.json'))) === path.dirname(__dirname); + console.log(chalk.yellow.bold('==Enact CLI Info==')); + if (isGlobal && fs.lstatSync(gCLI).isSymbolicLink()) { + const ver = gitInfo(__dirname) || require('../package.json').version; + console.log(`Enact CLI: ${ver}`); + console.log(chalk.cyan('\tSymlinked from'), path.dirname(__dirname)); + } else { + console.log(`@enact/cli: ${require('../package.json').version}`); + } + console.log(`Installed Globally: ${isGlobal}`); + if (isGlobal) console.log(`Global Modules: ${gm}`); + console.log(); + + // Display info on in-house components, likely to be linked in + console.log(chalk.yellow.bold('==Enact Components==')); + [ + '@enact/dev-utils', + 'babel-preset-enact', + 'eslint-config-enact', + 'eslint-plugin-enact', + 'postcss-resolution-independence' + ].forEach(dep => logVersion(dep)); + console.log(); + + // Display info on notable 3rd party components + console.log(chalk.yellow.bold('==Third Party Components==')); + console.log(`Babel: ${require('@babel/core/package.json').version}`); + console.log(`ESLint: ${require('eslint/package.json').version}`); + console.log(`Jest: ${require('jest/package.json').version}`); + console.log(`LESS: ${require('less/package.json').version}`); + console.log(`Webpack: ${require('webpack/package.json').version}`); + } else { + const app = require('@enact/dev-utils').optionParser; + const meta = require(path.join(app.context, 'package.json')); + const bl = require(resolveSync('browserslist', { + basedir: path.dirname(require.resolve('@enact/dev-utils/package.json')), + preserveSymlinks: false + })); + app.setEnactTargetsAsDefault(); + console.log(chalk.yellow.bold('==Project Info==')); + console.log(`Name: ${app.name}`); + console.log(`Version: ${gitInfo(app.context) || meta.version}`); + console.log(`Path: ${app.context}`); + console.log(`Theme: ${(app.theme || {}).name}`); + if (app.proxer) console.log(`Serve Proxy: ${app.proxy}`); + if (app.template) console.log(`Template: ${app.template}`); + if (app.externalStartup) console.log(`External Startup: ${app.externalStartup}`); + if (app.deep) console.log(`Deep: ${app.deep}`); + if (app.forceCSSModules) console.log(`Force CSS Modules: ${app.forceCSSModules}`); + console.log(`Resolution Independence: ${JSON.stringify(app.ri)}`); + console.log(`Browserslist: ${bl.loadConfig({path: app.context})}`); + console.log(`Environment: ${app.environment}`); + console.log(); + console.log(chalk.yellow.bold('==Dependencies==')); + if (meta.dependencies) { + Object.keys(meta.dependencies).forEach(dep => { + logVersion(dep, app.context); + }); + } + if (dev && meta.devDependencies) { + console.log(); + console.log(chalk.yellow.bold('==Dev Dependencies==')); + Object.keys(meta.devDependencies).forEach(dep => { + logVersion(dep, app.context); + }); + } + } + resolve(); + } catch (e) { + reject(e); + } + }); +} + +function cli(args) { + const opts = minimist(args, { + boolean: ['cli', 'help'], + alias: {h: 'help'} + }); + if (opts.help) displayHelp(); + + api({cliInfo: opts.cli, dev: opts.dev}).catch(err => { + console.error(chalk.red('ERROR: ') + 'Failed to display info.\n' + err.message); + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/license.js b/packages/apps/doctor/scripts/license.js new file mode 100644 index 00000000..41baebce --- /dev/null +++ b/packages/apps/doctor/scripts/license.js @@ -0,0 +1,65 @@ +/* eslint-env node, es6 */ +const path = require('path'); +const chalk = require('chalk'); +const checker = require('license-checker'); +const minimist = require('minimist'); + +// The following modules reside in `@enact/cli` but end up in production builds of apps +const pkgPathResolve = m => path.dirname(require.resolve(m + '/package.json')); +const enactCLIProdModules = ['@babel/core', 'core-js'].map(pkgPathResolve); + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact license'; + + console.log(' Usage'); + console.log(` ${e} [options] []`); + console.log(); + console.log(' Arguments'); + console.log(' module Optional module path'); + console.log(' (default: '); + console.log(); + console.log(' Options'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + process.exit(0); +} + +function api({modules = []} = {}) { + if (!modules.length) { + modules = modules.concat(enactCLIProdModules, '.'); + } + + return Promise.all( + modules.map(m => { + return new Promise((resolve, reject) => { + checker.init({start: m}, (err, json) => { + if (err) { + reject(new Error(`Unable to process licenses for ${m}.\n${err.message}`)); + } else { + resolve(json || {}); + } + }); + }); + }) + ).then(values => values.reduce((a, b) => Object.assign(a, b))); +} + +function cli(args) { + const opts = minimist(args, { + boolean: ['help'], + alias: {h: 'help'} + }); + if (opts.help) displayHelp(); + + api({modules: opts._}) + .then(licenses => console.log(JSON.stringify(licenses, null, 2))) + .catch(err => { + console.error(chalk.red('ERROR: ') + err.message); + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/lint.js b/packages/apps/doctor/scripts/lint.js new file mode 100644 index 00000000..243f920e --- /dev/null +++ b/packages/apps/doctor/scripts/lint.js @@ -0,0 +1,86 @@ +/* eslint-env node, es6 */ +const cp = require('child_process'); +const path = require('path'); +const glob = require('glob'); +const minimist = require('minimist'); + +const globOpts = { + ignore: ['**/node_modules/**', 'build/**', '**/dist/**', 'coverage/**', 'tests/**'], + nodir: true +}; + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact lint'; + + console.log(' Usage'); + console.log(` ${e} [options] []`); + console.log(); + console.log(' Arguments'); + console.log(' target Optional target file or directory'); + console.log(' (default: cwd)'); + console.log(); + console.log(' Options'); + console.log(' -l, --local Scan with local eslint config'); + console.log(' -s, --strict Scan with strict eslint config'); + console.log(' -f, --fix Attempt to fix viable problems'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + process.exit(0); +} + +function shouldESLint() { + return glob.sync('**/*.+(js|jsx|ts|tsx)', globOpts).length > 0; +} + +function eslint({strict = false, local = false, fix = false, eslintArgs = []} = {}) { + let args = []; + if (strict) { + args.push('--no-eslintrc', '--config', require.resolve('eslint-config-enact/strict')); + } else if (!local) { + args.push('--no-eslintrc', '--config', require.resolve('eslint-config-enact')); + } + if (local) { + args.push('--ignore-pattern', '**/node_modules/*'); + args.push('--ignore-pattern', 'build/*'); + args.push('--ignore-pattern', '**/dist/*'); + args.push('--ignore-pattern', 'coverage/*'); + } + if (fix) args.push('--fix'); + if (eslintArgs.length) { + args = args.concat(eslintArgs); + } else { + args.push('.'); + } + return new Promise((resolve, reject) => { + const opts = {env: process.env, cwd: process.cwd()}; + const child = cp.fork(path.join(require.resolve('eslint'), '..', '..', 'bin', 'eslint'), args, opts); + child.on('close', code => { + if (code !== 0) { + reject(); + } else { + resolve(); + } + }); + }); +} + +function api(opts) { + return Promise.resolve().then(() => shouldESLint() && eslint(opts)); +} + +function cli(args) { + const opts = minimist(args, { + boolean: ['local', 'strict', 'fix', 'help'], + alias: {l: 'local', s: 'strict', framework: 'strict', f: 'fix', h: 'help'} + }); + if (opts.help) displayHelp(); + + api({strict: opts.strict, local: opts.local, fix: opts.fix, eslintArgs: opts._}).catch(() => { + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/pack.js b/packages/apps/doctor/scripts/pack.js new file mode 100644 index 00000000..f3d6dce2 --- /dev/null +++ b/packages/apps/doctor/scripts/pack.js @@ -0,0 +1,308 @@ +/* eslint-env node, es6 */ +const path = require('path'); +const chalk = require('chalk'); +const filesize = require('filesize'); +const fs = require('fs-extra'); +const minimist = require('minimist'); +const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); +const printBuildError = require('react-dev-utils/printBuildError'); +const stripAnsi = require('strip-ansi'); +const webpack = require('webpack'); +const {optionParser: app, mixins, configHelper: helper} = require('@enact/dev-utils'); + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact pack'; + + console.log(' Usage'); + console.log(` ${e} [options]`); + console.log(); + console.log(' Options'); + console.log(' -o, --output Specify an output directory'); + console.log(' -w, --watch Rebuild on file changes'); + console.log(' -p, --production Build in production mode'); + console.log(' -i, --isomorphic Use isomorphic code layout'); + console.log(' (includes prerendering)'); + console.log(' -l, --locales Locales for isomorphic mode; one of:'); + console.log(' Locale list'); + console.log(' - Read locales from JSON file'); + console.log(' "none" - Disable locale-specific handling'); + console.log(' "used" - Detect locales used within ./resources/'); + console.log(' "tv" - Locales supported on webOS TV'); + console.log(' "signage" - Locales supported on webOS signage'); + console.log(' "all" - All locales that iLib supports'); + console.log(' -s, --snapshot Generate V8 snapshot blob'); + console.log(' (requires V8_MKSNAPSHOT set)'); + console.log(' -m, --meta JSON to override package.json enact metadata'); + console.log(' -c, --custom-skin Build with a custom skin'); + console.log(' --stats Output bundle analysis file'); + console.log(' --verbose Verbose log build details'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + /* + Private Options: + --entry Specify an override entrypoint + --no-minify Will skip minification during production build + --framework Builds the @enact/*, react, and react-dom into an external framework + --externals Specify a local directory path to the standalone external framework + --externals-public Remote public path to the external framework for use injecting into HTML + --externals-polyfill Flag whether to use external polyfill (or include in framework build) + --ilib-additional-path Specify iLib additional resources path + */ + process.exit(0); +} + +function details(err, stats, output) { + let messages; + if (err) { + if (!err.message) return err; + let msg = err.message; + + // Add additional information for postcss errors + if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) { + msg += '\nCompileError: Begins at CSS selector ' + err['postcssNode'].selector; + } + + // Generate pretty/formatted warnins/errors + messages = formatWebpackMessages({ + errors: [msg], + warnings: [] + }); + } else { + // Remove any ESLint fixable notices since we're not running via eslint command + // and don't support a `--fix` optiob ourselves; don't want to confuse devs + stats.compilation.warnings.forEach(w => { + const eslintFix = /\n.* potentially fixable with the `--fix` option./gm; + w.message = w.message.replace(eslintFix, ''); + }); + + // Generate pretty/formatted warnins/errors + const statsJSON = stats.toJson({all: false, warnings: true, errors: true}); + messages = formatWebpackMessages(statsJSON); + } + + if (messages.errors.length) { + return new Error(messages.errors.join('\n\n')); + } else if ( + typeof process.env.CI === 'string' && + process.env.CI.toLowerCase() !== 'false' && + messages.warnings.length + ) { + // Ignore sourcemap warnings in CI builds. See #8227 for more info. + const filteredWarnings = messages.warnings.filter(w => !/Failed to parse source map/.test(w)); + if (filteredWarnings.length) { + console.log( + chalk.yellow( + '\nTreating warnings as errors because process.env.CI = true. \n' + + 'Most CI servers set it automatically.\n' + ) + ); + return new Error(filteredWarnings.join('\n\n')); + } + } else { + copyPublicFolder(output); + if (messages.warnings.length) { + console.log(chalk.yellow('Compiled with warnings:\n')); + console.log(messages.warnings.join('\n\n') + '\n'); + } else { + console.log(chalk.green('Compiled successfully.')); + } + if (process.env.NODE_ENV === 'development') { + console.log( + chalk.yellow( + 'NOTICE: This build contains debugging functionality and may run' + + ' slower than in production mode.' + ) + ); + } + console.log(); + + printFileSizes(stats, output); + console.log(); + } +} + +function copyPublicFolder(output) { + const staticAssets = './public'; + if (fs.existsSync(staticAssets)) { + fs.copySync(staticAssets, output, { + dereference: true + }); + } +} + +// Print a detailed summary of build files. +function printFileSizes(stats, output) { + const assets = stats + .toJson({all: false, assets: true, cachedAssets: true}) + .assets.filter(asset => /\.(js|css|bin)$/.test(asset.name)) + .map(asset => { + const size = fs.statSync(path.join(output, asset.name)).size; + return { + folder: path.relative(app.context, path.join(output, path.dirname(asset.name))), + name: path.basename(asset.name), + size: size, + sizeLabel: filesize(size) + }; + }); + assets.sort((a, b) => b.size - a.size); + const longestSizeLabelLength = Math.max.apply( + null, + assets.map(a => stripAnsi(a.sizeLabel).length) + ); + assets.forEach(asset => { + let sizeLabel = asset.sizeLabel; + const sizeLength = stripAnsi(sizeLabel).length; + if (sizeLength < longestSizeLabelLength) { + const rightPadding = ' '.repeat(longestSizeLabelLength - sizeLength); + sizeLabel += rightPadding; + } + console.log(' ' + sizeLabel + ' ' + chalk.dim(asset.folder + path.sep) + chalk.cyan(asset.name)); + }); +} + +function printErrorDetails(err, handler) { + console.log(); + if (process.env.TSC_COMPILE_ON_ERROR === 'true') { + console.log( + chalk.yellow( + 'Compiled with the following type errors (you may want to check ' + + 'these before deploying your app):\n' + ) + ); + printBuildError(err); + } else { + console.log(chalk.red('Failed to compile.\n')); + printBuildError(err); + if (handler) handler(); + } +} + +// Create the production build and print the deployment instructions. +function build(config) { + if (process.env.NODE_ENV === 'development') { + console.log('Creating a development build...'); + } else { + console.log('Creating an optimized production build...'); + } + + return new Promise((resolve, reject) => { + const compiler = webpack(config); + compiler.run((err, stats) => { + err = details(err, stats, config.output.path); + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +// Create the build and watch for changes. +function watch(config) { + // Make sure webpack doesn't immediate bail on errors when watching. + config.bail = false; + if (process.env.NODE_ENV === 'development') { + console.log('Creating a development build and watching for changes...'); + } else { + console.log('Creating an optimized production build and watching for changes...'); + } + copyPublicFolder(config.output.path); + webpack(config).watch({}, (err, stats) => { + err = details(err, stats, config.output.path); + if (err) { + printErrorDetails(err); + } + console.log(); + }); +} + +function api(opts = {}) { + if (opts.meta) { + let meta = opts.meta; + if (typeof meta === 'string') { + try { + meta = JSON.parse(opts.meta); + } catch (e) { + throw new Error('Invalid metadata; must be a valid JSON string.\n' + e.message); + } + } + app.applyEnactMeta(meta); + } + + if (opts['custom-skin']) { + app.applyEnactMeta({template: path.join(__dirname, '..', 'config', 'custom-skin-template.ejs')}); + } + + // Do this as the first thing so that any code reading it knows the right env. + const configFactory = require('../config/webpack.config'); + const config = configFactory( + opts.production ? 'production' : 'development', + opts.isomorphic, + opts['ilib-additional-path'] + ); + + // Set any entry path override + if (opts.entry) helper.replaceMain(config, path.resolve(opts.entry)); + + // Set any output path override + if (opts.output) config.output.path = path.resolve(opts.output); + + mixins.apply(config, opts); + + // Remove all content but keep the directory so that + // if you're in it, you don't end up in Trash + return fs.emptyDir(config.output.path).then(() => { + // Start the webpack build + if (opts.watch) { + // This will run infinitely until killed, even through errors + watch(config); + } else { + return build(config); + } + }); +} + +function cli(args) { + const opts = minimist(args, { + boolean: [ + 'custom-skin', + 'minify', + 'framework', + 'externals-corejs', + 'stats', + 'production', + 'isomorphic', + 'snapshot', + 'verbose', + 'watch', + 'help' + ], + string: ['externals', 'externals-public', 'locales', 'entry', 'ilib-additional-path', 'output', 'meta'], + default: {minify: true}, + alias: { + o: 'output', + p: 'production', + i: 'isomorphic', + l: 'locales', + s: 'snapshot', + m: 'meta', + c: 'custom-skin', + w: 'watch', + h: 'help' + } + }); + if (opts.help) displayHelp(); + + process.chdir(app.context); + api(opts).catch(err => { + printErrorDetails(err, () => { + process.exit(1); + }); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/serve.js b/packages/apps/doctor/scripts/serve.js new file mode 100644 index 00000000..959090ad --- /dev/null +++ b/packages/apps/doctor/scripts/serve.js @@ -0,0 +1,353 @@ +/* eslint-env node, es6 */ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const minimist = require('minimist'); +const clearConsole = require('react-dev-utils/clearConsole'); +const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware'); +const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); +const openBrowser = require('react-dev-utils/openBrowser'); +const redirectServedPathMiddleware = require('react-dev-utils/redirectServedPathMiddleware'); +const ignoredFiles = require('react-dev-utils/ignoredFiles'); +const {choosePort, createCompiler, prepareProxy, prepareUrls} = require('react-dev-utils/WebpackDevServerUtils'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const webpack = require('webpack'); +const WebpackDevServer = require('webpack-dev-server'); +const {optionParser: app} = require('@enact/dev-utils'); + +// Any unhandled promise rejections should be treated like errors. +process.on('unhandledRejection', err => { + throw err; +}); + +// As react-dev-utils assumes the webpack production packaging command is +// "npm run build" with no way to modify it yet, we provide a basic override +// to console.log to ensure the correct output is displayed to the user. +// prettier-ignore +console.log = (log => (data, ...rest) => + typeof data === 'undefined' + ? log() + : typeof data === 'string' + ? log(data.replace(/npm run build/, 'npm run pack-p'), ...rest) + : log.call(this, data, ...rest))(console.log); + +function displayHelp() { + let e = 'node ' + path.relative(process.cwd(), __filename); + if (require.main !== module) e = 'enact serve'; + + console.log(' Usage'); + console.log(` ${e} [options]`); + console.log(); + console.log(' Options'); + console.log(' -b, --browser Automatically open browser'); + console.log(' -i, --host Server host IP address'); + console.log(' -f, --fast Enables experimental frast refresh'); + console.log(' -p, --port Server port number'); + console.log(' -m, --meta JSON to override package.json enact metadata'); + console.log(' -v, --version Display version information'); + console.log(' -h, --help Display help information'); + console.log(); + process.exit(0); +} + +function hotDevServer(config, fastRefresh) { + // Keep webpack alive when there are any errors, so user can fix and rebuild. + config.bail = false; + // Ensure the CLI version of Chalk is used for webpackHotDevClient + // since tslint includes an out-of-date local version. + config.resolve.alias.chalk = require.resolve('chalk'); + config.resolve.alias['ansi-styles'] = require.resolve('ansi-styles'); + + // Include an alternative client for WebpackDevServer. A client's job is to + // connect to WebpackDevServer by a socket and get notified about changes. + // When you save a file, the client will either apply hot updates (in case + // of CSS changes), or refresh the page (in case of JS changes). When you + // make a syntax error, this client will display a syntax error overlay. + // Note: instead of the default WebpackDevServer client, we use a custom one + // to bring better experience. + if (!fastRefresh) { + config.entry.main.unshift(require.resolve('react-dev-utils/webpackHotDevClient')); + } else { + // Use experimental fast refresh plugin instead as dev client access point + // https://github.com/facebook/react/tree/master/packages/react-refresh + config.plugins.unshift( + new ReactRefreshWebpackPlugin({ + overlay: false + }) + ); + // Append fast refresh babel plugin + config.module.rules[1].oneOf[0].options.plugins = [require.resolve('react-refresh/babel')]; + } + return config; +} + +function devServerConfig(host, port, protocol, publicPath, proxy, allowedHost) { + let https = false; + const {SSL_CRT_FILE, SSL_KEY_FILE} = process.env; + if (protocol === 'https' && [SSL_CRT_FILE, SSL_KEY_FILE].every(f => f && fs.existsSync(f))) { + https = { + cert: fs.readFileSync(SSL_CRT_FILE), + key: fs.readFileSync(SSL_KEY_FILE) + }; + } + const disableFirewall = !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true'; + + return { + // WebpackDevServer 2.4.3 introduced a security fix that prevents remote + // websites from potentially accessing local content through DNS rebinding: + // https://github.com/webpack/webpack-dev-server/issues/887 + // https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a + // However, it made several existing use cases such as development in cloud + // environment or subdomains in development significantly more complicated: + // https://github.com/facebookincubator/create-react-app/issues/2271 + // https://github.com/facebookincubator/create-react-app/issues/2233 + // While we're investigating better solutions, for now we will take a + // compromise. Since our WDS configuration only serves files in the `public` + // folder we won't consider accessing them a vulnerability. However, if you + // use the `proxy` feature, it gets more dangerous because it can expose + // remote code execution vulnerabilities in backends like Django and Rails. + // So we will disable the host check normally, but enable it if you have + // specified the `proxy` setting. Finally, we let you override it if you + // really know what you're doing with a special environment variable. + // Note: ["localhost", ".localhost"] will support subdomains - but we might + // want to allow setting the allowedHosts manually for more complex setups + allowedHosts: disableFirewall ? 'all' : [allowedHost], + // Enable HTTPS if the HTTPS environment variable is set to 'true' + https, + host, + port, + // Allow cross-origin HTTP requests + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': '*', + 'Access-Control-Allow-Headers': '*' + }, + static: [ + { + // By default WebpackDevServer serves physical files from current directory + // in addition to all the virtual build products that it serves from memory. + // This is confusing because those files won’t automatically be available in + // production build folder unless we copy them. However, copying the whole + // project directory is dangerous because we may expose sensitive files. + // Instead, we establish a convention that only files in `public` directory + // get served. Our build script will copy `public` into the `build` folder. + // In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%: + // + // In JavaScript code, you can access it with `process.env.PUBLIC_URL`. + // Note that we only recommend to use `public` folder as an escape hatch + // for files like `favicon.ico`, `manifest.json`, and libraries that are + // for some reason broken when imported through webpack. If you just want to + // use an image, put it in `src` and `import` it from JavaScript instead. + directory: path.resolve(app.context, 'public'), + publicPath, + // By default files from `contentBase` will not trigger a page reload. + watch: { + // Reportedly, this avoids CPU overload on some systems. + // https://github.com/facebook/create-react-app/issues/293 + // src/node_modules is not ignored to support absolute imports + // https://github.com/facebook/create-react-app/issues/1065 + ignored: [ + ignoredFiles(path.resolve(app.context, 'src')), + '/node_modules[\\/](?!@enact[\\/](?!.*node_modules))/' + ] + } + }, + { + directory: path.resolve(app.context, '__mocks__'), + publicPath, + // By default files from `contentBase` will not trigger a page reload. + watch: { + // Reportedly, this avoids CPU overload on some systems. + // https://github.com/facebook/create-react-app/issues/293 + // src/node_modules is not ignored to support absolute imports + // https://github.com/facebook/create-react-app/issues/1065 + ignored: [ + ignoredFiles(path.resolve(app.context, 'src')), + '/node_modules[\\/](?!@enact[\\/](?!.*node_modules))/' + ] + } + } + ], + client: { + webSocketURL: { + // Enable custom sockjs pathname for websocket connection to hot reloading server. + // Enable custom sockjs hostname, pathname and port for websocket connection + // to hot reloading server. + hostname: process.env.WDS_SOCKET_HOST, + pathname: process.env.WDS_SOCKET_PATH, + port: process.env.WDS_SOCKET_PORT + }, + overlay: { + errors: true, + warnings: false + } + }, + devMiddleware: { + // It is important to tell WebpackDevServer to use the same "publicPath" path as + // we specified in the webpack config. When homepage is '.', default to serving + // from the root. + // remove last slash so user can land on `/test` instead of `/test/` + publicPath: publicPath.slice(0, -1) + }, + historyApiFallback: { + // ensure JSON file requests correctly 404 error when not found. + rewrites: [{from: /.*\.json$/, to: context => context.parsedUrl.pathname}], + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, + index: publicPath + }, + // `proxy` is run between `before` and `after` `webpack-dev-server` hooks + proxy, + setupMiddlewares(middlewares, devServer) { + if (!devServer) { + throw new Error('webpack-dev-server is not defined'); + } + + // Optionally register app-side proxy middleware if it exists + const proxySetup = path.join(process.cwd(), 'src', 'setupProxy.js'); + if (fs.existsSync(proxySetup)) { + require(proxySetup)(devServer.app); + } + + middlewares.unshift( + // Keep `evalSourceMapMiddleware` + // middlewares before `redirectServedPath` otherwise will not have any effect + // This lets us fetch source contents from webpack for the error overlay + evalSourceMapMiddleware(devServer) + ); + + middlewares.push( + // Redirect to `PUBLIC_URL` or `homepage`/`enact.publicUrl` from `package.json` + // if url not match + redirectServedPathMiddleware(publicPath) + ); + + return middlewares; + } + }; +} + +function serve(config, host, port, open) { + // We attempt to use the default port but if it is busy, we offer the user to + // run on a different port. `detect()` Promise resolves to the next free port. + return choosePort(host, port).then(resolvedPort => { + if (resolvedPort == null) { + // We have not found a port. + return Promise.reject(new Error('Could not find a free port for the dev-server.')); + } + const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; + const publicPath = getPublicUrlOrPath(true, app.publicUrl, process.env.PUBLIC_URL); + const urls = prepareUrls(protocol, host, resolvedPort, publicPath.slice(0, -1)); + + // Create a webpack compiler that is configured with custom messages. + const compiler = createCompiler({ + appName: app.name, + config, + urls, + useYarn: false, + useTypeScript: fs.existsSync('tsconfig.json'), + webpack + }); + // Hook into compiler to remove potentially confusing messages + compiler.hooks.afterEmit.tapAsync('EnactCLI', (compilation, callback) => { + compilation.warnings.forEach(w => { + if (w.message) { + // Remove any --fix ESLintinfo messages since the eslint-loader config is + // internal and eslist is used in an embedded context. + const eslintFix = /\n.* potentially fixable with the `--fix` option./gm; + w.message = w.message.replace(eslintFix, ''); + } + }); + callback(); + }); + // Load proxy config + const proxySetting = app.proxy; + const proxyConfig = prepareProxy(proxySetting, './public', publicPath); + // Serve webpack assets generated by the compiler over a web sever. + const serverConfig = Object.assign( + {}, + devServerConfig(host, resolvedPort, protocol, publicPath, proxyConfig, urls.lanUrlForConfig) + ); + const devServer = new WebpackDevServer(serverConfig, compiler); + // Launch WebpackDevServer. + devServer.startCallback(err => { + if (err) return console.log(err); + if (process.stdout.isTTY) clearConsole(); + console.log(chalk.cyan('Starting the development server...\n')); + if (open) { + openBrowser(urls.localUrlForBrowser); + } + }); + + ['SIGINT', 'SIGTERM'].forEach(sig => { + process.on(sig, () => { + devServer.close(); + process.exit(); + }); + }); + + if (process.env.CI !== 'true') { + // Gracefully exit when stdin ends + process.stdin.on('end', () => { + devServer.close(); + process.exit(); + }); + } + }); +} + +function api(opts) { + if (opts.meta) { + let meta; + try { + meta = JSON.parse(opts.meta); + } catch (e) { + throw new Error('Invalid metadata; must be a valid JSON string.\n' + e.message); + } + app.applyEnactMeta(meta); + } + + // We can disable the typechecker formatter since react-dev-utils includes their + // own formatter in their dev client. + process.env.DISABLE_TSFORMATTER = 'true'; + + // Use inline styles for serving process. + process.env.INLINE_STYLES = 'true'; + + // Setup the development config with additional webpack-dev-server customizations. + const configFactory = require('../config/webpack.config'); + const fastRefresh = process.env.FAST_REFRESH || opts.fast; + const config = hotDevServer(configFactory('development'), fastRefresh); + + // Tools like Cloud9 rely on this. + const host = process.env.HOST || opts.host || '0.0.0.0'; + const port = parseInt(process.env.PORT || opts.port || 8080); + + // Start serving + if (['node', 'async-node', 'webworker'].includes(app.environment)) { + return Promise.reject(new Error('Serving is not supported for non-browser apps.')); + } else { + return serve(config, host, port, opts.browser); + } +} + +function cli(args) { + const opts = minimist(args, { + string: ['host', 'port', 'meta'], + boolean: ['browser', 'fast', 'help'], + alias: {b: 'browser', i: 'host', p: 'port', f: 'fast', m: 'meta', h: 'help'} + }); + if (opts.help) displayHelp(); + + process.chdir(app.context); + + api(opts).catch(err => { + //console.error(chalk.red('ERROR: ') + (err.message || err)); + console.log(err); + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/scripts/test.js b/packages/apps/doctor/scripts/test.js new file mode 100644 index 00000000..cb9d280e --- /dev/null +++ b/packages/apps/doctor/scripts/test.js @@ -0,0 +1,168 @@ +/* eslint-env node, es6 */ +const path = require('path'); +const {execSync} = require('child_process'); +const {packageRoot} = require('@enact/dev-utils'); +const chalk = require('chalk'); +const jest = require('jest'); +const resolve = require('resolve'); + +// Makes the script crash on unhandled rejections instead of silently +// ignoring them. In the future, promise rejections that are not handled will +// terminate the Node.js process with a non-zero exit code. +process.on('unhandledRejection', err => { + throw err; +}); + +function isInGitRepository() { + try { + execSync('git rev-parse --is-inside-work-tree', {stdio: 'ignore'}); + return true; + } catch (e) { + return false; + } +} + +function isInMercurialRepository() { + try { + execSync('hg --cwd . root', {stdio: 'ignore'}); + return true; + } catch (e) { + return false; + } +} + +// This is a very dirty workaround for https://github.com/facebook/jest/issues/5913. +// We're trying to resolve the environment ourselves because Jest does it incorrectly. +function resolveJestDefaultEnvironment(name) { + const jestDir = path.dirname(resolve.sync('jest', {basedir: __dirname})); + const jestCLIDir = path.dirname(resolve.sync('jest-cli', {basedir: jestDir})); + const jestConfigDir = path.dirname(resolve.sync('jest-config', {basedir: jestCLIDir})); + return resolve.sync(name, {basedir: jestConfigDir}); +} + +function testEnvironment(args) { + const env = ( + args.reverse().find((curr, i, a) => curr.startsWith('--env=') || a[i + 1] === '--env') || 'jsdom' + ).replace(/^--env=/, ''); + args.reverse(); + let resolvedEnv; + try { + resolvedEnv = resolveJestDefaultEnvironment(`jest-environment-${env}`); + } catch (e) { + // ignore + } + if (!resolvedEnv) { + try { + resolvedEnv = resolveJestDefaultEnvironment(env); + } catch (e) { + // ignore + } + } + return resolvedEnv || env; +} + +function assignOverrides(config) { + const {meta} = packageRoot(); + const overrides = Object.assign({}, meta.jest); + const supportedKeys = [ + 'clearMocks', + 'collectCoverageFrom', + 'coveragePathIgnorePatterns', + 'coverageReporters', + 'coverageThreshold', + 'displayName', + 'extraGlobals', + 'globalSetup', + 'globalTeardown', + 'moduleNameMapper', + 'resetMocks', + 'resetModules', + 'restoreMocks', + 'snapshotSerializers', + 'testMatch', + 'transform', + 'transformIgnorePatterns', + 'watchPathIgnorePatterns' + ]; + if (overrides) { + supportedKeys.forEach(key => { + if (Object.prototype.hasOwnProperty.call(overrides, key)) { + if (Array.isArray(config[key]) || typeof config[key] !== 'object') { + // for arrays or primitive types, directly override the config key + config[key] = overrides[key]; + } else { + // for object types, extend gracefully + config[key] = Object.assign({}, config[key], overrides[key]); + } + delete overrides[key]; + } + }); + const unsupportedKeys = Object.keys(overrides); + if (unsupportedKeys.length) { + const isOverridingSetupFile = unsupportedKeys.includes('setupFilesAfterEnv'); + + if (isOverridingSetupFile) { + console.error( + chalk.red( + 'We detected ' + + chalk.bold('setupFilesAfterEnv') + + ' in your package.json.\n\n' + + 'Remove it from Jest configuration, and put the initialization code in ' + + chalk.bold('src/setupTests.js') + + '.\nThis file will be loaded automatically.\n' + ) + ); + } else { + console.error( + chalk.red( + '\nOut of the box, Enact CLI only supports overriding ' + + 'these Jest options:\n\n' + + supportedKeys.map(key => chalk.bold(' \u2022 ' + key)).join('\n') + + '.\n\n' + + 'These options in your package.json Jest configuration ' + + 'are not currently supported by Enact CLI:\n\n' + + unsupportedKeys.map(key => chalk.bold(' \u2022 ' + key)).join('\n') + + '\n\nIf you wish to override other Jest options, you need to ' + + 'eject from the default setup. You can do so by running ' + + chalk.bold('npm run eject') + + ' but remember that this is a one-way operation. ' + + 'You may also file an issue with Enact CLI to discuss ' + + 'supporting more options out of the box.\n' + ) + ); + } + process.exit(1); + } + } +} + +function api(args = []) { + const config = require('../config/jest/jest.config'); + + // @TODO: readd dotenv parse support + + // Watch unless on CI, in coverage mode, or explicitly running all tests + const wIndex = args.indexOf('--watch'); + if (wIndex > -1 && !process.env.CI && !args.includes('--coverage') && !args.includes('--watchAll')) { + // https://github.com/facebook/create-react-app/issues/5210 + const hasSourceControl = isInGitRepository() || isInMercurialRepository(); + args[wIndex] = hasSourceControl ? '--watch' : '--watchAll'; + } + + // Apply safe override options from package.json + assignOverrides(config); + + args.push('--config', JSON.stringify(config)); + args.push('--env', testEnvironment(args)); + + return jest.run(args); +} + +function cli(args) { + api(args).catch(() => { + process.exit(1); + }); +} + +module.exports = {api, cli}; +if (require.main === module) cli(process.argv.slice(2)); diff --git a/packages/apps/doctor/src/App/App.js b/packages/apps/doctor/src/App/App.js deleted file mode 100644 index 9282ac8d..00000000 --- a/packages/apps/doctor/src/App/App.js +++ /dev/null @@ -1,26 +0,0 @@ -import kind from '@enact/core/kind'; -import ThemeDecorator from '@enact/sandstone/ThemeDecorator'; -import Panels from '@enact/sandstone/Panels'; - -import MainPanel from '../views/MainPanel'; - -import './attachErrorHandler'; - -import css from './App.module.less'; - -const App = kind({ - name: 'App', - - styles: { - css, - className: 'app' - }, - - render: (props) => ( - - - - ) -}); - -export default ThemeDecorator(App); diff --git a/packages/apps/doctor/src/App/App.module.less b/packages/apps/doctor/src/App/App.module.less deleted file mode 100644 index f9db12c8..00000000 --- a/packages/apps/doctor/src/App/App.module.less +++ /dev/null @@ -1,3 +0,0 @@ -.app { - /* styles can be put here */ -} diff --git a/packages/apps/doctor/src/App/attachErrorHandler.js b/packages/apps/doctor/src/App/attachErrorHandler.js deleted file mode 100644 index 357d9a9d..00000000 --- a/packages/apps/doctor/src/App/attachErrorHandler.js +++ /dev/null @@ -1,29 +0,0 @@ -import {onWindowReady} from '@enact/core/snapshot'; -import {error} from '@enact/webos/pmloglib'; - -// Logs any uncaught exceptions to the system logs for future troubleshooting. Payload can be -// customized by the application for its particular requirements. -const handleError = (ev) => { - let stack = ev.error && ev.error.stack || null; - - if (stack && stack.length > 512) { - // JSON must be limitted to 1024 characters so we truncate the stack to 512 for safety - stack = ev.error.stack.substring(0, 512); - } - - error('app.onerror', { - message: ev.message, - url: ev.filename, - line: ev.lineno, - column: ev.colno, - stack - }, ''); - - // Calling preventDefault() will avoid logging the error to the console - // ev.preventDefault(); -}; - -onWindowReady(() => { - window.addEventListener('error', handleError); -}); - diff --git a/packages/apps/doctor/src/App/package.json b/packages/apps/doctor/src/App/package.json deleted file mode 100644 index add0ba2a..00000000 --- a/packages/apps/doctor/src/App/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "main": "App.js" -} \ No newline at end of file diff --git a/packages/apps/doctor/src/api.js b/packages/apps/doctor/src/api.js new file mode 100644 index 00000000..35017c3f --- /dev/null +++ b/packages/apps/doctor/src/api.js @@ -0,0 +1,11 @@ +import axios from 'axios'; + +const instance = axios.create({ + baseURL: 'http://localhost:3000/api', +}); + +export const getHospitals = async hospitalName => + await instance.get(`/hospitals?name=${hospitalName}`); + +export const createDoctor = async doctor => + await instance.post('/doctors', { data: doctor }); diff --git a/packages/apps/doctor/src/components/README.md b/packages/apps/doctor/src/components/README.md deleted file mode 100644 index b1a7853e..00000000 --- a/packages/apps/doctor/src/components/README.md +++ /dev/null @@ -1 +0,0 @@ -Reusable components for your application go here \ No newline at end of file diff --git a/packages/apps/doctor/src/index.js b/packages/apps/doctor/src/index.js index 9a1166af..71fd1169 100644 --- a/packages/apps/doctor/src/index.js +++ b/packages/apps/doctor/src/index.js @@ -1,24 +1,26 @@ -/* global ENACT_PACK_ISOMORPHIC */ -import {createRoot, hydrateRoot} from 'react-dom/client'; +import { RouterProvider } from 'react-router-dom'; +import ReactDOM from 'react-dom/client'; +import { AnimatePresence } from 'framer-motion'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ChakraProvider } from '@chakra-ui/react'; -import App from './App'; -import reportWebVitals from './reportWebVitals'; +import theme from '../../../common/theme'; -const appElement = (); +import router from './router'; + +const client = new QueryClient(); + +const root = ReactDOM.createRoot(document.getElementById('root')); // In a browser environment, render instead of exporting if (typeof window !== 'undefined') { - if (ENACT_PACK_ISOMORPHIC) { - hydrateRoot(document.getElementById('root'), appElement); - } else { - createRoot(document.getElementById('root')).render(appElement); - } + root.render( + + + + + + + , + ); } - -export default appElement; - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. -// Learn more: https://github.com/enactjs/cli/blob/master/docs/measuring-performance.md -reportWebVitals(); diff --git a/packages/apps/doctor/src/reportWebVitals.js b/packages/apps/doctor/src/reportWebVitals.js deleted file mode 100644 index 2237bf6d..00000000 --- a/packages/apps/doctor/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = (onPerfEntry) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/packages/apps/doctor/src/router.js b/packages/apps/doctor/src/router.js new file mode 100644 index 00000000..c13ebbe2 --- /dev/null +++ b/packages/apps/doctor/src/router.js @@ -0,0 +1,17 @@ +import { Outlet, createHashRouter } from 'react-router-dom'; +import SignUp from './views/SignUp/SignUp'; + +const router = createHashRouter([ + { + path: 'auth', + element: , + children: [ + { + path: 'sign-up', + element: , + }, + ], + }, +]); + +export default router; diff --git a/packages/apps/doctor/src/store.js b/packages/apps/doctor/src/store.js new file mode 100644 index 00000000..b0f9108f --- /dev/null +++ b/packages/apps/doctor/src/store.js @@ -0,0 +1,24 @@ +import { configureStore, createSlice } from '@reduxjs/toolkit'; + +const doctorSlice = createSlice({ + name: 'doctor', + initialState: {}, + reducers: { + setDoctor: (_, action) => { + return action.payload; + }, + }, +}); + +export const store = configureStore({ + reducer: { + doctor: doctorSlice.reducer, + }, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: false, + }), + devTools: process.env.NODE_ENV !== 'production', +}); + +export const { setDoctor } = doctorSlice.actions; diff --git a/packages/apps/doctor/src/views/MainPanel.js b/packages/apps/doctor/src/views/MainPanel.js deleted file mode 100644 index 3e8e7483..00000000 --- a/packages/apps/doctor/src/views/MainPanel.js +++ /dev/null @@ -1,16 +0,0 @@ -import Button from '@enact/sandstone/Button'; -import kind from '@enact/core/kind'; -import {Panel, Header} from '@enact/sandstone/Panels'; - -const MainPanel = kind({ - name: 'MainPanel', - - render: (props) => ( - -
- - - ) -}); - -export default MainPanel; diff --git a/packages/apps/doctor/src/views/README.md b/packages/apps/doctor/src/views/README.md deleted file mode 100644 index e18ab3d1..00000000 --- a/packages/apps/doctor/src/views/README.md +++ /dev/null @@ -1 +0,0 @@ -Composite components that make up a distinct view go here \ No newline at end of file diff --git a/packages/apps/doctor/src/views/SignUp/SignUp.js b/packages/apps/doctor/src/views/SignUp/SignUp.js new file mode 100644 index 00000000..70bb32c4 --- /dev/null +++ b/packages/apps/doctor/src/views/SignUp/SignUp.js @@ -0,0 +1,311 @@ +import { createDoctor, getHospitals } from '../../api'; +import { + Box, + Button, + ButtonGroup, + Link as ChakraLink, + Container, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + HStack, + Heading, + Icon, + Input, + Select, + Text, + Textarea, + UnorderedList, + VStack, + useCheckboxGroup, +} from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; +import { FaSearch } from 'react-icons/fa'; +import { Link as ReactRouterLink, useNavigate } from 'react-router-dom'; +import { useCallback, useState } from 'react'; +import styles from '@housepital/common/css/HideScrollBar.module.css'; +import { fbSignUp } from '../../../firebase'; +import CustomCheckbox from '@housepital/common/CustomCheckox'; + +const fields = [ + '내과', + '신경과', + '정신건강의학과', + '외과', + '정형외과', + '신경외과', + '흉부외과', + '성형외과', + '마취통증의학과', + '산부인과', + '소아청소년과', + '안과', + '이비인후과', + '피부과', + '비뇨의학과', + '영상의학과', + '방사선종양학과', + '병리과', + '진단검사의학과', + '결핵과', + '재활의학과', + '예방의학과', + '가정의학과', + '응급의학과', + '핵의학과', + '직업환경의학과', +]; + +const SignUp = function () { + const [hospitals, setHospitals] = useState([]); + const [selectedHospitalId, setSelectedHospitalId] = useState(''); + const [selectedFields, setSelectedFields] = useState([]); + + const navigate = useNavigate(); + const { + register, + getValues, + setValue, + handleSubmit, + formState: { errors }, + } = useForm({ mode: 'onBlur', defaultValues: { ykiho: '' } }); + const checkboxGroup = useCheckboxGroup({ + onChange: values => setSelectedFields(values), + }); + + const onSearchFieldClick = useCallback(async () => { + const hospitalName = getValues('hospital'); + if (!hospitalName) { + const { data } = await getHospitals(hospitalName); + + if (data.length === 0) { + // TODO: webOS.notification 전송 + } else { + setHospitals(data); + } + } + }, [getValues]); + + const onHospitalClick = useCallback( + hospital => { + setSelectedHospitalId(hospital.id); + setValue('hospital', hospital.name); + setHospitals([]); + }, + [setSelectedHospitalId, setValue], + ); + + const onSubmit = useCallback( + async data => { + if (selectedHospitalId === '') { + // TODO: 병원이 선택되지 않은 경우 + // TODO: webOS.notification 사용 + } + const doctorId = await fbSignUp(data); + const response = await createDoctor({ + doctorId, + hospitalId: selectedHospitalId, + fields: selectedFields, + ...data, + }); + + if (response.isSucess) { + navigate('/'); + } else { + //TODO: 가입 실패 + // TODO: webOS.notification 사용 + console.log(response.message); + } + }, + [selectedHospitalId, selectedFields, navigate], + ); + + return ( + + + Housepital 회원가입 + + 이미 계정이 있으신가요? 로그인 + + + + + + 소속 병원 + + + + + {hospitals.length !== 0 && ( + + {hospitals.map(hospital => { + return ( + // TODO: key를 id에서 ykiho로 변경해야 함 + + + + {hospital.name} + + {hospital.address} + + + + ); + })} + + )} + + + + 이메일 + + {errors.email?.message} + + + + 이름 + + {errors.username?.message} + + + + 비밀번호 + + {errors.password?.message} + + + + 비밀번호 확인 + { + if (getValues('password') !== checkPassword) { + return '비밀번호가 일치하지 않습니다.'; + } + }, + }, + })} + /> + {errors.checkPassword?.message} + + + + 전공 + {/* //TODO: 공공데이터포털에서 진료분야를 어떻게 알아올 수 있는 지 확인해야 함 */} + + {errors.specialty?.message} + + + + 진료 분야 + + {fields.map(field => ( + + + {field} + + + ))} + + + + + 소개글 +