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}
+
+
+ ))}
+
+
+
+
+ 소개글
+
+
+
+
+
+
+
+
+ );
+};
+
+export default SignUp;
diff --git a/packages/apps/doctor/src/views/SignUp/package.json b/packages/apps/doctor/src/views/SignUp/package.json
new file mode 100644
index 00000000..8da61382
--- /dev/null
+++ b/packages/apps/doctor/src/views/SignUp/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "SignUp.js"
+}
diff --git a/packages/apps/doctor/webos-meta/appinfo.json b/packages/apps/doctor/webos-meta/appinfo.json
index 90f7d8f4..d117260e 100644
--- a/packages/apps/doctor/webos-meta/appinfo.json
+++ b/packages/apps/doctor/webos-meta/appinfo.json
@@ -1,12 +1,12 @@
{
- "id": "doctor",
- "version": "1.0.0",
- "vendor": "LGE-SVL",
- "type": "web",
- "main": "index.html",
- "title": "Enact App Template",
- "icon": "icon.png",
- "miniicon": "icon-mini.png",
- "largeIcon": "icon-large.png",
- "uiRevision": 2
+ "id": "com.housepital.app.doctor",
+ "version": "1.0.0",
+ "vendor": "LGE-SVL",
+ "type": "web",
+ "main": "index.html",
+ "title": "Housepital for Doctor",
+ "icon": "icon.png",
+ "miniicon": "icon-mini.png",
+ "largeIcon": "icon-large.png",
+ "uiRevision": 2
}
diff --git a/packages/apps/user/src/views/Appointment/AppointmentList/AppointmentList.js b/packages/apps/user/src/views/Appointment/AppointmentList/AppointmentList.js
index e2bc3ced..74702950 100644
--- a/packages/apps/user/src/views/Appointment/AppointmentList/AppointmentList.js
+++ b/packages/apps/user/src/views/Appointment/AppointmentList/AppointmentList.js
@@ -18,7 +18,6 @@ import {
SimpleGrid,
Radio,
RadioGroup,
- useCheckbox,
useCheckboxGroup,
Link as ChakraLink,
} from '@chakra-ui/react';
@@ -27,39 +26,7 @@ import specialties from '../Specialties';
import { DoctorList, HospitalList, FavoriteList } from '../dataList';
import BackButton from '../../../components/BackButton/BackButton';
import AppointmentCard from '../../../components/AppointmentCard/AppointmentCard';
-
-function CheckCard({ ...checkboxProps }) {
- const { getInputProps, getCheckboxProps } = useCheckbox(checkboxProps);
-
- const input = getInputProps();
- const checkbox = getCheckboxProps();
-
- return (
-
-
-
- {checkboxProps.children}
-
-
- );
-}
+import CustomCheckbox from '@housepital/common/CustomCheckox';
function AppointmentList() {
const { category } = useParams();
@@ -193,13 +160,13 @@ function AppointmentList() {
{specialties.map(specialty => (
-
{specialty}
-
+
))}
diff --git a/packages/common/CustomCheckox/CustomCheckox.jsx b/packages/common/CustomCheckox/CustomCheckox.jsx
new file mode 100644
index 00000000..4372bd2c
--- /dev/null
+++ b/packages/common/CustomCheckox/CustomCheckox.jsx
@@ -0,0 +1,36 @@
+import { Box, useCheckbox } from '@chakra-ui/react';
+
+function CustomCheckbox({ ...checkboxProps }) {
+ const { getInputProps, getCheckboxProps } = useCheckbox(checkboxProps);
+
+ const input = getInputProps();
+ const checkbox = getCheckboxProps();
+
+ return (
+
+
+
+ {checkboxProps.children}
+
+
+ );
+}
+
+export default CustomCheckbox;
diff --git a/packages/common/CustomCheckox/package.json b/packages/common/CustomCheckox/package.json
new file mode 100644
index 00000000..c501bbbb
--- /dev/null
+++ b/packages/common/CustomCheckox/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "CustomCheckox.jsx"
+}