diff --git a/.gitignore b/.gitignore index 9a809d748..d393f047c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ typings/ # Optional npm cache directory .npm +.cache # Optional eslint cache .eslintcache diff --git a/.travis.yml b/.travis.yml index 6e3566c93..873277f3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,20 @@ script: - cp packages/esm-app-shell/dist/*.* dist/@openmrs/esm-app-shell/$TRAVIS_COMMIT/ - cp packages/esm-app-shell/dist/*.* dist/@openmrs/esm-app-shell/latest/ deploy: - provider: s3 - access_key_id: "$DIGITAL_OCEAN_SPACES_KEY_ID" - secret_access_key: "$DIGITAL_OCEAN_SPACES_ACCESS_KEY" - bucket: "$DIGITAL_OCEAN_SPACES_BUCKET" - endpoint: "$DIGITAL_OCEAN_SPACES_ENDPOINT" - cache-control: "max-age=31536000" - local_dir: dist - skip_cleanup: true - acl: public_read - on: - branch: master + - provider: s3 + access_key_id: "$DIGITAL_OCEAN_SPACES_KEY_ID" + secret_access_key: "$DIGITAL_OCEAN_SPACES_ACCESS_KEY" + bucket: "$DIGITAL_OCEAN_SPACES_BUCKET" + endpoint: "$DIGITAL_OCEAN_SPACES_ENDPOINT" + cache-control: "max-age=31536000" + local_dir: dist + skip_cleanup: true + acl: public_read + on: + branch: master + - provider: npm + run_script: "ci:publish" + api_token: "$NPM_AUTH_TOKEN" + edge: true + on: + tags: true diff --git a/lerna.json b/lerna.json index 13e9aa977..72b7d3c86 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.2.0" + "version": "3.0.1" } diff --git a/package.json b/package.json index 97b6397c8..9aecdea20 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ ], "scripts": { "run:shell": "lerna run watch --scope @openmrs/esm-app-shell --stream", - "run:omrs": "run-openmrs", + "run:omrs": "openmrs", + "ci:publish": "lerna publish from-package --yes", "verify": "lerna run lint && lerna run test && lerna run typescript", "prettier": "prettier 'packages/**/src/**/*' --write" }, diff --git a/packages/esm-api/package.json b/packages/esm-api/package.json index 0249c3321..5253d3c32 100644 --- a/packages/esm-api/package.json +++ b/packages/esm-api/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-api", - "version": "3.1.0", + "version": "3.0.1", "license": "MPL-2.0", "description": "The javascript module for interacting with the OpenMRS API", "browser": "dist/openmrs-esm-api.js", @@ -12,6 +12,7 @@ "typescript": "tsc", "lint": "eslint src --ext ts,tsx" }, + "keywords": ["openmrs", "microfrontends"], "directories": { "lib": "dist", "src": "src" @@ -40,8 +41,8 @@ "single-spa": "4.x" }, "devDependencies": { - "@openmrs/esm-config": "^0.3.0", - "@openmrs/esm-error-handling": "^1.3.0", + "@openmrs/esm-config": "^3.0.1", + "@openmrs/esm-error-handling": "^3.0.1", "@types/fhir": "0.0.31", "@types/react": "^16.9.46", "react": "^16.13.1", diff --git a/packages/esm-api/src/environment.ts b/packages/esm-api/src/environment.ts new file mode 100644 index 000000000..42a9b0794 --- /dev/null +++ b/packages/esm-api/src/environment.ts @@ -0,0 +1,6 @@ +export function isDevEnabled() { + return ( + window.spaEnv === "development" || + localStorage.getItem("openmrs:devtools") === "true" + ); +} diff --git a/packages/esm-app-shell/package.json b/packages/esm-app-shell/package.json index 03b81229c..9443903bb 100644 --- a/packages/esm-app-shell/package.json +++ b/packages/esm-app-shell/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-app-shell", - "version": "0.3.0", + "version": "3.0.1", "license": "MPL-2.0", "main": "dist/openmrs.js", "scripts": { @@ -10,6 +10,7 @@ "watch": "webpack-dev-server", "lint": "eslint src --ext ts,tsx" }, + "keywords": ["openmrs", "microfrontends"], "browserslist": [ "extends browserslist-config-openmrs" ], @@ -22,11 +23,11 @@ }, "homepage": "https://github.com/openmrs/openmrs-esm-core#readme", "dependencies": { - "@openmrs/esm-api": "^3.1.0", - "@openmrs/esm-config": "^0.3.0", - "@openmrs/esm-error-handling": "^1.3.0", - "@openmrs/esm-extensions": "^0.3.0", - "@openmrs/esm-styleguide": "^1.5.0", + "@openmrs/esm-api": "^3.0.1", + "@openmrs/esm-config": "^3.0.1", + "@openmrs/esm-error-handling": "^3.0.1", + "@openmrs/esm-extensions": "^3.0.1", + "@openmrs/esm-styleguide": "^3.0.1", "carbon-components": "^10.19.0", "carbon-icons": "^7.0.7", "i18next": "^19.6.0", diff --git a/packages/esm-app-shell/src/index.ejs b/packages/esm-app-shell/src/index.ejs index 9bd49a1ad..1d6e2014f 100644 --- a/packages/esm-app-shell/src/index.ejs +++ b/packages/esm-app-shell/src/index.ejs @@ -31,6 +31,7 @@ initializeSpa({ apiUrl: "<%= openmrsApiUrl %>", spaPath: "<%= openmrsPublicPath %>", + env: "<%= openmrsEnvironment %>", }); diff --git a/packages/esm-app-shell/src/index.ts b/packages/esm-app-shell/src/index.ts index b87658014..19aa46fa5 100644 --- a/packages/esm-app-shell/src/index.ts +++ b/packages/esm-app-shell/src/index.ts @@ -53,6 +53,7 @@ function runShell() { function setupPaths(config: SpaConfig) { window.openmrsBase = config.apiUrl; window.spaBase = config.spaPath; + window.spaEnv = config.env || "production"; window.getOpenmrsSpaBase = () => `${window.spaBase}/`; } diff --git a/packages/esm-app-shell/src/types.ts b/packages/esm-app-shell/src/types.ts index eadcccd9c..bb3b5495a 100644 --- a/packages/esm-app-shell/src/types.ts +++ b/packages/esm-app-shell/src/types.ts @@ -3,12 +3,15 @@ declare global { getOpenmrsSpaBase(): string; openmrsBase: string; spaBase: string; + spaEnv: SpaEnvironment; importMapOverrides: { getCurrentPageMap: () => Promise; }; } } +export type SpaEnvironment = "production" | "development" | "test"; + export interface ImportMap { imports: Record; } @@ -22,6 +25,11 @@ export interface SpaConfig { * The base path for the SPA root path. */ spaPath: string; + /** + * The environment to use. + * @default production + */ + env?: SpaEnvironment; } export interface Activator { diff --git a/packages/esm-app-shell/webpack.config.js b/packages/esm-app-shell/webpack.config.js index 124081633..849a6f3e8 100644 --- a/packages/esm-app-shell/webpack.config.js +++ b/packages/esm-app-shell/webpack.config.js @@ -17,6 +17,7 @@ const openmrsProxyTarget = process.env.OMRS_PROXY_TARGET || "https://openmrs-spa.org/"; const openmrsFavicon = process.env.OMRS_FAVICON || "favicon.ico"; const openmrsImportmapDef = process.env.OMRS_ESM_IMPORTMAP; +const openmrsEnvironment = process.env.OMRS_ENV || process.env.NODE_ENV || ''; const openmrsImportmapUrl = process.env.OMRS_ESM_IMPORTMAP_URL || "importmap.json"; @@ -111,6 +112,7 @@ module.exports = { openmrsFavicon, openmrsImportmapDef, openmrsImportmapUrl, + openmrsEnvironment, }, }), new CopyWebpackPlugin({ diff --git a/packages/esm-config/package.json b/packages/esm-config/package.json index 11fb2880c..4d71a4801 100644 --- a/packages/esm-config/package.json +++ b/packages/esm-config/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-config", - "version": "0.3.0", + "version": "3.0.1", "license": "MPL-2.0", "description": "A configuration library for the OpenMRS Single-Spa framework.", "browser": "dist/openmrs-esm-module-config.js", @@ -13,6 +13,7 @@ "typescript": "tsc", "lint": "eslint src --ext ts,tsx" }, + "keywords": ["openmrs", "microfrontends"], "directories": { "lib": "dist", "src": "src" diff --git a/packages/esm-error-handling/package.json b/packages/esm-error-handling/package.json index fa58b66c2..3141d5131 100644 --- a/packages/esm-error-handling/package.json +++ b/packages/esm-error-handling/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-error-handling", - "version": "1.3.0", + "version": "3.0.1", "license": "MPL-2.0", "description": "An ES module to help with error handling", "browser": "dist/openmrs-esm-error-handling.js", @@ -12,6 +12,7 @@ "typescript": "tsc", "lint": "eslint src --ext ts,tsx" }, + "keywords": ["openmrs", "microfrontends"], "directories": { "lib": "dist", "src": "src" @@ -34,6 +35,6 @@ "@openmrs/esm-styleguide": "*" }, "devDependencies": { - "@openmrs/esm-styleguide": "^1.5.0" + "@openmrs/esm-styleguide": "^3.0.1" } } diff --git a/packages/esm-extensions/package.json b/packages/esm-extensions/package.json index 256133f4c..85a247aa6 100644 --- a/packages/esm-extensions/package.json +++ b/packages/esm-extensions/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-extensions", - "version": "0.3.0", + "version": "3.0.1", "license": "MPL-2.0", "description": "Coordinates extensions and extension points in the OpenMRS Frontend", "browser": "dist/openmrs-esm-extensions.js", @@ -12,6 +12,7 @@ "typescript": "tsc", "lint": "eslint src --ext ts,tsx" }, + "keywords": ["openmrs", "microfrontends"], "directories": { "lib": "dist", "src": "src" @@ -27,9 +28,6 @@ "url": "https://github.com/openmrs/openmrs-esm-core/issues" }, "homepage": "https://github.com/openmrs/openmrs-esm-core#readme", - "keywords": [ - "openmrs" - ], "dependencies": { "systemjs-webpack-interop": "^2.1.2" }, @@ -39,7 +37,7 @@ "single-spa": "4.x" }, "devDependencies": { - "@openmrs/esm-config": "^0.3.0", + "@openmrs/esm-config": "^3.0.1", "@types/react": "^16.9.46", "react": "^16.13.1", "single-spa": "^4.4.1" diff --git a/packages/esm-styleguide/package.json b/packages/esm-styleguide/package.json index 501f4df59..30a4f12d8 100644 --- a/packages/esm-styleguide/package.json +++ b/packages/esm-styleguide/package.json @@ -1,6 +1,6 @@ { "name": "@openmrs/esm-styleguide", - "version": "1.5.0", + "version": "3.0.1", "license": "MPL-2.0", "description": "The styleguide for OpenMRS SPA", "browser": "dist/openmrs-esm-styleguide.js", @@ -13,6 +13,7 @@ "build": "webpack --mode=production", "lint": "eslint src" }, + "keywords": ["openmrs", "microfrontends", "styleguide"], "directories": { "lib": "dist", "src": "src" @@ -24,10 +25,6 @@ "type": "git", "url": "git+https://github.com/openmrs/openmrs-esm-core.git" }, - "keywords": [ - "openmrs", - "styleguide" - ], "bugs": { "url": "https://github.com/openmrs/openmrs-esm-core/issues" }, diff --git a/packages/run-openmrs/.babelrc b/packages/openmrs/.babelrc similarity index 100% rename from packages/run-openmrs/.babelrc rename to packages/openmrs/.babelrc diff --git a/packages/run-openmrs/package.json b/packages/openmrs/package.json similarity index 80% rename from packages/run-openmrs/package.json rename to packages/openmrs/package.json index 9aa993592..585c44ade 100644 --- a/packages/run-openmrs/package.json +++ b/packages/openmrs/package.json @@ -1,10 +1,10 @@ { - "name": "run-openmrs", - "version": "0.3.0", + "name": "openmrs", + "version": "3.0.1", "license": "MPL-2.0", "main": "dist/index.js", "bin": { - "run-openmrs": "./dist/cli.js" + "openmrs": "./dist/cli.js" }, "scripts": { "start": "npm run watch", @@ -19,16 +19,18 @@ "bugs": { "url": "https://github.com/openmrs/openmrs-esm-core/issues" }, + "keywords": ["openmrs", "microfrontends", "cli", "tool"], "homepage": "https://github.com/openmrs/openmrs-esm-core#readme", "dependencies": { "@babel/core": "^7.11.4", "@babel/preset-env": "^7.11.0", "@babel/preset-react": "^7.10.4", "@babel/preset-typescript": "^7.10.4", - "@openmrs/esm-app-shell": "^0.3.0", + "@openmrs/esm-app-shell": "^3.0.1", "@types/react": "^16.9.46", "@types/systemjs": "^6.1.0", "autoprefixer": "^9.6.1", + "axios": "0.20.0", "babel-loader": "^8.1.0", "browserslist-config-openmrs": "^1.0.1", "clean-webpack-plugin": "^3.0.0", @@ -39,15 +41,21 @@ "file-loader": "^4.2.0", "fork-ts-checker-webpack-plugin": "^5.1.0", "html-webpack-plugin": "^4.5.0", + "inquirer": "^7.3.3", "mini-css-extract-plugin": "^0.8.0", "postcss-loader": "^4.0.2", "raw-loader": "^3.1.0", "style-loader": "^1.0.0", + "tar": "^6.0.5", "ts-loader": "^8.0.3", "typescript": "^4.0.2", "webpack": "^4.44.1", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0", "yargs": "16.0.3" + }, + "devDependencies": { + "@types/inquirer": "^6.5.0", + "@types/tar": "^4.0.3" } } diff --git a/packages/openmrs/src/cli.ts b/packages/openmrs/src/cli.ts new file mode 100644 index 000000000..8d1728c56 --- /dev/null +++ b/packages/openmrs/src/cli.ts @@ -0,0 +1,134 @@ +#!/usr/bin/env node + +import * as yargs from "yargs"; +import { fork } from "child_process"; +import { resolve } from "path"; +import { getImportmap } from "./utils"; + +import type * as commands from "./commands"; + +const runner = resolve(__dirname, `runner.js`); +const root = resolve(__dirname, ".."); + +type Commands = typeof commands; +type CommandNames = keyof Commands; + +function runCommand( + type: T, + args: Parameters[0] +) { + const ps = fork(runner, [], { cwd: root }); + + ps.send({ + type, + args, + }); +} + +yargs.command( + "debug", + "Starts a new debugging session of the OpenMRS app shell. This uses Webpack as a debug server with proxy middleware.", + (argv) => + argv + .number("port") + .default("port", 8080) + .describe("port", "The port where the dev server should run.") + .string("backend") + .default("backend", "https://openmrs-spa.org/") + .describe("backend", "The backend to proxy API requests to.") + .string("importmap") + .default("importmap", "importmap.json") + .describe( + "importmap", + "The import map to use. Can be a path to a valid import map to be taken literally, an URL, or a fixed JSON object. Alternatively, use the string `@` to debug the current microfrontend." + ), + (args) => + runCommand("runDebug", { + ...args, + importmap: getImportmap(args.importmap, args.port), + }) +); + +yargs.command( + "build", + "Builds a new app shell using the provided configuration.", + (argv) => + argv + .string("target") + .default("target", "dist") + .describe( + "target", + "The target directory where the build artifacts will be stored." + ) + .string("importmap") + .default("importmap", "importmap.json") + .describe( + "importmap", + "The import map to use. Can be a path to a valid import map to be taken literally, an URL, or a fixed JSON object." + ), + (args) => + runCommand("runBuild", { + ...args, + importmap: getImportmap(args.importmap), + target: resolve(process.cwd(), args.target), + }) +); + +yargs.command( + "assemble", + "Assembles an import map incl. all required resources.", + (argv) => + argv + .string("target") + .default("target", "dist") + .describe( + "target", + "The target directory where the gathered artifacts will be stored." + ) + .string("config") + .default("config", "microfrontends.json") + .describe( + "config", + "The configuration for gathering the list of microfrontends to include." + ) + .choices("mode", ["config", "survey"]) + .default("mode", "survey") + .describe( + "mode", + "The source of the microfrontends to assemble. `config` uses a configuration file specified via `--config`. `survey` starts an interactive command-line survey." + ), + (args) => + runCommand("runAssemble", { + ...args, + config: resolve(process.cwd(), args.config), + target: resolve(process.cwd(), args.target), + }) +); + +yargs.command( + ["start", "$0"], + "Starts the app shell using the provided configuration. This uses express for serving static files with some proxy middleware.", + (argv) => + argv + .number("port") + .default("port", 8080) + .describe("port", "The port where the dev server should run.") + .string("backend") + .default("backend", "https://openmrs-spa.org/") + .describe("backend", "The backend to proxy API requests to.") + .string("importmap") + .default("importmap", "importmap.json") + .describe( + "importmap", + "The import map to use. Can be a path to a valid import map to be taken literally, an URL, or a fixed JSON object." + ), + (args) => runCommand("runStart", { ...args }) +); + +yargs + .epilog( + "For more information visit https://github.com/openmrs/openmrs-esm-core." + ) + .help() + .demandCommand() + .strict().argv; diff --git a/packages/openmrs/src/commands/assemble.ts b/packages/openmrs/src/commands/assemble.ts new file mode 100644 index 000000000..aabcc1fe4 --- /dev/null +++ b/packages/openmrs/src/commands/assemble.ts @@ -0,0 +1,165 @@ +import { + readFileSync, + writeFileSync, + unlinkSync, + existsSync, + mkdirSync, + createReadStream, +} from "fs"; +import { resolve, dirname, basename } from "path"; +import { execSync } from "child_process"; +import { prompt, Question } from "inquirer"; +import { logInfo, untar } from "../utils"; +import axios from "axios"; + +export interface AssembleArgs { + target: string; + mode: string; + config: string; +} + +interface NpmSearchResult { + objects: Array<{ + package: { + name: string; + version: string; + }; + }>; +} + +async function readConfig(mode: string, config: string) { + switch (mode) { + case "config": + if (!existsSync(config)) { + throw new Error(`Could not find the config file "${config}".`); + } + + logInfo(`Reading configuration ...`); + + return JSON.parse(readFileSync(config, "utf8")); + case "survey": + logInfo(`Loading available microfrontends ...`); + + const packages = await axios + .get( + `https://registry.npmjs.org/-/v1/search?text=keywords:openmrs&size=250` + ) + .then((res) => res.data) + .then((res) => + res.objects + .map((m) => ({ + name: m.package.name, + version: m.package.version, + })) + .filter((m) => m.name.endsWith("-app")) + ); + const questions: Array = []; + + for (const pckg of packages) { + questions.push( + { + name: pckg.name, + message: `Include microfrontend "${pckg.name}"?`, + default: false, + type: "confirm", + }, + { + name: pckg.name, + askAnswered: true, + message: `Version for "${pckg.name}"?`, + default: pckg.version, + type: "string", + when(ans) { + return ans[pckg.name]; + }, + } as Question + ); + } + + const answers = await prompt(questions); + + return { + publicUrl: ".", + microfrontends: Object.keys(answers) + .filter((m) => answers[m]) + .reduce((prev, curr) => { + prev[curr] = answers[curr]; + return prev; + }, {}), + }; + } +} + +async function downloadPackage( + cacheDir: string, + esmName: string, + esmVersion: string +) { + const packageName = `${esmName}@${esmVersion}`; + const command = `npm pack ${packageName}`; + mkdirSync(cacheDir, { recursive: true }); + const result = execSync(command, { + cwd: cacheDir, + }); + return result.toString("utf8").split("\n").filter(Boolean).pop() ?? ""; +} + +async function extractFiles(sourceFile: string, targetDir: string) { + mkdirSync(targetDir, { recursive: true }); + const packageRoot = "package"; + const rs = createReadStream(sourceFile); + const files = await untar(rs); + const packageJson = JSON.parse( + files[`${packageRoot}/package.json`].toString("utf8") + ); + const entryModule = + packageJson.browser ?? packageJson.module ?? packageJson.main; + const fileName = basename(entryModule); + const sourceDir = dirname(entryModule); + + Object.keys(files) + .filter((m) => m.startsWith(`${packageRoot}/${sourceDir}`)) + .forEach((m) => { + const content = files[m]; + const fileName = m.replace(`${packageRoot}/${sourceDir}/`, ""); + const targetFile = resolve(targetDir, fileName); + mkdirSync(dirname(targetFile), { recursive: true }); + writeFileSync(targetFile, content); + }); + + unlinkSync(sourceFile); + return fileName; +} + +export async function runAssemble(args: AssembleArgs) { + const config = await readConfig(args.mode, args.config); + const importmap = { + imports: {}, + }; + + logInfo(`Assembling the importmap ...`); + + const { microfrontends = {}, publicUrl = "." } = config; + const cacheDir = resolve(process.cwd(), ".cache"); + + mkdirSync(args.target, { recursive: true }); + + await Promise.all( + Object.keys(microfrontends).map(async (esmName) => { + const esmVersion = microfrontends[esmName]; + const tgzFileName = await downloadPackage(cacheDir, esmName, esmVersion); + const dirName = tgzFileName.replace(".tgz", ""); + const fileName = await extractFiles( + resolve(cacheDir, tgzFileName), + resolve(args.target, dirName) + ); + importmap.imports[esmName] = `${publicUrl}/${dirName}/${fileName}`; + }) + ); + + writeFileSync( + resolve(args.target, "importmap.json"), + JSON.stringify(importmap, undefined, 2), + "utf8" + ); +} diff --git a/packages/run-openmrs/src/commands/build.ts b/packages/openmrs/src/commands/build.ts similarity index 77% rename from packages/run-openmrs/src/commands/build.ts rename to packages/openmrs/src/commands/build.ts index 2c8ec874d..049335b52 100644 --- a/packages/run-openmrs/src/commands/build.ts +++ b/packages/openmrs/src/commands/build.ts @@ -1,4 +1,4 @@ -import { ImportmapDeclaration, loadConfig } from "../utils"; +import { ImportmapDeclaration, loadConfig, logInfo } from "../utils"; /* eslint-disable no-console */ @@ -11,9 +11,10 @@ export function runBuild(args: BuildArgs) { const webpack = require("webpack"); const config = loadConfig({ importmap: args.importmap, + env: "production", }); - console.log(`[OpenMRS] Running build process ...`); + logInfo(`Running build process ...`); const compiler = webpack({ ...config, @@ -33,7 +34,7 @@ export function runBuild(args: BuildArgs) { }) ); - console.log(`[OpenMRS] Build finished.`); + logInfo(`Build finished.`); } }); } diff --git a/packages/run-openmrs/src/commands/debug.ts b/packages/openmrs/src/commands/debug.ts similarity index 64% rename from packages/run-openmrs/src/commands/debug.ts rename to packages/openmrs/src/commands/debug.ts index 049ba8edc..7f68fb168 100644 --- a/packages/run-openmrs/src/commands/debug.ts +++ b/packages/openmrs/src/commands/debug.ts @@ -1,6 +1,4 @@ -import { ImportmapDeclaration, loadConfig } from "../utils"; - -/* eslint-disable no-console */ +import { ImportmapDeclaration, loadConfig, logInfo, logWarn } from "../utils"; export interface DebugArgs { port: number; @@ -15,12 +13,14 @@ export function runDebug(args: DebugArgs) { const config = loadConfig({ importmap: args.importmap, backend: args.backend, + env: "development", }); - console.log(`[OpenMRS] Starting the dev server ...`); + logInfo(`Starting the dev server ...`); const options = { ...config.devServer, + port: args.port, publicPath: config.output.publicPath, stats: { colors: true }, }; @@ -28,11 +28,11 @@ export function runDebug(args: DebugArgs) { const server = new WebpackDevServer(webpack(config), options); const port = args.port; - server.listen(port, "localhost", function (err) { + server.listen(port, "localhost", (err?: Error) => { if (err) { - console.warn(`[OpenMRS] Error: ${err}`); + logWarn(`Error: ${err}`); + } else { + logInfo(`Listening at http://localhost:${port}`); } - - console.log(`[OpenMRS] Listening at http://localhost:${port}`); }); } diff --git a/packages/openmrs/src/commands/index.ts b/packages/openmrs/src/commands/index.ts new file mode 100644 index 000000000..dfbc98e0c --- /dev/null +++ b/packages/openmrs/src/commands/index.ts @@ -0,0 +1,4 @@ +export * from "./assemble"; +export * from "./build"; +export * from "./debug"; +export * from "./start"; diff --git a/packages/openmrs/src/commands/start.ts b/packages/openmrs/src/commands/start.ts new file mode 100644 index 000000000..f40dffe3d --- /dev/null +++ b/packages/openmrs/src/commands/start.ts @@ -0,0 +1,7 @@ +/* eslint-disable no-console */ + +export interface StartArgs { + port: number; +} + +export function runStart(args: StartArgs) {} diff --git a/packages/run-openmrs/src/index.ts b/packages/openmrs/src/index.ts similarity index 100% rename from packages/run-openmrs/src/index.ts rename to packages/openmrs/src/index.ts diff --git a/packages/openmrs/src/runner.ts b/packages/openmrs/src/runner.ts new file mode 100644 index 000000000..0e309a394 --- /dev/null +++ b/packages/openmrs/src/runner.ts @@ -0,0 +1,15 @@ +import * as commands from "./commands"; +import { logFail } from "./utils"; + +process.on("message", async ({ type, args }) => { + const command = commands[type]; + + if (typeof command === "function") { + try { + await command(args); + } catch (err) { + logFail(err.message); + process.exit(1); + } + } +}); diff --git a/packages/openmrs/src/utils/config.ts b/packages/openmrs/src/utils/config.ts new file mode 100644 index 000000000..2b116b79a --- /dev/null +++ b/packages/openmrs/src/utils/config.ts @@ -0,0 +1,35 @@ +import { ImportmapDeclaration } from "./importmap"; +import { setEnvVariables } from "./variables"; + +export interface WebpackOptions { + backend?: string; + importmap?: ImportmapDeclaration; + env?: string; +} + +export function loadConfig(options: WebpackOptions = {}) { + const variables: Record = {}; + + if (typeof options.backend === "string") { + variables.OMRS_PROXY_TARGET = options.backend; + } + + if (typeof options.env === "string") { + variables.OMRS_ENV = options.env; + } + + if (typeof options.importmap === "object") { + switch (options.importmap.type) { + case "inline": + variables.OMRS_ESM_IMPORTMAP = options.importmap.value; + break; + case "url": + variables.OMRS_ESM_IMPORTMAP_URL = options.importmap.value; + break; + } + } + + setEnvVariables(variables); + + return require("@openmrs/esm-app-shell/webpack.config.js"); +} diff --git a/packages/openmrs/src/utils/debugger.ts b/packages/openmrs/src/utils/debugger.ts new file mode 100644 index 000000000..383aaea1e --- /dev/null +++ b/packages/openmrs/src/utils/debugger.ts @@ -0,0 +1,25 @@ +import { logInfo, logWarn } from "./logger"; + +function debug(configPath: string, port: number) { + const webpack = require("webpack"); + const WebpackDevServer = require("webpack-dev-server"); + const config = require(configPath); + + const options = { + ...config.devServer, + port, + stats: { colors: true }, + }; + + const server = new WebpackDevServer(webpack(config), options); + + server.listen(port, "localhost", (err?: Error) => { + if (err) { + logWarn(`Error: ${err}`); + } else { + logInfo(`Listening at http://localhost:${port}`); + } + }); +} + +process.on("message", ({ source, port }) => debug(source, port)); diff --git a/packages/openmrs/src/utils/importmap.ts b/packages/openmrs/src/utils/importmap.ts new file mode 100644 index 000000000..c6b078e65 --- /dev/null +++ b/packages/openmrs/src/utils/importmap.ts @@ -0,0 +1,78 @@ +import { resolve, basename } from "path"; +import { existsSync, readFileSync } from "fs"; +import { logFail, logWarn } from "./logger"; +import { startWebpack } from "./webpack"; + +export interface ImportmapDeclaration { + type: "inline" | "url"; + value: string; +} + +export function checkImportmapJson(value: string) { + try { + const content = JSON.parse(value); + return typeof content === "object" && typeof content.imports === "object"; + } catch { + return false; + } +} + +export function getImportmap( + value: string, + basePort?: number +): ImportmapDeclaration { + if (value === "@" && basePort) { + const projectFile = resolve(process.cwd(), "package.json"); + + if (!existsSync(projectFile)) { + logFail(`No "package.json" found in the current directory.`); + return process.exit(1); + } + + const configPath = resolve(process.cwd(), "webpack.config.js"); + + if (!existsSync(configPath)) { + logFail(`No "webpack.config.json" found in the current directory.`); + return process.exit(1); + } + + const project = require(projectFile); + const port = basePort + 1; + const file = basename(project.browser || project.module || project.main); + + startWebpack(configPath, port); + + return { + type: "inline", + value: `{ "imports": { "${project.name}": "http://localhost:${port}/${file}" } }`, + }; + } else if (!/https?:\/\//.test(value)) { + const path = resolve(process.cwd(), value); + + if (existsSync(path)) { + const content = readFileSync(path, "utf8"); + const valid = checkImportmapJson(content); + + if (!valid) { + logWarn( + `The importmap provided in "${value}" does not seem right. Skipping.` + ); + } + + return { + type: "inline", + value: valid ? content : "", + }; + } else if (checkImportmapJson(value)) { + return { + type: "inline", + value, + }; + } + } + + return { + type: "url", + value, + }; +} diff --git a/packages/openmrs/src/utils/index.ts b/packages/openmrs/src/utils/index.ts new file mode 100644 index 000000000..9d55f4bea --- /dev/null +++ b/packages/openmrs/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from "./config"; +export * from "./importmap"; +export * from "./logger"; +export * from "./untar"; +export * from "./variables"; diff --git a/packages/openmrs/src/utils/logger.ts b/packages/openmrs/src/utils/logger.ts new file mode 100644 index 000000000..7aaad2857 --- /dev/null +++ b/packages/openmrs/src/utils/logger.ts @@ -0,0 +1,13 @@ +/* eslint-disable no-console */ + +export function logInfo(message: string) { + console.log(`[openmrs] ${message}`); +} + +export function logWarn(message: string) { + console.warn(`[openmrs] ${message}`); +} + +export function logFail(message: string) { + console.error(`[openmrs] ${message}`); +} diff --git a/packages/openmrs/src/utils/untar.ts b/packages/openmrs/src/utils/untar.ts new file mode 100644 index 000000000..ff957d6b1 --- /dev/null +++ b/packages/openmrs/src/utils/untar.ts @@ -0,0 +1,36 @@ +import * as tar from "tar"; +import { createGunzip } from "zlib"; +import { EventEmitter } from "events"; + +const TarParser = tar.Parse as any; + +interface ReadEntry extends EventEmitter { + path: string; + mode: number; + ignore: boolean; +} + +export interface PackageFiles { + [file: string]: Buffer; +} + +export function untar(stream: NodeJS.ReadableStream): Promise { + return new Promise((resolve, reject) => { + const files: PackageFiles = {}; + stream + .on("error", reject) + .pipe(createGunzip()) + .on("error", reject) + .pipe(new TarParser()) + .on("error", reject) + .on("entry", (e: ReadEntry) => { + const content: Array = []; + const p = e.path; + + e.on("error", reject); + e.on("data", (c: Buffer) => content.push(c)); + e.on("end", () => (files[p] = Buffer.concat(content))); + }) + .on("end", () => resolve(files)); + }); +} diff --git a/packages/openmrs/src/utils/variables.ts b/packages/openmrs/src/utils/variables.ts new file mode 100644 index 000000000..a3ea69cce --- /dev/null +++ b/packages/openmrs/src/utils/variables.ts @@ -0,0 +1,5 @@ +export function setEnvVariables(envVariables: Record) { + Object.keys(envVariables).forEach((key) => { + process.env[key] = envVariables[key]; + }); +} diff --git a/packages/openmrs/src/utils/webpack.ts b/packages/openmrs/src/utils/webpack.ts new file mode 100644 index 000000000..5c2cd177f --- /dev/null +++ b/packages/openmrs/src/utils/webpack.ts @@ -0,0 +1,11 @@ +import { resolve } from "path"; +import { fork } from "child_process"; + +export function startWebpack(source: string, port: number) { + const runner = resolve(__dirname, "debugger.js"); + const ps = fork(runner, [], { cwd: process.cwd() }); + + ps.send({ source, port }); + + return ps; +} diff --git a/packages/run-openmrs/tsconfig.json b/packages/openmrs/tsconfig.json similarity index 100% rename from packages/run-openmrs/tsconfig.json rename to packages/openmrs/tsconfig.json diff --git a/packages/run-openmrs/src/cli.ts b/packages/run-openmrs/src/cli.ts deleted file mode 100644 index 854f53a1f..000000000 --- a/packages/run-openmrs/src/cli.ts +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node - -import * as yargs from "yargs"; -import { fork } from "child_process"; -import { resolve } from "path"; -import { getImportmap } from "./utils"; - -import type * as commands from "./commands"; - -const runner = resolve(__dirname, `runner.js`); -const root = resolve(__dirname, ".."); - -type Commands = typeof commands; -type CommandNames = keyof Commands; - -function runCommand( - type: T, - args: Parameters[0] -) { - const ps = fork(runner, [], { cwd: root }); - - ps.send({ - type, - args, - }); -} - -yargs.command( - "debug", - "Starts a new debugging session of the OpenMRS app shell.", - (argv) => - argv - .number("port") - .default("port", 8080) - .describe("port", "The port where the dev server should run.") - .string("backend") - .default("backend", "https://openmrs-spa.org/") - .describe("backend", "The backend to proxy API requests to.") - .string("importmap") - .default("importmap", "importmap.json") - .describe( - "importmap", - "The importmap to use. Can be a path to a valid import map to be taken literally, an URL, or a fixed JSON object." - ), - (args) => - runCommand("runDebug", { - ...args, - importmap: getImportmap(args.importmap), - }) -); - -yargs.command( - "build", - "Builds a new app shell using the provided configuration.", - (argv) => - argv - .string("target") - .default("target", "dist") - .describe( - "target", - "The target directory where the build artifacts will be stored." - ) - .string("importmap") - .default("importmap", "importmap.json") - .describe( - "importmap", - "The importmap to use. Can be a path to a valid import map to be taken literally, an URL, or a fixed JSON object." - ), - (args) => - runCommand("runBuild", { - ...args, - importmap: getImportmap(args.importmap), - target: resolve(process.cwd(), args.target), - }) -); - -yargs - .epilog( - "For more information visit https://github.com/openmrs/openmrs-esm-core." - ) - .help() - .demandCommand() - .strict().argv; diff --git a/packages/run-openmrs/src/commands/index.ts b/packages/run-openmrs/src/commands/index.ts deleted file mode 100644 index eb8ff8a37..000000000 --- a/packages/run-openmrs/src/commands/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./debug"; -export * from "./build"; diff --git a/packages/run-openmrs/src/runner.ts b/packages/run-openmrs/src/runner.ts deleted file mode 100644 index 9e472bf8f..000000000 --- a/packages/run-openmrs/src/runner.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as commands from "./commands"; - -process.on("message", ({ type, args }) => { - const command = commands[type]; - - if (typeof command === "function") { - command(args); - } -}); diff --git a/packages/run-openmrs/src/utils/index.ts b/packages/run-openmrs/src/utils/index.ts deleted file mode 100644 index 4e8a69a37..000000000 --- a/packages/run-openmrs/src/utils/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { resolve } from "path"; -import { existsSync, readFileSync } from "fs"; - -export interface WebpackOptions { - backend?: string; - importmap?: ImportmapDeclaration; -} - -export interface ImportmapDeclaration { - type: "inline" | "url"; - value: string; -} - -export function setEnvVariables(envVariables: Record) { - Object.keys(envVariables).forEach((key) => { - process.env[key] = envVariables[key]; - }); -} - -export function loadConfig(options: WebpackOptions = {}) { - const variables: Record = {}; - - if (typeof options.backend === "string") { - variables.OMRS_PROXY_TARGET = options.backend; - } - - if (typeof options.importmap === "object") { - switch (options.importmap.type) { - case "inline": - variables.OMRS_ESM_IMPORTMAP = options.importmap.value; - break; - case "url": - variables.OMRS_ESM_IMPORTMAP_URL = options.importmap.value; - break; - } - } - - setEnvVariables(variables); - - return require("@openmrs/esm-app-shell/webpack.config.js"); -} - -export function checkImportmapJson(value: string) { - try { - const content = JSON.parse(value); - return typeof content === "object" && typeof content.imports === "object"; - } catch { - return false; - } -} - -export function getImportmap(value: string): ImportmapDeclaration { - if (!/https?:\/\//.test(value)) { - const path = resolve(process.cwd(), value); - - if (existsSync(path)) { - const content = readFileSync(path, "utf8"); - const valid = checkImportmapJson(content); - - if (!valid) { - console.warn( - `The importmap provided in "${value}" does not seem right. Skipping.` - ); - } - - return { - type: "inline", - value: valid ? content : "", - }; - } else if (checkImportmapJson(value)) { - return { - type: "inline", - value, - }; - } - } - - return { - type: "url", - value, - }; -} diff --git a/yarn.lock b/yarn.lock index c0fccbe80..58e42e32e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2726,6 +2726,14 @@ resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" integrity sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA== +"@types/inquirer@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" + integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -2781,6 +2789,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= +"@types/minipass@*": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" + integrity sha512-wuzZksN4w4kyfoOv/dlpov4NOunwutLA/q7uc00xU02ZyUY+aoM5PWIXEKBMnm0NHd4a+N71BMjq+x7+2Af1fg== + dependencies: + "@types/node" "*" + "@types/node@*", "@types/node@>= 8": version "14.10.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.10.0.tgz#15815dff82c8dc30827f6b1286f865902945095a" @@ -2888,6 +2903,14 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74" integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== +"@types/tar@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/tar/-/tar-4.0.3.tgz#e2cce0b8ff4f285293243f5971bd7199176ac489" + integrity sha512-Z7AVMMlkI8NTWF0qGhC4QIX0zkV/+y0J8x7b/RsHrN0310+YNjoJd8UrApCiGBCWtKjxS9QhNqLi2UJNToh5hA== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + "@types/testing-library__jest-dom@^5.9.1": version "5.9.2" resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.2.tgz#59e4771a1cf87d51e89a5cc8195cd3b647cba322" @@ -2895,6 +2918,13 @@ dependencies: "@types/jest" "*" +"@types/through@*": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/uglify-js@*": version "3.9.3" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" @@ -3618,6 +3648,13 @@ axe-core@^3.3.2, axe-core@^3.5.4: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== +axios@0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd" + integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA== + dependencies: + follow-redirects "^1.10.0" + axobject-query@^2.1.2: version "2.2.0" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -6932,7 +6969,7 @@ focus-lock@^0.7.0: resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a" integrity sha512-LI7v2mH02R55SekHYdv9pRHR9RajVNyIJ2N5IEkWbg7FT5ZmJ9Hw4mWxHeEUcd+dJo0QmzztHvDvWcc7prVFsw== -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.13.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== @@ -8126,7 +8163,7 @@ inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0: +inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -13669,7 +13706,7 @@ tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@^6.0.2: +tar@^6.0.2, tar@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg==