diff --git a/.github/workflows/dhis2-deploy-netlify.yml b/.github/workflows/dhis2-deploy-netlify.yml index 1b8bf10ab..10bf297f0 100644 --- a/.github/workflows/dhis2-deploy-netlify.yml +++ b/.github/workflows/dhis2-deploy-netlify.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - uses: c-hive/gha-yarn-cache@v1 - run: yarn install --frozen-lockfile diff --git a/.github/workflows/dhis2-verify-lib.yml b/.github/workflows/dhis2-verify-lib.yml index e7f8a08b5..852355167 100644 --- a/.github/workflows/dhis2-verify-lib.yml +++ b/.github/workflows/dhis2-verify-lib.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - uses: c-hive/gha-yarn-cache@v1 - run: yarn install --frozen-lockfile @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - uses: c-hive/gha-yarn-cache@v1 - run: yarn install --frozen-lockfile @@ -59,7 +59,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - uses: actions/download-artifact@v2 with: @@ -81,7 +81,7 @@ jobs: token: ${{env.GH_TOKEN}} - uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - uses: actions/download-artifact@v2 with: diff --git a/adapter/src/utils/localeUtils.js b/adapter/src/utils/localeUtils.js index aa52f6a9a..6f2d57fb1 100644 --- a/adapter/src/utils/localeUtils.js +++ b/adapter/src/utils/localeUtils.js @@ -132,9 +132,10 @@ export const setMomentLocale = async (locale) => { for (const localeName of localeNameOptions) { try { - await import( - /* webpackChunkName: "moment-locales/[request]" */ `moment/locale/${localeName}` - ) + // Since Vite prefers importing the ESM form of moment, we need + // to import the ESM form of the locales here to use the same + // moment instance + await import(`moment/dist/locale/${localeName}`) moment.locale(localeName) break } catch { diff --git a/cli/config/jest.config.js b/cli/config/jest.config.js index a2a059a18..4277b08fe 100644 --- a/cli/config/jest.config.js +++ b/cli/config/jest.config.js @@ -3,6 +3,9 @@ module.exports = { transform: { '^.+\\.[t|j]sx?$': require.resolve('./jest.transform.js'), }, + // Need to apply babel transformations to modules under moment/dist/, + // which use ES module format. See localeUtils.js in the adapter + transformIgnorePatterns: ['/node_modules/(?!moment/dist/)'], moduleNameMapper: { '\\.(css|less)$': require.resolve('./jest.identity.mock.js'), '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': diff --git a/cli/package.json b/cli/package.json index 4a962c853..8aba5025e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -2,7 +2,7 @@ "name": "@dhis2/cli-app-scripts", "version": "11.2.2", "engines": { - "node": ">=14" + "node": "^18.0.0 || >=20.0.0" }, "repository": { "type": "git", @@ -29,7 +29,7 @@ "@babel/preset-react": "^7.0.0", "@babel/preset-typescript": "^7.6.0", "@dhis2/app-shell": "11.2.2", - "@dhis2/cli-helpers-engine": "^3.2.0", + "@dhis2/cli-helpers-engine": "^3.2.2", "@jest/core": "^27.0.6", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@yarnpkg/lockfile": "^1.1.0", diff --git a/cli/src/commands/build.js b/cli/src/commands/build.js index 20819901d..9cc6e10a3 100644 --- a/cli/src/commands/build.js +++ b/cli/src/commands/build.js @@ -10,7 +10,7 @@ const parseConfig = require('../lib/parseConfig') const { isApp } = require('../lib/parseConfig') const makePaths = require('../lib/paths') const makePlugin = require('../lib/plugin') -const { injectPrecacheManifest } = require('../lib/pwa') +const { injectPrecacheManifest, compileServiceWorker } = require('../lib/pwa') const makeShell = require('../lib/shell') const { validatePackage } = require('../lib/validatePackage') const { handler: pack } = require('./pack.js') @@ -139,6 +139,9 @@ const handler = async ({ } if (config.pwa.enabled) { + reporter.info('Compiling service worker...') + await compileServiceWorker({ config, paths, mode }) + reporter.info( 'Injecting supplementary precache manifest...' ) diff --git a/cli/src/commands/start.js b/cli/src/commands/start.js index 660cea866..0b28c475b 100644 --- a/cli/src/commands/start.js +++ b/cli/src/commands/start.js @@ -116,11 +116,9 @@ const handler = async ({ if (config.pwa.enabled) { reporter.info('Compiling service worker...') - await compileServiceWorker({ - config, - paths, - mode: 'development', - }) + await compileServiceWorker({ config, paths, mode }) + // don't need to inject precache manifest because no precaching + // is done in development environments } reporter.print('') @@ -132,6 +130,7 @@ const handler = async ({ ) reporter.print('') + // todo: split up app and plugin starts const shellStartPromise = shell.start({ port: newPort }) if (config.entryPoints.plugin) { diff --git a/cli/src/lib/compiler/compile.js b/cli/src/lib/compiler/compile.js index 94842eae1..8b0865727 100644 --- a/cli/src/lib/compiler/compile.js +++ b/cli/src/lib/compiler/compile.js @@ -10,14 +10,11 @@ const { createAppEntrypointWrapper, createPluginEntrypointWrapper, } = require('./entrypoints.js') -const { - extensionPattern, - normalizeExtension, -} = require('./extensionHelpers.js') +const { extensionPattern } = require('./extensionHelpers.js') const watchFiles = ({ inputDir, outputDir, processFileCallback, watch }) => { const compileFile = async (source) => { - const relative = normalizeExtension(path.relative(inputDir, source)) + const relative = path.relative(inputDir, source) const destination = path.join(outputDir, relative) reporter.debug( `File ${relative} changed or added... dest: `, @@ -125,7 +122,8 @@ const compile = async ({ watchFiles({ inputDir: paths.src, outputDir: outDir, - processFileCallback: compileFile, + // todo: handle lib compilations with Vite + processFileCallback: isAppType ? copyFile : compileFile, watch, }), isAppType && diff --git a/cli/src/lib/compiler/entrypoints.js b/cli/src/lib/compiler/entrypoints.js index 943d548b0..6814a3760 100644 --- a/cli/src/lib/compiler/entrypoints.js +++ b/cli/src/lib/compiler/entrypoints.js @@ -2,7 +2,6 @@ const path = require('path') const { reporter, chalk } = require('@dhis2/cli-helpers-engine') const fs = require('fs-extra') const { isApp } = require('../parseConfig') -const { normalizeExtension } = require('./extensionHelpers.js') const verifyEntrypoint = ({ entrypoint, basePath, resolveModule }) => { if (!entrypoint.match(/^(\.\/)?src\//)) { @@ -82,12 +81,11 @@ exports.verifyEntrypoints = ({ const getEntrypointWrapper = async ({ entrypoint, paths }) => { const relativeEntrypoint = entrypoint.replace(/^(\.\/)?src\//, '') - const outRelativeEntrypoint = normalizeExtension(relativeEntrypoint) const shellAppSource = await fs.readFile(paths.shellSourceEntrypoint) return shellAppSource .toString() - .replace(/'.\/D2App\/app'/g, `'./D2App/${outRelativeEntrypoint}'`) + .replace(/'.\/D2App\/app\.jsx'/g, `'./D2App/${relativeEntrypoint}'`) } exports.createAppEntrypointWrapper = async ({ entrypoint, paths }) => { diff --git a/cli/src/lib/paths.js b/cli/src/lib/paths.js index 59aa52e6f..047e4e498 100644 --- a/cli/src/lib/paths.js +++ b/cli/src/lib/paths.js @@ -39,7 +39,7 @@ module.exports = (cwd = process.cwd()) => { readmeDefault: path.join(__dirname, '../../config/init.README.md'), shellSource, - shellSourceEntrypoint: path.join(shellSource, 'src/App.js'), + shellSourceEntrypoint: path.join(shellSource, 'src/App.jsx'), shellSourcePublic: path.join(shellSource, 'public'), base, @@ -55,17 +55,17 @@ module.exports = (cwd = process.cwd()) => { i18nLocales: path.join(base, './src/locales'), d2: path.join(base, './.d2/'), - appOutputFilename: 'App.js', + appOutputFilename: 'App.jsx', shell: path.join(base, './.d2/shell'), shellSrc: path.join(base, './.d2/shell/src'), - shellAppEntrypoint: path.join(base, './.d2/shell/src/App.js'), + shellAppEntrypoint: path.join(base, './.d2/shell/src/App.jsx'), shellAppDirname, shellApp: path.join(base, `./.d2/shell/${shellAppDirname}`), shellPluginBundleEntrypoint: path.join( base, - './.d2/shell/src/plugin.index.js' + './.d2/shell/src/plugin.index.jsx' ), - shellPluginEntrypoint: path.join(base, './.d2/shell/src/Plugin.js'), + shellPluginEntrypoint: path.join(base, './.d2/shell/src/Plugin.jsx'), shellSrcServiceWorker: path.join( base, './.d2/shell/src/service-worker.js' diff --git a/cli/src/lib/pwa/compileServiceWorker.js b/cli/src/lib/pwa/compileServiceWorker.js index 0a2be598f..eaca5395d 100644 --- a/cli/src/lib/pwa/compileServiceWorker.js +++ b/cli/src/lib/pwa/compileServiceWorker.js @@ -10,10 +10,9 @@ const getPWAEnvVars = require('./getPWAEnvVars') * dir for use with a dev server. In production mode, compiles a minified * service worker and outputs it into the apps `build` dir. * - * Currently used only for 'dev' SWs, since CRA handles production bundling. - * TODO: Use this for production bundling as well, which gives greater control - * over 'injectManifest' configuration (CRA omits files > 2MB) and bundling - * options. + * This could be migrated to a Vite config. Note that it still needs to be + * separate from the main app's Vite build because the SW needs a + * single-file IIFE output * * @param {Object} param0 * @param {Object} param0.config - d2 app config @@ -23,22 +22,22 @@ const getPWAEnvVars = require('./getPWAEnvVars') */ function compileServiceWorker({ config, paths, mode }) { // Choose appropriate destination for compiled SW based on 'mode' - const outputPath = - mode === 'production' - ? paths.shellBuildServiceWorker - : paths.shellPublicServiceWorker + const isProduction = mode === 'production' + const outputPath = isProduction + ? paths.shellBuildServiceWorker + : paths.shellPublicServiceWorker const { dir: outputDir, base: outputFilename } = path.parse(outputPath) // This is part of a bit of a hacky way to provide the same env vars to dev // SWs as in production by adding them to `process.env` using the plugin // below. - // TODO: This could be cleaner if the production SW is built in the same - // way instead of using the CRA webpack config, so both can more easily - // share environment variables. + // TODO: This could be refactored to be simpler now that we're not using + // CRA to build the service worker const env = getEnv({ name: config.title, ...getPWAEnvVars(config) }) const webpackConfig = { mode, // "production" or "development" + devtool: isProduction ? false : 'source-map', entry: paths.shellSrcServiceWorker, output: { path: outputDir, diff --git a/cli/src/lib/pwa/getPWAEnvVars.js b/cli/src/lib/pwa/getPWAEnvVars.js index eb6e39a03..1d636d972 100644 --- a/cli/src/lib/pwa/getPWAEnvVars.js +++ b/cli/src/lib/pwa/getPWAEnvVars.js @@ -33,9 +33,14 @@ function stringifyPatterns(patternsList) { * @param {Object} config */ function getPWAEnvVars(config) { - if (!isApp(config.type) || !config.pwa.enabled) { + if (!isApp(config.type)) { return null } + if (!config.pwa.enabled) { + // Explicitly adding this value to the env helps pare down code in + // non-PWA apps when doing static bundle analysis + return { pwa_enabled: 'false' } + } return { pwa_enabled: JSON.stringify(config.pwa.enabled), pwa_caching_omit_external_requests_from_app_shell: JSON.stringify( diff --git a/cli/src/lib/pwa/injectPrecacheManifest.js b/cli/src/lib/pwa/injectPrecacheManifest.js index fa2d2a01e..dde112cb4 100644 --- a/cli/src/lib/pwa/injectPrecacheManifest.js +++ b/cli/src/lib/pwa/injectPrecacheManifest.js @@ -36,25 +36,24 @@ function logManifestOutput({ count, filePaths, size, warnings }) { * `workbox-build`. */ module.exports = function injectPrecacheManifest(paths, config) { - // See https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.injectManifest + // See https://developer.chrome.com/docs/workbox/modules/workbox-build#injectmanifest_mode const injectManifestOptions = { swSrc: paths.shellBuildServiceWorker, swDest: paths.shellBuildServiceWorker, globDirectory: paths.shellBuildOutput, globPatterns: ['**/*'], - // Skip index.html, (plugin.html,) and `static` directory; - // CRA's workbox-webpack-plugin handles it smartly globIgnores: [ - 'static/**/*', - paths.launchPath, - paths.pluginLaunchPath, + // skip moment locales -- they result in many network requests and + // slow down service worker installation + '**/moment-locales/*', + '**/*.map', ...config.pwa.caching.globsToOmitFromPrecache, ], additionalManifestEntries: config.pwa.caching.additionalManifestEntries, - injectionPoint: 'self.__WB_BUILD_MANIFEST', + injectionPoint: 'self.__WB_MANIFEST', // Skip revision hashing for files with hash or semver in name: - // (see https://regex101.com/r/z4Hy9k/1/ for RegEx details) - dontCacheBustURLsMatching: /\.[a-z0-9]{8}\.|\d+\.\d+\.\d+/, + // (see https://regex101.com/r/z4Hy9k/3/ for RegEx details) + dontCacheBustURLsMatching: /[.-][A-Za-z0-9-_]{8}\.|\d+\.\d+\.\d+/, } return injectManifest(injectManifestOptions).then(logManifestOutput) diff --git a/cli/src/lib/shell/env.js b/cli/src/lib/shell/env.js index 962517191..d3acf29e2 100644 --- a/cli/src/lib/shell/env.js +++ b/cli/src/lib/shell/env.js @@ -35,6 +35,8 @@ module.exports = ({ port, ...vars }) => { ...filterEnv(), ...makeShellEnv(vars), }), + ...filterEnv(), + ...makeShellEnv(vars), PORT: port, PUBLIC_URL: process.env.PUBLIC_URL, } diff --git a/cli/src/lib/shell/index.js b/cli/src/lib/shell/index.js index d2e75c946..d7b04a14c 100644 --- a/cli/src/lib/shell/index.js +++ b/cli/src/lib/shell/index.js @@ -22,7 +22,7 @@ module.exports = ({ config, paths }) => { build: async () => { await exec({ cmd: 'yarn', - args: ['run', 'build'], + args: ['build'], cwd: paths.shell, env: getEnv({ ...baseEnvVars, ...getPWAEnvVars(config) }), pipe: false, @@ -31,10 +31,11 @@ module.exports = ({ config, paths }) => { start: async ({ port }) => { await exec({ cmd: 'yarn', - args: ['run', 'start'], + args: ['start'], cwd: paths.shell, env: getEnv({ ...baseEnvVars, port, ...getPWAEnvVars(config) }), - pipe: false, + // this option allows the colorful and interactive output from Vite: + stdio: 'inherit', }) }, // TODO: remove? Test command does not seem to call this method diff --git a/examples/simple-app/d2.config.js b/examples/simple-app/d2.config.js index 715669fa0..5aebf50ce 100644 --- a/examples/simple-app/d2.config.js +++ b/examples/simple-app/d2.config.js @@ -8,7 +8,7 @@ const config = { // standalone: true, // Don't bake-in a DHIS2 base URL, allow the user to choose entryPoints: { - app: './src/App.js', + app: './src/App.jsx', }, dataStoreNamespace: 'testapp-namespace', diff --git a/examples/simple-app/src/Alerter.js b/examples/simple-app/src/Alerter.jsx similarity index 100% rename from examples/simple-app/src/Alerter.js rename to examples/simple-app/src/Alerter.jsx diff --git a/examples/simple-app/src/App.js b/examples/simple-app/src/App.jsx similarity index 74% rename from examples/simple-app/src/App.js rename to examples/simple-app/src/App.jsx index 3e1a3c037..ee37f898c 100644 --- a/examples/simple-app/src/App.js +++ b/examples/simple-app/src/App.jsx @@ -1,7 +1,7 @@ import { useDataQuery } from '@dhis2/app-runtime' import moment from 'moment' import React from 'react' -import { Alerter } from './Alerter.js' +import { Alerter } from './Alerter.jsx' import style from './App.style.js' import i18n from './locales/index.js' @@ -23,6 +23,10 @@ const Component = () => {

{i18n.t('Hello {{name}}', { name: data.me.name })}

{i18n.t('Have a great {{dayOfTheWeek}}!', { + // NB: This won't localize on a dev build due to + // Vite's monorepo dep pre-bundling behavior. + // `moment` localization works outside the monorepo + // and in production here though dayOfTheWeek: moment.weekdays(true)[moment().weekday()], })} diff --git a/package.json b/package.json index d74bfcb84..c35e6bdd0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ ] }, "devDependencies": { - "@dhis2/cli-style": "^10.4.3", + "@dhis2/cli-style": "^10.5.2", "@dhis2/cli-utils-docsite": "^3.0.0", "concurrently": "^6.0.0", "serve": "^12.0.0" diff --git a/pwa/src/service-worker/set-up-service-worker.js b/pwa/src/service-worker/set-up-service-worker.js index 4bc5bfad7..54ee77127 100644 --- a/pwa/src/service-worker/set-up-service-worker.js +++ b/pwa/src/service-worker/set-up-service-worker.js @@ -26,7 +26,6 @@ import { setUpKillSwitchServiceWorker, getClientsInfo, claimClients, - CRA_MANIFEST_EXCLUDE_PATTERNS, } from './utils.js' export function setUpServiceWorker() { @@ -60,11 +59,8 @@ export function setUpServiceWorker() { // In development, static assets are handled by 'network first' strategy // and will be kept up-to-date. if (PRODUCTION_ENV) { - // Precache all of the assets generated by your build process. - // Their URLs are injected into the manifest variable below. - // This variable must be present somewhere in your service worker file, - // even if you decide not to use precaching. See https://cra.link/PWA. - // Includes all built assets and index.html + // Injection point for the precache manifest from workbox-build, + // a manifest of app assets to fetch and cache upon SW installation const precacheManifest = self.__WB_MANIFEST || [] // todo: also do this routing for plugin.html @@ -122,39 +118,22 @@ export function setUpServiceWorker() { return matchPrecache(indexUrl) }) } - // NOTE: This route must come before any precacheAndRoute calls + // NOTE: This route must come before any precacheAndRoute calls, since + // precacheAndRoute creates routes for ALL previously precached files registerRoute(navigationRouteMatcher, navigationRouteHandler) - // Handle the rest of files in the manifest - filter out index.html, - // and all moment-locales, which bulk up the precache and slow down - // installation significantly. Handle them network-first in app shell - const restOfManifest = precacheManifest.filter((e) => { - if (e === indexHtmlManifestEntry) { - return false - } - // Files from the precache manifest generated by CRA need to be - // managed here, because we don't have access to their webpack - // config - const entryShouldBeExcluded = CRA_MANIFEST_EXCLUDE_PATTERNS.some( - (pattern) => pattern.test(e.url) - ) - return !entryShouldBeExcluded - }) + // Handle the rest of files in the manifest - filter out index.html + const restOfManifest = precacheManifest.filter( + (e) => e !== indexHtmlManifestEntry + ) precacheAndRoute(restOfManifest) - // Same thing for built plugin assets + // Injection point for built plugin assets; injected by the workbox + // webpack plugin in the plugin's webpack config + // todo: reinvestigate after switching plugin builds to Vite + // (maybe will be covered by the first injection point above) const pluginPrecacheManifest = self.__WB_PLUGIN_MANIFEST || [] precacheAndRoute(pluginPrecacheManifest) - - // Similar to above; manifest injection from `workbox-build` - // Precaches all assets in the shell's build folder except in `static` - // (which CRA's workbox-webpack-plugin handle smartly). - // Additional files to precache can be added using the - // `additionalManifestEntries` option in d2.config.js; see the docs and - // 'injectPrecacheManifest.js' in the CLI package. - // '[]' fallback prevents an error when switching pwa enabled to disabled - const sharedBuildManifest = self.__WB_BUILD_MANIFEST || [] - precacheAndRoute(sharedBuildManifest) } // Handling pings: only use the network, and don't update the connection diff --git a/pwa/src/service-worker/utils.js b/pwa/src/service-worker/utils.js index 4fd83b313..84cdb4f4c 100644 --- a/pwa/src/service-worker/utils.js +++ b/pwa/src/service-worker/utils.js @@ -11,11 +11,6 @@ const APP_ADAPTER_URL_PATTERNS = [ /\/api(\/\d+)?\/userSettings/, // useLocale /\/api(\/\d+)?\/me\?fields=id$/, // useVerifyLatestUser ] -// Note that the CRA precache manifest files start with './' -// TODO: Make this extensible with a d2.config.js option -export const CRA_MANIFEST_EXCLUDE_PATTERNS = [ - /^\.\/static\/js\/moment-locales\//, -] // '[]' Fallback prevents error when switching from pwa enabled to disabled const APP_SHELL_URL_FILTER_PATTERNS = JSON.parse( diff --git a/shell/public/index.html b/shell/index.html similarity index 64% rename from shell/public/index.html rename to shell/index.html index 5eff6b809..dd10d7d18 100644 --- a/shell/public/index.html +++ b/shell/index.html @@ -10,6 +10,7 @@ + - %REACT_APP_DHIS2_APP_NAME% | DHIS2 + %DHIS2_APP_NAME% | DHIS2
+ + + +