From 5f5fb3410d94f3f8867bc32cc1dba2385cd33e4b Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 13 Aug 2024 14:45:55 +0200 Subject: [PATCH 01/15] migrate monorepo --- packages/independent/monorepo/license | 21 ++ packages/independent/monorepo/package.json | 38 +++ packages/independent/monorepo/readme.md | 179 ++++++++++++++ .../independent/monorepo/scripts/test.mjs | 12 + .../internal/collect_workspace_packages.js | 56 +++++ .../internal/compare_two_package_versions.js | 47 ++++ .../monorepo/src/internal/dependency_graph.js | 65 +++++ .../src/internal/fetch_workspace_latests.js | 37 +++ .../monorepo/src/internal/increase_version.js | 13 + packages/independent/monorepo/src/main.js | 3 + .../monorepo/src/publish_packages.js | 60 +++++ .../monorepo/src/sync_packages_versions.js | 225 ++++++++++++++++++ .../monorepo/src/upgrade_external_versions.js | 173 ++++++++++++++ 13 files changed, 929 insertions(+) create mode 100644 packages/independent/monorepo/license create mode 100644 packages/independent/monorepo/package.json create mode 100644 packages/independent/monorepo/readme.md create mode 100644 packages/independent/monorepo/scripts/test.mjs create mode 100644 packages/independent/monorepo/src/internal/collect_workspace_packages.js create mode 100644 packages/independent/monorepo/src/internal/compare_two_package_versions.js create mode 100644 packages/independent/monorepo/src/internal/dependency_graph.js create mode 100644 packages/independent/monorepo/src/internal/fetch_workspace_latests.js create mode 100644 packages/independent/monorepo/src/internal/increase_version.js create mode 100644 packages/independent/monorepo/src/main.js create mode 100644 packages/independent/monorepo/src/publish_packages.js create mode 100644 packages/independent/monorepo/src/sync_packages_versions.js create mode 100644 packages/independent/monorepo/src/upgrade_external_versions.js diff --git a/packages/independent/monorepo/license b/packages/independent/monorepo/license new file mode 100644 index 0000000000..311d2eaf9d --- /dev/null +++ b/packages/independent/monorepo/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 jsenv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/independent/monorepo/package.json b/packages/independent/monorepo/package.json new file mode 100644 index 0000000000..63c75bd7fe --- /dev/null +++ b/packages/independent/monorepo/package.json @@ -0,0 +1,38 @@ +{ + "name": "@jsenv/monorepo", + "version": "0.0.8", + "description": "Helpers to manage packages in a monorepo", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/jsenv/core", + "directory": "packages/independent/monorepo" + }, + "engines": { + "node": ">=20.0.0" + }, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "import": "./src/main.js" + }, + "./*": "./*" + }, + "main": "./src/main.js", + "files": [ + "/src/" + ], + "scripts": { + "test": "node ./scripts/test.mjs" + }, + "dependencies": { + "@jsenv/urls": "2.5.2", + "@jsenv/filesystem": "4.10.2", + "@jsenv/package-publish": "1.10.7", + "@jsenv/humanize": "1.2.8", + "semver": "7.6.3" + } +} diff --git a/packages/independent/monorepo/readme.md b/packages/independent/monorepo/readme.md new file mode 100644 index 0000000000..9f37765e48 --- /dev/null +++ b/packages/independent/monorepo/readme.md @@ -0,0 +1,179 @@ +# @jsenv/monorepo + +Helpers to manage multiple packages from a single repository. For example when using [NPM workspaces](https://docs.npmjs.com/cli/v8/using-npm/workspaces). + +This packages helps to perform 2 tasks that are a bit painful to do "by hand" inside a monorepo: "publish a new version" and "upgrade dependencies". + +## Publish a new version + +Publishing a new version of a package in a monorepo by hand is time consuming and error prone. It's because you have to ensure packages versions are properly updated according to their inter-dependency. Let's see it with a basic example where a monorepo contains two packages and you make a change to one of them. + +_packages/main/package.json:_ + +```json +{ + "name": "main", + "version": "3.4.2", + "dependencies": { + "util": "1.0.0" + } +} +``` + +_packages/util/package.json:_ + +```json +{ + "name": "util", + "version": "1.0.0" +} +``` + +Now you update "version" in "packages/util/package.json" + +```diff +{ + "name": "util", +- "version": "1.0.0" ++ "version": "1.1.0" +} +``` + +At this point you are supposed to update "packages/main/package.json" like this: + +```diff +{ + "name": "main", +- "version": "3.4.2", ++ "version": "3.4.3", + "dependencies": { +- "util": "1.0.0" ++ "util": "1.1.0" + } +} +``` + +In a monorepo with many packages this is hard to do correctly and time consuming. You can automate the painful part as follows: + +1. Run _syncPackagesVersions_ +2. Review changes with a tool like "git diff" +3. Run _publishPackages_ + +### syncPackagesVersions + +_syncPackagesVersions_ is an async function ensuring versions in all package.json are in sync for all packages in the workspace. It update versions in "dependencies", "devDependencies" and increase "version" if needed. This ensure all versions are in sync before publishing. + +```js +import { syncPackagesVersions } from "@jsenv/monorepo"; + +await syncPackagesVersions({ + directoryUrl: new URL("./", import.meta.url), +}); +``` + +### Review changes + +Each package might need to increase their package.json "version" differently. When it's required _syncPackagesVersions_ increases PATCH number ("1.0.3" becomes "1.0.4"). After that it's up to you to review these changes to decide if you keep PATCH increment or want to increment MINOR or MAJOR instead. + +### publishPackages + +_publishPackages_ is an async function that will publish all packages in the monorepo on NPM. But only the packages that are not already published. + +```js +import { publishPackages } from "@jsenv/monorepo"; + +process.env.NPM_TOKEN = "token_auhtorized_to_publish_on_npm"; + +await publishPackages({ + directoryUrl: new URL("./", import.meta.url), +}); +``` + +## Upgrade dependencies + +As a maintainer of a package with many dependencies you periodically want to check if there is new versions of your dependencies to stay up to date. We'll first see what NPM packages are usually doing and why it's a problem. Then we'll see how to avoid that problem. Finally we'll see how to use a function to upgrade dependencies because it can be a bit time consuming to do by hand. + +NPM introduced what usage of ^ or "\*" in your _package.json_. + +```json +{ + "dependencies": { + "foo": "^1.0.0" + } +} +``` + +But it causes a problem. + +### The problem + +As a result "npm install" auto updates to latest versions if any is found. In the end any npm install can change the behaviour of your code if a new version was published since the last npm install. + +The sequence of events looks as below + +```console +[7h00] npm install +[7h01] npm downloads `foo@1.1.0` +[7h30] `foo@1.2.0` is published on NPM +[8h00] npm install +[8h01] npm downloads `foo@1.2.0` +``` + +Whenever you or someone else ends up with foo version `1.2.0` it can break the code or lead to different code behaviour. People will loose time trying to understand what's going on only to realize it comes from the new version. + +### But package-lock.json fixes that right? + +_package-lock.json_ fixes that but only if you run `npm ci`. +And people are still used to start a project using `npm install + npm start`. + +The problem is that `npm install` does too many things. +Most of the time you don't want to update your deps. The 2 most common scenarios are: + +- "I want want to install deps on a fresh project" +- "I want to ensure my deps are in sync after git pull in a branch" + +"I want to update all my deps" happens from time to time but is usually not what you had in mind before executing "npm install" + +Moreover the usage of _package-lock.json_ remains optional. +And _package-lock.json_ can be problematic https://github.com/npm/cli/issues/4828, some project disable package-lock to avoid these kind of issues until the situation becomes better. + +## How to avoid the problem + +Use explicit version in the package.json + +BAD + +```json +{ + "dependencies": { + "foo": "^1.0.0", + "bar": "2.*" + } +} +``` + +GOOD + +```json +{ + "dependencies": { + "foo": "1.1.3", + "bar": "2.0.0" + } +} +``` + +As a result there is no ambiguity on the version being used and we know the exact version in the glimpse of an eye. +**You control when the version gets updated** + +Once versions are fixed you can update whenever you want by running "npm outdated" and decide what to update by hand. + +But inside large codebases with a lot of packages this process takes time, you can use the following function to perform "npm outdated" + update the versions in the package.json + +```js +import { upgradeExternalVersions } from "@jsenv/monorepo"; + +await upgradeExternalVersions({ + directoryUrl: new URL("./", import.meta.url), +}); +``` diff --git a/packages/independent/monorepo/scripts/test.mjs b/packages/independent/monorepo/scripts/test.mjs new file mode 100644 index 0000000000..7e7ef2d738 --- /dev/null +++ b/packages/independent/monorepo/scripts/test.mjs @@ -0,0 +1,12 @@ +import { executeTestPlan, nodeWorkerThread } from "@jsenv/test"; + +await executeTestPlan({ + rootDirectoryUrl: new URL("../", import.meta.url), + testPlan: { + "tests/**/*.test.mjs": { + node: { + runtime: nodeWorkerThread(), + }, + }, + }, +}); diff --git a/packages/independent/monorepo/src/internal/collect_workspace_packages.js b/packages/independent/monorepo/src/internal/collect_workspace_packages.js new file mode 100644 index 0000000000..2a333d7a62 --- /dev/null +++ b/packages/independent/monorepo/src/internal/collect_workspace_packages.js @@ -0,0 +1,56 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { urlToRelativeUrl } from "@jsenv/urls"; +import { listFilesMatching } from "@jsenv/filesystem"; + +export const collectWorkspacePackages = async ({ directoryUrl }) => { + const workspacePackages = {}; + const rootPackageUrl = new URL("package.json", directoryUrl); + const rootPackageFileInfo = readPackageFile(rootPackageUrl); + const rootPackage = { + isRoot: true, + packageUrl: rootPackageUrl, + packageObject: rootPackageFileInfo.object, + updateFile: rootPackageFileInfo.updateFile, + }; + workspacePackages[rootPackageFileInfo.object.name] = rootPackage; + + const patterns = {}; + const { workspaces = [] } = rootPackage.packageObject; + workspaces.forEach((workspace) => { + const workspaceUrl = new URL(workspace, rootPackageUrl).href; + const workspaceRelativeUrl = urlToRelativeUrl(workspaceUrl, rootPackageUrl); + const pattern = `${workspaceRelativeUrl}/package.json`; + patterns[pattern] = true; + }); + + const packageDirectoryUrls = await listFilesMatching({ + directoryUrl, + patterns, + }); + packageDirectoryUrls.forEach((packageDirectoryUrl) => { + const packageUrl = new URL("package.json", packageDirectoryUrl); + const packageFileInfo = readPackageFile(packageUrl); + workspacePackages[packageFileInfo.object.name] = { + packageUrl, + packageObject: packageFileInfo.object, + updateFile: packageFileInfo.updateFile, + }; + }); + return workspacePackages; +}; + +const readPackageFile = (url) => { + const packageFileContent = String(readFileSync(new URL(url))); + const hasFinalNewLine = + packageFileContent[packageFileContent.length - 1] === "\n"; + return { + object: JSON.parse(packageFileContent), + updateFile: (data) => { + let dataAsJson = JSON.stringify(data, null, " "); + if (hasFinalNewLine) { + dataAsJson += "\n"; + } + writeFileSync(new URL(url), dataAsJson); + }, + }; +}; diff --git a/packages/independent/monorepo/src/internal/compare_two_package_versions.js b/packages/independent/monorepo/src/internal/compare_two_package_versions.js new file mode 100644 index 0000000000..d0ada234c5 --- /dev/null +++ b/packages/independent/monorepo/src/internal/compare_two_package_versions.js @@ -0,0 +1,47 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +// https://github.com/npm/node-semver#readme +const { + gt: versionGreaterThan, + prerelease: versionToPrerelease, +} = require("semver"); + +export const VERSION_COMPARE_RESULTS = { + SAME: "same", + GREATER: "greater", + SMALLER: "smaller", + DIFF_TAG: "diff_tag", +}; + +export const compareTwoPackageVersions = (firstVersion, secondVersion) => { + if (firstVersion === secondVersion) { + return VERSION_COMPARE_RESULTS.SAME; + } + if (versionGreaterThan(firstVersion, secondVersion)) { + return VERSION_COMPARE_RESULTS.GREATER; + } + const firstVersionPrerelase = versionToPrerelease(firstVersion); + const secondVersionPrerelease = versionToPrerelease(secondVersion); + if (firstVersionPrerelase === null && secondVersionPrerelease === null) { + return VERSION_COMPARE_RESULTS.SMALLER; + } + if (firstVersionPrerelase !== null && secondVersionPrerelease === null) { + return VERSION_COMPARE_RESULTS.SMALLER; + } + if (firstVersionPrerelase === null && secondVersionPrerelease !== null) { + return VERSION_COMPARE_RESULTS.SMALLER; + } + const [firstReleaseTag, firstPrereleaseVersion] = firstVersionPrerelase; + const [secondReleaseTag, secondPrereleaseVersion] = firstVersionPrerelase; + if (firstReleaseTag !== secondReleaseTag) { + return VERSION_COMPARE_RESULTS.DIFF_TAG; + } + if (firstPrereleaseVersion === secondPrereleaseVersion) { + return VERSION_COMPARE_RESULTS.SAME; + } + if (firstPrereleaseVersion > secondPrereleaseVersion) { + return VERSION_COMPARE_RESULTS.GREATER; + } + return VERSION_COMPARE_RESULTS.SMALLER; +}; diff --git a/packages/independent/monorepo/src/internal/dependency_graph.js b/packages/independent/monorepo/src/internal/dependency_graph.js new file mode 100644 index 0000000000..4e0a3b730e --- /dev/null +++ b/packages/independent/monorepo/src/internal/dependency_graph.js @@ -0,0 +1,65 @@ +export const buildDependencyGraph = (workspacePackages) => { + const dependencyGraph = {}; + Object.keys(workspacePackages).forEach((packageName) => { + dependencyGraph[packageName] = { + dependencies: [], + }; + }); + const findDependency = (packageName, dependencyName) => { + const trace = []; + const visit = (name) => { + if (name === dependencyName) return true; + trace.push(name); + return dependencyGraph[name].dependencies.some((name) => visit(name)); + }; + const found = dependencyGraph[packageName].dependencies.some((name) => { + return visit(name); + }); + return found ? trace : null; + }; + const markPackageAsDependentOf = (packageName, dependencyName) => { + const dependencyTrace = findDependency(dependencyName, packageName); + if (dependencyTrace) { + throw new Error( + `Circular dependency between ${packageName} and ${dependencyName}`, + ); + } + dependencyGraph[packageName].dependencies.push(dependencyName); + }; + Object.keys(workspacePackages).forEach((packageName) => { + const workspacePackage = workspacePackages[packageName]; + const { packageObject } = workspacePackage; + const { dependencies = {} } = packageObject; + Object.keys(dependencies).forEach((dependencyName) => { + const dependencyAsWorkspacePackage = workspacePackages[dependencyName]; + if (dependencyAsWorkspacePackage) { + markPackageAsDependentOf(packageName, dependencyName); + } + }); + }); + return dependencyGraph; +}; + +export const orderByDependencies = (dependencyGraph) => { + const visited = []; + const sorted = []; + const visit = (packageName, importerPackageName) => { + const isSorted = sorted.includes(packageName); + if (isSorted) return; + const isVisited = visited.includes(packageName); + if (isVisited) { + throw new Error( + `Circular dependency between ${packageName} and ${importerPackageName}`, + ); + } + visited.push(packageName); + dependencyGraph[packageName].dependencies.forEach((dependencyName) => { + visit(dependencyName, packageName); + }); + sorted.push(packageName); + }; + Object.keys(dependencyGraph).forEach((packageName) => { + visit(packageName); + }); + return sorted; +}; diff --git a/packages/independent/monorepo/src/internal/fetch_workspace_latests.js b/packages/independent/monorepo/src/internal/fetch_workspace_latests.js new file mode 100644 index 0000000000..6283751f7e --- /dev/null +++ b/packages/independent/monorepo/src/internal/fetch_workspace_latests.js @@ -0,0 +1,37 @@ +import { createTaskLog } from "@jsenv/humanize"; +import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetchLatestInRegistry.js"; + +export const fetchWorkspaceLatests = async (workspacePackages) => { + const packageNames = Object.keys(workspacePackages); + let done = 0; + const total = packageNames.length; + const latestVersions = {}; + const fetchTask = createTaskLog(`fetch latest versions`); + try { + await Promise.all( + packageNames.map(async (packageName) => { + const workspacePackage = workspacePackages[packageName]; + if (workspacePackage.packageObject.private) { + latestVersions[packageName] = workspacePackage.packageObject.version; + } else { + const latestPackageInRegistry = await fetchLatestInRegistry({ + registryUrl: "https://registry.npmjs.org", + packageName, + }); + const registryLatestVersion = + latestPackageInRegistry === null + ? null + : latestPackageInRegistry.version; + latestVersions[packageName] = registryLatestVersion; + } + done++; + fetchTask.setRightText(`${done}/${total}`); + }), + ); + fetchTask.done(); + return latestVersions; + } catch (e) { + fetchTask.fail(); + throw e; + } +}; diff --git a/packages/independent/monorepo/src/internal/increase_version.js b/packages/independent/monorepo/src/internal/increase_version.js new file mode 100644 index 0000000000..e1a19cd449 --- /dev/null +++ b/packages/independent/monorepo/src/internal/increase_version.js @@ -0,0 +1,13 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +// https://github.com/npm/node-semver#readme +const { parse, inc } = require("semver"); + +export const increaseVersion = (version) => { + const { prerelease } = parse(version); + if (prerelease.length === 0) { + return inc(version, "patch"); + } + return inc(version, "prerelease", prerelease[0]); +}; diff --git a/packages/independent/monorepo/src/main.js b/packages/independent/monorepo/src/main.js new file mode 100644 index 0000000000..d9ddfa609c --- /dev/null +++ b/packages/independent/monorepo/src/main.js @@ -0,0 +1,3 @@ +export { upgradeExternalVersions } from "./upgrade_external_versions.js"; +export { syncPackagesVersions } from "./sync_packages_versions.js"; +export { publishPackages } from "./publish_packages.js"; diff --git a/packages/independent/monorepo/src/publish_packages.js b/packages/independent/monorepo/src/publish_packages.js new file mode 100644 index 0000000000..86ee073875 --- /dev/null +++ b/packages/independent/monorepo/src/publish_packages.js @@ -0,0 +1,60 @@ +import { createLogger, UNICODE } from "@jsenv/humanize"; +import { publish } from "@jsenv/package-publish/src/internal/publish.js"; +import { collectWorkspacePackages } from "./internal/collect_workspace_packages.js"; +import { fetchWorkspaceLatests } from "./internal/fetch_workspace_latests.js"; +import { + compareTwoPackageVersions, + VERSION_COMPARE_RESULTS, +} from "./internal/compare_two_package_versions.js"; + +export const publishPackages = async ({ directoryUrl }) => { + const workspacePackages = await collectWorkspacePackages({ directoryUrl }); + const registryLatestVersions = await fetchWorkspaceLatests(workspacePackages); + const toPublishPackageNames = Object.keys(workspacePackages).filter( + (packageName) => { + const workspacePackage = workspacePackages[packageName]; + const registryLatestVersion = registryLatestVersions[packageName]; + if (registryLatestVersion === null) { + return true; + } + const result = compareTwoPackageVersions( + workspacePackage.packageObject.version, + registryLatestVersion, + ); + return ( + result === VERSION_COMPARE_RESULTS.GREATER || + result === VERSION_COMPARE_RESULTS.DIFF_TAG + ); + }, + ); + if (toPublishPackageNames.length === 0) { + console.log(`${UNICODE.OK} packages are published on registry`); + return; + } + + const packageSlugs = toPublishPackageNames.map( + (name) => `${name}@${workspacePackages[name].packageObject.version}`, + ); + + console.log(`${UNICODE.INFO} ${ + toPublishPackageNames.length + } packages to publish + - ${packageSlugs.join(` + - `)}`); + await toPublishPackageNames.reduce( + async (previous, toPublishPackageName, index) => { + await previous; + await publish({ + logger: createLogger({ logLevel: "info" }), + packageSlug: packageSlugs[index], + rootDirectoryUrl: new URL( + "./", + workspacePackages[toPublishPackageName].packageUrl, + ), + registryUrl: "https://registry.npmjs.org", + token: process.env.NPM_TOKEN, + }); + }, + Promise.resolve(), + ); +}; diff --git a/packages/independent/monorepo/src/sync_packages_versions.js b/packages/independent/monorepo/src/sync_packages_versions.js new file mode 100644 index 0000000000..adde757dbf --- /dev/null +++ b/packages/independent/monorepo/src/sync_packages_versions.js @@ -0,0 +1,225 @@ +import { UNICODE } from "@jsenv/humanize"; +import { collectWorkspacePackages } from "./internal/collect_workspace_packages.js"; +import { + buildDependencyGraph, + orderByDependencies, +} from "./internal/dependency_graph.js"; +import { fetchWorkspaceLatests } from "./internal/fetch_workspace_latests.js"; +import { + compareTwoPackageVersions, + VERSION_COMPARE_RESULTS, +} from "./internal/compare_two_package_versions.js"; +import { increaseVersion } from "./internal/increase_version.js"; + +export const syncPackagesVersions = async ({ + directoryUrl, + packagesRelations = {}, +}) => { + const workspacePackages = await collectWorkspacePackages({ directoryUrl }); + const registryLatestVersions = await fetchWorkspaceLatests(workspacePackages); + + const outdatedPackageNames = []; + const toPublishPackageNames = []; + for (const packageName of Object.keys(workspacePackages)) { + const workspacePackage = workspacePackages[packageName]; + const workspacePackageVersion = workspacePackage.packageObject.version; + const registryLatestVersion = registryLatestVersions[packageName]; + const result = + registryLatestVersion === null + ? VERSION_COMPARE_RESULTS.GREATER + : compareTwoPackageVersions( + workspacePackageVersion, + registryLatestVersion, + ); + if (result === VERSION_COMPARE_RESULTS.SMALLER) { + outdatedPackageNames.push(packageName); + continue; + } + if (!workspacePackage.packageObject.private) { + if ( + result === VERSION_COMPARE_RESULTS.GREATER || + result === VERSION_COMPARE_RESULTS.DIFF_TAG + ) { + toPublishPackageNames.push(packageName); + } + } + } + + const versionUpdates = []; + const dependencyUpdates = []; + if (outdatedPackageNames.length) { + outdatedPackageNames.forEach((outdatedPackageName) => { + const workspacePackage = workspacePackages[outdatedPackageName]; + workspacePackage.packageObject.version = + registryLatestVersions[outdatedPackageName]; + workspacePackage.updateFile(workspacePackage.packageObject); + }); + console.warn( + `${UNICODE.WARNING} ${outdatedPackageNames.length} packages modified because they where outdated. +Use a tool like "git diff" to see the new versions and ensure this is what you want`, + ); + return { + versionUpdates, + dependencyUpdates, + }; + } + if (toPublishPackageNames.length === 0) { + console.log(`${UNICODE.OK} packages are published on registry`); + } else { + console.log( + `${UNICODE.INFO} ${ + toPublishPackageNames.length + } packages could be published + - ${toPublishPackageNames.map( + (name) => `${name}@${workspacePackages[name].packageObject.version}`, + ).join(` + - `)}`, + ); + } + + const packageFilesToUpdate = {}; + const updateDependencyVersion = ({ + packageName, + dependencyType, + dependencyName, + from, + to, + }) => { + const packageDeps = + workspacePackages[packageName].packageObject[dependencyType]; + const version = packageDeps[dependencyName]; + // ignore local deps + if ( + version.startsWith("./") || + version.startsWith("../") || + version.startsWith("file:") + ) { + return; + } + dependencyUpdates.push({ + packageName, + dependencyName, + from, + to, + }); + packageDeps[dependencyName] = to; + packageFilesToUpdate[packageName] = true; + }; + const updateVersion = ({ packageName, from, to }) => { + const workspacePackage = workspacePackages[packageName]; + versionUpdates.push({ + packageName, + from, + to, + }); + workspacePackage.packageObject.version = to; + packageFilesToUpdate[packageName] = true; + }; + dependencies: { + const dependencyGraph = buildDependencyGraph(workspacePackages); + const packageNamesOrderedByDependency = + orderByDependencies(dependencyGraph); + packageNamesOrderedByDependency.forEach((packageName) => { + const workspacePackage = workspacePackages[packageName]; + const { dependencies = {} } = workspacePackage.packageObject; + Object.keys(dependencies).forEach((dependencyName) => { + const dependencyAsWorkspacePackage = workspacePackages[dependencyName]; + if (!dependencyAsWorkspacePackage) { + return; + } + const versionInDependencies = dependencies[dependencyName]; + const version = dependencyAsWorkspacePackage.packageObject.version; + if (versionInDependencies === version) { + return; + } + updateDependencyVersion({ + packageName, + dependencyType: "dependencies", + dependencyName, + from: versionInDependencies, + to: version, + }); + if (!toPublishPackageNames.includes(packageName)) { + updateVersion({ + packageName, + from: workspacePackage.packageObject.version, + to: increaseVersion(workspacePackage.packageObject.version), + }); + if (!workspacePackage.packageObject.private) { + toPublishPackageNames.push(packageName); + } + } + }); + }); + } + dev_dependencies: { + Object.keys(workspacePackages).forEach((packageName) => { + const workspacePackage = workspacePackages[packageName]; + const { devDependencies = {} } = workspacePackage.packageObject; + Object.keys(devDependencies).forEach((devDependencyName) => { + const devDependencyAsWorkspacePackage = + workspacePackages[devDependencyName]; + if (!devDependencyAsWorkspacePackage) { + return; + } + const versionInDevDependencies = devDependencies[devDependencyName]; + const version = devDependencyAsWorkspacePackage.packageObject.version; + if (versionInDevDependencies === version) { + return; + } + updateDependencyVersion({ + packageName, + dependencyType: "devDependencies", + dependencyName: devDependencyName, + from: versionInDevDependencies, + to: version, + }); + }); + }); + } + package_relations: { + Object.keys(packagesRelations).forEach((packageName) => { + const relatedPackageNames = packagesRelations[packageName]; + const someRelatedPackageUpdated = relatedPackageNames.some( + (relatedPackageName) => { + return ( + dependencyUpdates.some( + (dependencyUpdate) => + dependencyUpdate.packageName === relatedPackageName, + ) || + versionUpdates.some( + (versionUpdate) => + versionUpdate.packageName === relatedPackageName, + ) + ); + }, + ); + if (someRelatedPackageUpdated) { + const packageInfo = workspacePackages[packageName]; + updateVersion({ + packageName, + from: packageInfo.packageObject.version, + to: increaseVersion(packageInfo.packageObject.version), + }); + } + }); + } + Object.keys(packageFilesToUpdate).forEach((packageName) => { + const workspacePackage = workspacePackages[packageName]; + workspacePackage.updateFile(workspacePackage.packageObject); + }); + const updateCount = versionUpdates.length + dependencyUpdates.length; + if (updateCount === 0) { + console.log(`${UNICODE.OK} all versions in package.json files are in sync`); + } else { + console.log( + `${UNICODE.INFO} ${updateCount} versions modified in package.json files + Use a tool like "git diff" to review these changes`, + ); + } + + return { + versionUpdates, + dependencyUpdates, + }; +}; diff --git a/packages/independent/monorepo/src/upgrade_external_versions.js b/packages/independent/monorepo/src/upgrade_external_versions.js new file mode 100644 index 0000000000..fae47ccf1b --- /dev/null +++ b/packages/independent/monorepo/src/upgrade_external_versions.js @@ -0,0 +1,173 @@ +/* + * Try to upgrade all packages that are external to a monorepo. + * - "external" means a package that is not part of the monorepo + * - "upgrade" means check if there is a more recent version on NPM registry + * and if yes, update the version in the package.json + * + * Versions declared in "dependencies", "devDependencies" + * + * Be sure to check ../readme.md#upgrade-dependencies + */ + +import { UNICODE, createTaskLog } from "@jsenv/humanize"; +import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetchLatestInRegistry.js"; +import { collectWorkspacePackages } from "./internal/collect_workspace_packages.js"; +import { + compareTwoPackageVersions, + VERSION_COMPARE_RESULTS, +} from "./internal/compare_two_package_versions.js"; + +export const upgradeExternalVersions = async ({ directoryUrl }) => { + const internalPackages = await collectWorkspacePackages({ directoryUrl }); + const internalPackageNames = Object.keys(internalPackages); + let externalPackages = {}; + collect_external_packages: { + const addExternalPackage = ({ + internalPackageName, + name, + type, + version, + }) => { + // ignore local deps + if ( + version.startsWith("./") || + version.startsWith("../") || + version.startsWith("file:") + ) { + return; + } + // "*" means package accept anything + // so there is no need to update it, it's always matching the latest version + if (version === "*") { + return; + } + const existing = externalPackages[name]; + if (existing) { + externalPackages[name].push({ + internalPackageName, + type, + version, + }); + return; + } + externalPackages[name] = []; + externalPackages[name].push({ + internalPackageName, + type, + version, + }); + }; + for (const internalPackageName of internalPackageNames) { + const internalPackage = internalPackages[internalPackageName]; + const internalPackageObject = internalPackage.packageObject; + const { dependencies = {}, devDependencies = {} } = internalPackageObject; + const dependencyNames = Object.keys(dependencies); + dependencyNames.forEach((dependencyName) => { + addExternalPackage({ + internalPackageName, + type: "dependencies", + name: dependencyName, + version: dependencies[dependencyName], + }); + }); + const devDependencyNames = Object.keys(devDependencies); + devDependencyNames.forEach((devDependencyName) => { + addExternalPackage({ + internalPackageName, + type: "devDependencies", + name: devDependencyName, + version: devDependencies[devDependencyName], + }); + }); + } + } + const externalPackageNames = Object.keys(externalPackages); + console.log( + `${UNICODE.INFO} ${externalPackageNames.length} external packages found`, + ); + + const latestVersions = {}; + fetch_latest_versions: { + let done = 0; + const total = externalPackageNames.length; + const fetchTask = createTaskLog(`fetch latest versions`); + try { + await Promise.all( + externalPackageNames.map(async (externalPackageName) => { + const latestPackageInRegistry = await fetchLatestInRegistry({ + registryUrl: "https://registry.npmjs.org", + packageName: externalPackageName, + }); + if (latestPackageInRegistry === null) { + latestVersions[externalPackageName] = null; + console.warn( + `${UNICODE.WARN} "${externalPackageName}" not published on NPM`, + ); + } else { + const registryLatestVersion = latestPackageInRegistry.version; + latestVersions[externalPackageName] = registryLatestVersion; + } + done++; + fetchTask.setRightText(`${done}/${total}`); + }), + ); + fetchTask.done(); + } catch (e) { + fetchTask.fail(); + throw e; + } + } + + const packageFilesToUpdate = {}; + const updates = []; + for (const externalPackageName of externalPackageNames) { + const externalPackageRefs = externalPackages[externalPackageName]; + for (const externalPackageRef of externalPackageRefs) { + const internalPackageName = externalPackageRef.internalPackageName; + const internalPackageDeps = + internalPackages[internalPackageName].packageObject[ + externalPackageRef.type + ]; + const versionDeclared = internalPackageDeps[externalPackageName]; + const registryLatestVersion = latestVersions[externalPackageName]; + if (registryLatestVersion === null) { + continue; + } + const comparisonResult = compareTwoPackageVersions( + versionDeclared, + registryLatestVersion, + ); + if (comparisonResult === VERSION_COMPARE_RESULTS.GREATER) { + console.warn( + `${UNICODE.WARNING} ${externalPackageName} version declared in ${internalPackageName} "${externalPackageRef.type}" (${versionDeclared}) is greater than latest version in registry (${registryLatestVersion})`, + ); + continue; + } + if (comparisonResult === VERSION_COMPARE_RESULTS.SMALLER) { + updates.push({ + packageName: internalPackageName, + dependencyName: externalPackageName, + from: versionDeclared, + to: registryLatestVersion, + }); + internalPackageDeps[externalPackageName] = registryLatestVersion; + packageFilesToUpdate[internalPackageName] = true; + } + } + } + Object.keys(packageFilesToUpdate).forEach((packageName) => { + const internalPackage = internalPackages[packageName]; + internalPackage.updateFile(internalPackage.packageObject); + }); + if (updates.length === 0) { + console.log( + `${UNICODE.OK} all versions declared in package.json files are in up-to-date with registry`, + ); + } else { + console.log( + `${UNICODE.INFO} ${updates.length} versions modified in package.json files +Use a tool like "git diff" to review these changes then run "npm install"`, + ); + } + return updates; +}; From 77800c0cfb18ee24a2f75c43827f68879e2b7f3f Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 13 Aug 2024 14:48:53 +0200 Subject: [PATCH 02/15] migrate package-publish --- ...y-published-github-workflow-screenshot.png | Bin 0 -> 27597 bytes .../publishing-github-workflow-screenshot.png | Bin 0 -> 59356 bytes packages/independent/package-publish/license | 21 ++ .../independent/package-publish/package.json | 36 ++++ .../independent/package-publish/readme.md | 62 ++++++ .../package-publish/scripts/test.mjs | 12 ++ .../src/internal/fetch_latest_in_registry.js | 51 +++++ .../src/internal/needs_publish.js | 63 ++++++ .../package-publish/src/internal/publish.js | 185 ++++++++++++++++++ .../src/internal/read_project_package.js | 32 +++ .../src/internal/set_npm_config.js | 25 +++ .../independent/package-publish/src/main.js | 8 + .../package-publish/src/publish_package.js | 182 +++++++++++++++++ .../tests/fetchLatestInRegistry.test.mjs | 24 +++ .../tests/needsPublish.test.mjs | 82 ++++++++ .../tests/publishPackage.xtest.mjs | 133 +++++++++++++ .../tests/self-test.manual-test.mjs | 24 +++ .../tests/setNpmConfig.test.mjs | 46 +++++ .../package-publish/tests/testHelper.js | 43 ++++ 19 files changed, 1029 insertions(+) create mode 100644 packages/independent/package-publish/docs/already-published-github-workflow-screenshot.png create mode 100644 packages/independent/package-publish/docs/publishing-github-workflow-screenshot.png create mode 100644 packages/independent/package-publish/license create mode 100644 packages/independent/package-publish/package.json create mode 100644 packages/independent/package-publish/readme.md create mode 100644 packages/independent/package-publish/scripts/test.mjs create mode 100644 packages/independent/package-publish/src/internal/fetch_latest_in_registry.js create mode 100644 packages/independent/package-publish/src/internal/needs_publish.js create mode 100644 packages/independent/package-publish/src/internal/publish.js create mode 100644 packages/independent/package-publish/src/internal/read_project_package.js create mode 100644 packages/independent/package-publish/src/internal/set_npm_config.js create mode 100644 packages/independent/package-publish/src/main.js create mode 100644 packages/independent/package-publish/src/publish_package.js create mode 100644 packages/independent/package-publish/tests/fetchLatestInRegistry.test.mjs create mode 100644 packages/independent/package-publish/tests/needsPublish.test.mjs create mode 100644 packages/independent/package-publish/tests/publishPackage.xtest.mjs create mode 100644 packages/independent/package-publish/tests/self-test.manual-test.mjs create mode 100644 packages/independent/package-publish/tests/setNpmConfig.test.mjs create mode 100644 packages/independent/package-publish/tests/testHelper.js diff --git a/packages/independent/package-publish/docs/already-published-github-workflow-screenshot.png b/packages/independent/package-publish/docs/already-published-github-workflow-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..54ccee55ad0c6624d6c1774c6c265420088b20b2 GIT binary patch literal 27597 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 b`zO)THRy~Iz(3=9@IX#VQL;?jDCmCxet19Y literal 0 HcmV?d00001 diff --git a/packages/independent/package-publish/docs/publishing-github-workflow-screenshot.png b/packages/independent/package-publish/docs/publishing-github-workflow-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec9ef6a8dd5dc3e995a8a5b88b0e692240fdf91 GIT binary patch literal 59356 zcmaI7V|-*?vpyV7%$eAeiEZ1qolNYBZEIrNwr$(i#Om0|zwhTgU+(kFc|Yy$-oM>z zRjs|M>bkC4Ve+zK2(Z|&ARr(J65_&&ARyrTz|ZB-U;g>UR7M&E1h(H?NJw5nNQhAW zm#vApl`#m2cvxaGl=|0I^x>NhOfpABP+z{*&ec~T}fv7@)^0icZuoaLf zDDn_!BHy&(2rUJKYYD>-9Ar(L?Ay~vKTJfhtHD!w+41r3qsXsIkMMUw<5maf=rumLbF+`` z+Pdo2=70ExmOtRkp(;37v+>Z?aDsU_UMXev8yVS!P&`fRAfm}_JcVH-h$`>%j zFNYa3_3an6>8y2yRRstpUv+#;|LbL#19T~Bk#(OHNX8d=qmPEDOhkU37d`UJ>?SN8 zDE=-uUpPK?Mf<}7>pf}%P?(W?tTJ%xoR}^o>Y0giKR5t?JpxEBA=rSwh%fjPAwp=c zVhw5qI0qq;6*!jw3^~+muYnzW1_;6qbqjQrFU}5Y3le?+{tS|CfB^s{QV=;Xqy!m3 zF~pHjXau*;50{8-1Pa!_FGrmmU(!GD_cu)#Yu}q3uo8$e-?Ci29HtqOW9$VOo39YhDRtpEHjH9O807)F00K%oV{3wqo4s-Fj7=?Vb~ zjin­pi&axEwr%l-oeg}6Lcy8yaW*dms+0HHPF4mb-8ctoN883uGrj7UCG)c)QA zBUPgt29lKi6nAN=L)sz?rpRZ%LPM6?Lv>T?uQ&lReYAsz`VloMEAuPkE3Yfw=VG{s z#oHfeW3cRAAd`w`hcZa+cYXj)S|GdvSFq&@@{@=_E!$l=Hp$I!=! zB$*C~4tNqJS@PWF_e-!AXcjC^NluwgO*zss3UQU9shUNrl&f?vqc?Ee@;Y+4g}h@u zlRRVJB%ZZygkXgsi6QAAoBAw*6oZ&xbzq6ZnMEuI968pCWL!!$`hiI(=tvd$vb(9rnS zAgtNmNNbK@UT)!GMrMg&k-dz+kbBZ{0__OvM0?zkS3ZMvr|izqnaEpFUV2tdwOBcG zF@v|rv=p(RRjpS1sGMDJRgztzSF)YQXO&}wY*jOtGqe*+G&?i<+qPxAW)ct|TdUA8 zOPc>BAJmA&sK`FaKI;1G*Rrs7i8hJCQ68m?(tPvq6a4vglbX~_tnG@0^CS){8(UiX zZKEv?IIeN?Bs2ii?aRzdDTr4$@b@)bE?vW^b~-h9qXHlr4gm z4=+hBfi9g@rdMpQrmWgoNm^g7Dq4G4FR!q!b!8gyhOn7lW!P|~>C|kJwxhRSJs>_9 zzG}V-K!Jz2hA@WEiW!R~8FlTekX6JN&tn(Y%j#uZWHM#Qv45Tz?#Au?8Zg$cy4K#> z0C#xRzSmx@LaXAjacp*N_Oju*Y`e62lzcqGf5F#|p^Qm}R}y91C*O}MPAp!zZ8##M zGp$z9o!km-&UUGC35ki5E07DDX42!?{;`d{ZNSIXjpjZ1q5jGLdGVnmO+y-@F^P+4 z2d$1agDnmNk3p<2SXo|q2<87c)YmzPpJgY1^<%M61mx}S;$!)hI%8_!Hv+x zP_zNoq22wwy|O*g!`F#zj&2{nP5%r?By?;#?Iyt5_b8Flh$)&Ry$M}UMtmFsJf9yQ zI9v%jqxcf~qzNSO4jt_6?A5LK8-kaF(}`+59;wG^>c824|4C~gHvfG!MYo3p(*Z%^ z-#q#;dN7);D5!*37+EMLipoft^bw?wigl|*y$Ej8koFwe=<3DYoNecfmzgmFJDCUO4Zp zOMgZVLLJ&zTdgQRVQ%AhilZl+(p_vnV#g;tvrH4!5*}%?<9*&u&$1(Y@Ymy73XRI& zmpa$&)&tk^tz5RfTli}U%X4hS0MRdk?gloyA(;^_x>RXO%3To+Qf=kM>?JMs&Ape_ z?cIq0smo;dq@_gjBx~B&hLHB7DczXP#skd*w*$RLmhHg;1ZJD#WasSuOLAjb|wYG&( zgHFq-`~5k~k)>%Bm#%G#flq$dX4U(hOnZ7U*E!dzEyspM$Ax2ise{eYGuNdH?bdyl z#{vHBR@5ueBNu@%@A0MP%;Nmy1-@&_3*aJuLM%UKnYWjB)T<28aGZKrKejFHQ+`%^ z#j$+0x&FcZ_Poz4yM4Az((Q9L?JeM?aHn`=eD6EivnsINyIy_RedL#yznVDj*(G5f|1d7`OHrO(3v;;x)_4wJ_V3q9v%&Y-V3&B6>3q#3hoML&ag{S{>^f ze97mc8XUckd`z$QC(kF+SAByHGTtvbTAn|ymP%jy3a8`VSzutm{Cx=tp`rixrH4z1 z@QsT~0u~hl0#tw>8Tr31LB90+bI5E{v&ynBUdM{uFz2@c+wTQT~)`7xv*yR7AX@-rLrHD+cgaQ+Fn9OSah*M9@l@$8yg%;7UZ7?QN7KfB- z{@#?V?1x+5>c!6Xd?foXSd6#xEfd6aR@*|sL87T=&z zalOZkS-dw3nuxwZTMj0Fk&(Q1>*BsUngwTeoB_F6Z%q8)^-gKB))b>q{Gqmp|I+_R z$*oeBaQbrA*0|^tb*d6IESW-2bg|VXQMlP?OxZ3T3(`d7*Ev7R3b2JY1GM~4bE=>&PYt6&9UlNTj|Esf2Ap>#9{3svdTp00d z)Ei2p<0xvwpiAZ0{!Ew7Lbn_paY$Jr)to`Mj^pV7ii*qdjnja6JXy(+^;Sb|8;e_? zI;XoG-MiB{a;3&~``iODq308T*?5m#C1yY5{doOSN7+jh{#Bq*Dt$?6H~dIIr&t!d zdai{38RfsWsK5^1_Zp{hQfxlwZ|`0=KYbOAh9XO)=WVy%l2fn6&MiFC_XcBZ2gVaP z;8@I;;y9g8WO?!0XG~jfF%7b^0x_AoJeCpLTpn$Fe^u*r3Wgxz!Q1i4v+W>ViW3cl z5^F)j=?8-xzMRaPI(~M4l55NrF#p^EQL%V)y;gQj7>QkUqV{G8CE!w}c8VgPU!&J+ z@p!~1F&y$7sOc3=;SEC_RF&;$v4`@TAr`EJ7ktDi$(Cccf@wpSJgWYla-6KAtjTj zIq){FOb2K*+Nd{M>@!WKGej*_E9K$aQI2;dLK*hSk>Fd2FSgKTV%8X<4_Hzzzl2%y z?s6JJGwyLp;TrHFGTm%sV-%TAXU!I`)~5#0OkXj~zq#e^B{b_RxIY1ij3<6?G4g`t zN_aotkYqmH{zxoUPYh2WmvXt>VwltEa1E18G_nk!WLe;$udXVIk|QfqzJN1HF)br+ z>?-MX>&}})E0h2DPGUY%gTm~x1%{^Dt}0^zaMB^+RMC|gDa!sai?ma=cO?pN=){bXx)JX&F%gaiqHKNGv``ZSh2NwOmX$; z1T;^Dh1?+55rW6Sng-V{uUVA-ApyecLX z3x3|w6cUfed7>psXChxDLe$=HnDtU4^UdwXmRQ}g1g~*I6oS|7<&NUL>1Qb=xCwwh zhJf?QW03#MRew(4^TR`l;X*;9{xY1^dbP}d>2-}Ym`b}Qr|oj1H^jMq_3dHutkfyO zAwAP=9( z1b%RVSf1%X|5N~HRZRD5iv4f3w9nZ+NCy@CmZ4y0!N0ji4*DCdW~8BTj6nQQOs;*t z*i1kOQi%mi`gfGIJDPMh#WZH!g*+!Mi={e*LUHr`YOq^)qOr(){d#B5$0^^mn;vD^ zL6)oEALv;235i7SIM%Ea3)#01_m3;WZ_jY+tPi^8b0w6<6Db0ClYA%AhnuBJg^Ckr z9IT5?dA8kWVvy>VJa@wijz?2ElmItg%JIWPfn044Yc3G)EV^n*??1;1gU706^3k*a zvgNLlca|t@uHKQCwZ^kk+#&!WG@j>L!*{PJ549>?6?8DYg%VM>+do4zr;nS>3EUct zr(F8TXfztK8lWX09~`?^mbv@rZ00xjWNV1H+|nGk==2{3GK4?=JB9bW27;tmK1J{u zM6p+-PG|7S)IM9P&4dR;*HAsQFc6#1`n!$owMwtv_*Hoi@E6;~@0=!5IsO3%xW64U zyvlVmv^vL6aALx9Kxfl|2i1sfBvB}&eHIq!naC8g+Z$5oo#y+jFdv*Py_QAIe7QT5 zIT%k;{p5_|C6!G0jsEdc?~X!099*Pf8SjL9i;~`Jt=^eItJ5XjFB(~db6)N182kP{ zH)+qqk^M05y4I(8$yg{^a(uh`rfvK5<@3%T@F6d|et{i7-Hbn!WSa^?(BFPbli z8_oHA!I#tU5c)JsFl>HulSqw#J(*q^rDXD?khk)7`1Q7UzZEri)0N8h~Ch{;F{NN6^GU*FSvZozBX7mPnhIKS+V81&(r zw-97Kp21Q>@&-YXoNu+x%L-izpgt-z1i#TR?3I8_jNks55=mt;R*c+75(+7IuT*ao zedY{kt(?G#e!qXDa3Y`s`y@H;>h{ zzY4IDadDGa-sH*7d&*^hqujWwm08{O2LUE-ZtWh&>~YJ>`QX)NP1c?-vE<9a@-wG6!#N(Oi`vRg`?>x=wRWdy<(d=o z-bmPAs-P=azlG9Iv3ne5)ku~U`;6rZ6)m)@W*4N*F0Y7F?@iydMzhHY&e<0yOJ(%| zXp|DOT2u>+duKdOCG9PM2UU6{RMJ01{btraoE4%0Aup?FziCqr&E;Y-pN|}E?YymD z02b73*yN@*3iBVpH6nbWbgDnCP8)8@57CWEoaHjSF(%{jQt8R;8amFm*~vh>c>~Q| zSH0oh%hQzt8hQvCUg@(*2!X+5mh$

v*$Izkr2KDKU$+mfV}DtJU^u6tq6SbT-@8 ziWn3H*}otIXb}it2Cb+_Ca9ZkAr>hAh(pW#thIEICm)zx2v7`rq(N|2aS;Xn!)LUc zJ}>}%RTkZ-jRx4dh(6I~p+P30i0MX0vR_^pRK5Uu!OgcaQYF7ePj>?u%DWb}uG3JL z=NmNKz`u;_T8-lR@rI@K$f-88<#c0y)x#ZWHiSjgh>cSsI38F2p_DE$Ks=z*w1is}ExB(}!j%`R{7 ziWz@dc3=<(2oMoYJVD*J71aGDq(g%lfPxwf_4jEdnErK$ryL-hdJAyZQPlVw>hY`b z1!6ISU79hbzZ55g(4O+p&`ci!owTJDf14h^IzJE>)d0{Bu>Rs#WC&hzh(N5*9NlDP z)&E8^paO){goG2W>aGzt@PD<9;2Y3)KJwL4lvMv6vwaDX(Sf#cQ+J5Cefg_xNU%WH z@Rcl+B%=Ly^~;Y84F|LhAAKv2JK%4&^+5dtVhI+`;>KYAwyuGYK--YuY!$Ku{$|@w zfFRH{NT*H`#*lx*$bDcy+d_hxpU|_2fOu}9HUNo4It4)U=fkEh5}Am*(PB0ObwF;% z-Y+zpwlaD6+G$`T+e_5xY+)(+>d$f21R}3>soM1ADSkVO}*6VhP+Uar0$U{h+x1c3Y^iTI>0 z^M$~Fiv4G}Z?KDGSWFfWcsvby?*xE#Avo{%pkIe0C}mDJ4S?g>e5L#^+wwz^SllwG z=dM^S)H^CwRww#LLGXFkObr&R8FfD=Q3<=T=1G-0+nm5QY*+KJ&?wdEd%RrOEL5vS zt+kpRF=T?8t>?p|aqsd|Y7O1LZL9onF?RRxQ2Kbwo>yz^DlnVP z&)BqDEaPA)`E0Duk#9|=P$FHF-5&_UqdaJ*WEPFYEYm%iFLXi^x%~HL^!x)INMXY5 ze7>Um9zl0<5>Iyba&HMs!21WkTJFkLL+ZRW80~@*_mjuxBcp8jEzTR4A#ScfLTDT5 z_%)V|V>xC!y|Lx-YP-1cjI|~DTj*~wBm#y0p~>T(k9SZ->8;MY zoo=YZbt3Pr3K1%7PR~H&E`)UEe1-gB&7s(y9%8$zEuOO5dp5`8k=P!xq;LOz`3WGw zh=wb>pzL_u-$G3$GeqGICkxf8b%V^J^rzb02N^9!*7kak1Hp=?GYZ{qG3JV7(kYzE zl)uqfC?^g^SEiR(97u{fd$?TW^aWY3y|p@+%+m2q7i4O&QPG@EU@46zme$6CX7@CB zZl)zRc%Gmp+8o?$UrbcxK5urrB+v2NZr8=?pH{#)U9ZRh7dfbeer=t6(fVH$&csNAQVW?X@=$HoX7dW28oqqUvwU>GKf*B8zI766Yr8-zPj zy0Z1aT&9;1)t$MtU2WMybipfqP-}!XMtU&_?GkBsu~C`^vqGwa9CYb)$2K5RD2h!z zDG+O}YA{`PPUCUlTy&tXetejcELWpdTM-P1;Y^{h-y86=NrqGxLaH#p^XI;vnm|XM zZ|U4!^~38+V==d)jq9pXQ(MH?tzi`4a{d9mEx#<&Wl15M(ee#gaNp>3izbFRnq58L z{9%YYd5n3<;mQ9rkAIg=WsJMx)Y42a^(GVjLMuB{{dln@P~Xa9GL|w+i@wFT_T@C- zD2U4M_rKyy&u|!jis6tRw~v5}&8EAhEh8n=Od`x5mI22N@_dArlBkOcD%54Tz8-S8B+s6ruVuhiSn@*N$|4&b)Y~V%1xxZnGZ~3$>j{nLIwa=94~{jFC25ZPtiM4*yDX zelgH~p#-z_!7KSD6TkgLV=!gA(phHH$LXv|a6CUqPJk72!*m$TYcw90`9p@@!mCa{ z_FhMYOprz^*+PMcR1i(Wc%fuMjg(@Cx&q11lbO=;O)u@I=jYkT)cQpFy_t_rNy`{c zS3yLx%l=l& zb^I}qbx48f63G{fmQO$1+;gCx7>y^(mqeqSA%Uh$0S!rd*?5LqXpkUYq_tQqm7>$* zY5BG|nFQF@oFGtNgHjIe+06y!i%e*wevYIu~*Y@e&z^hih) zi?PvB=9xvGWpl>ols2Dto84@8NoMzs!ZE0dJ_vZcrPWe7kYG+1ayt#1hNAJV?43(nr;esGnfCPBy*JsBE=P$>`uLOS z^c02zVfM=N+X>7bSenG5aaCH32*71BxtwyOGLB;N#qMeLpP=B7`0c8DUdOZfvQbQ& z_Nt`I8>;%gAmI&Nadj|m2A6uLtJ0k3wOJ&u*15&IG*y6pDE*MLDF7-thY#nCjn$|m z;idE`Z$bct_J`zz;q3(6HAmz%8frvshP}}E6q;{@07l!CFD}R_S<@!Vi8lx1ie(8D zC@KtFgDrRg56`O)U)k*dIc-Bo)U^B*>|Hij$Zs3%K60B#r%0!KZez^oh^|y>s44Z` za$NIJ_pLuN{}rP7Xa1=$pnnP+X|!tNWc)pl8ye}mgPL&f z;u`B~4c8~;ayuOgfVrA;3wG(AX33;~Su?qMyu&++W#D5v_r1JxRat93%Ez`usoM*x zM6wZk=yQ2{Wn$ZjTTLthre5!@Sk~Am)KS|EuDtHSoA`C!^41G4n3x*uHzKwtlYmDW zr4!{oy5|(Q$oY88bTbF>(=%5-kls_8zyzvVMnesqTzD8Q7FJ9q;?)hMGgJOOC4C(p ze~#H~KsoWEft3*xZEbufD*}>i>Ii^B%g7f<2IWR)z43q5ZBdtasdLuhbh+eaF?E6H zoCck1w@5~#RLQM7pn`nTyIVKRSGOGkO^BKt6@W^G3HGG~-(SpA(V|o+!%criQZqn? z_PyL_6@uBY%2IFk27fuL?l#x|^bS^SBe^OdpimSZ(<>oQESFDhy4onbft&U(#N%=) zyVd^CWAi--AuUWjipV{;(YLvxH(mZyrgLhiHY^ceJY(v1OXc7Jja0)>@RtN?G-Na! zCIs`9q{;l(>L>OQikkqwBD2H8v~sQDB4KLE!|8l-YJm8;i;WSr?b|Dzt)WVAkGS*2 z&Cn0-g45HK>6iPiQ`PRSP70CmKOI;)UhY$Aulqh9qZ8ZN;cn-BMJJRM2H?aT%){(?UyKg9j^JQlG`u1FQb%nt-<+dh`ov>W1JKa+ zr}t#?dJUB*PG4@WY&;`Azt}Kdrpobta%US#daX4nU~d(UCzbB(B!s~~FxA8P$sft$ zEwb%5*7RsM?Q^h-N%ehP7t6j24a;W83+AiQLVSxh-_&W|c|>ryd$?Gqq(ZQ=fi8!W zN4q&Tgz+Y3;}$Bh#VUemt<8JPdiGMb9D z81;;Syc;}Tc8=-2oy40?<&E+DuUrP=M?W0tqfm~xXgecL)7RNqUwWHR`hjt-)*AcX zAIruIem}4cv7u8`J*wLuK3VYbaz?iWUluIMneC!3m-;_P!Mx+svmx1W zHfbbNL;BRILD%EyJr1AV@QasHH~xPZwCqFX7wuuTBR~2lCA|*EFqeEAF`0B$)ks=i zVyM2U&652pYpsQQ;jIG1OBIq^;WNRm)@6LLZ1LyEt6UfPYJkq*GeVU2WC^ceYjZvADu_ zu|gA>^pefJ#~Yh753+xAvjF?=$h2Nb=MT91z&&2V!_ojOP)QYVR65Hg2O`np z8{0qg`80?xk3%Bp&wHxadVf_Y37Qn`fP0>1`=t1}{lUa7IZIL)NoFJ&*yei;78+}H=Rg@aX=+MVveF8Zh%#j z$#&F_0a~?P(+ZhF4AmsK|IvoOkp3x^S{eU8lq=3P8kGhq`>KTI zzjrQuBtO5_82ww#joB3j*wasC`Nn`{b&qho)BM|^pJz+9Io}7Rx|1W>k-Pi2?Ar|o zP@*T#qVbw9ilWbI#7$Rk4AvhmB+0M4xCwX*p`xD0lj%l^y^8gj_FSuq<+5s8-+0n) z$E%gQ3=$%;rr7ippN3ul^hf3t=KGk@mOR-G`j`& zZA9*1Jo)}96<`C1_Ln-QJ7zoP%N0?fQY*?6feTT+Ca`W@ELY8qBZ&wZyX`K2xU8}b z9_!co7Ve_hVn-B#X+S&@&%(drI@kR8O^X7sw5L<#jk0Ph~RA1Ale?I@A;6m*Idz0EkbfREl16I+mW)Y${T{ zSW`->*dlsiHgiP4@yy4xp=XR%4kXJ#nqmR8W_a&Eni7 z2lGd%*mSwt5EVnZAR~w8CaK_BuF$Sp@)O@KuosexpKGAto5h!{x2~tF)T<9S9FDdw z;BmX5YxB6Gy+4~@Z^1~C>-)xpcrr$|%2$ZD-aH4?$BsFDBHCO>6^R}`)*37X%9H2{ z+Zwy`uoufpgW)7|yLyMamcYtvcOnX8?)%g$_)h&=ErDdY!;(yCqn;_9Wl+7F1$%MEj45 zUn2DCj8F1u{2(ovNJ+wTcx53LiQJpdOkhz!tx*?9S&y;S;x@O{eYT#x)^HDVQP!1w zP%N1mK38=AOS3r?g;?R{=P0>A$CGUT_Tl8hh7XZ)I`_e3-h?(jHO9mFrWDeLhkh5$ zQKWDAgUx*vWJ^teFPTwZd9jz4*Vg)adYv(x#zB0!DRkIt3_fpOa8&xN-}|f3cdnQl z=o2RmyEQ*CG;g@rHm4Km;b?+lpa_E9+HAfUp2lV^xSV1NCIUUcc~d;9RBH7ssa$5| z#_4bf-)OTqM+`Tuspl;KlsJpL-@A*iUhawuB;sdsK;NS{cYnS_t||jdui`X^_E3YJ zzT3oCC_oL&*R=_(4~~1bPS+2heACh+ZT5|o4UxAVAkk#C(4nDpy86mRsnnJCqBjuf zLcEmYkKE;RMZ31wo>f%eFdX{@=`HKwe66ZLc1E#(LgwoPZlf@)J!Jx2ae&#;(j>Fn zoNFSRG;5p7MMa-{GM*OODudpPoopKYlz6!uHk%cHNDOW+4%>%Hv%BM2Sp$(4Yn9sl z>Un!~4L!Jk#aX*|)LbaPV3XIl=vXpY3~WEr53`xRJX&b6&fCK&>%g^ z2+i!m2n>pH5HmX0d@&_1pbJ0_8U)p`u8 zYIZxLMGS*zc(kBrYxMA`jiZBrq?I@JTyrmD&=S`PF8Wzds!o`DT%j>F8}>kfVv{ z3PkwszJcpB)f<^LxI`LZ)+fC+{eJOW9(FO8G90{inJ>^p1%01N#L0fV4 zwDV^RbDA1ePHkU@*7>Bd%SJJXN7OZj{hY$(P|iB8#|)WxpJpI1T@L$7rfLoPJe9$q z!b4W@R#GgtzGRUhiqqS@gj-=^?SIv_NZAgvKU_i|c~96tGXUv5^7T1^N=q|ncX*wP ziioEV@Oew^5!_}HbxJu|fz@=qEN;emGPr*%ba@Mh@ov&)$yNETLbG@Y^-ezk?ZART zc-x9-sazB7s5hFT8E4yK!FI%)u)bLC=d<~+Bk=0d|d9j@){Ux$J_7=TNyGPPw~l z8_N=^0%>9&|79=lxx4Hmb{a>EO#pBaAUF>_S9?|Kz9;dM*Ym7V)jg+{Dsi; z&az^do^{9VK5Y!|Qhonoui-0Mq_LmANxNiTl*L=k2eYMlk`}JRlXFh5#&?v{*&R8xF;ShQRskB^2uu<}j=vhONg-5w9; zixP{qia|G&TRpu(QLiwl1I5DBEiJzeC=u{@{S61h2jNb(D1q4@+VSC><8-DJuJQVW zUoM+Bzm_}H?4?P}g;J}C`C_dxB#p&Z{EA==7Y1-@tYr1p{?veE}?vU&5#JE+CcK zjcNZ7%9}r`zH>f@O;5u_ecKwXzCA35q@MFfC6yG1`PxzH(iaE=_LNB~m6Xs$z?=f) zJfzfH+$A%gd8+ifd#C<+_^n}X1Xyq)#7fm^ayFu>GFJ*G({=NuYA8( ziU(>{49BK6=~*)$;85IQ=!CLwPuCQ{F1d=qZkd1<8B(K4{r#aRW$yB>ohm$i{|HnP zYy_+a;>|XHZ%61^&%% z%Hi{3XMQRl5_!%jHf$8`9`e#l0~4aIk6)zb2`1{n`!0shb>mb5NLm&C;2upc^?0n3 z{Jqq9)U4bsV6@idMJ88aTa*E8S%qxFBmB(bM&I6h2}jJV3|u@=nJ56Z2+agQA+2cU@X+YBN@~pN zd-r_K6;dfD>9jxQ;CEVGdG!rQP{Ln;e&Z2!TojEXX^s+jyxc72?c9dua=&JeWN4JA zPg-d5663VrE08Sq*&B+=&myz`e)Qp7^Zk|ERyXa{vPXpI#%#6%rdJx!Hz>s^_3QtO zJVLJzw_Ne3>9FR6IAJXXa5FFslhDB2~!WG;ZP6Rp^&l)~7}4>H42Qh=;rV1& zF`HgE)PcohqKM+GVL9~<^KR7(&C)m;h_Rs?w;KN*!0lM((QQwTir>C zbB)dV7OdOhn)}k({z8Z{B=aOg@Olej$@Xgh$zF6**N2Dl1rfgwdls}zZh*crdADjl zO$To(gQI-t5x}h%Nx-Ig5Jmu;N9douAM9t07#f;e_-yV8n%mMT%&D()q?C#U^epBJ zzo%~MIdEQQ^lUd=}nU}!}QhuQJ|?tHo3EjRh}A!B0p ztG4NQY>}#?B&@7H|7PM%Z#T%L9T5-hcEh%ZU$g!&stFl;)VQ0@g!F?#rAkr9Tr1Y~ z{fpk|=R1G+S2k8AYF)>x4<3^d>$#d!Zh>z1`*6($E1`PtEFmaFe8w@VF>p-g_+dAI zc=}Ytr2e5sGDmZ*~z6z5H7Y<^;a-E1BS1Ptkj&icH0z%S{n?~rITilHRC zr3pS$FqeE+{+d9|i20Y#wLjlcadz4F$`^kXE`GzOjcT24T^OA-d#+z1Rdz1u1YYp( zcaU(mRXW|dQ=jZ6T2`KEJne?g^s{+UIdp!67xxKi4Q8dM?O7O|sf;FyqZRakAKNaQ zTLyg{M#G4W(2f;)u32|1cwFAn8fXTFEK2(8pvdTNBTmTy$DR@CKgX z#PsGj-#GjH11lyx(?>NvnL^q*yPeLGP-$gSp5n1zB@?NuhL{_&9ge2TYq5@=x}9E= z#txsZzc%=M3OFH}U>hI@rkQU%Nt|*3)iz-UVR!9_aX3usmuzmYKEY@_>V;YuG{?07 zD8$(zne-EvB~JGIORKU_7tq{5Vl+C{gr465>WAV}hu`sr%PuN)+k-!Lf;jgkOikcu zc}o?G79?n5s_6OiJ_}_lnPria3}oj5uR-960ugA|8|Irg@hcC%|8QR1V(`)EwA87o zf2iuOh!#kVNMkiGSktfMbb?8eOVM)TvR}pkR1pH8{AxQ!FSsP~22@p;`##z~vo*^j zFzV+RLLp&45&J^Rx*7PK0aGJC6c?GHalCopv-K4Ii9%2uXWaHGAzW_vfqYv3H(R= za|UpZ##+3>d1ABK3e<3Ca(V@0Ul9$=|77`c!afzmCfjcEkO7V z*cEE@)*TMVhQ8fZ1o7joAmNucyjW{pC(Z)7&fJZDRZWO}#W_A-Wm265!&6P6(E}c_ zkTM@+vt8X&cqxg^FMGD!NQrga?GsTwuT*+Ns9~7@dRVD=(?PH_`ccXUD3;0Q{dzE# zDVE9+V_bO_+YeQ0m{|6WL2*Ez4~sRO-RlKj<>%L%_gAEuH7A@BIwrTQ8KAJIUVk{e z20SA6(WsItcG`<{h?O!@TPu8RI7m()HB3kZ23~eDliPo)EXDb?ZU_T{l%49eUeHoK zSCHc$MP%`3G1xQI;Xa@r^I;#_Yn= zynq*@MMTFecYpt@X#52K`3J|Ys%Z6*{*4a$&;G)F9V?Ik3788c$8?fj%@w0XjEXhT zALVA*Vlf}NHw$1mPQ83tJ5~Gc?gM=&4H1)X0_ysL;t3miJq0$JZ~D0fUiLA#9A)(U zKDB*4-?@m7y)3TurK{`y;Pf%xd8LG=Q;=We1o{x%tzT`kxy@cu3`Tv8xWf!*9{ymj zLPt8hn|jIS*jRja)0H-Je|th1bg1L1Kt!6br+c*u01AaXrohtzPvPqA+>scxyNRo8 z+QBu&fvNg5^R*6YjVUbK!yGS?8mw-5t?p5VBY6E>E~os->g8X0YlvIZB9N!cE_gie zQnGkGEYtzMSTPo;H=Mu&pUy#`zy_+XVj5k*^AuC!U^En2*8qccZb%bEulcXQs(@R< z7HwF?qggV)-=7hKL?TXIW1GD`?bk8}JuiGv(|!)#%@$f;O?ha13^RiOUfRV#a_pwh_$p&(pdV98q_On|XI6G9(Gj(XVoZ28NBI>3h+2+=cmhzY>o7<(zmj!ms zT^r$Fzx-Jv8vU|xTwY^*+`B|jfw@~E$;P%b>K=5-rsj0U5>**&-)O_tKEW4u8X6LO zqHb5yw~=$M!nnM0XXo|2p%9iqq(U<5)fG0gsdnXHsCzF+* z(r(o_O^%z&=EA5*GyEl)Mhzq2Q1aesaC1x{upa4Gh4{MHYujYxL13JOh`?OpK1mlD z6mFE|F6h|7XLLAys$_V7wy2a2B9qEENh|qiqjNme{YhyuK?R<5}E+mUydT@?=@B5SX3Y%aL5eAVR_wn~Yz2-;>4hO0WX zT8XxbPPIm(zu{{-86K}^$}i8f%@V_4cw2PUwy%Pkh@U26eM2a!3FOx}^TN=iRl1!; zd3?s^>8sW3B(`;b#E~6)Hipt;a(F(ZTcblB!FGl9M^w0G+}II?TL?CqLj1abxry4- zO}ui|Y6(lA%{#nO959hKBT6clmP7k`cVTK;SGFJcXSR&4XA|sC8J@mCu1t50D^s^t zfkOz_Uds>f>0>Gruy~lUk5MvMj0W%o7=%)?z*>+`*JdoH0<{USlrL29+@p`cefG~0 z6yQN9s_t{3^jBsNfzDSGSNoj`9)QIwe>jo4s(BbV%T{?aZ-9i)+w>|8y9PJJ$HZ~w zb7-e_RIRJgB53~#`gJdoU0gxfhHGjhy^)N5dGfHjm(O*kNFoHe(ylg6vi_<65q!a$ zl*wdD6{_0dOdCDyF9oq529KFhQ%+gUF5hpk*s#Ol8TRzoNvGs9T9Z`rp8tvnUrWAVywB zjneJVRYO+B2c%b6wk>4*S@bGaYL(;UT(9t%G@=Al?-`B^oGx3w^l&Q_DvG>#t^P8O zg%TO{^YS3a{WSlzcvLyWQ{c3Gq&IJQxokT&d(g^-zrz5T#681n_-J8mcwWWJsb__I zCa&7THN>K~z1LB5IpdJ;oxNPWNuLKCL9~EB3&PT?$C#rDe1iCCdSs%A)U4}jqqCTH zcS*JFr$pm3O+{{v>hKFr3nY|_PEtAX7R_n+GsMNH(v2exwA2sJ%>t{FxnRZ0UAFsD zOf9Y6LDN4a45G2kHe0Hfb=^~1IYXh5So1iy$k1{g{e5c;p6c#7@nq5UN$mb> z3V4|ua$VCZAuS_CyS*gTb#Q3Z;!Gx!s$D}fqCNkQwzrImGu+ZPahCwWJ!o)(dvFLI z+}+*XEdqmP1>&fH`;ZSd7_#9ww^0kA)WMGmp>&{V>!8srStG!P}fo>1Km8FY~6rBE=qEft- z`nmj}VR*L1AY}XYVWU8S`7|s%5a)HcDk*qLg8-!x>-PFb4r@ubkiS&mXvPDWJVbuB z{2!}f3bgm}OrZKMze`y^rc%ivYZqH_5VP^8_*+(dAQb*GtdzQo{5Nh~246lDPV(3N zs-A2Lh#F}fxiC1m>o(dx5v9W_n!P&+@*&q4m@^?QjTP0_+W^f-Cz!Lwa(w3J1hQx2d}ktdhuyK zo0q>2-;^IS3Hur7@$cpPup)DICyPqfk1Z}kp2NFkKwIc+e?59v&UX+ubo6&n!g7hZ zc$r)dgM{TuTu$fAwKDtHZcZ2dqo-*g2(26uCHBge6N(<5 z8*`7jJ({{cm!cMdHopR!7)<-xNfrsgAT)-rU1WpeJnCr zx?~?`P645S`#N9>(OeQ4?Hh|my_zP(W_9#bqrI_Sf0fn&5x}x{)o{p*xq1gmb+L$K zgADfK@pN6x@kyVq)aCV+w&~S{uG>q*`|XYG$!a-w{@BJC3^-xqeN!^b7b* zD)lXfyH1AQyjUMJ@e6+J4gYYdWclO=o^~(DwOoO~Tf59P`_LVD9ef3y_o$ zu2O-(Q_?;T8@J47~iB` zg=&kIXQ=yLF$=R*w@@8C*XV6D)ceY$j$h`Juebivx4c?nmp%mpWR1UGyuQr;+ncWj+deaaR%|=LxJev z5XIp!Ds`(UQKpgq*{SAujKYfa&HEgQE6ne537VdM;SA)b+d9sM$Xx=%)hW^Z zBJT0Ia350B*!T4#fE941Pn8*T&OmqpkfWQfChI5~!nIV3{qx6s)e)~`=QHcdNe}L0 zmQ1F#R{~o{%w~knS9%&=2kkBasCKt=7&VGnWIB44+HH^9Mqa8Ubg1)d;l~!M3zL_x z^7#zKLH7~(%{Q1d-MET=(u3^GAMIILT>LD&Q&S%fh|y+p&Gsf_l2YD_ za>Rxx!k|SADNf!9o3!TumYp6ighIxJE4%^Jo4S(vf|+k({a>ZEUM7_Hxlg}5YBzRw z;$be^KGH_uT#=xE)rx8V&}EQH;TCTa(^(zxd*`TtW^SUIF{2YuE4h!bQNB>?K<>58 zjY= ze?Pr-5-!5g<98tfIj}~-&IY{Md`sv$*xAEql3AAZC581*Q)|fv@2`Q!v8j*SaM$2R z59TaTO(l`TgJY33EFd(Wk1r3btLFJLdR+uOnBwhwwKZ| zqE3JZV_Yx%bi}=%*?L*%=?@75dt=Gy+G`jNPf!rjJHKVB(R!`a0@~2kn49cg|0_V)cMV}C$u7??mwA2(QxJk zmC{?&#zR(ofLe3fi+%s>>e}a)F&QuJQ>Yc-FY@@y1|L11?3B9hFxq;p4{RSRN`#1py^R?B4_@dC-Qt$3wyhzeuBDA|bZzuExwgAA_JD+tbVyD;g zD4Uj@3x-AqLH?jPVAjlZ*`zj-ZBNzk@jEJDJ8?gYCLc^BfU({A~^`PI#DYtr`{L4DA#<90V_C>@h8WYs7drH@i5A zACUuXnPDD#gU4f&W-AM!Y|@C9UoH}r6|tzO0_?5<0>2Kc)x4v zOu1%^&FK0OV8KzHr^orqEc36d{#kSLlPIKMH4L&*RJVd8mr+sZ`;AWWedF$ay!c+o z*Sy5F;!o%98*v|E?C9f)2dJf5gMq2Ea9&fN0G_|z+B4!J^S%0=Djq}1O8r^o%F>%eD}ji8_lNefo}J|oSX{;(X?R%{^~G5Swc zGsY@J%9ivr+$r3GmnbmiXFQL;JQ_I?v8OuW7)#S}{7O$O=1bVbz76yV5Gny_hgb9V zv#F;Q6Zftz5Zq-&qmo!Any@~$UH(PrJ~TI6`)G>2A9(*{nS3MBd>IV$-|4i7okDs$ zNO(5!vp?FN=xMPk#Vm1lu-@q^vbJht2D?+SGN@cLOx|CGhY!w9o8V`6R@V7rDmi#3 zexsMHhsYG_H(8%7E3UY!?}@h)Vu0zEwRx)?23uH8q;rh4*RP=4L?>E~;Arjw!hUt@5? zFagMW*Lg1gjm!FX=4>-Z@Gm2F9zO#9-y>1~5%>#W{ToEP$BnxFUvp}_oPQ&HhgwbC zmH!Qm{trRRcN3CN}@EFc)iLvut~G$pwJn768b%+pDAtn~P`{*)8TR?rp|oHsH{ZpUMst zEO}&*qia`ODHgWxW;5nf>TW2SO5igsVHP)xuS@f(+|e2qtAlW7Y2{*1g-h8GIX ze$*xD8vO}(Bdt*UMrk(RQ}jm+v+A#QQRe0##CACZHY{vWs%|v7k^KuyCLqk?a=lU$ zD}sP~tg`m0tz0gLYD;~QkQ2y|C1D>0jb=i5bhy;t9!(cn4|^+~NC0~Y-hxQ=MjKtT zjq-RIrY^x_scbw0v6@Lc#^Ov(sPl+_;$d4Tm33hiDC)r7sW(IOS@kT5zXINN|Z@vFFdn= zuU1Os@^j+7R-4=0r!J5;$TxEReuOM2;}WH@BL24u%>?ag16#Ff6W^K3L`MC|{uem(|ZQQ3a`=s=i1tZ_9=O$olh!hy@co{Y0l|0!`Knr1Uyot(*Kwj7qJ z4W6-BIh8nBD6N*yPUn8YTx)aw^L%%7X6r?0*UQisMj;&&(Arir^;~Cea=Z(B`?y6? ztkPumt1W}IOl`7wPXbVeR1}KTnH6sdpjzYycF^Wm95-tvChfGB+yo&Wqvf6pdcR(2 zqGRWeV1KhqtIfZ`Y_a`~yAo8%()zmhN+H{YvDxzliT`r@tL1uY zpjzEkmd6vk+`zczp?8tIT=4xF_mB6H)1){m#UF03yRgFlKj2JnO5=oA8f%rdOq`Y8 z-kH}A_xJf0OReDX`<~$*|0SRJQ1|;My*C6mHkzjIUH$WG@I0p9@`2{o@z7h8)*r|M ze3zda4JZZbf5sh7A8ngmZ}Ou(Q>@9qxq!Le;AG^Xa6)DnE{Z% zLdTK>0#%p!=WF=k6biPxlMf!xg#tb@spJ4C#6#;@Sc)0r(R@EhIQF^J6=4K$%=C&? zKA+$>(wBl;prg+DbYNTO!|reh(iegj4D<*wPM5u7XmMGr=i}V&-ykLbW>vc77f|^biYX$=z{5>3|-RvY~yr!#_p!oZ+@BQK4b7cf;%~Y0y3E?M-o~$qB zBs8Pnqn4k`wR4!iL`7Kw@yYAGp`7kZa9Et$%0l>Vxr^2&d)%#O2&0_I#?f>RsWmEv z!cS+*?ZSB+9y%E@*-26o(6=XojmTed)Ad&{wA0WHhvT9>7M6}3e0n-g=4N1OIDO}h zkG6$}sX%a6wGT#>1YmQ0F|7Nl$wchLHZaCA%r$ll=5LGOpj#`3(Ww^$RW(o7tKuOA zN=#lC%fHoyV3VR@bxk)==0_EP2wI(>vz9e$CY}JJ=L|vRzvEu)jA2r*}qaI zK&s?wvAkC8$VK(DG;S)tbeJ)?3&9@9iypu2q?KyCbLBBHQUQzcVtk@V%M`Z4`??`# zTH)WHhcsxKde-QwKOkwuCkyl|=gWaG>M@?Du)ObUM#+1Kmye6Dt%Q#af-5oYb5;S? zVRah5U{0GpUQc4(ZbRVgL5r5toL_vOpSCygttvOgGIKw&)Rs$kFpCJ#{#%E9CY|QM zHFtcE5RH4A>9sWr<5!C-lk&6QAI!QI%IIS^IQNctVsDX7Wv?=WA9UUiD>~-TABZTs zzBOG@3!gC|d0a^LpDUHR)ND{QWYc}cLJz7U2&fNi>_m3mb{&bQE$hDAIZKO=BG7&O za82M5)T|M4j5nG`Sa~!jWlp=@w!I~OcnS>lVp^+9n}07(FnjR#!qUSGszThM-zRI8 z=Lr&UN0PwS34Ycoohzqj*RzhCE_uQ_Dt#v0 z=hZ2wniAC{SjH`lHM)86i;qjv(AA09ImO#g!_t<+sKn(nid%C7HHU5O_!>)5{1D_^ zaDZ3xw5Y1O?`}G?d@0oAasdz>(*FdeTA^EC1?s(>uqhm7EctR0RQUpkaOJI&`XVR^? z6_Rk~k<-}OG1YKcamyrX@OnrwR#s@$h$J~EP#WLNgrQNCU%#!@7*|LaN5oUbDPIAI zh1sf16cI1kkHlZ)8cjb0gCjFls`Rb|nj7Gd33>k!BIR^A7O?c-oSZ3`FrkszP6pPp zX~PL$rCOZMvW!mQDuKlCkEp@*31t7t-+7^`_L4tte%DUr2$OAurxe=GV61LI7eUS@ z%JCIj#NOx??T}N_8l*1TDqvwusx%5m;6*>*;(w=BF3YFyC^TgYDfuPs zwS8kRKoaPDebV}51{zwM84!Z{vm3(tIE_Kq55%irD26QW0va)&%lRA?qdfhmu~+2D zf^{WR6Te!CQ|3)jKHTH?!rhtj@aD7fGyd4?>pki;;mnBo`vn==WgYS-rUoe)Ko0_* zE(HIH!t3=hj>h!o{PspZ|0;QGl~SecjtEt{qPEQf5<{182x@o`!b`N(TB{+9W>ghL zBXl9<1{RA`>G2Ezgyprp+!xtrAf+iP+;%bMh)k9&RG4&j5*~6tY#{NTohE`ByB@o% z80cLKc>20)V?gC-G0z6Q8ayjf2|D~pb@)~uP$2uH}-Tnp3L(7 z|26j%6#IX}JteGF0oS#n>7$HB+nvG#@zlYl4t$SW-ieey`|mh5a=*CMn9T(k&{}P- zV#a^s`=Y&)M!vcF)nUIK9uf}BzKQQxhTZmNR@N_E8HXL}>c z3hZ7)r?g6?Fn_gLP}Q=!Bn7Y&XBQP6@A*H_sd28%wA%$PXvK$b1|fE20RqQDuZwbCv0XFn%5MB44dA+WQfs6f%w#-Xe1-q! z_}VxnmOWVfal~H!mw0Fzo;hwWd3R38ka}>pcWdNcZ?<2_Vez3}9ed*78sLJocRzj1 zaOx^(l6F2{t!lwcgojBa7Q5HgJ>t-UC+2gGBhvMl@71P&5&KVA>J-Rb74sNPyoap4 zRl1-s((uey>3dfWS@Kh9=HqELJ4D-g8O2r$gv<>a^n86E)@sF{jCKu=$VQ>9*PF%b%onobOBO0Klz8ekQ#x#-quHcAmg^M3SBg4h zmevM@0dgvimHCAt<1#=_HgKcEJ0tn(Z=_m2e*y@YXW@ldPW*z|OM2?)hQ9zzM#Yah znPU*)yVV<*GxGg8cJaqQPJY{PkBF3eSLq2S>0V{KKU^>hTEzw_9>6{WC-!W%A#S{4 zmmg%@6CB;|YcR0np|oJ68=*5{=Zp0`z~y)2vlfH-i4SGeJQW3t=M$+lgFk3Io#O8w z36%SGpv7O3y#7T@0T2^EnyWzP7f3J!=UVdxD+1XWH4*#&fTreNdiCmr?N{{X{}Yez*LGfNonCl-?Kw%1iQ$2p^1qx+942O?4f=i#fLop{b>;1>qnyD?P&9p%D zx;l~r=`Z>1ME(L)57T)g+z}f2nDpAktUm8$M>D5O=rv4wvsiN?tbk_janqyQ)tjmzYPL_B5Iwwi9{0CJ4!= zuv>^7MKXCmFq9iJ#|-u!;QwIQzwo#249G+o5p+p%1(A#c0egiPV1I)VPQW2xdTo8> zs-e}XGxf21gx0-U1?+o-*bX;+1-aL)jL6qqz0b$m zRozXjg)2_MpxfO6Oa0+lQz{sH?tN>9H6ydtRn1+F=>x$xs z`Am768+qfxccM6AL8xNcqTkDf2;Ebgn^vmCNTZ2EMvnG?`QtAHq6LsvHCNowWsm!` z>fAqYS?m;8yu)#sV(2s)2f+ljwQgAD{p7;0{0N)9FC35n6#vM%Hf&fp+f?P_-5-@Vh(X>s7<{P>ScamsgdQ z$vCOH2C5q|1TUgQr zK+AyaYQUfX_MfO#jwMR&#m+Amx9+E%MaH}|eUY3jouV*B*B~DUJfMd zv7a}0m*&9T?XN%*6U>w+|H{tZZoZ}tAMv%?N7wt)q^(f*-9foP(C1KeD2yd+{tJ2c zj84f3#RQK?JYLDW{}Z~x5AfZ8#h=*e4>1hOg5kO!u&r}p7_guJ7~Lu#pSaNznAsVK zq)d-p({*&wiEvjvgj=pzxNjx<@p*?@Z|s=C&=ADlrV&9`2A0XU>146V9HbxKc2aq` zdWb8aQD+dp$i4u{Ap~>(69QMxn0mwVT2LN*EiIP)9oF07m41CfWBzvDe!JcKDhJ6} z-N{0@eF>V%i)8w@aLAtZ;X26uLEp7H0{eOr{_mh$;_~zToy_>jfXUd)E^PmLdCnsY zLh0>dD`(*Lc1gbHH!N6k;k$lDNw?o`(0wG6_`Afy6AU&jyq4@rBnGB!nA+sXGt`zw z9}w*s*L3@t>PvhGMAqdHD-u{V@-Me3e-*O*x7Z4)QZ(P~NC60Y;Bns^0H=z1_qoB` z-`{+^YVB7r%odu_8y3Dme5_DTWjBuXuPt7bPqff>ibIs@6VsSX7ZP{He@eiXFIdsg zTAdR$K42%r-v+T8NMIuEV%hD^S8=q$_J zMT2X_s!~D%;& zE_lw_3-?NfF(DpHCYLODc7YfxDgzs%iQwS42#w@b!@%RO`aG7>pnx%F?xia^9pS1s z?Iglv^j^wKrqrRuQwKZwS*tOz$i0EiVQ0e|ok_1f(w=6JK~0U&V{uvyy8kJ`nPG!h5&;#I>rzx*t9xd4pO%_W>P1-rPk%`yS-9DewVtdA93O&ypYNHm{;Fdd8k`k-%Rj* z)^!hvvn%6a#~>H_Ptp)U8g<}l!8_jrUb-yZu@2R1>UXAcf@hP*jM7kYPYR^des<&I zN3ec#orZ8+0<0<^%a=hhJ}=+GhqB$Y2m_51k_d_Ihp=`%a)u-# z69`@D5GS8+Rf7)!3>ce)7p(e0x4)x<=jVYMI|lp%x*A5@48DLae3LX9O|?|{qI(4=ky&#vft^ejALYIL zE!HP+;}V4gu&}^bB1)cfIi4A<+H?EPuSl}Jw`I}2-QyFf6tSaX{4A2>4Y1L7>%^^h z6^JF5A%6X%1A5*G`nII-p-$B_$BIGpX8!IU=tJE+;5m%_*6z@>ypv@Znm;_cMg!u+ zcHWd6DHj3k>PwN**X@J5GJV10;;}tGbkQLMxca?C=9^8y&Hyx?2@+L$13$%e zri{t|;}k3%#|MxPKtx_@k8<>E8& zS>^is3(N`oKM5^DAZYebX0vdVif?_j==WZ5Ao9jrp$MSdc3h?gXcZy)m~fF zun-BD#h!Thxq>2K%PP41n!eQbUefkw+kL&*EQ4()#{QOD081Y0f}bC`an%iVr6ckP zAX&4P|3%${SFK?GJiT1Wn zyE5y8ECt^lYL?J?T%E#fS~}Xm>2xc1F)rs1QZq^wPhCyQ6zE2mHa<61jNkaf|K)s{ zfFZERd0QxCE}QVcr6T$As2B*eQ+lgZ$sdJDwwxkEG$O0jN_{A z22qoOd2OyVZGN65w3a^l+Lemyj?rn2zQHlT=Rv-~YH7^m#ju0bz~GexE5I-cf48^> z^Y%%fY;@Cme1j|7nY@oMqc z%dAfSK)UB!kA~J?0}T41Gzio>-oO>!vbxDMJO))}qEUdL&YNRfGu1K1d~Q!S94_9r z`u3v299nlHR5)LM)>_L}f@8{B#%a=kAT;t#;~%kb&1P%2m0uKji5DRc*!B`R0Z z_pp8lf3P|=^k}gx3WB_Kj!+{yo-cxm&tid!RGP*AC`R1U)FWYTg$M4Olz5fi$N`9& zEe;A#M}LDu#k`ztEjc?lAogJM)RkdfEx3yv-afo%#S`k`?uEfEyo9su4CeQETJHVw z%S(vwJIurO%7neP!b*)Tz+vL{e!9@mVCiu{52`EW`byh80qN}NoM3=1CEj2Bw(z1hzG2beWo?!qZ!u?qDME~^dh&&HdVwFW4PQl6b58TTR%Oc&_1=P!efZ;zic zI%;hhYBPtu`OzN(@mvjc^@pvqv-88JRZZYphWVAQ zkGQc#{yD2}t?cmOJ$BUSqPoQyMJ)5?x@Gk|;@o?8e=OGbvpbM@?v}QA3zA~Cz5cuv zh;Q~Rc5}H!awcgm=B2ZtlIb!W<#eo1b?zYJdCe12KSR0Z%k6hgGvx-`>xg(lQurO}{t3Z_({ z7`!Pa>Ai~2?dNB6>YtM0%GX)W3KIkFZa4ez`!iLqG{}qj(0+}vA;aaQEXYv(%$G+E zoi2?QB}7|j1VEYBCw={FK6UHJ-hccsPW1r8>m= zMlHn?9}?Z{y_q*hp3#Gq zD3;OS0pu0{?O4MkI*M0&ykUu`@EAquVt^T&+y{&y76wFO?J4V(N51swM3~Lx(~hOF z2rkx*G~%+kNtkY3en-^ShDc5CSNK5pE#ZW zu1$1ey>ZrLsM=5=<}4HQ{d`PAxkzqAx;B%$*Fj9krTH{myC+CA&6+bC(o^X>gj z(fhi^tLf3%{uF)lh6Q^`b%BkA@(~O;ZMf$8T9freGGjc(Xa-5PPLDwx+B;3DS7cqa zB%Kf2viLiJt_Pi3t57ciy9tc>78yw*|0cLVnn>iG)s0fw3Ot4EWjKg{cG3_M3VxY* z0*xwp#BOg0^&m3oO0OA`odRKWw7<8AIfx16?Dh_Tcy%TAdYEh=hxfPUxK2zZIx}O8 zYVCof^y|{~wgQ(R4cg?K=fPhhH@@iSO8Nx)C|-EO33Rl)Sl;Zs2)GF`1}n*HBL~kH zKIJ2~t}2?ici<&g!F2-o>#2P>OC#nj>`DkJeVLt`R*It7tWK*gIJ^$O7MW+bEMspV zw#E5qu)n#&_mA!iCAKkdBmPCTexEYlciA82o!cSJ9rzAnQS1#T%uCmI; zn-ZKxWb^aTI&MQs3N?_68I*PNYbENE>95!Et}&~rYKKkvfm2J%q)u!=mUK7Op&x4~}9z_sjTPg*? zG>T!wR4}Y9i(#Gpw9;x(pH&bt?JNgJumEK8!HtHG0`fy`gYeIOQ;eLzE zZrI=%)~g(NumK z$W>|40@oc0(zorjIA5xfxnX^;JJFBhBgbGLYw8Y_YOB@gj~{he2MgTG(fR0KnygF< z={761nP+i&6-pi$Cjt|+vn6e-g(k0vikdII2n=X0;)LJ><5TlS_7d{1fO0ch?MT5= zs_Pe-_cZ#R=TGXXL35jxmt_obw*}f3^t|1=#1uHD14vq)RBG52$_4ViTXRC!@-oEw z^+NIr*>D%En>&BM5o-2{;46$06o}(gr1Fy&Ontyzy=C!=^nXcHc!C2B^bsJ+{c_n; zROD@$xkZLb=WP`;6Q zUiup=3ZM1mLAAlEhph^>j{d=XE#18G0$1OrLS5#cN<4rafI{f3kZ;Fb37w`AJ#ZbB^_#_KM3Go^1ACG@0&is!b ziIIFKiOWTb1^$o6|NY}*7mDoYBl3FZ@!kJ;@jsqKv`d_sconrkUM%wO7yY+Cb?Hw6 zJb|#)ckblt|9k_$YrYQ(ZU!6l3kJdoqmVQQVAa+b4GGkl&oP(DywsN!DMwmu`!l_S zQ97S%C;;(+9HJg$^x^m%Fvx4{prK&Ok)Ae=t<;Vv0YGz}FNw_*6nE?~Di>ka$6)Q=Xq zRy_FLvQI9&ODTjfQ|A`Eg!c`+HM>W-lhKHFHH9Or;LCscA~(Z*7!NT0reN_oKO2sw z1_JeZ31C1&4!iBn@+_-hzvOJWKBk@ZQnFgz*>_-9S`UjHkwO(u_0ya;V>N6#PeQcG zZZ8*Su0$>X1_dF9MYlgsGi5$VR|J`d8?ZpRIfnby8l!v>A&tZs`zp1}yj)Y^3cBB6 zlLaCEbY(GZD+31bT0^m{1<-8z{{CuJ~aUo;Po4E>K<$vZt-MNA@Wt*}zI~;!} z2|{E+XnHvD!y%bwGL zn;|0Mx5V=6rg80dkE{$YC$Ar=te)g$>K(qkp6^2XqLH}?1hgja7x4J$h-D!cpPAecns+PWh?%ad(Wy;==^Df}cEw8nK22eKVFF$i zGx1H7>9n(pV*6(z&b%KvEvdh1Axm=?@09YxPuj$J*&X|WrZJ#c;$760;sU^-q&dwm?FG`ds(d4vy zx-AJw{h_0^G#J9mt$)l!a_io9{WwqiVsW0D1QC*&OBRYT30U1thj(G)I33NNRK!Y& zrIML-OI;>BTbyN&ev;K)!tD)DSM@*(rH5jU7W6>hbL2^L8%{&Gz1-vP4Qc9#1=AZm#yH=)#JiPAlP{Q5G3e6MVRd6`eP3(B1 z0vVsrPBy@LqkgEU{Z9%D%j7svY5nXdoaA;y@(lUxM|un1X681XTb`Y6f~G^ZBGk|J za=u;{>T>a9Z!C|`HG@>*y!4R(#u%$obdA<)jP zbu7UQ)D__!q7OmbIVQAPY>bA(_$r+a9(+j1t6ZYhGcVu48mC|mFA=G=HT|C;{&HZ0)q#~^YJp8@1h8h^Ed$9EDF8Z ztVz(-+sh|O)oFtvlkVIhhu2>5Vx>Ab;n4jwHY{IuTTjJp04fu&a-_Oe2>`k>7QK{J z9FrB+)&;-$?C)tD&zJoHC&$QWN!bstit{x}y@1QvTvd3pwQ_LHMw=<}3E;@g4^Lr3 zOAMkJh1egCSBEELcEGwL+#EV0iIO1IzB4Z@_Otb)sygWGx=pXLSn2;$bTQ7yxY<*-q%22-pPm~)?XNo*#F_E<2A2qBS^O#M&0Rde}8ifeZHC?=n&MA^; zAP7RxDL^_aLSaJPp^(h&5smy^3lJLFU4p^zQbA}1(sC*Dv@Gg-pl>gBTtc5+*IV2( zfDHV|K6aH5C&O7hwNfS`5J;<7_xviE$iS@7U`qN0`!)0sRgOXW3avNC*FSjr=77G? z+InLw6PRaW?nyW_g`9oOBwnT0pDmTrFnIxwA@@Vczg)c_0ipU;tI?(E@FEIhU~3GA zNA(W8m+AN-;&~lV@VTr)XfQz(AL#@vf9ruL`an;pl;v39Lk**iyFOu(}z{)5wRHM-vSFVC!?da|0f!X>U zjIYa3;!ifET=pap0h8PBu6M?m*K7DZ@%SI3*i6O~Sres8K3?A@YY5kLB)Oc==-PQ) zHN@?hFC4d-)f=qP42PK?&*#hF^M1s~G~XS5-46h!8kjF@Xf`_*oVT@lLzBxSQC0R` z_~-sY=?Q%ZCJYC<6H`R%pFA!%eJA@nTQw&J5MXcWbXFtrW5uO9^m~8tU+wgvS%y53 z@=EOCz9fdKguC9B*|b*qxWendygqdtj<2~}m~FRxbpb45a^AI$q-z(jRNIp3xQbZh zFtkg%kZRP4{sJ`^tGOZd%le7(TiV;J+L(nh8}APLrySAobEzf@32yoSTFv@y3Us=r zjcyia@ePN6>Izg8;rki1@$F|WWwhzl1$B{b?0!Oh)cc^85YO=FRBF>rAK2w?RgZw4 zUtliNY;RkjtYc6>g~?X;I4R22!`aFJYntO%-R=NZ(`@0$@t+H_x91zqe<9`N7UvA9 zN1&ex2_77bc(RZK%q>8<1_R24=}E!fy>(L^Q~LcOkVLE?t0`>ga+{ejp`8(F&6B{v z*DFxPwc^c{O=E!uulSRpHp+vL$1Sz~Vv&C_>er|B6%hgiI9@`;+*qXxSR>p}0ZbzT z{zb|{*{o8XQCsUE&_s~M&TddltWXQpny5buNTgGRvRz<)U8WPfFsDPMgkdQR;)}qr zQ2C(ln-8ymH1dTlEiwe8`OdXGR4$ulme zd!nhr#OUP%$AB!ZDXM3ybSDwfN2ELEK2kN!VPwxR? zcmFFuF4Jlf3%t?kbNrP;Dv1g}8clu8)crleRTL5@{0ZQ3rcjKoE3wh&G+bb zFvxef55{KR!J&Sy=n04ZjP8aM=8NBYY`5Kkl1k=Q{1DRsK+A;ft5^G-eJMWO$~s_z zGNHsDMy9tV85i~Y3Q+R%=?}Um*dG7>G4;g&L_$uX-X8AXPfxc=OnUa&E>+pL9UpHK zTdmGMiZ!}+Ix<;HNrk!g2NQCU>&CmZOtqN3tNzf7Vvq?UwSe9fr^m9KB22&b1(-p> zER~I2vwxhUqSfMFi+Ur_cfH4aMw--VGD>o~R8ycQ54jA9>tatNI*ymTKfdC8A@nx$ zVOH8lbi-=o`11bD91fS92AxtNpx$!1H>{W~2lPl;c1qPqk6#6(-RmtmZ;T^# z7zbY2)ZXLy7MoZ+V3(YM+2Ij}*wFWxVm3{Up)>Xz}Gwk z1VE?XXli_sB`(M1wO(&Gr1f)Gb27r}?Ef5Q7b}AJm7YmqQX`RLMQN-z+9<@I|-4L$d@)N?)B;A20RmiDB*Ix{^jc_9F@)uvMK%jiRnQ#agE^CuU(%z(>d#{DfTyETb~SNmTHX9an(pf!t0Sy z9~M)qji=Gbeh^*zqp^m?){~Wj*szji-w5CC6ozP9=V|0tKIpEs*K8j^<5UO8mH;a-P|twFdFt+^Egy zarx*!o>|f6nwh^hwTW!{bQjl=0`0l~g#*bTIjzNcMB?hqM7lPl)au_wfls3_0Z9AT z{3n)ciIi{UfHPk}K9u z)#3tUgiI*=BSBUV1+iazy2C;S(e7#>+9E38fLbM>7(nkADn5FSC#5V;VY0liWxvI+ zo~?9f8~j;1IAj#dOUnZK;gz-QU-((@P=R}1HC)RO#FZuHhLLMh8^tv+jnys{|4wlp z*vGL=R|74%6xd0OLhTAxo}Iu_tv;+Us@e2`uo}hCIBeWI{|r@5wRQ_^wYGbiVfoBY zipATsOR&Q0(^Xs+W+44ot|?R9!lAJ6XTS^>*om88e-hf(3O0Fn&Zm;$FlmSKkw}E? z*y)8fM6`#ax>{(77z1uKr&HJ}j-Emfa9B|(Ou#!`wL*RxzIh~^V0;{T94DubVSK0X zVAavl#!sq~`Cf@xN6{7{;$dYRS5WZq(SE>tS@Ryz=v#upP(%fO&du`5DXG?taBLYXFPNX@g}<& zzm!xi+Ul8#cXA)I4R1E0;$lY)g0CQ*Et}ceg5JO-s*1@IVZ)>V6E&v-jX+4N?*jl= zBew@-t@CM*O=P>V43P5B|HVi>0MpKx0M2=l74}l9q*W}*L8Xuzu;??IMvck8Oc(O7 z2Ju#kTk}oQi_haxOv`LTR($}9J_0rXywSB4JGY?omZyd>(g}%DwT^{VhCw>Na;-Ie za}aGu=G|)8b^o2`!d1IJkTotHZ!F}w@mTHO6|1iBPUoSpoyKpA@4hjH zIK|PrDgxJn!?#i%5zO_{a`%30BcGgrYz916pq*QRfV&gbtfjtK5qS#2QaA8kS@4h z@6Tf?v-|pm=lx%#y=7EgZMt@u0Kwgz;1b;3-95OwZ6vt6I|=R*JUGGKg1bAxg1gfz zZ@rbO>aR|p?$JLp!eFprueH~6&wE}M3xxKx#bh2DX5eyzZKKpwzV^r5F9&_sY$W+U zem6a4;H=1<=t#JXg3~mDgus40y7H+I_yZi?12EA+iIJN|oAJ**d_gKZ5soHYLNnM9 za#%FPATtD-rsgP7j`j0I9cZ-b7=aba6$+d2_Gk3;!cERyMDMB5(a#2nC$vo*wlsiz zT5nCaq-Uq$w@5u?+8wSA!;P^3qZ6nM+Eu_QatYPVD?)>nKyP2 zZQ-V4D)^n?>(t1E(h%SeXUgegOI1~u0aT@Ic8%a5F)=N)^IL+40 zjMr$}^Y>zwxaHDjj4}-Jg%xV_Z%^m7M?BABkZg-C?I* z-Hwi2ddK<$;VPluh3%TjS~* zpdRa$QmG3@%n-$yOmWyQk@*_K>U;r_+aJyeT5n~GDCG;J+W}g*7KD_2H}Ng|M^bIf z)3fNGyyN+6ROeAN+sw<@Z7omAak{fKcWWKC%^WO)-;#8Wp%Jcip@V^>4BUK8!_UH$ zjCV*o)-#$61q^sjwb-ny_BL1Z=qA*a^B|~k3YI!SvloRV3JW~|4r906l~3m=&j<3i zm9Mp&p{>q4ksxN>C$O^FaXC$j!=ThOY(x#aU@?>@I)y}_mA#5rw!wuTCXJ)XUq}K@ z^ljF#h4}=h7x~qu63B;(y<6Ci5IhpqiJDM?ZiAcr${AP%G3oS35+gwP7*I~3KsZ#` zXb9}*vMFE*Jvh{QD9%r_2a2%z7l#dM&dZ7HdODv|^TLcsU+2okbnQo?O%@>m#GN~# zR*T7e)QlrGyBXxhEpKA6*+*NTr`SDg@CnrQ#C4JI7u` zgJwe_rEM(nyh^3OoA^9koW(qvU^-k(mW>cQ4O7LXS}0B@H@i#&?@Y)$sA3Y>4uc1Z zOHVI53!v-Sz^}~S6NIoNJ5O9^&hcsE{&cnAAVTTZ=HWug$s?+L7i#b^_8#pkr6khk z1ZOJSu%}*C+U*8yIS`+$<&csuBqo!Dx@UL0t-`p&(;3?JL#>@32N|j1+}_63V4S|X zTD{Oij(1Kn=EJf7c;8ORbWoD_>r3E`ke2(6EzPe@O8*o8$&`W9M6#Xp;M2?@ILnol z^5vzYXM?d?&s)7sO7M1OMZ#SL8@+w0vA_p&b*DW3tpB(ekblF-HGE_Hc{1qX*YHeb zF~fr$mCOe-MUz-D%mOA)_syI5wuZo_8cH|6`bLQ)hxbYWd#90D3^inft`4L<;1>um zI0bR`kXLjt37#1kYw@u^hBD~B_T&*uWsk_dy6Z(5ZEszrP6wd)-0jE5-4KH_8rny2 z0BlLXDmi8iZ3&{vxxH7<8~hjP8^R@)+QeG?rc2!Rseh71;<=Nm4FH!^nfd0}<90T1Nd2N#@SM_U=$mn{ zUpL)%maeURmdL0skV<)58|;j9DwVPL5lHB7(H?#LUJ?@?cYrO}pr&A!pTbk{yA7yB zV&<`2yMD(yyTD&5*qFuqqBb^=T`rLVVRHttX|gnSd)*z$l2}*a$K_kH=9{Pje$cC) zu`w2&-a|CX>uzrB=}5}_07rBGE^G#yQLTo;TTAOy!pku01hOlv?Y;GNxiT)L-EZv} z|D;C98PHF0BX{#)s?3HmUIje*Wf^f9r$?#NRIp{5{N|v=R1NQ2^s=8@7HsCwF1aF* zNMwyE zKiv#{KI3G|^4~l)U0^bnx6~qC!(HZ>yw9K$DBe!JZi~cvMLf(?_2Gw>G}G~DMuasm z5Fn(pLhnI%&*;@rl{$gJ{&iBuF)(kXbHr0fsok4!qP+7wk z0QC?YDg;Ot&u2e=POF3{R(>mc#kSk~MqP|t<)7m}KXS13!nEd}c;0=2obcSPYs>V{ zA28AKYD3bf@jv+c+bvh-yND*Ud=4p)I@OUY@@Idmnrirw_wzU$lDs&+^OUFG7v~vY z;DuE=KOz$6MgwgfjJ6X*;YHsXeJj7|w)G!Zf3%Ulqtum*ww}8cw(|B3@&KF(Ax>NP zAFlpR^emwnjFZ*L?NDlq{;9VCrHBt+S(gXsGK*yWgXyjOS-zFPi_xzRy$OvaoKhbD zSr;*T5}4lJpMkw{TJYoJKVFAF5!zdFoL8o0wYJZHGnIcMSiezVkO72n=tgr%>3=75 ze!?4}qJm7@0w%Sk$;y9zIsg8>{NtkhjQT|!e-$_X_wUh(6b^iW@1U6)L*YNCH^2Gn zzp+T1w|!Ai4!_70vZA!UbHHusbi^Lj04xwFs=4Q-kx+Rw48C z;iR0`#G3fd4+&b}{@Y14c!o13dD3a#unm6zl0xYY{aR-^jT{pUdl0l8v9h9rP?pMP ztzIE#FPeyw%g48pjy7iP8WK<=cQ{k|yCdGQXyF?wlxaR<1X!pbkkMYOT7v37yu+iQ zTWs)_=3~*7>kDS-!@mFu4m7qC9&x^1NImp_;eWkO&T`V%cx)(!O^$o@FmMs+K`>~F zJ&@P$dJx3>vo7|AW$4mEY0Y>)?N5BVn()EpGy3lB9C+34_!D-fw9v^s2FkKaC_^)i zwIns(ZVE1hMxt{=pcg{50N#E%tD8TJyPei9#=NEH!n{BNguaw;&Lt*ETb(Z(;a{nqkpCnT$ zBgbbBDE;`6`61b`0ceCpvsG?#Brnxmy<7PS9aSL_xtkQiL6kvkzSNuQh5VH*HZlTZ zm7Ou%$QqnCv;Rta!% zfTXEtC^@&lQGsn-XDa0a3{`*R2`yB!b(cb#mR@#-j;#%j60 zTd^F1Tp*Ji17ybx^60`>;|1 z-JQ%Qrvxx3f#F$T9eYQSf3q87DuIqn}_FS#T(3X11qIG0U)g6@~Qh2Wm9;jCX{h)||fd*gQ&I~X>( z3R>J^`C3{{kbe)fTN~Ww4>&__4`xN+YMV9spA7(rCH&z{)aBN@%f2FrDZ%D?fJ@Q2 zzqYqo-Y1AH@zL*&W>MPG1TKHsYkp`eNQP2xJ=+J1QS&u{1W zv;h4h>jTp9S}l|&ryOuF*^SNz56E4scH(GrzwEbEJH(`0R(uH%tLOh|ldJB{Fb(RM z+ysAw_C|&XC+QE&%8B;l4`^rhdyJ|L;&=92HvOA=rkk)?tIqd>U4(=OWJN7OY^pyq zO$PMw+eo`a2_w7UICj?r5ge4c&wkZaFXOX8+yOaE zffqU&PC3F&HCwn&V8yW3xbJEIkiA=AsS01(=*f6xu)2&WeWFIA*^St6WJ`DA>va*4 zQocBvF`82pPOh!uw=R*8`*1L)gs2ts-H~(n;`%!EMmX2U({Ph)%$^E1h{h)XH!V#0>W{}Shs!ZDWyC+g%WC|9?oVvr z`#rn6!{I|R-Q?CL*mJQ&BR%_glwD4B4M1eK8o_P(T*qV)T!H<#PbvBFW_?ovqAi;o zV7OfGEvRx{fU?5e+(4)$blVVrIkG^!t58c7iU_7?$4P-}GqMR@un_D-Dm;grN$>=3 zjB&jDV5BdlI6CrU*7?QXseKjx7v(xf0R(Qao=xcehL6()Q&rYt(`}7TcL>0Xg~yi=924?Y{gQ8 zdHMQu(0RM8DX{ik3?zzw+UO**GBs z;2cz7rB=2;n&m@!ZOp?=PHhkBMHQ4V0dW?Oc%B3kVTGh=rr4KFUF)^P(~tGrAwLa(nGS zt8MY4%}ZGx=j8SjzrIn3&yHNZJDRAZ(|z0{DphObN7#&xmWa(>NV1%vhvTbO9K035<7V=*H~MWJa#w8=<@YR>Tby)7i(WZmRo~xy0q*>{UA$Pm zu|(NGN*I|@Y$l`7h;GV9{|~#>4aYp;jNguV`krU4=S8)GHl2WV1QbMZF6wmK-xU`z z*__nZtL+ads>-iJ>C z9poObFMOlv9i0^L+useko~}-gd2oAWpIm&PHWtdIj7xQ@15%JYu1`L&ONJm4l1*ui z3WeDN(Q{eZo)fDruGE#8mPw1A?Y~g^?;7-;95(7S+=-N{me5{q~#o0)=QSqbY3ZH6}lL z(-@k>Y<^#x0H`gp)m!ev=M5gf@fj}tSVl8NuiY+O_e?jJC8&F465`JvGwBR>v-xH^ zQSOO{+4e!U`;L%f?jsAOQSVt^>`aA9`W$G5Z0WKq5Q?OX@F^YM_Wih!2$j-SkkN_# z`Qji8jSSzPCvrhaQNe>Wu0a(3tNO4JfQg`-gtoO`P%`l zQ|sTvCvv(~lf?mH7jk6lfW=(HF*yS0t(Mkqbe9PQh#v#C+cyFpi|GdeYR?86VcW~e zZ3)#{pyx}{Qt?}i#u=SvgU&D>maQgORPb}*t8R^ZOnT7?p)hVlu=b0WyC|nBfBLzT zq%j;8jU=EfH>O8R$$<2T|i*q89k;x9~>7wa~&t>~- z&rqzvI3%`+Qcid$Ghe;KlBCUlQcf_tUEyxNNo$+ErQ7=r@aGimX#ZbB5s}URp=3~ zS&V2gHw~2e?A3?KnawVw3+bShRxuke+nv!IJ|1s2zN5?e_4zA9pwht=%WBcUg~!E$ zauQv2YFRlOzFz68sej)*HRBwXIl!Cn#O8Zx!45**F_4^mY zi$|*Qw@$lbShD9s#-YKzsCY5CBEbnt`z!ZOqjR#pBJfaR;Karg8>yENhL2fnMw()Iwh#*oA5h0WkmJimN8{V+HUep6LEC1LZxbrEx4sROSnQ) z9tB2p;7(uA@%oY-J@;m2U>m3*0H74+`SoiRRj?NGKG2#2Q9@Oqs>l)4Of)mR$;RoB zCrT1?)qntH{8c^+b3E!L*bua_xxi1L?CC{D{0&V4X`1niD25TWLT?y7`mQ->cGX5_ z9LO#hEEYzk04X!8fJ+Zvzu~`IF$=+l6Se>^kjb7q+uquuQ<`NNH|SR;Q=qSPx$_QM zhW`{)7@R{A3>SIo_fmTZ#XRoK{{nfk+VVB^Jw(yzkk@ECPzpskGkRJ5!+oJ@vpqbK zzP8<5UE=xja?dL@9{6ZiZYbBh>UwDp5%0Rfsdc-g&aC1HrE&HLySKSuvO+8Kf%$9^ zg>}FpX!m~6#L5wk!w#P*q|p0HsNplD|9zYZJJFzh?dkl;6`1F5t`Obm<_m+e!6Kma=mwxj@%)gy2!|iGf%mYMnd# zt))YKL8dTFwwjxkkf-rXzVQw?A5(}AUhJwS1h5yUZ}tb3PNVa;Tcy5A{$Vidq+mY2 z*X;w1caYBgB;s}*H@duhsZq}58(N^ysS z$XfiDW{k^L# zx4nF1w;nueqDLk}Syht`!S%3H=N$WSNGkN(xw_@8*~4xRAb`Sc8=%-J_#b-$4--Rc zt;FFrLsqro5HS0|)zP~f11~|8%I{qB{doCXM{a3V)URw%ZToG%gfOg{mW2iOHA`Fn z+bRKbN@~&9z|t09i^D6NAS6|PD0jr1_BK!D_|?KX+Nh;orH&^*htDf5M8<(K>+^L{ z2=s)tOLS*lI{q$DXpqgmrB?nHwBg>ToauulT zPHVXxN|=8JA=tOLRP^!Y&EE-WatdvXE;strsZCw(4l;zJsSMETn$9h8uoHba%YZ21 z#`YiuI(!DQCSC=6?`45q+6E+&GOj@dlNP9$wM0Bw8?XiPX}finOKVXyN`=knKrmLX zeS>(PxuYibNl2s%^d&JxqR^Y*teitTakFit^Nyekv+mKUG(OiL|3&<(!)t99`27#2 z11Vjg1U{f&aBz3B{rRzk^i|z-EJeuzU$8|c{?AaTT>?VbH}-Q31`m7J93ncYIDEtF z^)pi}DjUuE07@hl)|a3SZYtgm;bR6+7H_vEvQ6}6XE~4-Dc=PJC2RjCq>t?=S)iJ` zILV7aze!6{5D0gdt*Q@V#gSz#XpaHH00xAm;g^kQ4d$w#KvGrjLPj@DGNoJL6W6^{ z4g_0)8_MMCi>nlG2mz%kSr>Ab10LqMN5Gyf2ggT;5O&NyPGw2oR;(0cMr8sXSBx9q zDpyE8-^ELIH@>p9^%4lHgAvWepS}-|>!sU+mm8 z_$Qr@v~R*gFBmJOht0}t*1^{~%@cN=3Bsnu#^z7U5Sr@AE%c+~!V_@UVc428v^Doc zdEYyl0()OUY12%pbNkDsMA&`FJ zJcpo9Su8Ue(}gm_yD%qt3{NG$E~y}0hN)Ewkgww#h9Uc`WBmv&x?2c&-82>=UmwR( zxTa9ggY$jdpO>6z`uNF#v0G}>X`zCZdz*7vBe&6eW%>o-H)NW zZhUZKZ3Rk&jI9?8?JJbWsthhCXkQFl?MBpXx6!3~66%bOu3XIBl8WVqNOO-z`@3L^ zp0QDTn>4=b{W-N_Q&0EfDx|z!x%7yJQ11?33#9Z%!M0|hwq}wxUf@sWn(yOs9s)l5NSwX&yL>V|AH&M@^r6~^{fF5P%u;^sn`Fi2x0OI< zlIV?2=0{2Q9R7Pao^=ON;H=&a&Pj&zo>S>?k4sUfC*~BXArw52X8cF+C(sC7TxbP4UyqJmkocdA}S*bBw$-$VUozF5rvC6A-HV)$eQ_Y~}r%^To9Im5{l`Lt@Y= zNi&+AvD5v^KZT0p4S3&FODW;~O)^*b2%@Uvi_J^xA_OEMF6{e77cGcvI19I#N;Q#{ zT)F=uagIg*kvM@xiuOwx4 zflAj0HhXsE!ZMDk-4ieoZeHJWis{sQRB%>Kv^6!JLP}bCULDbOSNy(R<8wHm-c~Tx zec2;qoOUxehu${3J!w;I_+)wh_JmU!>6B!so$vV6l=r#O`|+S>=Q?V!wuIqUA_Ki7^-?|WXS3AkV8!i?^* z7WLl!f@wr`y4UdqvA>w;$d|X6ka_Ql+#?@@6k?W_I262>P)H?|?|7BTCGVOJ{P zg4}Snh27L7{RE1Kgym2Z45SR|HtygfS^0!6Aj1i;2^Zp)&XkEBdb)kCGGNBW>wASvKDv`OnOz&Yxu zD>WwDkhTLTRxx{?g~yE^W-VuO%%D9W0RI}imO10V)6B9WtEYYOGe;7=J&|9*fpsE_ zHAEi?K4mcxS)&2+NsX?zA~LJchVv^aOU~K+hCcJJ)P!!v6aLA`h3HWzP7uS(qL`+4C?+JGe*p@;7ezx-N(TGT~0b~wYpSh>QS_vZcC z`HuD``0PJ`iL@0O8zXjaELQ9W3;`-R##6h}1}ESXWTdTIV}R<&Ysp9rG$FiMf9tOB$a{5Ll}a?x7`yP~d~*fm?IAJ$_8^_}8@`#qnF6!(A-sSX z1Z1F}P5CI0&HJRs>wBKST+Kcrb;FnUZ%@j2U=cXC=8;|2wEQK-?SUMs2@dMcW^Qs zMj!O-=Y~2=Lwnoz)?F3Py;0scd7XGo*pU3tsbJU6Q8OvfB%6lZeY74CdRu{%Vwz!z z$dVr%i3!DuCvX79@}kG@D>a_QZ;VDfj@)(Ey#w8_D^Q)C*l^5^%YAnR?&tAOFA&xr zImAy{1<4R0uQdOk2ruJ6-FRZ#JN|9@EjxZvMNP~Ee5>Hmfs|NdvE?;C*l zy+Yw?w&neKfBiYi{>3F}A<$U}YnDW2#sAzk{?NDImf;IH^ppLF8?Y>W_dSs+=rThD z)^s!>*lNDiQY-VuR$B$wmhXkyTftTOo92lNg^bfAeUJcR;91}kk(9oKdOA7iM0;Vb ztpDu~Lb|dA6nfQ(MI=!N6dvW!)&YAHNetQREO|=ev>?Q7qaz^9G=CyTK2|Fj5S z4Z46HYs~LZ10XfQU7rz!p@io9mO}jJmwO(O&;J6T0?1(f8T!yZU4Sa5)IXwGELPSq zlk3C*bh}onYd01Q70W%!&-AVaYBg9YAmFkUYE*AqY<4;SE+4ZR-AhbYYW{RU^|3NT zHQ$3R0+~3#u#RHnd!6<46645zZm5KlmEyNbrMMef;oh*F$vmz?sdRDA8#acjH?+XG z&-vHVV$x(iNB0<~{?v#cxpol3rqhn@>NOU;(xnOyEs4_AkV+4Tp{ z;rXGzr)>x0Y-Rf!FMKlv4kFXvAKuKod&aj^q)7SvM<}Ew8HX~PDO7>NWwI$YtWrxc z40!JqHu}LpqehfTpeY2AAT5S)Ng-bX{uT`rm9Rkiv-Acbn^h7>kD#o{cdYzC^{We= zfOGfL?@ca&aRNU3tH(X6HrT(Z_}k#l*r@-}EZn5j^CIAJ43p31mO|0hDYlB%QvEKe zq}4#SKW6!io0*FPA{0#EbnoVzK6<>u69iZ@2Lh60XZk#-*l^qIYF^*>9a&SyRBtp| zC?-Rif>OR{As&aF^0k%P!OJt0YO{@dy4T^HwP0*EZ@oL_3Wr7tA-7kGUYi%jirW3j zaL3CyLg}b2uSpdpM%{v&#XY~O;lO!0u~%oV|D*M*YPZa?~R*oYt61Nn!*A|G&2ieV_ejSF$PO zaGTwK-S`raT)+Ie`tB(HArHd;hdh|fb=3Ldef4l{?Qys9R`=x*6p{TtR~PYZM=+T+ zK-oSW-k}7j$})&Odav2wvb)87x&L7*Q|vl^vc!iO%+uBYO+8k5tevTT8|d)>@J7qo z2-BHH1qM0+9RQDhVXrYkRMz%VgyJ(iZ}q*EM_~sbQGlrYRDu}XerNkCtLX)VbS8Jb z7cJRb9EDsOqk8LJ`zlZIr;-FMw_r!pgN03SlJP#c-Dv>EWhhf-F@bLa zE}P9c;&M`1`R+gk;JWbEz41))3*CnSl2ZM~7e_Mwx0p5Qv zjRd5|Nxq9j2XK+dRcvOnc#DB-K2eVMRq2cR^J?lyC=$+&`3O*#CzX^)y$u3t%e@?; z9amR$hG^h_(rs6_n*DUFATqEqB1~faG$A-SSEFED33TeRU$tBvOdk+g&nsSz4PFjp zxL?+rKScH;;7ycFe-#7zhg8oS0j-Y}XM$n=wMh#PPW`!!)Y=k%p+zPNJc=QT%^-&e z>A~@OK87*Qfl0#PMf~MAxs2s|yq)#2X4F4j{7Ig#?ZcCXG_vbsfxOYixIW}g*rDk` zqh1~s7I2Pg^AFFFIHVNgM3L_h_bF34Oo*8A;8KhyL1*;Edq9JOn1_W4^`DIw zK}nI$WEp5nHkOj{$i4A{b>CMun0oK0$g&T7-ngt&Y&%h6(*LLb*Jck!icD!|Qvp_O zJW#Mu?R{G9*O}+N5T>-#t^DZY?P={$K|hwkCzIK7lhfjQ%6xfPmJrL@*6f-ehSIH) zXhv7;+=BSKlsLT{c#Ea5+pWnO;MSahk$yX@+>WiphM4_pM^FbjN z-4d;-EVH*q=^}j*@|rmy0Z&EmzWx0wwACjOM{4bvZIHYFq={DtSPBbj{3$iK9wjmbDGbJkVp0!0J-XkeY)IHxi zaENg?dG!}FEpX14~ zX-z*orI^U)3xh%wjv^(YzRgZw#LDguKA({`yf z*hjxAOt}X4(<-;mte&aP^KExuq!_Ti#fb=-+y5;sWhwc(&fKoGy7z2TY@`t@$_<1_ zYp?P8&%DCQED)e<@p>Qn9wRZ@Y!Ac~jqCGAp5G!R&$rv+bz$ermc5)5%R>(=R1;n5 zExSfIz)-&=QY!Sa^&cEByAIO8-y7mn{;ZB|Z@h~oH&Q`xqJWN4O=T{$=;{_TVljCE zo1=1j6$%>iWik@Qa*Bt$wIw~A&F5+Jea+e()!(4}ZgZfF4rFWM_8N;TM`1Hj`Lw!i z@iBhc8i-Yh_Le0tf8eo47|@c!uHB#C8-Eo8V3jco+&Ti2X2^3R1|;)|Lay(Y+_!f| zW44RU1hMtzNq!vsG&QI1qF!OpC@-<4iR&UvaLYht_iqkHc3-N|D8g>~PJeBZO7uEw zvp-Iu73`u>%Aus=p9PX|;C9TN60yf8QpiaA(&~zjrg?_B-9PSC1dk7DsKiM6eDMhk%y4)CR3-?R8I|#;hN;Hx%6N@}|f3@TEH%6{= zJ>^e71IFHPkcdclbKfMyBGE~9-f_F0@UkGihFRzP<_|>W1BPQb!kgP&IWct3Qn1Ul zY85PQ`?ENWvrn$VOL4)rM!C|kA(#oL`4X|yK(}9Sh-lE+8vBKT;@Q2$`^-8I1;%9! z>=wpL=x%YLU|7?4#xb5O@SPBykeGEEbpPe> zg_E4`iY_vn0^`hOD0NNwWQqz&_3%_9#vULEJ@*;PjU~YH7rVA47JjmSDq(NJ9Z-%E z#(6j2ZfssH$nXz|PbdaonDp(xBtFITkQpu1Cu9QvfBw6~w}8FB-iY-4G_Al~dedff zZFTIiRZvoA^pmFlnhD6|)#7~g9GQ>V{~4J))7bGf4AN3-Q}|sUMR5M7Mh!kpB2qOF zprLz3FX2Wt5Je<%Q}qsY9E`s8+-PUL1$r+7(&|Q%E!)qh+>$?+x18muKtFtBt?( zJ*Qg>@_y0ZhTSMW&QNj3KMXsOw^JJkybQa8sUp)_@s^|Q1cq-y&`vcg)CnruQ$C#O zozS&%2w(0D$t~8JvVG{@ZJPD+3o~=-V83xgY#+cSwb1rlY6J;v-i}4?f$n4m$I^Hh zHyuKr9v`79D4%xAbsMunE-Bb0?c}?MzAkETx-EEuqg|H|XsH8bfH??w>?*0sg&8G9 z5r0lq{-B6XS4tdL$jrI}pTZ+9$EgOo^!>@6&@nOx?y7tmemPZxG<5M>wr%sSPiRM2Yy%CvLgjsJ3 zC<7-!1xC-#twys^tE5p0XM!nS3o`>HkN?ThlZd7G%h8j$$qs@+wGbn^(FCSj zFz$`o_&@~AI%hClPaMMy8}F$8)>_Nat<@%{k8s_)`Vl)reSGYfh(ug8bsC6At8}UZ z)l5Z{j0Oa5+ek%kZf2^zd6=LSHbxmde`K+hEll7tS=D*RrPLOh&HmQvc*|o==GFzg zuT1aOFLw8!vc`Q8FBADo4(f3rnT7D@a=P);Gsrs=YR-9D-R_0gV2|;S`?tC$y$s@K zN`Urdg>bipPKdsmX(Lo5gqL?aw+yC^r-|e)XGMv8T)1#4B0?$Kug|w_3tnBk#s3;O z5F-GAJ{7|L6|o;YRT-2@S&!EnlgBczhehA9zy9{`GzE_}N<%u#F@`1SB^RM*EEgcO3Y$&(DS)bO#>+3 z-9-&Uibi@C$HD(-L<)}TZm^uItL@#;-3n%gIfX6>X>?NDscdTj+3LN=2l~P2)x&=G z7StWEdCQP`?b@1;p;=##0Xc||Zv4V}Vx$R%&dsw)<17Hz?!( zeI^w4+r1kwk`jh@IRXnHcz+^vzEz#6TS!yh?-^LB+a_DR7=xFcQ;<=f1-%Q6So3~V zqt>NjkZ?+6LBeQ&Z~zmB&g|jpgc=_b!@Pd=!^g@c)n9+hcMC!peY7~P>5?uSoK}!o zc?`PEvU?gBNbpnUMB#qAGw+*%BUlQg+wS=;cE;qC0rzhAZ}(1dfzi-1eI7B5)rXeA zFx_Ejgi9K9lLg!+F%y3Ax;t4%eaH~pEO#`AZYY|}6QiVsY_wgrODvFRtQ2OOjr?UT zD*pWDICK=!;=La=A*+F3k(wXx+_!bAf;Ej#Jt2fvDV*$b3RR;arH=yT&2VfU77CK# z@;WXW?+y$b=`*jTL?3}Aii`+MI@x>)#73Cq9;?D?P23pclL<=TAOypHFqYssT_nuc zuG=z~clBA^TA4&f{Ovo2E_j=+x_@Y+0N}R*^YmlA;BIWC*w$n2i%s+ zYz7(y!qZ3U{&ElFAWBv_$1#iBSsI1Sv@}VcONe)LY;a;A?Ys!jCSr2~4r0$iKV3eH zvu~l7!lrefKqybAjFEFg7_VoC6eWG}Xr*3T72|G`i>#=MdSK<9KD@`_4_P)Z(gNvt z;n$Qjr)*JuLgo=XC-3)5%LaJca6Ya~6oF2(3#!Vif98tswdn>ZBn3{w(w$o8V@r;DX>+G#x2AywvI?rm1SYwBnj`awf!U=aN`&(7@G@3_@^w!CXC z*)_63P+O{G$@}IEsf7J3FjOD;$T0fIG@aGy>nPsbWch(g3HNe>FdgBYe-1a&!`8Go zcqlx^d4CjsS3}!)uocCO1)#_w7+%ktv<^+{d7Z%@NW`*zxg*t40M=AduRCjj5>lLZ zIt-dMQA>^1B6>EkB)>>G0?yhQvEr_SAqVsNWa4B=7gD=d3@19@8jYwI>wK=%ZJ}=^ zl&OVgw7%;kbpgU+!DiMo4?Jtl<+0K|it)#k`oEfgP@WCHE4|JQ%lOX3@v|cGD zw(ENoCKWzy6zZSfBPO{NXk&oOm`3;!H^k6G4(9bD1PkwZKDP2@AMZV&+AZq=svRYs zfPRfH?yQQqUX2aT=bhS32ky7K3p8Ff1$6@rup*hnUGH*1-TinwySp^<`EiB@l8^3) zqH*G=0{3*_hx27360rmvp91EA64HQLk-A_$U;~FJ#xwsVrb!d^;;#Gl#(l_>6n>O3 z&KdV)u;SSG4v7d?2wMMW|j-gtb#D!DOSSPcEk zrn}hnI|UmDB%%{y;z0BZV7u-`3UmK^+{$+OS&WyC;;s!B!EWfgDHF4Y@mdi&+7Rl*>x7_y?vbiTD;}%K(0hw z@R%tGG0uW+OEc9@8#KpLcP73tW?Wi!DFfnmeXz3oIfuLV2&?w50PhIX5> zO`P%-L`3Hr;jwwO1NcszF7!?SRczhdYjN0-Z6CXGHNU8i-G&B#yt}#GH{Pf>mrW;; z-z)A+(;rKi_1dr@e2KoT_j%p<-uQa_q}Lzd>!5o6_rZ3f0|>FB#z8vSt#0(4ogeV& zDnSAVC!CRG$pAkffH)X_#GMD;^Tyi!gEpa007#11k;>GnRCBl2g7o}J90;X6KSt2! zU=dd}TdbQrtLKYqKG!XdS7$U!BcRFUF)lGOvyFxhF_aL`0eWm5eR!K<^ZS5iKzgXp zY@E0G{Tsfuj*e+RlwQcDhT?cwKE91&h6#+5;9z;>#0#TwuM#3Yrk=;ae7dvX;rnS$ zV1X{{Wx4be$Z=}qFYTyg$}(&|uYmEs1a|z^n-|_BLybbW`NF57JjD90gHLU3dpQOv zb$H%ikgJ)#;(P>Kq%eEdo#T1GZicP#Gni$dS~K(>a{$~ZaSF%LjNhIj+po7e?G||K zFlRsMXskv?mQ}<3dolm68VH>HF0h0J*X6X=)>iUJx`>2_b36l@vzB3b8WK{A}7&U54 znyR2WT%I9`LKl`}Dg4S?o^R(&zz@+8)EbLXA{(Kz$i|PFJb)A+# zuK|IInCgF3=l)6=l|;7Jl3OKroOw^JXqxvw5w%wmuy6YO^LpA+{cKb-!T%kaQ9-mL z0Mzl7owH2&KWN&&|9He_Jxtr2^dF>?sDfB*0s|8+ntGz~=HO!T0^3GpaeVWKMw2t% zyuq)M5*1QSn=OO%lfkiTf;zUc0PGxr~W(HUjEfOD>nw&{TGr2yy*e*-TV2x7d^V=rNt`y0}X8@|2D3pt5 z_$P6PfCtU|2WM*YoAQuFVCo@JCk3F3V(Fi-Ar2(W4RUyF$mvKSC`W|c(-baJvLGh< z&MFA3&rU`*@~MpdNrs^Lr9jA=Q#9IHSJ0BiRjnjFz0VJYELv)UmD6<4BH#|9C@V#{N6NUDpm(7;yWC(`xZDVehhtdz@r6zT2)oFyw^kpf@_z_1sWfbBzB-#EjYmp3 z$t9JTen3dC^ZNKrQzA45fBHnE`yD$HFsKvRrjSR3nY};x#c1|Kc8+KuI%?5VMCV2E z{I1r(R{&s$+Y(5W93MY=T5%4hP60Ard$!$3&IGR#rStMB26 zwQopYc8|3^2F!W3NA5R(z`RCOe(NuxcU6t(H$V5)At7J3g($3JU-STf|4--D7-O#!1eTh`rGd zm2B(ybNK}j_@Bn$boe~uK0n{CqjNc$lq%PkBKT<}ZGr<^`>6&CXz9iEOJtw6D0G)t zA%=81{6#E)6H7dpRrR&iJo*S{_g8uR4_O_aw=ogPB^HU+ul#(}zU{o|#9nT{H4x|H z#pK_Uh^-`QwE*sVkVsra`_DSSU&*b|TNYbrh_*iH?gRGyK{$8(=oMOzl+o#onlpv! z(WR<2I^+*e_mN?6FngDK<0amECtG?iA_U`EJPJre9w3rWHuGt)7$XV-!7oy@gIT=Z z(kFDwlNHO2?r5YWbefE@sm<6%`M|Pw@=WdN8*ZPifq&XZCm`p@5i}d)xHr;u;1T(xq@OffYAfM!bypp>Ugn8Kqgw;v zeSeWkuiGwst_hxNvM5bL`%cDC0ts(K3z-QBl~kzn**3%F|AA+nE_`gaJDx>9^Nm=k z->kQ<(O%a_B)Z(ISA>na_u-h&c z0j#Z@uO!7o)zB#C-nWs>L)3qRmE^Od079k#;N%a6YeNekNQ34Ofz?D`}YKGQe zzV35O7{#@8!9l53vg~{)PN|TIsqFB|xDD8RJ>L#2pY~By8No*xh}MzUicZcgNb zBaLgB}XCdW@MN0!{80A@Ec%tjwhz{;t8R?Y4lk@4SfD5TT9d$M z6sJVi?1ftI+QBlt{N1bZXuLY(VUK3#U+m;EUu2_J1?E8oDER~7oD(%E?3TRbGKUl+ z42&Gfv>#LV%En1}Y0}Z2o2;mT(2{T24kRRJ0(A(xeewAN!xF&uFqDs6c1+P&N+Dkl z#U!vyKlphW7q8~)coy0v{ExarPszPBGN6(i+aK7ZqL}LNx{pX}u=tH2rY|^}tJ2J` z&X4y*Q$j}4xZ@KZN0@-gF6wi;*e{m{laTpB8O*OIy42?HTXE(qjXzf4*S@qn_u#HgWRkGAA>(MVwLQ8|y#$7eY8fCR(2mq6 zyCZ@QC&i!UEOF9xI)jYj%=JgTAn+J!_|1%Son>502b~C(;5zhE0(F4Q8MocB|vD>dy~*hq=p^{B`@#(?p^D?|KQD+vu0+^ ze3&(J=IlMQ&W;_-P;_CLno$ACSvz@SXS8y|X&Emw(9OBmsmDY z#MmXirAd}c=6@BIENreCi}3x}d8OsBr(#m2{+bzBVcAq(-?Px{?=doJtNQip+ls=0 zRFU2?%Y6D-i3oaK)`z@NOd{ONHeg`SBystxtLpeon~J9Em}R@X#o2f?Jy57UP8X)2 zigh!#v;s72ea*&Ko-sFXz3bL8x4Hzb%PM2vYw{-i!THebXHBT-K`muxldeuIW@|1qFI!_yrPt7`R9Dw|C_dzjag(zr+-rh*G1D^wBpzGV|QK*l`VF`FnpbPUO{M{p+Hrl0^pH9k}v)B|J zn#_?la0GPfyQyRW=wQ)_^K8ZJhLMY>J>z2^TnI|H=RTS>pNG3E(#@Tex{e-b*>RLsdC-6y*XE+>XJ=9_{Ec zS>@GpvCEoaM8m=sdlkrXX+&UNk(Mnj>o;mu0qJ@BxRC}=1Wm3*EhL!DRI00OJcDNi zNCNPflWw*Xp7IVQTUfgZ7dOW*_sL~S8^Wx!t)B!69O|>T#WY_IHVQTYF<^_k{AqGf z2RY|5MVvjm+ zk2Pyh94)T%h*lltK6+Qb zLsOe?7oDul3beLt#_Ly84MDd`YUdi_ry2*jJ0v6kR3z%HbRfV1WpLA24CeZ7aOE|- zrE8%FVk4~(+7L<~17;0I$8IU-Z3qLkFFEDMFb6WV#!#<1!(4TGuG*7!+PNa}l~LaY zuiZQhNG6s>)`SIe;Wk?Z*|@^@|3cIpb*7-DzzK}XwH@DPoJLRialOR+m{QrYeindh z!rQ|X1HPvt9&_my0eyoJ}7LlU_9zrN3`Cc-%dqo-El$qL#EtW+Y!tML(D2a)om@4gu z6>-#K{!WY$NpJLLKk_xLMT(&NW!)tYwC4B|@LXSUsY0Sxk6F0G$hMU9CKC*48EI$2 zNS9!XS%kt{Jk^T0;+K7A$j>F%v&uWNqh#GO@7C$ogG9_~GE@zrVksC2J-%ldrGa&n z>Aqdg5Oi3NX*9BaQv}yW?jm{6_M)Bc>dDnNaBg$^?Fl~Z%L9iBUNDt%B;6A{Yd8GR zeak)bto;pOole=iDrlVC=w{~t`4TR!!6By2gXaQ8Pl8V|Dcg&9sj`n>mUtz4M7b{A zbxZs8AnO`lIOcH=yeJ$)j<(CW^t1@x#ISx|MLvB#Ww4kd5eB_~n79?X6?^?5u z6azy@^`qAZ=F;nFx6n&i_dJeU-&L5brrWCD_=eaN$gDmn#WwG+3*y2|c*Sv}-E_hL zjFOeoW~$Us$3UbBtW|}Z4z33oi&B5Z4Pp!Va>WgTZhTq$BMSAy2^)z*Xw2@vdbRab z`ZDIY=p<$nYcCjIe_Cc@bQc6qbvYy4U{Cz}Q}RzFUo*laYHrOkVB^lfCSgH`6=-*H z5iRBs@S4a6QCMXrpK?4qB6Q_pOcD32DVVQ^2i-H6p!+R9;3Il%FjZ+FmQ7wWwaasN zd`zz||H-5RC=A<7K)$InZo+ZBKd`1P#@LiE$E+J3#$)M2QPy8trNtt4&Qbt!@aW1q{pHa1G9L}Ksw?GTyd=XK`ufF;tNz+iSLupR`A+ZF>ZwI8&|(+Pva~^8V!;5(kaaR@PVzG zk6GbK|FOB$6sCeAl$FajFSWA(4XiuSCC4cJru)OSw?rH6lMA8*eTwZ!k=`6yVY16q zrc?bLeB4VWvP{&YB&AM$rUp9r@+}XxxOEI1jgO9gDb#z}d!%Uf4t8Jl}TiBE7zRjd5|HSZZ!nhb^5qWZ1UtNeb zE8X2SwQ9Sjka%1?tiB4{7UUb1MjPOVc1r{`!(c(+%X*U^uCyT|;i=sZ-{O0vMW`zy zynLbieln|Z=APRNfmp6y1sW1QzEf{10sjTZ&56UC8HYJGV%7seIy%FT=TgiYz0$5F z*O4`1k#9MlZu&dTw4AM*-+A-gxF?z3__A;US{>P)=gSAt%X=1jJ?EJFs1Kfc;2v}1=jkP z%OiTe?C(5#&EqjQJC6jQ>}rjk<(YM{rA&gG%3b9RYh*NR%4>bGub|g(%AJ0?&3(pmcjysVFk~Eq_%T|@M3o8k-{cK?GN*HW5b=F|-@@8qM}(@bt9zV52{SwlZs%cgb*$C@ z`Uiipv@%aJ<_=(R!r!~VJJafWK84d`f{2R4>6K8WB#dlh|!hj8DCfkj*z8Dtek^Kn1cf&zx7A;%n zbr>s?^~BdN((JZUU>1gy*G0vAo*J{dSE`2W1=B(ky_BoKy`u%EiTY#rm)Ao*!f$38 zVB_ueKIULMFm@>_<^9%~ex#%uIsh#Ke|0JU)hdHGBqF+6(ci+f=#aw9vFyz5s{S;G zW_PO$IavM*TpJ=+Dc=p*Au;wdUzgYGHo z{!HyvzmgZHj?XE&(o?$9@07lY`XH#=mZ9@LbPr{F&^e$A+A}d2p0_Mo*&2{zw7If6 zTHA5@$2D0~tlqfpTgKIiYeDdBmI$1X(i9pP``nl+(SnGTzQ}2fz9}C8}dZ; zD&!2s4kIf|$Y(>$bxNl-$^y~?ucTAbF2fe&l-vPWbYp<{(TDdD;~($-B~u0Rjq0ga zy_Q4de(yV$_c+rj4$;-8ub{rEekD3lv zj0xr8k=P~3F0ehX0ga1f=HSIYP{sLVWe2)|^!M1ei-27i`wD9km#W+u ze)AR!9=`pKBx|dqG|1nyk_HcaPUq03tP-ZUWLvJNmFjSr;~6L-#vU!sJgaI}e}H{c zNzK5h=i;Z!aL3}-N|2jEpz0e=?o+m_*?yx;k_Srg2g_;}rqgrBNI7@T``PyyNjtt$ z7)=eP6pyFgl#xCfiv+_|Angg@1uqV_JJpOGP9<|^Z- z=NNx%arTksEmm<@BcMM_VDQ(P^RYE7TsTpriaew>Hnqzi=E8*n#-#C$|6DmD>P7&ct2$rM54= z9cMf8dC|3y472ihPU+`TApZuQ8xB7}@aRmvOUEoxgSN!tF-PaBH~rk^8KZjAlBxQf z299ZGP8Jh@TaAXyR#HNAs!8GK3<m}bT-2}dhPV@MqXP? z`KR_mIiD<%z>iNj&|a|@nbz~Ok?$V(bGa*hcS`4rO?#w~i^#1H|L7MN-aDn}w3h+t zFIpus6yG8>sr z7&`d!yuN28`bfN~?~`(#RLv45yllp=Zzay^fr&cEG8KNc@9n*LPM6+0=KWl`<2oA` zl)o3}76w3WPSwVzN!mssCcSrI@J7?rqD9!~b}&_j=RB!sG1n>Im8hCzW(l@h;Tv4k z;aK$`G@68pZlwI%d%{TC!43Dp5cZsBAIoFO&_-Xq_r**QPY5>=?uW!|+FEBgJpMww z6gyHXCJZ%%KcFyh$_j#4FfIrd6t8G17h&p*t?TJ{L%cZLI+mhHt|u|*%MIL85qIL= zxsekq%(}I%Tw_oxbVNnn$b$Adrz;r^{J_Hglqq<104Ca^uDmWBGIun~H@7A0P0LBeyHc|f0Vuk`7t2}O;n zg?v62zIv`)8uiI`}BjaB|+-+tr{U)8fzjdl>e)Af;l>D1$! zj(yog;D%ll94V5kzhj0)KVMU#{$UOX#kS{Dtz(#`!Kxyua_XH)6S5xC z_suD#d<+j~N$dvYOFGm2h<^@}wY{j2~_&T@9R;qWFCSm-8;ZJnPHH-jZ@T ztWD$TrX1382PRg*pl=9eZy7f*!^oKlJ^ z5=t%xpK47%F0ZoZej)sse;Q=0ib6$*tPO8>$5vRw2Em~C+rdw_Y6>F~J$^_tU&N(& z_0e_M+0o4X&d0)u+6Z6!6cS&2_SRN0I3wQm`JqGizwA3F-5)j;qdb_(@iY(gkKp*) za&kwAWf}4K)+J*~aZ&w4!N5IE_nlgsFQ{Z+)!vi}x&TLcng0OS1iI4$g2d*8Pq;T<+ z$}c^MVEgCMtNx%;aMK=K(mll>^ zoCiIT-M47bh3;#bZfz)U_P(MF)qX4O>9Kc0sE>!cZtic9vG9x$niHyj!r)*m?eztD zhX5@ZYJc`aCcYZX*$b&2+DKW<1u3w-NB$T=YBU=1xBkA4##XZ}fbq_yf$rUXa!Vf8 z6mxrE-XpR?g=4~6AIEn;B8BkeN| z^obCj|Ag$Vbib7WNNby(854W<5vB>C?dh?EH@iEY;*l5*$9|84(w j?ST4!yZ@h#4knPSzg>bEwq+?@BNlB9y~m|$)*=4`>e=_? literal 0 HcmV?d00001 diff --git a/packages/independent/package-publish/license b/packages/independent/package-publish/license new file mode 100644 index 0000000000..311d2eaf9d --- /dev/null +++ b/packages/independent/package-publish/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 jsenv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/independent/package-publish/package.json b/packages/independent/package-publish/package.json new file mode 100644 index 0000000000..3604bfe813 --- /dev/null +++ b/packages/independent/package-publish/package.json @@ -0,0 +1,36 @@ +{ + "name": "@jsenv/package-publish", + "version": "1.10.7", + "description": "Publish package to one or many registry.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/jsenv/core", + "directory": "packages/independent/package-publish" + }, + "engines": { + "node": ">=20.0.0" + }, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "import": "./src/main.js" + }, + "./*": "./*" + }, + "main": "./src/main.js", + "files": [ + "/src/" + ], + "scripts": { + "test": "node --experimental-json-modules ./scripts/test.mjs" + }, + "dependencies": { + "@jsenv/filesystem": "4.10.2", + "@jsenv/humanize": "1.2.8", + "semver": "7.6.3" + } +} diff --git a/packages/independent/package-publish/readme.md b/packages/independent/package-publish/readme.md new file mode 100644 index 0000000000..effafbc65d --- /dev/null +++ b/packages/independent/package-publish/readme.md @@ -0,0 +1,62 @@ +# Package publish [![npm package](https://img.shields.io/npm/v/@jsenv/package-publish.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/package-publish) + +Publish package to one or many registry. + +# Presentation + +- Can be used to automate "npm publish" during a workflow +- Allows to publish on many registries: both npm and github registries for instance. + +You can use it inside a GitHub workflow or inside any other continuous environment like Travis or Jenkins. + +Screenshot taken inside a github workflow when the package.json version is already published: ![already published github workflow screenshot](./docs/already-published-github-workflow-screenshot.png) + +Screenshot taken inside a github workflow when the package.json version is not published: ![publishing github workflow screenshot](./docs/publishing-github-workflow-screenshot.png) + +# Installation + +```console +npm install --save-dev @jsenv/package-publish +``` + +# Documentation + +The api consist into one function called _publishPackage_. + +_publishPackage_ is an async function publishing a package on one or many registries. + +```js +import { publishPackage } from "@jsenv/package-publish" + +const publishReport = await publishPackage({ + rootDirectoryUrl: new URL('./', import.meta.url) + registriesConfig: { + "https://registry.npmjs.org": { + token: process.env.NPM_TOKEN, + }, + "https://npm.pkg.github.com": { + token: process.env.GITHUB_TOKEN, + }, + }, +}) +``` + +## rootDirectoryUrl + +_rootDirectoryUrl_ parameter is a string leading to a directory containing the package.json. + +This parameter is **required**. + +## registriesConfig + +_registriesConfig_ parameter is an object configuring on which registries you want to publish your package. + +This parameter is **required**. + +## logLevel + +_logLevel_ parameter is a string controlling verbosity of logs during the function execution. + +This parameter is optional. + +— see also https://github.com/jsenv/jsenv-logger#loglevel diff --git a/packages/independent/package-publish/scripts/test.mjs b/packages/independent/package-publish/scripts/test.mjs new file mode 100644 index 0000000000..7e7ef2d738 --- /dev/null +++ b/packages/independent/package-publish/scripts/test.mjs @@ -0,0 +1,12 @@ +import { executeTestPlan, nodeWorkerThread } from "@jsenv/test"; + +await executeTestPlan({ + rootDirectoryUrl: new URL("../", import.meta.url), + testPlan: { + "tests/**/*.test.mjs": { + node: { + runtime: nodeWorkerThread(), + }, + }, + }, +}); diff --git a/packages/independent/package-publish/src/internal/fetch_latest_in_registry.js b/packages/independent/package-publish/src/internal/fetch_latest_in_registry.js new file mode 100644 index 0000000000..71e22d5ca0 --- /dev/null +++ b/packages/independent/package-publish/src/internal/fetch_latest_in_registry.js @@ -0,0 +1,51 @@ +// https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#getpackageversion +// https://github.com/npm/registry-issue-archive/issues/34 +// https://stackoverflow.com/questions/53212849/querying-information-about-specific-version-of-scoped-npm-package + +export const fetchLatestInRegistry = async ({ + registryUrl, + packageName, + token, +}) => { + const requestUrl = `${registryUrl}/${packageName}`; + const response = await fetch(requestUrl, { + method: "GET", + headers: { + // "user-agent": "jsenv", + accept: + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ...(token + ? { + authorization: `token ${token}`, + } + : {}), + }, + }); + const responseStatus = response.status; + if (responseStatus === 404) { + return null; + } + if (responseStatus !== 200) { + throw new Error( + writeUnexpectedResponseStatus({ + requestUrl, + responseStatus, + responseText: await response.text(), + }), + ); + } + const packageObject = await response.json(); + return packageObject.versions[packageObject["dist-tags"].latest]; +}; + +const writeUnexpectedResponseStatus = ({ + requestUrl, + responseStatus, + responseText, +}) => `package registry response status should be 200. +--- request url ---- +${requestUrl} +--- response status --- +${responseStatus} +--- response text --- +${responseText}`; diff --git a/packages/independent/package-publish/src/internal/needs_publish.js b/packages/independent/package-publish/src/internal/needs_publish.js new file mode 100644 index 0000000000..23c00440f6 --- /dev/null +++ b/packages/independent/package-publish/src/internal/needs_publish.js @@ -0,0 +1,63 @@ +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +// https://github.com/npm/node-semver#readme +const { + gt: versionGreaterThan, + prerelease: versionToPrerelease, +} = require("semver"); + +export const PUBLISH_BECAUSE_NEVER_PUBLISHED = "never-published"; +export const PUBLISH_BECAUSE_LATEST_LOWER = "latest-lower"; +export const PUBLISH_BECAUSE_TAG_DIFFERS = "tag-differs"; + +export const NOTHING_BECAUSE_LATEST_HIGHER = "latest-higher"; +export const NOTHING_BECAUSE_ALREADY_PUBLISHED = "already-published"; + +export const needsPublish = ({ registryLatestVersion, packageVersion }) => { + if (registryLatestVersion === null) { + return PUBLISH_BECAUSE_NEVER_PUBLISHED; + } + if (registryLatestVersion === packageVersion) { + return NOTHING_BECAUSE_ALREADY_PUBLISHED; + } + if (versionGreaterThan(registryLatestVersion, packageVersion)) { + return NOTHING_BECAUSE_LATEST_HIGHER; + } + const registryLatestVersionPrerelease = versionToPrerelease( + registryLatestVersion, + ); + const packageVersionPrerelease = versionToPrerelease(packageVersion); + if ( + registryLatestVersionPrerelease === null && + packageVersionPrerelease === null + ) { + return PUBLISH_BECAUSE_LATEST_LOWER; + } + if ( + registryLatestVersionPrerelease === null && + packageVersionPrerelease !== null + ) { + return PUBLISH_BECAUSE_LATEST_LOWER; + } + if ( + registryLatestVersionPrerelease !== null && + packageVersionPrerelease === null + ) { + return PUBLISH_BECAUSE_LATEST_LOWER; + } + const [registryReleaseTag, registryPrereleaseVersion] = + registryLatestVersionPrerelease; + const [packageReleaseTag, packagePreReleaseVersion] = + packageVersionPrerelease; + if (registryReleaseTag !== packageReleaseTag) { + return PUBLISH_BECAUSE_TAG_DIFFERS; + } + if (registryPrereleaseVersion === packagePreReleaseVersion) { + return NOTHING_BECAUSE_ALREADY_PUBLISHED; + } + if (registryPrereleaseVersion > packagePreReleaseVersion) { + return NOTHING_BECAUSE_LATEST_HIGHER; + } + return PUBLISH_BECAUSE_LATEST_LOWER; +}; diff --git a/packages/independent/package-publish/src/internal/publish.js b/packages/independent/package-publish/src/internal/publish.js new file mode 100644 index 0000000000..a8721a7cd2 --- /dev/null +++ b/packages/independent/package-publish/src/internal/publish.js @@ -0,0 +1,185 @@ +import { removeEntry } from "@jsenv/filesystem"; +import { createTaskLog } from "@jsenv/humanize"; +import { exec } from "node:child_process"; +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { setNpmConfig } from "./set_npm_config.js"; + +export const publish = async ({ + logger, + packageSlug, + logNpmPublishOutput, + rootDirectoryUrl, + registryUrl, + token, +}) => { + const publishTask = createTaskLog(`publish ${packageSlug} on ${registryUrl}`); + try { + // process.env.NODE_AUTH_TOKEN + const previousValue = process.env.NODE_AUTH_TOKEN; + const restoreProcessEnv = () => { + process.env.NODE_AUTH_TOKEN = previousValue; + }; + process.env.NODE_AUTH_TOKEN = token; + // updating package.json to publish on the correct registry + let restorePackageFile = () => {}; + const rootPackageFileUrl = new URL("./package.json", rootDirectoryUrl); + const rootPackageFileContent = readFileSync(rootPackageFileUrl); + const packageObject = JSON.parse(String(rootPackageFileContent)); + const { publishConfig } = packageObject; + const registerUrlFromPackage = publishConfig + ? publishConfig.registry || "https://registry.npmjs.org" + : "https://registry.npmjs.org"; + if (registryUrl !== registerUrlFromPackage) { + restorePackageFile = () => + writeFileSync(rootPackageFileUrl, rootPackageFileContent); + packageObject.publishConfig = packageObject.publishConfig || {}; + packageObject.publishConfig.registry = registryUrl; + writeFileSync( + rootPackageFileUrl, + JSON.stringify(packageObject, null, " "), + ); + } + // updating .npmrc to add the token + const npmConfigFileUrl = new URL("./.npmrc", rootDirectoryUrl); + let restoreNpmConfigFile; + let npmConfigFileContent; + try { + npmConfigFileContent = String(readFileSync(npmConfigFileUrl)); + restoreNpmConfigFile = () => + writeFileSync(npmConfigFileUrl, npmConfigFileContent); + } catch (e) { + if (e.code === "ENOENT") { + restoreNpmConfigFile = () => removeEntry(npmConfigFileUrl); + npmConfigFileContent = ""; + } else { + throw e; + } + } + writeFileSync( + npmConfigFileUrl, + setNpmConfig(npmConfigFileContent, { + [computeRegistryTokenKey(registryUrl)]: token, + [computeRegistryKey(packageObject.name)]: registryUrl, + }), + ); + try { + const reason = await new Promise((resolve, reject) => { + const command = exec( + "npm publish --no-workspaces", + { + cwd: fileURLToPath(rootDirectoryUrl), + stdio: "silent", + }, + (error) => { + if (error) { + // publish conflict generally occurs because servers + // returns 200 after npm publish + // but returns previous version if asked immediatly + // after for the last published version. + + // TODO: ideally we should catch 404 error returned from npm + // it happens it the token is not allowed to publish + // a repository. And when we detect this we display a more useful message + // suggesting the token rights are insufficient to publish the package + + // npm publish conclit + if (error.message.includes("EPUBLISHCONFLICT")) { + resolve({ + success: true, + reason: "already-published", + }); + } else if ( + error.message.includes("Cannot publish over existing version") + ) { + resolve({ + success: true, + reason: "already-published", + }); + } else if ( + error.message.includes( + "You cannot publish over the previously published versions", + ) + ) { + resolve({ + success: true, + reason: "already-published", + }); + } + // github publish conflict + else if ( + error.message.includes( + "ambiguous package version in package.json", + ) + ) { + resolve({ + success: true, + reason: "already-published", + }); + } else { + reject(error); + } + } else { + resolve({ + success: true, + reason: "published", + }); + } + }, + ); + if (logNpmPublishOutput) { + command.stdout.on("data", (data) => { + logger.debug(data); + }); + command.stderr.on("data", (data) => { + // debug because this output is part of + // the error message generated by a failing npm publish + logger.debug(data); + }); + } + }); + if (reason === "already-published") { + publishTask.setRightText(`(already published)`); + } + publishTask.done(); + return { + success: true, + reason, + }; + } finally { + restoreProcessEnv(); + restorePackageFile(); + restoreNpmConfigFile(); + } + } catch (e) { + publishTask.fail(); + console.error(e.stack); + return { + success: false, + reason: e, + }; + } +}; + +const computeRegistryTokenKey = (registryUrl) => { + if (registryUrl.startsWith("http://")) { + return `${registryUrl.slice("http:".length)}/:_authToken`; + } + if (registryUrl.startsWith("https://")) { + return `${registryUrl.slice("https:".length)}/:_authToken`; + } + if (registryUrl.startsWith("//")) { + return `${registryUrl}/:_authToken`; + } + throw new Error( + `registryUrl must start with http or https, got ${registryUrl}`, + ); +}; + +const computeRegistryKey = (packageName) => { + if (packageName[0] === "@") { + const packageScope = packageName.slice(0, packageName.indexOf("/")); + return `${packageScope}:registry`; + } + return `registry`; +}; diff --git a/packages/independent/package-publish/src/internal/read_project_package.js b/packages/independent/package-publish/src/internal/read_project_package.js new file mode 100644 index 0000000000..dc05409650 --- /dev/null +++ b/packages/independent/package-publish/src/internal/read_project_package.js @@ -0,0 +1,32 @@ +import { fileURLToPath } from "node:url"; +import { readFileSync } from "node:fs"; + +export const readProjectPackage = ({ rootDirectoryUrl }) => { + const packageFileUrlObject = new URL("./package.json", rootDirectoryUrl); + let packageInProject; + try { + const packageString = String(readFileSync(packageFileUrlObject)); + try { + packageInProject = JSON.parse(packageString); + } catch (e) { + if (e.name === "SyntaxError") { + throw new Error(`syntax error while parsing project package.json +--- syntax error stack --- +${e.stack} +--- package.json path --- +${fileURLToPath(packageFileUrlObject)}`); + } + throw e; + } + } catch (e) { + if (e.code === "ENOENT") { + throw new Error( + `cannot find project package.json +--- package.json path --- +${fileURLToPath(packageFileUrlObject)}`, + ); + } + throw e; + } + return packageInProject; +}; diff --git a/packages/independent/package-publish/src/internal/set_npm_config.js b/packages/independent/package-publish/src/internal/set_npm_config.js new file mode 100644 index 0000000000..74e9e67b0c --- /dev/null +++ b/packages/independent/package-publish/src/internal/set_npm_config.js @@ -0,0 +1,25 @@ +export const setNpmConfig = (configString, configObject) => { + return Object.keys(configObject).reduce((previous, key) => { + return setOrUpdateNpmConfig(previous, key, configObject[key]); + }, configString); +}; + +const setOrUpdateNpmConfig = (config, key, value) => { + const assignmentIndex = config.indexOf(`${key}=`); + if (assignmentIndex === -1) { + if (config === "") { + return `${key}=${value}`; + } + return `${config} +${key}=${value}`; + } + + const beforeAssignment = config.slice(0, assignmentIndex); + const nextLineIndex = config.indexOf("\n", assignmentIndex); + if (nextLineIndex === -1) { + return `${beforeAssignment}${key}=${value}`; + } + + const afterAssignment = config.slice(nextLineIndex); + return `${beforeAssignment}${key}=${value}${afterAssignment}`; +}; diff --git a/packages/independent/package-publish/src/main.js b/packages/independent/package-publish/src/main.js new file mode 100644 index 0000000000..b5323d4530 --- /dev/null +++ b/packages/independent/package-publish/src/main.js @@ -0,0 +1,8 @@ +/* + * This file is the entry point of this codebase + * - It is responsible to export the documented API + * - It should be kept simple (just re-export) to help reader to + * discover codebase progressively + */ + +export { publishPackage } from "./publish_package.js"; diff --git a/packages/independent/package-publish/src/publish_package.js b/packages/independent/package-publish/src/publish_package.js new file mode 100644 index 0000000000..78918077c4 --- /dev/null +++ b/packages/independent/package-publish/src/publish_package.js @@ -0,0 +1,182 @@ +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { createLogger } from "@jsenv/humanize"; +import { fetchLatestInRegistry } from "./internal/fetch_latest_in_registry.js"; +import { + needsPublish, + NOTHING_BECAUSE_ALREADY_PUBLISHED, + NOTHING_BECAUSE_LATEST_HIGHER, + PUBLISH_BECAUSE_LATEST_LOWER, + PUBLISH_BECAUSE_NEVER_PUBLISHED, + PUBLISH_BECAUSE_TAG_DIFFERS, +} from "./internal/needs_publish.js"; +import { publish } from "./internal/publish.js"; +import { readProjectPackage } from "./internal/read_project_package.js"; + +export const publishPackage = async ({ + logLevel, + rootDirectoryUrl, + registriesConfig, + logNpmPublishOutput = true, + updateProcessExitCode = true, +} = {}) => { + const logger = createLogger({ logLevel }); + logger.debug( + `publishPackage(${JSON.stringify( + { rootDirectoryUrl, logLevel, registriesConfig }, + null, + " ", + )})`, + ); + rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl); + assertRegistriesConfig(registriesConfig); + + logger.debug(`reading project package.json`); + const packageInProject = readProjectPackage({ rootDirectoryUrl }); + + const { name: packageName, version: packageVersion } = packageInProject; + logger.info(`${packageName}@${packageVersion} found in package.json`); + + const report = {}; + await Promise.all( + Object.keys(registriesConfig).map(async (registryUrl) => { + const registryReport = { + packageName, + packageVersion, + registryLatestVersion: undefined, + action: undefined, + actionReason: undefined, + actionResult: undefined, + }; + report[registryUrl] = registryReport; + + if (packageInProject.private) { + registryReport.action = "nothing"; + registryReport.actionReason = "package is private"; + return; + } + + logger.debug(`check latest version for ${packageName} in ${registryUrl}`); + const registryConfig = registriesConfig[registryUrl]; + + try { + const latestPackageInRegistry = await fetchLatestInRegistry({ + registryUrl, + packageName, + ...registryConfig, + }); + const registryLatestVersion = + latestPackageInRegistry === null + ? null + : latestPackageInRegistry.version; + registryReport.registryLatestVersion = registryLatestVersion; + + const needs = needsPublish({ packageVersion, registryLatestVersion }); + registryReport.action = + needs === PUBLISH_BECAUSE_NEVER_PUBLISHED || + needs === PUBLISH_BECAUSE_LATEST_LOWER || + needs === PUBLISH_BECAUSE_TAG_DIFFERS + ? "publish" + : "nothing"; + registryReport.actionReason = needs; + } catch (e) { + registryReport.action = "nothing"; + registryReport.actionReason = e; + if (updateProcessExitCode) { + process.exitCode = 1; + } + } + }), + ); + + // we have to publish in serie because we don't fully control + // npm publish, we have to enforce where the package gets published + await Object.keys(report).reduce(async (previous, registryUrl) => { + await previous; + + const registryReport = report[registryUrl]; + const { action, actionReason, registryLatestVersion } = registryReport; + + if (action === "nothing") { + if (actionReason === NOTHING_BECAUSE_ALREADY_PUBLISHED) { + logger.info( + `skip ${packageName}@${packageVersion} publish on ${registryUrl} because already published`, + ); + } else if (actionReason === NOTHING_BECAUSE_LATEST_HIGHER) { + logger.info( + `skip ${packageName}@${packageVersion} publish on ${registryUrl} because latest version is higher (${registryLatestVersion})`, + ); + } else if (actionReason === "package is private") { + logger.info( + `skip ${packageName}@${packageVersion} publish on ${registryUrl} because found private: true in package.json`, + ); + } else { + logger.error(`skip ${packageName}@${packageVersion} publish on ${registryUrl} due to error while fetching latest version. +--- error stack --- +${actionReason.stack}`); + } + + registryReport.actionResult = { success: true, reason: "nothing-to-do" }; + return; + } + + if (actionReason === PUBLISH_BECAUSE_NEVER_PUBLISHED) { + logger.info( + `publish ${packageName}@${packageVersion} on ${registryUrl} because it was never published`, + ); + } else if (actionReason === PUBLISH_BECAUSE_LATEST_LOWER) { + logger.info( + `publish ${packageName}@${packageVersion} on ${registryUrl} because latest version is lower (${registryLatestVersion})`, + ); + } else if (actionReason === PUBLISH_BECAUSE_TAG_DIFFERS) { + logger.info( + `publish ${packageName}@${packageVersion} on ${registryUrl} because latest tag differs (${registryLatestVersion})`, + ); + } + + const { success, reason } = await publish({ + logger, + packageSlug: `${packageName}@${packageVersion}`, + logNpmPublishOutput, + rootDirectoryUrl, + registryUrl, + ...registriesConfig[registryUrl], + }); + registryReport.actionResult = { success, reason }; + if (!success && updateProcessExitCode) { + process.exitCode = 1; + } + }, Promise.resolve()); + + return report; +}; + +const assertRegistriesConfig = (value) => { + if (typeof value !== "object" || value === null) { + throw new TypeError(`registriesConfig must be an object, got ${value}`); + } + + Object.keys(value).forEach((registryUrl) => { + const registryMapValue = value[registryUrl]; + if (typeof registryMapValue !== "object" || value === null) { + throw new TypeError( + `Unexpected value in registriesConfig for ${registryUrl}. It must be an object, got ${registryMapValue}`, + ); + } + + if ( + `token` in registryMapValue === false || + registryMapValue.token === "" + ) { + throw new TypeError( + `Missing token in registriesConfig for ${registryUrl}.`, + ); + } + + const { token } = registryMapValue; + if (typeof token !== "string") { + throw new TypeError( + `Unexpected token in registriesConfig for ${registryUrl}. It must be a string, got ${token}.`, + ); + } + }); +}; diff --git a/packages/independent/package-publish/tests/fetchLatestInRegistry.test.mjs b/packages/independent/package-publish/tests/fetchLatestInRegistry.test.mjs new file mode 100644 index 0000000000..71a715a3a7 --- /dev/null +++ b/packages/independent/package-publish/tests/fetchLatestInRegistry.test.mjs @@ -0,0 +1,24 @@ +import { assert } from "@jsenv/assert"; + +import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetchLatestInRegistry.js"; + +const actual = await fetchLatestInRegistry({ + registryUrl: "https://registry.npmjs.org", + packageName: "@jsenv/toto", +}); +const expect = null; +assert({ actual, expect }); + +// if (!process.env.CI) { +// const { resolveUrl } = await import("@jsenv/util") +// const { loadEnvFile } = await import("./testHelper.js") + +// await loadEnvFile(resolveUrl("../../secrets.json", import.meta.url)) +// const actual = await fetchLatestInRegistry({ +// registryUrl: "https://registry.npmjs.org", +// packageName: "@jsenv/perf-impact", +// token: process.env.NPM_TOKEN, +// }) +// const expect = null +// assert({ actual, expect }) +// } diff --git a/packages/independent/package-publish/tests/needsPublish.test.mjs b/packages/independent/package-publish/tests/needsPublish.test.mjs new file mode 100644 index 0000000000..b74ec2acd7 --- /dev/null +++ b/packages/independent/package-publish/tests/needsPublish.test.mjs @@ -0,0 +1,82 @@ +import { assert } from "@jsenv/assert"; + +import { + needsPublish, + PUBLISH_BECAUSE_NEVER_PUBLISHED, + PUBLISH_BECAUSE_LATEST_LOWER, + PUBLISH_BECAUSE_TAG_DIFFERS, + NOTHING_BECAUSE_LATEST_HIGHER, + NOTHING_BECAUSE_ALREADY_PUBLISHED, +} from "@jsenv/package-publish/src/internal/needsPublish.js"; + +{ + const actual = needsPublish({ + packageVersion: "1.0.0", + registryLatestVersion: null, + }); + const expect = PUBLISH_BECAUSE_NEVER_PUBLISHED; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0", + registryLatestVersion: "1.0.0", + }); + const expect = NOTHING_BECAUSE_ALREADY_PUBLISHED; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0", + registryLatestVersion: "2.0.0", + }); + const expect = NOTHING_BECAUSE_LATEST_HIGHER; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "2.0.0", + registryLatestVersion: "1.0.0", + }); + const expect = PUBLISH_BECAUSE_LATEST_LOWER; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0-beta.0", + registryLatestVersion: "1.0.0-alpha.0", + }); + const expect = PUBLISH_BECAUSE_TAG_DIFFERS; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0-alpha.0", + registryLatestVersion: "1.0.0-alpha.1", + }); + const expect = NOTHING_BECAUSE_LATEST_HIGHER; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0-alpha.1", + registryLatestVersion: "1.0.0-alpha.0", + }); + const expect = PUBLISH_BECAUSE_LATEST_LOWER; + assert({ actual, expect }); +} + +{ + const actual = needsPublish({ + packageVersion: "1.0.0-alpha.0", + registryLatestVersion: "1.0.0-alpha.0", + }); + const expect = NOTHING_BECAUSE_ALREADY_PUBLISHED; + assert({ actual, expect }); +} diff --git a/packages/independent/package-publish/tests/publishPackage.xtest.mjs b/packages/independent/package-publish/tests/publishPackage.xtest.mjs new file mode 100644 index 0000000000..23339795e9 --- /dev/null +++ b/packages/independent/package-publish/tests/publishPackage.xtest.mjs @@ -0,0 +1,133 @@ +/** + * If you just runned npm publish for 6.0.0 + * fetchLatestInRegistry might return 5.0.0 because + * npm is not done handling the package you just published + * + * It means this test can fail if runned once and an other time shortly after. + * on npm it should be handled by EPUBLISHCONFLICT + * but on github the error says ambiguous version in package.json + * + */ + +import { createRequire } from "node:module"; + +import { assert } from "@jsenv/assert"; +import { ensureEmptyDirectory, writeFile } from "@jsenv/filesystem"; + +import { publishPackage } from "@jsenv/package-publish"; +import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetchLatestInRegistry.js"; +import { loadEnvFile, assertProcessEnvShape } from "./testHelper.js"; + +const require = createRequire(import.meta.url); +const { inc: incrementVersion } = require("semver"); + +if (!process.env.CI) { + await loadEnvFile(new URL("../../../../secrets.json", import.meta.url).href); +} +assertProcessEnvShape({ + GITHUB_TOKEN: true, +}); + +const tempDirectoryUrl = new URL("./temp/", import.meta.url).href; +const packageName = "@jsenv/package-publish-test"; +const fetchLatestVersionOnGithub = async () => { + const { version } = await fetchLatestInRegistry({ + registryUrl: "https://npm.pkg.github.com", + packageName, + token: process.env.GITHUB_TOKEN, + }); + return version; +}; +let latestVersionOnGithub = await fetchLatestVersionOnGithub(); + +// try to publish existing version on github +{ + await ensureEmptyDirectory(tempDirectoryUrl); + const packageFileUrl = new URL("package.json", tempDirectoryUrl).href; + const packageVersion = latestVersionOnGithub; + await writeFile( + packageFileUrl, + JSON.stringify({ + name: packageName, + version: packageVersion, + repository: { + type: "git", + url: "https://github.com/jsenv/package-publish-test", + }, + publishConfig: { + access: "public", + }, + }), + ); + + const actual = await publishPackage({ + rootDirectoryUrl: tempDirectoryUrl, + logLevel: "debug", + registriesConfig: { + "https://npm.pkg.github.com": { + token: process.env.GITHUB_TOKEN, + }, + }, + }); + const expect = { + "https://npm.pkg.github.com": { + packageName, + packageVersion, + registryLatestVersion: latestVersionOnGithub, + action: "nothing", + actionReason: "already-published", + actionResult: { + success: true, + reason: "nothing-to-do", + }, + }, + }; + assert({ actual, expect }); +} + +// publish new minor on github +{ + await ensureEmptyDirectory(tempDirectoryUrl); + const packageFileUrl = new URL("package.json", tempDirectoryUrl).href; + const packageVersion = incrementVersion(latestVersionOnGithub, "patch"); + await writeFile( + packageFileUrl, + JSON.stringify({ + name: packageName, + version: packageVersion, + repository: { + type: "git", + url: "https://github.com/jsenv/package-publish-test", + }, + publishConfig: { + access: "public", + }, + }), + ); + + const actual = await publishPackage({ + logLevel: "debug", + logNpmPublishOutput: false, + rootDirectoryUrl: tempDirectoryUrl, + registriesConfig: { + "https://npm.pkg.github.com": { + token: process.env.GITHUB_TOKEN, + }, + }, + }); + const expect = { + "https://npm.pkg.github.com": { + packageName, + packageVersion, + registryLatestVersion: latestVersionOnGithub, + action: "publish", + actionReason: "latest-lower", + actionResult: { + success: true, + reason: actual["https://npm.pkg.github.com"].actionResult.reason, + }, + }, + }; + assert({ actual, expect }); + latestVersionOnGithub = packageVersion; +} diff --git a/packages/independent/package-publish/tests/self-test.manual-test.mjs b/packages/independent/package-publish/tests/self-test.manual-test.mjs new file mode 100644 index 0000000000..5aeb28832c --- /dev/null +++ b/packages/independent/package-publish/tests/self-test.manual-test.mjs @@ -0,0 +1,24 @@ +import { publishPackage } from "@jsenv/package-publish"; +import { loadEnvFile } from "./testHelper.js"; + +const run = async () => { + if (!process.env.CI) { + await loadEnvFile(new URL("../secrets.json", import.meta.url).href); + } + + const projectDirectoryUrl = new URL("../", import.meta.url).href; + + const report = await publishPackage({ + projectDirectoryUrl, + registriesConfig: { + "https://registry.npmjs.org": { + token: process.env.NPM_TOKEN, + }, + "https://npm.pkg.github.com": { + token: process.env.GITHUB_TOKEN, + }, + }, + }); + console.log(report); +}; +run(); diff --git a/packages/independent/package-publish/tests/setNpmConfig.test.mjs b/packages/independent/package-publish/tests/setNpmConfig.test.mjs new file mode 100644 index 0000000000..116e7b0ad1 --- /dev/null +++ b/packages/independent/package-publish/tests/setNpmConfig.test.mjs @@ -0,0 +1,46 @@ +import { assert } from "@jsenv/assert"; + +import { setNpmConfig } from "@jsenv/package-publish/src/internal/setNpmConfig.js"; + +{ + const actual = setNpmConfig("", { whatever: 42 }); + const expect = "whatever=42"; + assert({ actual, expect }); +} + +{ + const actual = setNpmConfig(`whatever=41`, { whatever: 42 }); + const expect = `whatever=42`; + assert({ actual, expect }); +} + +{ + const actual = setNpmConfig("foo=bar", { whatever: 42 }); + const expect = `foo=bar +whatever=42`; + assert({ actual, expect }); +} + +{ + const actual = setNpmConfig( + `foo=bar +whatever=41`, + { whatever: 42 }, + ); + const expect = `foo=bar +whatever=42`; + assert({ actual, expect }); +} + +{ + const actual = setNpmConfig( + `foo=bar +whatever=41 +ding=dong`, + { whatever: 42 }, + ); + const expect = `foo=bar +whatever=42 +ding=dong`; + assert({ actual, expect }); +} diff --git a/packages/independent/package-publish/tests/testHelper.js b/packages/independent/package-publish/tests/testHelper.js new file mode 100644 index 0000000000..85219d12f6 --- /dev/null +++ b/packages/independent/package-publish/tests/testHelper.js @@ -0,0 +1,43 @@ +export const loadEnvFile = async (url) => { + try { + const data = await import(url); + Object.assign(process.env, data.default); + return true; + } catch (e) { + if ( + e.code === "MODULE_NOT_FOUND" || + // ENOENT is what jsenv throw for dynamic import not found + // ideally it should be updated (in the case of node.js) + // to trigger module not found error as node 13.6 does + e.code === "ENOENT" + ) { + throw new Error(`missing env file at ${url}`); + } + + if (e.code === "ERR_UNKNOWN_FILE_EXTENSION" && url.endsWith(".json")) { + console.error(`cannot import ${url} because json is not supported. +enabled them with --experimental-json-modules flag. +documentation at https://nodejs.org/docs/latest-v13.x/api/esm.html#esm_experimental_json_modules`); + return false; + } + throw e; + } +}; + +export const assertProcessEnvShape = (shape) => { + Object.keys(shape).forEach((key) => { + const expectation = shape[key]; + if (expectation === true) { + if (key in process.env === false) { + throw new Error(`missing process.env.${key}`); + } + return; + } + if (expectation === false) { + if (key in process.env) { + throw new Error(`unexpected process.env.${key}`); + } + return; + } + }); +}; From 19a6f2ee0e10da68c2b0627e17eb9c385f4ecf22 Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 13 Aug 2024 14:50:48 +0200 Subject: [PATCH 03/15] upgrade stuff --- package.json | 14 +++++++------- .../monorepo/src/upgrade_external_versions.js | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 884c80a6e0..b86bcb2359 100644 --- a/package.json +++ b/package.json @@ -91,17 +91,17 @@ "@babel/plugin-syntax-import-attributes": "7.24.7", "@babel/plugin-syntax-optional-chaining-assign": "7.24.7", "@eslint/compat": "1.1.1", - "@jsenv/assert": "./packages/independent/assert/", - "@jsenv/cli": "./packages/related/cli/", + "@jsenv/assert": "workspace:*", + "@jsenv/cli": "workspace:*", "@jsenv/core": "./", - "@jsenv/eslint-config": "./packages/independent/eslint-config/", + "@jsenv/eslint-config": "workspace:*", "@jsenv/file-size-impact": "14.2.1", "@jsenv/https-local": "3.0.7", - "@jsenv/monorepo": "0.0.8", + "@jsenv/monorepo": "workspace:*", "@jsenv/performance-impact": "4.3.1", - "@jsenv/plugin-as-js-classic": "./packages/related/plugin-as-js-classic/", - "@jsenv/snapshot": "./packages/independent/snapshot/", - "@jsenv/test": "./packages/related/test/", + "@jsenv/plugin-as-js-classic": "workspace:*", + "@jsenv/snapshot": "workspace:*", + "@jsenv/test": "workspace:*", "@playwright/browser-chromium": "1.46.0", "@playwright/browser-firefox": "1.46.0", "@playwright/browser-webkit": "1.46.0", diff --git a/packages/independent/monorepo/src/upgrade_external_versions.js b/packages/independent/monorepo/src/upgrade_external_versions.js index fae47ccf1b..a2be4abe1f 100644 --- a/packages/independent/monorepo/src/upgrade_external_versions.js +++ b/packages/independent/monorepo/src/upgrade_external_versions.js @@ -9,8 +9,8 @@ * Be sure to check ../readme.md#upgrade-dependencies */ -import { UNICODE, createTaskLog } from "@jsenv/humanize"; -import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetchLatestInRegistry.js"; +import { createTaskLog, UNICODE } from "@jsenv/humanize"; +import { fetchLatestInRegistry } from "@jsenv/package-publish/src/internal/fetch_latest_in_registry.js"; import { collectWorkspacePackages } from "./internal/collect_workspace_packages.js"; import { compareTwoPackageVersions, From 4ceb05912fe0f7b9960888f34334421ff4aed88f Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 13 Aug 2024 14:57:44 +0200 Subject: [PATCH 04/15] migrate perf impact --- .../performance-impact/CHANGELOG.md | 3 + .../docs/pull_request_comment.png | Bin 0 -> 85208 bytes .../independent/performance-impact/license | 21 ++ .../performance-impact/package.json | 38 +++ .../independent/performance-impact/readme.md | 39 ++++ .../performance-impact/scripts/test.mjs | 12 + .../src/import_metric_from_files.js | 56 +++++ .../src/internal/assertions.js | 47 ++++ .../comment/create_perf_impact_comment.js | 128 ++++++++++ .../src/internal/comment/format_impact.js | 35 +++ .../comment/jsenv_comment_parameters.js | 9 + .../comment/jsenv_format_group_summary.js | 62 +++++ .../jsenv_format_performance_impact_cell.js | 29 +++ .../jsenv_is_performance_impact_big.js | 15 ++ .../comment/render_group_impact_table.js | 140 +++++++++++ .../src/internal/compute_metrics_median.js | 15 ++ .../src/internal/format_metric_value.js | 11 + .../src/internal/format_metrics_log.js | 9 + .../src/internal/format_ratio.js | 12 + .../src/internal/measure_multiple_times.js | 119 ++++++++++ .../performance-impact/src/internal/median.js | 28 +++ .../performance-impact/src/main.js | 4 + .../src/report_performance_impact.js | 80 +++++++ .../performance-impact/src/start_measures.js | 119 ++++++++++ .../comment_snapshot/comment_snapshot.md | 221 ++++++++++++++++++ .../comment_snapshot.test.mjs | 31 +++ .../generate_comment_snapshot_file.mjs | 132 +++++++++++ .../duration_formatting.test.mjs | 32 +++ .../fixtures/0_single_file/main.js | 0 .../import_perf/fixtures/1_two_import/a.js | 1 + .../import_perf/fixtures/1_two_import/b.js | 1 + .../import_perf/fixtures/1_two_import/main.js | 2 + .../fixtures/2_two_import_and_shared/a.js | 3 + .../fixtures/2_two_import_and_shared/b.js | 3 + .../fixtures/2_two_import_and_shared/main.js | 3 + .../2_two_import_and_shared/shared.js | 1 + .../tests/import_perf/import_perf.test.mjs | 35 +++ .../tests/median/median.test.mjs | 9 + .../generate_perf_report.mjs | 56 +++++ .../performance_report.test.mjs | 8 + .../tests/timeout_jsenv/file.mjs | 9 + .../timeout_jsenv/timeout_jsenv.test.mjs | 30 +++ 42 files changed, 1608 insertions(+) create mode 100644 packages/independent/performance-impact/CHANGELOG.md create mode 100644 packages/independent/performance-impact/docs/pull_request_comment.png create mode 100644 packages/independent/performance-impact/license create mode 100644 packages/independent/performance-impact/package.json create mode 100644 packages/independent/performance-impact/readme.md create mode 100644 packages/independent/performance-impact/scripts/test.mjs create mode 100644 packages/independent/performance-impact/src/import_metric_from_files.js create mode 100644 packages/independent/performance-impact/src/internal/assertions.js create mode 100644 packages/independent/performance-impact/src/internal/comment/create_perf_impact_comment.js create mode 100644 packages/independent/performance-impact/src/internal/comment/format_impact.js create mode 100644 packages/independent/performance-impact/src/internal/comment/jsenv_comment_parameters.js create mode 100644 packages/independent/performance-impact/src/internal/comment/jsenv_format_group_summary.js create mode 100644 packages/independent/performance-impact/src/internal/comment/jsenv_format_performance_impact_cell.js create mode 100644 packages/independent/performance-impact/src/internal/comment/jsenv_is_performance_impact_big.js create mode 100644 packages/independent/performance-impact/src/internal/comment/render_group_impact_table.js create mode 100644 packages/independent/performance-impact/src/internal/compute_metrics_median.js create mode 100644 packages/independent/performance-impact/src/internal/format_metric_value.js create mode 100644 packages/independent/performance-impact/src/internal/format_metrics_log.js create mode 100644 packages/independent/performance-impact/src/internal/format_ratio.js create mode 100644 packages/independent/performance-impact/src/internal/measure_multiple_times.js create mode 100644 packages/independent/performance-impact/src/internal/median.js create mode 100644 packages/independent/performance-impact/src/main.js create mode 100644 packages/independent/performance-impact/src/report_performance_impact.js create mode 100644 packages/independent/performance-impact/src/start_measures.js create mode 100644 packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.md create mode 100644 packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs create mode 100644 packages/independent/performance-impact/tests/comment_snapshot/generate_comment_snapshot_file.mjs create mode 100644 packages/independent/performance-impact/tests/duration_formatting/duration_formatting.test.mjs create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/0_single_file/main.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/a.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/b.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/main.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/a.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/b.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/main.js create mode 100644 packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/shared.js create mode 100644 packages/independent/performance-impact/tests/import_perf/import_perf.test.mjs create mode 100644 packages/independent/performance-impact/tests/median/median.test.mjs create mode 100644 packages/independent/performance-impact/tests/performance_report/generate_perf_report.mjs create mode 100644 packages/independent/performance-impact/tests/performance_report/performance_report.test.mjs create mode 100644 packages/independent/performance-impact/tests/timeout_jsenv/file.mjs create mode 100644 packages/independent/performance-impact/tests/timeout_jsenv/timeout_jsenv.test.mjs diff --git a/packages/independent/performance-impact/CHANGELOG.md b/packages/independent/performance-impact/CHANGELOG.md new file mode 100644 index 0000000000..84ca308f96 --- /dev/null +++ b/packages/independent/performance-impact/CHANGELOG.md @@ -0,0 +1,3 @@ +# 3.0.0 + +- Rename performanceReportPath into performanceReportUrl diff --git a/packages/independent/performance-impact/docs/pull_request_comment.png b/packages/independent/performance-impact/docs/pull_request_comment.png new file mode 100644 index 0000000000000000000000000000000000000000..1d172e1e0169447c6c88dcd01397e49a6f8d0f1e GIT binary patch literal 85208 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 z∾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ÔHw|fAwP(wj6gnTM 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

)OvsNPp1vYYD-fW+YCdHN|gD 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.13.0" + }, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "import": "./src/main.js" + }, + "./*": "./*" + }, + "main": "./src/main.js", + "files": [ + "/src/" + ], + "scripts": { + "snapshot": "node ./tests/comment_snapshot/generate_comment_snapshot_file.mjs", + "test": "node ./scripts/test.mjs" + }, + "dependencies": { + "@jsenv/dynamic-import-worker": "1.2.1", + "@jsenv/filesystem": "4.10.2", + "@jsenv/github-pull-request-impact": "1.7.7", + "@jsenv/humanize": "1.2.8" + } +} diff --git a/packages/independent/performance-impact/readme.md b/packages/independent/performance-impact/readme.md new file mode 100644 index 0000000000..927a470145 --- /dev/null +++ b/packages/independent/performance-impact/readme.md @@ -0,0 +1,39 @@ +# Performance impact [![npm package](https://img.shields.io/npm/v/@jsenv/performance-impact.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/performance-impact) + +`@jsenv/performance-impact` analyses a pull request impact on your performance metrics. This analysis is posted in a comment of the pull request on GitHub. + +- Helps you to catch big performance variations before merging a pull request +- Gives you the ability to measure performances on your machine during dev +- Can be added to any automated process (GitHub workflow, Jenkins, ...) + +Disclaimer: This tool should not be used to catch small performance variations because they are hard to distinguish from the natural variations of performance metrics (see [performance variability](#Performance-variability)). + +## Pull request comment + +_Screenshot of a pull request comment_ + +![stuff](./docs/pull_request_comment.png) + +## Performance variability + +Performance metrics will change due to inherent variability, **even if there hasn't been a code change**. +It can be mitigated by measuring performance multiple times. +But you should always keep in mind this variability before drawing conclusions about a performance-impacting change. + +With time you'll be capable to recognize unusual variation in your performance metrics. + +## How to catch small performance impacts? + +Catching (very) small performance impacts with confidence requires repetition and time. You need to: + +1. Let your code be used a lot of times in a lot of scenarios and see the results. This could be scripts, real users or both. + +2. And or push your performance metrics in a tool like Kibana or DataDog and check the tendency of your performance metrics. + +In any case it means you have to wait before knowing the real performance impact. + +## Recommended approach to catch performance impacts + +1. measure some performance metrics +2. Big variations can be anticipated and catched by `@jsenv/performance-impact` +3. For small variations, upload performance metrics to a dashboard. Then, periodically watch the dashboard to check performance metrics tendency over time diff --git a/packages/independent/performance-impact/scripts/test.mjs b/packages/independent/performance-impact/scripts/test.mjs new file mode 100644 index 0000000000..7e7ef2d738 --- /dev/null +++ b/packages/independent/performance-impact/scripts/test.mjs @@ -0,0 +1,12 @@ +import { executeTestPlan, nodeWorkerThread } from "@jsenv/test"; + +await executeTestPlan({ + rootDirectoryUrl: new URL("../", import.meta.url), + testPlan: { + "tests/**/*.test.mjs": { + node: { + runtime: nodeWorkerThread(), + }, + }, + }, +}); diff --git a/packages/independent/performance-impact/src/import_metric_from_files.js b/packages/independent/performance-impact/src/import_metric_from_files.js new file mode 100644 index 0000000000..3784909a7c --- /dev/null +++ b/packages/independent/performance-impact/src/import_metric_from_files.js @@ -0,0 +1,56 @@ +import { importOneExportFromFile } from "@jsenv/dynamic-import-worker"; +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { createLogger } from "@jsenv/humanize"; +import { assertMetrics } from "./internal/assertions.js"; +import { computeMetricsMedian } from "./internal/compute_metrics_median.js"; +import { formatMetricsLog } from "./internal/format_metrics_log.js"; +import { measureMultipleTimes } from "./internal/measure_multiple_times.js"; + +export const importMetricFromFiles = async ({ + logLevel, + directoryUrl, + metricsDescriptions, +}) => { + const logger = createLogger({ logLevel }); + + directoryUrl = assertAndNormalizeDirectoryUrl(directoryUrl); + + const allMetrics = {}; + await Object.keys(metricsDescriptions).reduce( + async (previous, metricName) => { + await previous; + + const metricsDescription = metricsDescriptions[metricName]; + const { + file, + env, + params, + iterations, + msToWaitBetweenEachIteration = 100, + } = metricsDescription; + const url = new URL(file, directoryUrl).href; + + const measure = async () => { + const metrics = await importOneExportFromFile(url, { env, params }); + assertMetrics(metrics, `in ${file}`); + return metrics; + }; + + const metricsWithIterations = await measureMultipleTimes( + measure, + iterations, + { + msToWaitBetweenEachIteration, + }, + ); + const metrics = computeMetricsMedian(metricsWithIterations); + + logger.info(formatMetricsLog(metrics)); + + allMetrics[metricName] = metrics; + }, + Promise.resolve(), + ); + + return allMetrics; +}; diff --git a/packages/independent/performance-impact/src/internal/assertions.js b/packages/independent/performance-impact/src/internal/assertions.js new file mode 100644 index 0000000000..3625c00aaa --- /dev/null +++ b/packages/independent/performance-impact/src/internal/assertions.js @@ -0,0 +1,47 @@ +import { createDetailedMessage } from "@jsenv/humanize"; + +export const assertPerformanceReport = (performanceReport) => { + if (typeof performanceReport !== "object" || performanceReport === null) { + throw new TypeError( + `performanceReport must be an object, received ${performanceReport}.`, + ); + } + + Object.keys(performanceReport).forEach((groupName) => { + const metrics = performanceReport[groupName]; + assertMetrics(metrics); + }); +}; + +export const assertMetrics = (metrics, metricsTrace) => { + if (typeof metrics !== "object" || metrics === null) { + throw new TypeError( + createDetailedMessage(`metrics must be an object, got ${metrics}`, { + "metrics trace": metricsTrace, + }), + ); + } + + Object.keys(metrics).forEach((metricName) => { + const metric = metrics[metricName]; + if (typeof metric !== "object") { + throw new TypeError( + `metric must be an object, got ${metric} for ${metricName}`, + ); + } + + const { value } = metric; + if (typeof value !== "number") { + throw new TypeError( + `metric value must be a number, got ${value} for ${metricName}`, + ); + } + + const { unit } = metric; + if (unit !== undefined && unit !== "ms" && unit !== "byte") { + throw new TypeError( + `metric type must be undefined, "ms", or "byte", got ${unit} for ${metricName}`, + ); + } + }); +}; diff --git a/packages/independent/performance-impact/src/internal/comment/create_perf_impact_comment.js b/packages/independent/performance-impact/src/internal/comment/create_perf_impact_comment.js new file mode 100644 index 0000000000..950ae0bdaa --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/create_perf_impact_comment.js @@ -0,0 +1,128 @@ +import { renderGroupImpactTable } from "./render_group_impact_table.js"; + +export const createPerfImpactComment = ({ + pullRequestBase, + pullRequestHead, + beforeMergeData, + afterMergeData, + isPerformanceImpactBig, + formatGroupSummary, + formatPerformanceImpactCell, +}) => { + const warnings = []; + + // here we have are sure beforeMergeData and afterMergeData are valid + // because we assert their shape is correct in reportPerformanceImpact.js#collectInfo() + // for metrics here before merge but are no longer in afetr merge, we will ignore them + // for new metrics we'll show them without doing the diff + + const beforeMergeGroups = beforeMergeData; + const afterMergeGroups = afterMergeData; + + const groupNames = Object.keys(afterMergeGroups); + const metricCount = groupNames.reduce((previous, groupName) => { + const afterMergeMetrics = afterMergeGroups[groupName]; + return previous + Object.keys(afterMergeMetrics).length; + }, 0); + + if (metricCount === 0) { + const body = `

Performance impact

+ +

No impact to compute when merging ${pullRequestHead} into ${pullRequestBase}: there is no performance metric.

`; + + return { warnings, body }; + } + + const groups = []; + groupNames.forEach((groupName) => { + const afterMergeMetrics = afterMergeGroups[groupName]; + const afterMergeMetricCount = Object.keys(afterMergeMetrics).length; + if (afterMergeMetricCount === 0) { + // skip empty groups + return; + } + + const beforeMergeMetrics = beforeMergeGroups[groupName]; + const bigImpacts = beforeMergeMetrics + ? getBigImpacts({ + afterMergeMetrics, + beforeMergeMetrics, + isPerformanceImpactBig, + }) + : {}; + + groups.push( + renderDetails({ + summary: formatGroupSummary({ + groupName, + beforeMergeMetrics, + afterMergeMetrics, + }), + content: renderGroupImpactTable({ + formatPerformanceImpactCell, + groupName, + beforeMergeMetrics, + afterMergeMetrics, + bigImpacts, + }), + }), + ); + }); + + const body = `

Performance impact

+ +

Impact on ${metricCount} metrics when merging ${pullRequestHead} into ${pullRequestBase}. Before drawing conclusion, keep in mind performance variability.

+ +${groups.join(` + +`)}`; + + return { warnings, body }; +}; + +const getBigImpacts = ({ + afterMergeMetrics, + beforeMergeMetrics, + isPerformanceImpactBig, +}) => { + const bigImpacts = {}; + + Object.keys(afterMergeMetrics).forEach((metricName) => { + const metricBeforeMerge = beforeMergeMetrics[metricName]; + if (metricBeforeMerge === undefined) { + return; + } + + const metricAfterMerge = afterMergeMetrics[metricName]; + const metricUnit = metricAfterMerge.unit; + const metricValueAfterMerge = metricAfterMerge.value; + const metricValueBeforeMerge = metricBeforeMerge.value; + const metricValueDelta = metricValueAfterMerge - metricValueBeforeMerge; + if ( + !isPerformanceImpactBig({ + metricName, + metricUnit, + metricValueAfterMerge, + metricValueBeforeMerge, + metricValueDelta, + }) + ) { + return; + } + + bigImpacts[metricName] = { + metricValueAfterMerge, + metricValueBeforeMerge, + metricValueDelta, + }; + }); + + return bigImpacts; +}; + +const renderDetails = ({ summary, content, opened = false, indent = 0 }) => { + return `${" ".repeat(indent)} +${" ".repeat(indent + 2)}${summary} +${" ".repeat(indent + 2)}${content} +${" ".repeat(indent)}`; +}; diff --git a/packages/independent/performance-impact/src/internal/comment/format_impact.js b/packages/independent/performance-impact/src/internal/comment/format_impact.js new file mode 100644 index 0000000000..66d53d6d8f --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/format_impact.js @@ -0,0 +1,35 @@ +import { formatMetricValue } from "../format_metric_value.js"; +import { formatRatioAsPercentage } from "../format_ratio.js"; + +export const formatImpact = ({ + metricValueBeforeMerge, + metricValueAfterMerge, + metricUnit, +}) => { + const diff = metricValueAfterMerge - metricValueBeforeMerge; + + if (diff === 0) { + return ``; + } + + const diffFormatted = `${diff < 0 ? "-" : "+"}${formatMetricValue({ + value: Math.abs(diff), + unit: metricUnit, + })}`; + + return diffFormatted; +}; + +export const formatImpactAsPercentage = ({ + metricValueBeforeMerge, + metricValueAfterMerge, +}) => { + const diff = metricValueAfterMerge - metricValueBeforeMerge; + const diffRatio = + metricValueBeforeMerge === 0 + ? 1 + : metricValueAfterMerge === 0 + ? -1 + : diff / metricValueBeforeMerge; + return formatRatioAsPercentage(diffRatio); +}; diff --git a/packages/independent/performance-impact/src/internal/comment/jsenv_comment_parameters.js b/packages/independent/performance-impact/src/internal/comment/jsenv_comment_parameters.js new file mode 100644 index 0000000000..036d1abf1d --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/jsenv_comment_parameters.js @@ -0,0 +1,9 @@ +import { jsenvFormatGroupSummary } from "./jsenv_format_group_summary.js"; +import { jsenvFormatPerformanceImpactCell } from "./jsenv_format_performance_impact_cell.js"; +import { jsenvIsPerformanceImpactBig } from "./jsenv_is_performance_impact_big.js"; + +export const jsenvCommentParameters = { + isPerformanceImpactBig: jsenvIsPerformanceImpactBig, + formatGroupSummary: jsenvFormatGroupSummary, + formatPerformanceImpactCell: jsenvFormatPerformanceImpactCell, +}; diff --git a/packages/independent/performance-impact/src/internal/comment/jsenv_format_group_summary.js b/packages/independent/performance-impact/src/internal/comment/jsenv_format_group_summary.js new file mode 100644 index 0000000000..32f23ab884 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/jsenv_format_group_summary.js @@ -0,0 +1,62 @@ +import { formatRatioAsPercentage } from "../format_ratio.js"; + +export const jsenvFormatGroupSummary = ({ + groupName, + beforeMergeMetrics, + afterMergeMetrics, +}) => { + if (!beforeMergeMetrics) { + return `${groupName} (new)`; + } + + const allRatios = getAllImpactRatios({ + beforeMergeMetrics, + afterMergeMetrics, + }); + const ratiosCount = allRatios.length; + if (ratiosCount === 0) { + return `${groupName} (no impact)`; + } + + const ratioSum = allRatios.reduce((previous, ratio) => previous + ratio, 0); + if (ratioSum === 0) { + return `${groupName} (no impact)`; + } + + return `${groupName} (${formatRatioAsPercentage(ratioSum / ratiosCount)})`; +}; + +const getAllImpactRatios = ({ afterMergeMetrics, beforeMergeMetrics }) => { + let allRatios = []; + Object.keys(afterMergeMetrics).forEach((metricName) => { + const metricBeforeMerge = beforeMergeMetrics[metricName]; + if (!metricBeforeMerge) { + // it's new, let's ignore + return; + } + const metricAfterMerge = afterMergeMetrics[metricName]; + const metricValueBeforeMerge = metricBeforeMerge.value; + const metricValueAfterMerge = metricAfterMerge.value; + const metricDiff = metricValueAfterMerge - metricValueBeforeMerge; + if ( + metricDiff === 0 && + metricValueAfterMerge === 0 && + metricValueBeforeMerge === 0 + ) { + // there is no impact on a metric that is 0 + // we can ignore this metric + return; + } + + const metricDiffRatio = + metricDiff === 0 + ? 0 + : metricValueBeforeMerge === 0 + ? 1 + : metricValueAfterMerge === 0 + ? -1 + : metricDiff / metricValueBeforeMerge; + allRatios.push(metricDiffRatio); + }); + return allRatios; +}; diff --git a/packages/independent/performance-impact/src/internal/comment/jsenv_format_performance_impact_cell.js b/packages/independent/performance-impact/src/internal/comment/jsenv_format_performance_impact_cell.js new file mode 100644 index 0000000000..d6476ba2f5 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/jsenv_format_performance_impact_cell.js @@ -0,0 +1,29 @@ +import { formatImpact, formatImpactAsPercentage } from "./format_impact.js"; + +export const jsenvFormatPerformanceImpactCell = ({ + metricUnit, + metricValueAfterMerge, + metricValueBeforeMerge, +}) => { + // metric is new + if (metricValueBeforeMerge === undefined) { + return ``; + } + + const diff = metricValueAfterMerge - metricValueBeforeMerge; + if (diff === 0) { + return ``; + } + + const diffFormatted = formatImpact({ + metricUnit, + metricValueAfterMerge, + metricValueBeforeMerge, + }); + const diffAsPercentageFormatted = formatImpactAsPercentage({ + metricUnit, + metricValueBeforeMerge, + metricValueAfterMerge, + }); + return `${diffFormatted} / ${diffAsPercentageFormatted}`; +}; diff --git a/packages/independent/performance-impact/src/internal/comment/jsenv_is_performance_impact_big.js b/packages/independent/performance-impact/src/internal/comment/jsenv_is_performance_impact_big.js new file mode 100644 index 0000000000..6803c92dae --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/jsenv_is_performance_impact_big.js @@ -0,0 +1,15 @@ +export const jsenvIsPerformanceImpactBig = ({ + metricValueBeforeMerge, + metricValueDelta, +}) => { + const absoluteDelta = Math.abs(metricValueDelta); + const absoluteDeltaAsRatio = + absoluteDelta === 0 ? 0 : absoluteDelta / metricValueBeforeMerge; + const absoluteDeltaAsPercentage = absoluteDeltaAsRatio * 100; + + // absolute diff as percentage is below 5% -> not big + if (absoluteDeltaAsPercentage < 5) { + return false; + } + return true; +}; diff --git a/packages/independent/performance-impact/src/internal/comment/render_group_impact_table.js b/packages/independent/performance-impact/src/internal/comment/render_group_impact_table.js new file mode 100644 index 0000000000..331730c2ea --- /dev/null +++ b/packages/independent/performance-impact/src/internal/comment/render_group_impact_table.js @@ -0,0 +1,140 @@ +import { formatMetricValue } from "../format_metric_value.js"; + +const METRIC_NAME_MAX_LENGTH = 50; +const MAX_METRIC_PER_GROUP = 20; + +export const renderGroupImpactTable = ({ + formatPerformanceImpactCell, + // groupName, + beforeMergeMetrics, + afterMergeMetrics, + bigImpacts, +}) => { + return ` + + + + + + + + + ${renderPerfImpactTableBody({ + formatPerformanceImpactCell, + beforeMergeMetrics, + afterMergeMetrics, + bigImpacts, + })} + +
MetricBefore mergeAfter mergeImpact
`; +}; + +const renderPerfImpactTableBody = ({ + formatPerformanceImpactCell, + beforeMergeMetrics, + afterMergeMetrics, + bigImpacts, +}) => { + const lines = []; + const metricAllNames = Object.keys(afterMergeMetrics); + const metricCount = metricAllNames.length; + const metricNames = + metricCount > MAX_METRIC_PER_GROUP + ? metricAllNames.slice(0, MAX_METRIC_PER_GROUP) + : metricAllNames; + + metricNames.forEach((metricName) => { + const metricNameDisplayed = truncateMetricName(metricName); + const metric = afterMergeMetrics[metricName]; + const metricValueAfterMerge = metric.value; + const metricBeforeMerge = beforeMergeMetrics + ? beforeMergeMetrics[metricName] + : undefined; + if (!metricBeforeMerge) { + lines.push([ + `${metricNameDisplayed}`, + ``, + `${formatMetricValue(metric)}`, + `${formatPerformanceImpactCell({ + metricUnit: metric.unit, + metricValueAfterMerge, + metricValueBeforeMerge: undefined, + isBig: false, + })}`, + `:baby:`, + ]); + return; + } + + const isBig = Boolean(bigImpacts[metricName]); + const metricValueBeforeMerge = metricBeforeMerge.value; + lines.push([ + `${metricNameDisplayed}`, + `${formatMetricValue(metricBeforeMerge)}`, + `${formatMetricValue(metric)}`, + `${formatPerformanceImpactCell({ + metricUnit: metric.unit, + metricValueAfterMerge, + metricValueBeforeMerge, + isBig, + })}`, + `${renderEmojiCellContent({ + metricValueAfterMerge, + metricValueBeforeMerge, + })}`, + ]); + }); + if (metricNames !== metricAllNames) { + lines.push([ + `... ${ + metricCount - MAX_METRIC_PER_GROUP + } more ...`, + ]); + } + + return renderTableLines(lines); +}; + +const renderEmojiCellContent = ({ + metricValueAfterMerge, + metricValueBeforeMerge, +}) => { + const delta = metricValueAfterMerge - metricValueBeforeMerge; + if (delta === 0) { + return ":ghost:"; + } + + if (delta > 0) { + return ":arrow_upper_right:"; + } + + return ":arrow_lower_right:"; +}; + +const truncateMetricName = (metricName) => { + const length = metricName.length; + if (length > METRIC_NAME_MAX_LENGTH) { + return `${metricName.slice(0, METRIC_NAME_MAX_LENGTH - `…`.length)}…`; + } + return metricName; +}; + +const renderTableLines = (lines, { indentCount = 3, indentSize = 2 } = {}) => { + if (lines.length === 0) { + return ""; + } + + const cellLeftSpacing = indent(indentCount + 1, indentSize); + const lineLeftSpacing = indent(indentCount, indentSize); + + return `${lines.map( + (cells) => ` +${cellLeftSpacing}${cells.join(` +${cellLeftSpacing}`)}`, + ).join(` +${lineLeftSpacing} +${lineLeftSpacing}`)} +${lineLeftSpacing}`; +}; + +const indent = (count, size) => ` `.repeat(size * count); diff --git a/packages/independent/performance-impact/src/internal/compute_metrics_median.js b/packages/independent/performance-impact/src/internal/compute_metrics_median.js new file mode 100644 index 0000000000..7fc9e359cd --- /dev/null +++ b/packages/independent/performance-impact/src/internal/compute_metrics_median.js @@ -0,0 +1,15 @@ +import { median } from "./median.js"; + +export const computeMetricsMedian = (metrics) => { + const metricsMedian = {}; + + Object.keys(metrics).forEach((metricName) => { + const metricWithIteration = metrics[metricName]; + metricsMedian[metricName] = { + value: median(metricWithIteration.values), + unit: metricWithIteration.unit, + }; + }); + + return metricsMedian; +}; diff --git a/packages/independent/performance-impact/src/internal/format_metric_value.js b/packages/independent/performance-impact/src/internal/format_metric_value.js new file mode 100644 index 0000000000..c339075254 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/format_metric_value.js @@ -0,0 +1,11 @@ +import { humanizeFileSize, humanizeDuration } from "@jsenv/humanize"; + +export const formatMetricValue = ({ value, unit }) => { + return formatters[unit](value); +}; + +const formatters = { + ms: humanizeDuration, + byte: humanizeFileSize, + undefined: (value) => value, +}; diff --git a/packages/independent/performance-impact/src/internal/format_metrics_log.js b/packages/independent/performance-impact/src/internal/format_metrics_log.js new file mode 100644 index 0000000000..a169852923 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/format_metrics_log.js @@ -0,0 +1,9 @@ +import { formatMetricValue } from "./format_metric_value.js"; + +export const formatMetricsLog = (metrics) => { + const metricsReadable = {}; + Object.keys(metrics).forEach((metricName) => { + metricsReadable[metricName] = formatMetricValue(metrics[metricName]); + }); + return JSON.stringify(metricsReadable, null, " "); +}; diff --git a/packages/independent/performance-impact/src/internal/format_ratio.js b/packages/independent/performance-impact/src/internal/format_ratio.js new file mode 100644 index 0000000000..ef6732d450 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/format_ratio.js @@ -0,0 +1,12 @@ +import { setRoundedPrecision } from "@jsenv/humanize/src/utils/decimals.js"; + +export const formatRatioAsPercentage = (ratio) => { + const percentage = ratio * 100; + return `${percentage < 0 ? `-` : "+"}${setRoundedPrecision( + Math.abs(percentage), + { + decimals: 0, + decimalsWhenSmall: 1, + }, + )}%`; +}; diff --git a/packages/independent/performance-impact/src/internal/measure_multiple_times.js b/packages/independent/performance-impact/src/internal/measure_multiple_times.js new file mode 100644 index 0000000000..a63e53c6fd --- /dev/null +++ b/packages/independent/performance-impact/src/internal/measure_multiple_times.js @@ -0,0 +1,119 @@ +import { createDetailedMessage } from "@jsenv/humanize"; +import { assertMetrics } from "./assertions.js"; + +export const measureMultipleTimes = async ( + measure, + iterationCount = 5, + { msToWaitBetweenEachIteration = 0 } = {}, +) => { + if (typeof measure !== "function") { + throw new TypeError(`measure must be a function, received ${measure}`); + } + if (typeof iterationCount !== "number") { + throw new TypeError( + `iterationCount must be a number, received ${iterationCount}`, + ); + } + + const firstIterationMetrics = await measure(); + assertMetrics(firstIterationMetrics); + const firstIterationMetricNames = Object.keys(firstIterationMetrics); + if (firstIterationMetricNames.length === 0) { + throw new Error( + `measure must return a non empty object, received an object without key`, + ); + } + + const metricsWithIteration = {}; + firstIterationMetricNames.forEach((metricName) => { + const metric = firstIterationMetrics[metricName]; + metricsWithIteration[metricName] = { + values: [metric.value], + unit: metric.unit, + }; + }); + + if (iterationCount === 1) { + return metricsWithIteration; + } + + const iterationArray = new Array(iterationCount - 1).fill(); + await iterationArray.reduce(async (previous, _, index) => { + await previous; + + const currentMetrics = await measure(); + assertMetrics(currentMetrics); + const currentIterationMetricNames = Object.keys(currentMetrics); + const missingMetricNamesInCurrentIteration = + firstIterationMetricNames.filter( + (firstIterationMetricName) => + !currentIterationMetricNames.includes(firstIterationMetricName), + ); + const extraMetricNamesInCurrentIteration = + currentIterationMetricNames.filter( + (currentIterationMetricName) => + !firstIterationMetricNames.includes(currentIterationMetricName), + ); + if ( + missingMetricNamesInCurrentIteration.length || + extraMetricNamesInCurrentIteration.length + ) { + throw new Error( + createVariableMetricNamesErrorMessage({ + missingMetricNamesInCurrentIteration, + extraMetricNamesInCurrentIteration, + iterationIndex: index, + }), + ); + } + + firstIterationMetricNames.forEach((metricName) => { + const metricWithIteration = metricsWithIteration[metricName]; + const currentMetric = currentMetrics[metricName]; + if (currentMetric.unit !== metricWithIteration.unit) { + throw new Error( + createDetailedMessage( + `A metric unit has changed between iterations.`, + { + "metric unit on first iteration": metricWithIteration.unit, + [`metric unit on iteration ${index + 1}`]: currentMetric.unit, + "metric name": metricName, + }, + ), + ); + } + metricWithIteration.values.push(currentMetric.value); + }); + + // await a little bit to let previous execution the time to potentially clean up things + await new Promise((resolve) => { + setTimeout(resolve, msToWaitBetweenEachIteration); + }); + }, Promise.resolve()); + + return metricsWithIteration; +}; + +const createVariableMetricNamesErrorMessage = ({ + missingMetricNamesInCurrentIteration, + extraMetricNamesInCurrentIteration, + iterationIndex, +}) => { + return createDetailedMessage( + `Measure must return the same metric names when runned multiple times, on call number ${ + iterationIndex + 2 + }, metric names are different.`, + { + ...(missingMetricNamesInCurrentIteration.length + ? { + "missing metric names": missingMetricNamesInCurrentIteration, + } + : {}), + ...(extraMetricNamesInCurrentIteration.length + ? { + "extra metric names": extraMetricNamesInCurrentIteration, + } + : {}), + }, + ); +}; diff --git a/packages/independent/performance-impact/src/internal/median.js b/packages/independent/performance-impact/src/internal/median.js new file mode 100644 index 0000000000..d4b086a166 --- /dev/null +++ b/packages/independent/performance-impact/src/internal/median.js @@ -0,0 +1,28 @@ +/** + * + * https://jonlabelle.com/snippets/view/javascript/calculate-mean-median-mode-and-range-in-javascript + * https://www.sitepoint.com/measuring-javascript-functions-performance/#pitfall-3-relying-too-much-on-the-average + * + * median of [3, 5, 4, 4, 1, 1, 2, 3] = 3 + * + */ + +export const median = (numbers) => { + const numberCount = numbers.length; + const numbersSorted = numbers.slice(); + numbersSorted.sort(); + + const isEven = numberCount % 2 === 0; + if (isEven) { + const rightMiddleNumberIndex = numberCount / 2; + const leftMiddleNumberIndex = rightMiddleNumberIndex - 1; + const leftMiddleNumber = numbersSorted[leftMiddleNumberIndex]; + const rightMiddleNumber = numbersSorted[rightMiddleNumberIndex]; + const medianNumber = (leftMiddleNumber + rightMiddleNumber) / 2; + return medianNumber; + } + + const medianNumberIndex = (numberCount - 1) / 2; + const medianNumber = numbersSorted[medianNumberIndex]; + return medianNumber; +}; diff --git a/packages/independent/performance-impact/src/main.js b/packages/independent/performance-impact/src/main.js new file mode 100644 index 0000000000..ad59a4d117 --- /dev/null +++ b/packages/independent/performance-impact/src/main.js @@ -0,0 +1,4 @@ +export { readGitHubWorkflowEnv } from "@jsenv/github-pull-request-impact"; +export { importMetricFromFiles } from "./import_metric_from_files.js"; +export { reportPerformanceImpact } from "./report_performance_impact.js"; +export { startMeasures } from "./start_measures.js"; diff --git a/packages/independent/performance-impact/src/report_performance_impact.js b/packages/independent/performance-impact/src/report_performance_impact.js new file mode 100644 index 0000000000..30581e1b09 --- /dev/null +++ b/packages/independent/performance-impact/src/report_performance_impact.js @@ -0,0 +1,80 @@ +import { importOneExportFromFile } from "@jsenv/dynamic-import-worker"; +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { commentGitHubPullRequestImpact } from "@jsenv/github-pull-request-impact"; +import { assertPerformanceReport } from "./internal/assertions.js"; +import { createPerfImpactComment } from "./internal/comment/create_perf_impact_comment.js"; +import { jsenvCommentParameters } from "./internal/comment/jsenv_comment_parameters.js"; + +export const reportPerformanceImpact = async ({ + logLevel, + commandLogs, + cancelOnSIGINT, + rootDirectoryUrl, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + + installCommand = "npm install", + performanceReportUrl, + isPerformanceImpactBig = jsenvCommentParameters.isPerformanceImpactBig, + formatGroupSummary = jsenvCommentParameters.formatGroupSummary, + formatPerformanceImpactCell = jsenvCommentParameters.formatPerformanceImpactCell, + + runLink, + commitInGeneratedByInfo, +}) => { + rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl); + if (performanceReportUrl === "string") { + performanceReportUrl = new URL(performanceReportUrl, rootDirectoryUrl).href; + } else if (performanceReportUrl instanceof URL) { + } else { + throw new TypeError( + `performanceReportUrl must be a string or an url but received ${performanceReportUrl}`, + ); + } + + return commentGitHubPullRequestImpact({ + logLevel, + commandLogs, + cancelOnSIGINT, + rootDirectoryUrl, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + + collectInfo: async ({ execCommandInRootDirectory }) => { + await execCommandInRootDirectory(installCommand); + const performanceReport = + await importOneExportFromFile(performanceReportUrl); + assertPerformanceReport(performanceReport); + return { version: 1, data: performanceReport }; + }, + commentIdentifier: ``, + createCommentForComparison: ({ + pullRequestBase, + pullRequestHead, + beforeMergeData, + afterMergeData, + }) => { + return createPerfImpactComment({ + pullRequestBase, + pullRequestHead, + beforeMergeData, + afterMergeData, + isPerformanceImpactBig, + formatGroupSummary, + formatPerformanceImpactCell, + }); + }, + generatedByLink: { + url: "https://github.com/jsenv/workflow/tree/main/packages/performance-impact", + text: "@jsenv/performance-impact", + }, + runLink, + commitInGeneratedByInfo, + }); +}; diff --git a/packages/independent/performance-impact/src/start_measures.js b/packages/independent/performance-impact/src/start_measures.js new file mode 100644 index 0000000000..e99bb65256 --- /dev/null +++ b/packages/independent/performance-impact/src/start_measures.js @@ -0,0 +1,119 @@ +import { emitWarning, memoryUsage, resourceUsage } from "node:process"; +import v8 from "node:v8"; +import { runInNewContext } from "node:vm"; + +export const startMeasures = ({ + duration = true, // will be in milliseconds + memoryHeap = false, + filesystem = false, + gc = false, +} = {}) => { + if (gc && !global.gc) { + v8.setFlagsFromString("--expose_gc"); + global.gc = runInNewContext("gc"); + emitWarning(`node must be started with --expose-gc`, { + CODE: "EXPOSE_GC_IS_MISSING", + detail: "global.gc forced with v8.setFlagsFromString", + }); + } + + const measures = []; + if (duration) { + const beforeMs = Date.now(); + measures.push(() => { + const afterMs = Date.now(); + const duration = afterMs - beforeMs; + return { + duration, + }; + }); + } + if (filesystem) { + const _internalFs = process.binding("fs"); + const restoreCallbackSet = new Set(); + let fsOpenCall = 0; + let fsStatCall = 0; + const methodEffects = { + open: () => { + fsOpenCall++; + }, + openFileHandle: () => { + fsOpenCall++; + }, + stat: () => { + fsStatCall++; + }, + lstat: () => { + fsStatCall++; + }, + access: () => { + fsStatCall++; + }, + }; + for (const method of Object.keys(methodEffects)) { + const previous = _internalFs[method]; + if (typeof previous !== "function") { + continue; + } + _internalFs[method] = function (...args) { + methodEffects[method](...args); + return previous.call(this, ...args); + }; + restoreCallbackSet.add(() => { + _internalFs[method] = previous; + }); + } + const beforeRessourceUsage = resourceUsage(); + measures.push(() => { + for (const restoreCallback of restoreCallbackSet) { + restoreCallback(); + } + const afterRessourceUsage = resourceUsage(); + const fsRead = afterRessourceUsage.fsRead - beforeRessourceUsage.fsRead; + const fsWrite = + afterRessourceUsage.fsWrite - beforeRessourceUsage.fsWrite; + return { + fsOpenCall, + fsStatCall, + fsRead, + fsWrite, + }; + }); + } + + if (memoryHeap) { + if (gc) { + global.gc(); + } + const beforeMemoryUsage = memoryUsage(); + measures.push(() => { + if (gc) { + global.gc(); + } + const afterMemoryUsage = memoryUsage(); + return { + // total means "allocated" + memoryHeapTotal: + afterMemoryUsage.heapTotal - beforeMemoryUsage.heapTotal, + memoryHeapUsed: afterMemoryUsage.heapUsed - beforeMemoryUsage.heapUsed, + external: afterMemoryUsage.external - beforeMemoryUsage.external, + rss: afterMemoryUsage.rss - beforeMemoryUsage.rss, + arrayBuffers: + afterMemoryUsage.arrayBuffers - beforeMemoryUsage.arrayBuffers, + }; + }); + } + + const stop = () => { + let metrics = {}; + measures.forEach((measure) => { + metrics = { + ...metrics, + ...measure(), + }; + }); + return metrics; + }; + + return { stop }; +}; diff --git a/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.md b/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.md new file mode 100644 index 0000000000..623a460736 --- /dev/null +++ b/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.md @@ -0,0 +1,221 @@ +# metric +2% + +

Performance impact

+ +

Impact on 3 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (+2%) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
Duration for setTimeout(100)0.1 second0.1 second+0.002 second / +2%:arrow_upper_right:
Memory usage for setTimeout(100)50 B51 B+1 B / +2%:arrow_upper_right:
Number of filesystem read00:ghost:
+
+ +# metric + 100% + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (+100%) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second0.2 second+0.1 second / +100%:arrow_upper_right:
+
+ +# metric -0.2% + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (-0.2%) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second0.1 second-0 second / -0.2%:arrow_lower_right:
+
+ +# metric -100% + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (-100%) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second0 second-0.1 second / -100%:arrow_lower_right:
+
+ +# metric duration +0% + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (no impact) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second0.1 second:ghost:
+
+ +# add a group + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (new) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second:baby:
+
+ +# remove a group + +

Performance impact

+ +

No impact to compute when merging head into base: there is no performance metric.

+ +# add a metric + +

Performance impact

+ +

Impact on 1 metrics when merging head into base. Before drawing conclusion, keep in mind performance variability.

+ +
+ timeout (no impact) + + + + + + + + + + + + + + + + + +
MetricBefore mergeAfter mergeImpact
100ms0.1 second:baby:
+
+ +# remove a metric + +

Performance impact

+ +

No impact to compute when merging head into base: there is no performance metric.

diff --git a/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs b/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs new file mode 100644 index 0000000000..39ab842f0b --- /dev/null +++ b/packages/independent/performance-impact/tests/comment_snapshot/comment_snapshot.test.mjs @@ -0,0 +1,31 @@ +/** + +This test is meant to work like this: + +It reads comment_snapshot.md and ensure regenerating it gives the same output. +The goal is to force dev to regenerate comment_snapshot.md and ensure it looks correct +before commiting it. + +-> This is snapshot testing to force a human review when comment is modified. + +*/ + +import { readFileSync } from "node:fs"; +import { assert } from "@jsenv/assert"; + +const commentSnapshotFileUrl = new URL( + "./comment_snapshot.md", + import.meta.url, +); +const readCommentSnapshotFile = () => { + const fileContent = String(readFileSync(commentSnapshotFileUrl)); + return fileContent; +}; + +// disable on windows because it would fails due to line endings (CRLF) +if (process.platform !== "win32") { + const expect = readCommentSnapshotFile(); + await import("./generate_comment_snapshot_file.mjs"); + const actual = readCommentSnapshotFile(); + assert({ actual, expect }); +} diff --git a/packages/independent/performance-impact/tests/comment_snapshot/generate_comment_snapshot_file.mjs b/packages/independent/performance-impact/tests/comment_snapshot/generate_comment_snapshot_file.mjs new file mode 100644 index 0000000000..49b2031d1b --- /dev/null +++ b/packages/independent/performance-impact/tests/comment_snapshot/generate_comment_snapshot_file.mjs @@ -0,0 +1,132 @@ +import { createGitHubPullRequestCommentText } from "@jsenv/github-pull-request-impact"; +import { createPerfImpactComment } from "@jsenv/performance-impact/src/internal/comment/create_perf_impact_comment.js"; +import { jsenvCommentParameters } from "@jsenv/performance-impact/src/internal/comment/jsenv_comment_parameters.js"; +import { writeFileSync } from "node:fs"; + +const generateComment = (data) => { + return createGitHubPullRequestCommentText( + createPerfImpactComment({ + pullRequestBase: "base", + pullRequestHead: "head", + ...jsenvCommentParameters, + ...data, + }), + ); +}; + +const examples = { + "metric +2%": generateComment({ + beforeMergeData: { + timeout: { + "Duration for setTimeout(100)": { value: 100, unit: "ms" }, + "Memory usage for setTimeout(100)": { value: 50, unit: "byte" }, + "Number of filesystem read": { value: 0 }, + }, + }, + afterMergeData: { + timeout: { + "Duration for setTimeout(100)": { value: 102, unit: "ms" }, + "Memory usage for setTimeout(100)": { value: 51, unit: "byte" }, + "Number of filesystem read": { value: 0 }, + }, + }, + }), + "metric + 100%": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: { + timeout: { + "100ms": { value: 200, unit: "ms" }, + }, + }, + }), + "metric -0.2%": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: { + timeout: { + "100ms": { value: 99.8, unit: "ms" }, + }, + }, + }), + "metric -100%": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: { + timeout: { + "100ms": { value: 0, unit: "ms" }, + }, + }, + }), + "metric duration +0%": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + }), + "add a group": generateComment({ + beforeMergeData: {}, + afterMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + }), + "remove a group": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: {}, + }), + "add a metric": generateComment({ + beforeMergeData: { + timeout: {}, + }, + afterMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + }), + "remove a metric": generateComment({ + beforeMergeData: { + timeout: { + "100ms": { value: 100, unit: "ms" }, + }, + }, + afterMergeData: { + timeout: {}, + }, + }), +}; + +const exampleFileUrl = new URL("./comment_snapshot.md", import.meta.url); +const exampleFileContent = Object.keys(examples).map((exampleName) => { + return `# ${exampleName} + +${examples[exampleName]}`; +}).join(` + +`); +writeFileSync( + exampleFileUrl, + `${exampleFileContent} +`, +); diff --git a/packages/independent/performance-impact/tests/duration_formatting/duration_formatting.test.mjs b/packages/independent/performance-impact/tests/duration_formatting/duration_formatting.test.mjs new file mode 100644 index 0000000000..fec69ec617 --- /dev/null +++ b/packages/independent/performance-impact/tests/duration_formatting/duration_formatting.test.mjs @@ -0,0 +1,32 @@ +import { assert } from "@jsenv/assert"; +import { formatMetricValue } from "@jsenv/performance-impact/src/internal/format_metric_value.js"; + +{ + const actual = formatMetricValue({ unit: "ms", value: 0.168999 }); + const expect = `0 second`; + assert({ actual, expect }); +} + +{ + const actual = formatMetricValue({ unit: "ms", value: 2 }); + const expect = `0.002 second`; + assert({ actual, expect }); +} + +{ + const actual = formatMetricValue({ unit: "ms", value: 59 }); + const expect = `0.06 second`; + assert({ actual, expect }); +} + +{ + const actual = formatMetricValue({ unit: "ms", value: 1059.456 }); + const expect = `1.1 seconds`; + assert({ actual, expect }); +} + +{ + const actual = formatMetricValue({ unit: "ms", value: 1002.456 }); + const expect = `1 second`; + assert({ actual, expect }); +} diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/0_single_file/main.js b/packages/independent/performance-impact/tests/import_perf/fixtures/0_single_file/main.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/a.js b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/a.js new file mode 100644 index 0000000000..9233cce2f0 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/a.js @@ -0,0 +1 @@ +export const a = "a"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/b.js b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/b.js new file mode 100644 index 0000000000..59d1689930 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/b.js @@ -0,0 +1 @@ +export const b = "b"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/main.js b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/main.js new file mode 100644 index 0000000000..11dd7f0bfb --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/1_two_import/main.js @@ -0,0 +1,2 @@ +export { a } from "./a.js"; +export { b } from "./b.js"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/a.js b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/a.js new file mode 100644 index 0000000000..78b3fd0768 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/a.js @@ -0,0 +1,3 @@ +import "./shared.js"; + +export const a = "a"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/b.js b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/b.js new file mode 100644 index 0000000000..ad92d15fb0 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/b.js @@ -0,0 +1,3 @@ +import "./shared.js"; + +export const b = "b"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/main.js b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/main.js new file mode 100644 index 0000000000..a9375c557c --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/main.js @@ -0,0 +1,3 @@ +export { a } from "./a.js"; +export { b } from "./b.js"; +export { shared } from "./shared.js"; diff --git a/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/shared.js b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/shared.js new file mode 100644 index 0000000000..265b90b3a1 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/fixtures/2_two_import_and_shared/shared.js @@ -0,0 +1 @@ +export const shared = "shared"; diff --git a/packages/independent/performance-impact/tests/import_perf/import_perf.test.mjs b/packages/independent/performance-impact/tests/import_perf/import_perf.test.mjs new file mode 100644 index 0000000000..fd2b9de0b4 --- /dev/null +++ b/packages/independent/performance-impact/tests/import_perf/import_perf.test.mjs @@ -0,0 +1,35 @@ +import { assert } from "@jsenv/assert"; +import { startMeasures } from "@jsenv/performance-impact"; + +if (process.platform === "win32") { + process.exit(0); +} + +const test = async (fileRelativeUrl) => { + const measures = startMeasures({ + filesystem: true, + }); + await import(`${fileRelativeUrl}main.js`); + const { fsOpenCall, fsStatCall } = measures.stop(); + return { fsOpenCall, fsStatCall }; +}; +const actual = { + single: await test("./fixtures/0_single_file/"), + twoImport: await test("./fixtures/1_two_import/"), + twoImportAndShared: await test("./fixtures/2_two_import_and_shared/"), +}; +const expect = { + single: { + fsOpenCall: 1, + fsStatCall: 3, + }, + twoImport: { + fsOpenCall: 3, + fsStatCall: 4, + }, + twoImportAndShared: { + fsOpenCall: 4, + fsStatCall: 5, + }, +}; +assert({ actual, expect }); diff --git a/packages/independent/performance-impact/tests/median/median.test.mjs b/packages/independent/performance-impact/tests/median/median.test.mjs new file mode 100644 index 0000000000..294c1e9d8f --- /dev/null +++ b/packages/independent/performance-impact/tests/median/median.test.mjs @@ -0,0 +1,9 @@ +import { assert } from "@jsenv/assert"; +import { median } from "@jsenv/performance-impact/src/internal/median.js"; + +{ + const numbers = [102.344125, 104.741811, 100.027091, 103.003714, 105.492454]; + const actual = median(numbers); + const expect = 103.003714; + assert({ actual, expect }); +} diff --git a/packages/independent/performance-impact/tests/performance_report/generate_perf_report.mjs b/packages/independent/performance-impact/tests/performance_report/generate_perf_report.mjs new file mode 100644 index 0000000000..129a4afed8 --- /dev/null +++ b/packages/independent/performance-impact/tests/performance_report/generate_perf_report.mjs @@ -0,0 +1,56 @@ +import { computeMetricsMedian } from "@jsenv/performance-impact/src/internal/compute_metrics_median.js"; +import { measureMultipleTimes } from "@jsenv/performance-impact/src/internal/measure_multiple_times.js"; + +export const generatePerformanceReport = async () => { + const oneTimeout = await measureOneTimeout(); + + const twoTimeouts = computeMetricsMedian( + await measureMultipleTimes(measureTwoTimeouts, 5), + ); + + return { + groups: { + "setTimeout metrics": { + ...oneTimeout, + ...twoTimeouts, + }, + }, + }; +}; + +const measureOneTimeout = async () => { + return { + "with 100": { + type: "duration", + value: await measureATimeoutDuration(100), + }, + }; +}; + +// this will happen when code use multiple performance.measure +// we will receive an object representing each measures +const measureTwoTimeouts = async () => { + const [durationFor100MsTimeout, durationFor200MsTimeout] = await Promise.all([ + measureATimeoutDuration(200), + measureATimeoutDuration(400), + ]); + + return { + "with 200": { type: "duration", value: durationFor100MsTimeout }, + "with 400": { type: "duration", value: durationFor200MsTimeout }, + }; +}; + +const measureATimeoutDuration = async (ms) => { + const startTime = Date.now(); + let endTime; + + await new Promise((resolve) => { + setTimeout(() => { + endTime = Date.now(); + resolve(); + }, ms); + }); + + return endTime - startTime; +}; diff --git a/packages/independent/performance-impact/tests/performance_report/performance_report.test.mjs b/packages/independent/performance-impact/tests/performance_report/performance_report.test.mjs new file mode 100644 index 0000000000..7307917d29 --- /dev/null +++ b/packages/independent/performance-impact/tests/performance_report/performance_report.test.mjs @@ -0,0 +1,8 @@ +const generatePerfReportFileUrl = new URL( + "./generate_perf_report.mjs", + import.meta.url, +); +const { generatePerformanceReport } = await import(generatePerfReportFileUrl); + +const performanceReport = await generatePerformanceReport(); +console.log(JSON.stringify(performanceReport, null, " ")); diff --git a/packages/independent/performance-impact/tests/timeout_jsenv/file.mjs b/packages/independent/performance-impact/tests/timeout_jsenv/file.mjs new file mode 100644 index 0000000000..e0ffc7bf40 --- /dev/null +++ b/packages/independent/performance-impact/tests/timeout_jsenv/file.mjs @@ -0,0 +1,9 @@ +import { performance } from "node:perf_hooks"; + +performance.mark("start"); +await new Promise((resolve) => { + setTimeout(() => { + performance.measure("timeout", "start"); + resolve(); + }, 100); +}); diff --git a/packages/independent/performance-impact/tests/timeout_jsenv/timeout_jsenv.test.mjs b/packages/independent/performance-impact/tests/timeout_jsenv/timeout_jsenv.test.mjs new file mode 100644 index 0000000000..8a7de72c3c --- /dev/null +++ b/packages/independent/performance-impact/tests/timeout_jsenv/timeout_jsenv.test.mjs @@ -0,0 +1,30 @@ +import { computeMetricsMedian } from "@jsenv/performance-impact/src/internal/compute_metrics_median.js"; +import { measureMultipleTimes } from "@jsenv/performance-impact/src/internal/measure_multiple_times.js"; +import { execute, nodeWorkerThread } from "@jsenv/test"; + +const rootDirectoryUrl = new URL("./", import.meta.url); + +const measureFilePerformance = async (params) => { + const executionResult = await execute({ + runtime: nodeWorkerThread(), + ...params, + // measurePerformance: true, + compileServerCanWriteOnFilesystem: false, + collectPerformance: true, + }); + const { measures } = executionResult.performance; + const metrics = {}; + Object.keys(measures).forEach((measureName) => { + metrics[measureName] = { value: measures[measureName], unit: "ms" }; + }); + return metrics; +}; + +const metrics = await measureMultipleTimes(() => { + return measureFilePerformance({ + rootDirectoryUrl, + fileRelativeUrl: "file.mjs", + }); +}); + +console.log(metrics, computeMetricsMedian(metrics)); From 25e8cc0425488b10e74924ecb876a62b4f81b8bf Mon Sep 17 00:00:00 2001 From: dmail Date: Tue, 13 Aug 2024 15:01:26 +0200 Subject: [PATCH 05/15] migrate lighthouse impact --- .../lighthouse-impact/CHANGELOG.md | 20 + .../independent/lighthouse-impact/README.md | 235 + .../docs/comment-collapsed.png | Bin 0 -> 51258 bytes .../docs/comment-expanded.png | Bin 0 -> 54590 bytes .../docs/comment-links-highlighted.png | Bin 0 -> 51091 bytes .../independent/lighthouse-impact/license | 21 + .../lighthouse-impact/package.json | 40 + .../lighthouse-impact/scripts/test.mjs | 12 + .../generate/generate_lighthouse_report.js | 140 + .../src/generate/lighthouse_api.js | 44 + .../independent/lighthouse-impact/src/main.js | 3 + .../create_lighthouse_impact_comment.js | 271 + .../src/pr_impact/format_numeric_diff.js | 14 + .../src/pr_impact/jsenv_comment_parameters.js | 1 + .../src/pr_impact/patch_or_post_gists.js | 153 + .../report_lighthouse_impact_in_github_pr.js | 117 + .../src/run_lighthouse_on_playwright_page.js | 70 + .../tests/comment/comment.test.mjs | 31 + .../tests/comment/comment_snapshot.md | 616 ++ .../generate_comment_snapshot_file.mjs | 108 + .../lighthouse_report_examples/fcp-error.json | 5081 ++++++++++++++++ .../lighthouse_report_examples/normal.json | 5258 +++++++++++++++++ .../tests/ligthouse_report/index.html | 25 + .../lighthouse_report.test.mjs | 66 + 24 files changed, 12326 insertions(+) create mode 100644 packages/independent/lighthouse-impact/CHANGELOG.md create mode 100644 packages/independent/lighthouse-impact/README.md create mode 100644 packages/independent/lighthouse-impact/docs/comment-collapsed.png create mode 100644 packages/independent/lighthouse-impact/docs/comment-expanded.png create mode 100644 packages/independent/lighthouse-impact/docs/comment-links-highlighted.png create mode 100644 packages/independent/lighthouse-impact/license create mode 100644 packages/independent/lighthouse-impact/package.json create mode 100644 packages/independent/lighthouse-impact/scripts/test.mjs create mode 100644 packages/independent/lighthouse-impact/src/generate/generate_lighthouse_report.js create mode 100644 packages/independent/lighthouse-impact/src/generate/lighthouse_api.js create mode 100644 packages/independent/lighthouse-impact/src/main.js create mode 100644 packages/independent/lighthouse-impact/src/pr_impact/create_lighthouse_impact_comment.js create mode 100644 packages/independent/lighthouse-impact/src/pr_impact/format_numeric_diff.js create mode 100644 packages/independent/lighthouse-impact/src/pr_impact/jsenv_comment_parameters.js create mode 100644 packages/independent/lighthouse-impact/src/pr_impact/patch_or_post_gists.js create mode 100644 packages/independent/lighthouse-impact/src/report_lighthouse_impact_in_github_pr.js create mode 100644 packages/independent/lighthouse-impact/src/run_lighthouse_on_playwright_page.js create mode 100644 packages/independent/lighthouse-impact/tests/comment/comment.test.mjs create mode 100644 packages/independent/lighthouse-impact/tests/comment/comment_snapshot.md create mode 100644 packages/independent/lighthouse-impact/tests/comment/generate_comment_snapshot_file.mjs create mode 100644 packages/independent/lighthouse-impact/tests/comment/lighthouse_report_examples/fcp-error.json create mode 100644 packages/independent/lighthouse-impact/tests/comment/lighthouse_report_examples/normal.json create mode 100644 packages/independent/lighthouse-impact/tests/ligthouse_report/index.html create mode 100644 packages/independent/lighthouse-impact/tests/ligthouse_report/lighthouse_report.test.mjs diff --git a/packages/independent/lighthouse-impact/CHANGELOG.md b/packages/independent/lighthouse-impact/CHANGELOG.md new file mode 100644 index 0000000000..1124e02e19 --- /dev/null +++ b/packages/independent/lighthouse-impact/CHANGELOG.md @@ -0,0 +1,20 @@ +# 4.0.0 + +- launching chromium must be done before requesting a lighthouse report + - add runLighthouseOnPlaywrightPage + - future versions might export helpers for puppeteer and chrome-laucher + - remove dependency to chrome-laucher + - allow dev to control and see how the browser is started (screen dimensions, touch events, ...) +- reportLighthouseImpact -> reportLighthouseImpactInGithubPullRequest +- generateLighthouseReport -> runLighthouseOnPlaywrightPage + +# 3.1.0 + +- Add mobile = false param +- Add lighthouseSettings = {} param + +# 3.0.0 + +- Rename lighthouseReportPath into lighthouseReportUrl +- Rename jsonFileRelativeUrl into jsonFileUrl +- Rename htmlFileRelativeUrl into htmlFileUrl diff --git a/packages/independent/lighthouse-impact/README.md b/packages/independent/lighthouse-impact/README.md new file mode 100644 index 0000000000..a8c4233242 --- /dev/null +++ b/packages/independent/lighthouse-impact/README.md @@ -0,0 +1,235 @@ +# Lighthouse impact + +[![npm package](https://img.shields.io/npm/v/@jsenv/lighthouse-impact.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/lighthouse-impact) + +`@jsenv/lighthouse-impact` analyses a pull request impact on lighthouse score. This analysis is posted in a comment of the pull request on GitHub. + +- Helps you to catch lighthouse score impacts before merging a pull request +- Gives you the ability to get lighthouse report on your machine during dev +- Can be added to any automated process (GitHub workflow, Jenkins, ...) + +# Pull request comment + +_Screenshot of a pull request comment_ + +![screenshot of pull request comment](./docs/comment-collapsed.png) + +_Screenshot of comment when expanded_ + +![screenshot of pull request comment expanded](./docs/comment-expanded.png) + +# Installation + +The first thing you need is a script capable to generate a lighthouse report. + +```console +npm install --save-dev playwright +npm install --save-dev @jsenv/lighthouse-impact +``` + +_lighthouse.mjs_ + +```js +/* + * This file gives the ability to generate a lighthouse report + * - It starts a local server serving a single basic HTML file + * - It is meant to be modified to use your own server and website files + */ +import { chromium } from "playwright"; +import { createServer } from "node:http"; +import { readFileSync } from "node:fs"; +import { runLighthouseOnPlaywrightPage } from "@jsenv/lighthouse-impact"; + +const htmlFileUrl = new URL("./index.html", import.meta.url); +const html = String(readFileSync(htmlFileUrl)); + +const server = createServer((request, response) => { + response.writeHead(200, { + "content-type": "text/html", + }); + response.end(html); +}); +server.listen(8080); +server.unref(); + +const browser = await chromium.launch({ + args: ["--remote-debugging-port=9222"], +}); +const browserContext = await browser.newContext({ + // userAgent: "", + ignoreHTTPSErrors: true, + viewport: { + width: 640, + height: 480, + }, + screen: { + width: 640, + height: 480, + }, + hasTouch: true, + isMobile: true, + deviceScaleFactor: 1, +}); +const page = await browserContext.newPage(); +await page.goto(server.origin); + +export const lighthouseReport = await runLighthouseOnPlaywrightPage(page, { + chromiumPort: 9222, +}); +``` + +_index.html_ + +```html + + + + Title + + + + + Hello, World! + + +``` + +At this stage, you could generate a lighthouse report on your machine. + +Now it's time to configure a workflow to compare lighthouse reports before and after merging a pull request. + +## GitHub workflow + +_.github/workflows/lighthouse_impact.yml_ + +```yml +# This is a GitHub workflow YAML file +# see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +# +# For every push on a pull request, it +# - starts a machine on ubuntu +# - clone the git repository +# - install node, install npm deps +# - Executes report_lighthouse_impact.mjs + +name: lighthouse impact + +on: pull_request + +jobs: + lighthouse_impact: + runs-on: ubuntu-latest + name: lighthouse impact + steps: + - name: Setup git + uses: actions/checkout@v3 + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: "18.3.0" + - name: Install node modules + run: npm install + - name: Report lighthouse impact + run: node ./report_lighthouse_impact.mjs +``` + +_report_lighthouse_impact.mjs_ + +```js +/* + * This file is executed by lighthouse_impact.yml GitHub workflow. + * - it generates lighthouse report before and after merging a pull request + * - Then, it creates or updates a comment in the pull request + * See https://github.com/jsenv/workflow/tree/main/packages/performance-impact + */ + +import { + reportLighthouseImpactInGithubPullRequest, + readGitHubWorkflowEnv, +} from "@jsenv/lighthouse-impact"; + +await reportLighthouseImpactInGithubPullRequest({ + ...readGitHubWorkflowEnv(), + lighthouseReportUrl: new URL( + "./lighthouse.mjs#lighthouseReport", + import.meta.url, + ), +}); +``` + +## Other tools + +If you want to use an other tool than GitHub worflow to run the pull request comparison, like Jenkins, there is a few things to do: + +1. Replicate _lighthouse_impact.yml_ +2. Adjust _report_lighthouse_impact.mjs_ +3. Create a GitHub token (required to post comment on GitHub) + +### 1. Replicate _lighthouse_impact.yml_ + +Your script must reproduce the state where your git repository has been cloned and you are currently on the pull request branch. Something like the commands below. + +```console +git init +git remote add origin $GITHUB_REPOSITORY_URL +git fetch --no-tags --prune origin $PULL_REQUEST_HEAD_REF +git checkout origin/$PULL_REQUEST_HEAD_REF +npm install +node ./report_lighthouse_impact.mjs +``` + +### 2. Adjust _report_lighthouse_impact.mjs_ + +When outside a GitHub workflow, you cannot use _readGitHubWorkflowEnv()_. +It means you must pass several parameters to _reportLighthouseImpact_. +The example below assume code is executed by Travis. + +```diff +- import { reportLighthouseImpactInGithubPullRequest, readGitHubWorkflowEnv } from "@jsenv/lighthouse-impact" ++ import { reportLighthouseImpactInGithubPullRequest } from "@jsenv/lighthouse-impact" + +reportLighthouseImpactInGithubPullRequest({ +- ...readGitHubWorkflowEnv(), ++ rootDirectoryUrl: process.env.TRAVIS_BUILD_DIR, ++ repositoryOwner: process.env.TRAVIS_REPO_SLUG.split("/")[0], ++ repositoryName: process.env.TRAVIS_REPO_SLUG.split("/")[1], ++ pullRequestNumber: process.env.TRAVIS_PULL_REQUEST, ++ githubToken: process.env.GITHUB_TOKEN, // see next step + lighthouseReportUrl: "./lighthouse.mjs#lighthouseReport", +}) +``` + +### 3. Create a GitHub token + +The GitHub token is required to be able to post a commment in the pull request. +You need to create a GitHub token with `repo` scope at https://github.com/settings/tokens/new. +Finally you need to setup this environment variable. The exact way to do this is specific to the tools your are using. + +# Lighthouse report viewer + +The pull request comment can contain links to see lighthouse reports in [Lighthouse Report Viewer](https://googlechrome.github.io/lighthouse/viewer). + +![screenshot of pull request comment with links highlighted](./docs/comment-links-highlighted.png) + +To unlock this you need a GitHub token with the right to create gists. +Every github workflow has access to a magic token `secrets.GITHUB_TOKEN`. +But this token is not allowed to create gists. +We need to update the worflow file to use an other token that will have the rights to create gists. + +```diff +- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ++ GITHUB_TOKEN: ${{ secrets.LIGHTHOUSE_GITHUB_TOKEN }} +``` + +You can generate a new token at https://github.com/settings/tokens/new. That token needs `repo` and `gists` scope. Copy this token and add it to your repository secrets at https://github.com/REPOSITORY_OWNER/REPOSITORY_NAME/settings/secrets/new. For this example the secret is named `LIGHTHOUSE_GITHUB_TOKEN`. + +# How it works + +In order to analyse the impact of a pull request on lighthouse score this project does the following: + +1. Checkout pull request base branch +2. Generates a lighthouse report +3. Merge pull request into its base +4. Generates a second lighthouse report +5. Analyse differences between the two lighthouse reports +6. Post or update comment in the pull request diff --git a/packages/independent/lighthouse-impact/docs/comment-collapsed.png b/packages/independent/lighthouse-impact/docs/comment-collapsed.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a2a6a59dcbf8d661c8fab6cb4b5d3f24bda555 GIT binary patch literal 51258 zcmeFYV{oQR*9IDAV%xTDPi))C#J0@|pV*URV%xSRwryJ{dw=!4d%x%RsXBkYs;=k9 zeRuWUPxtDz*0rvyBNXJs;h?dhfq;PEBqcS=0+NVGN`X{CIY#&YOuPFILr6$=M=lXhPj;t3dPhu61VswKL=KG9 zMN&7QD-JgR1rS45O2s!2{?b4kfS_(Aj0&u!M;8^_hr9aLcA53K;o&yPcbmoY*tMVY zJk=(GG3Qh|)Iv~y?!YA%^#T}*7BoXKBqhS$^~6lh ziM~W!fq@^4`zYu;0oBAn?#xkSQegfvB4_omuEfY;XQ9<0{(TsPw4zHP6UGjR{2^EE zb94KNYeChDZ~8l=A9QBztmIKil>5hezrij?FkqB5%08yfpEeWHiPJ_5{VCHZZs5|+ zu$JpC_Vz!N(EAyn(+&B}NBtr_kT=%u@Sq?oujcHBlM+puFuuS>?D%tV3Ij zs{UZ1(;>tkNFjRsF;68+pP-S1Pg(KM31#&qXP|WqWqCr1GSkS!JrOwFC2EKwC7O9@ z0U>x?Zr_;MP#*9*PMM0Ulsyc2mhv*6SeQclV{ROoU(n#toUK>KzpdnF*xJUL!Wcqz zfB5@J;(RAs;dsS)FDO%Y3~*tHJQmB<*Nx&H0JQfDn7mq;UfLjQQa6o%Ief@*n;Je7BO+Wf2>Vum z3LywhH+D6WNsvx8${D!W79%BS(iSNv9Co*dJ(dn=Q@8FFA}@G{0Z=H!D$6tz&9vDsCtMM*nQz=1tb*^RiQ4y!`!hyaK$K| zu=heDf4q)pq60^&iOYd;x^E4=QsH0w46l$mk>r9{wrSmYbiSqZQfzrXF?+-KetYb- z+fqaoj8_{qRWcS{xSPO zQP04QJMq0JZ!z!okJyovGhZv355amEzR+j40^{rjW-voH!H|CcW?xrtEXv-u7cg;Q!~hWuu_Yl+sUO5^ zM6ig15jUbDzgU=}Q$^P#Xh|0ktB}3I2_#4p;70;bWp)zTZWn_LdVk0I^B?(yzvIG*lB?oW=mjQku`-ly9S+Ed=Y-HRRDj3ZF# zroyCjr?N+_MIA-0q`XsBRM9PkOp;xf;gjl+!c*v{#-?UQO+#y>?A@g9hDX4N!1UvL$Oq z@RXP4765jhf9jp^&9eJ=d)j9_bj?gyS~Wv88#HrTYCZg)$!{TUn{Mw9hQ>G9x@+^>CkB3X*o36H9MP_YmBtfwE3!kYj|l_HQCntHg!7#yY=LOud}OU2xe5U z)3I;boLh@EvNejhdb&EeLY}prp`E8acswlPF~VQKpW;pOcsta)$hh&IKNTgWDc8_7X})W&j4O!UqxJXxWT;mPlW<&g1M$IPs2{_X8$-ikks%2cUtd`R_ zWpqr|uAA-33Du~qShJD*2z`Ukdp3rA`y6)9wqYHzTAtU`p(df8p@JeNBic~H+$-Bb zHW)X9^!xo%_A;C~EoiwYTZlb~$(Re-(Q>d;Jmqo;CD7~X7j-%vJ&OLQq|%+qI?1eN zuoK^4Bt;QjBRd$k5;J2qNJo=ulloxMV+~;CsEd|!)+zZG8koLPC#tThb&TFk@-||3 zq<6HM{(MOPY4x%&vzS~Sv@lR*rCHU+Z}r{{kpq{66M>V+v9V!NL*+L+G!dVXQOjD( zZgOvTy{wc)mq<5yv2s#>eli%Tooml&?uz`z#2FN)CYaT||IH#)(q9zfxFgEx@{14Ayzk;^@qJqx3-1=Nt z?PSG=n;-qtjm8f{rK9tsPGeo_->5xlM5wjP&Pq#T$_O?U;nbx|!{qw2o4sakO^knP z%1Tx^+FVfCOF5q0GdOhda-aLP?nl3;rPsZfBg`qDI-cacsNR#nwTEsD9t}=9`5tPe zXQrR{vVAt}v_CxWz(`@Z>X7PrV64mwigT1!mF#maS zZ?&;lSwFo>uj|(7`w_bP(~0BUj(x4RLv_=3R?ltqz*RTQM*JPGjpwHu-UYi0{aL&F z)cwZ(YxZLmp8;?4z5R6Gr1*AhYnxGb3msi5I$=h)#zKr}NON%-9)O=3L zhuNF9O_j}M2>(>)14bT0_Cww4<{SOT`>NoA^ypZix>i*wL}&4 zgN;5(S0}dqHQZyxRonIBZ-Ng2PjV#+@?csdD@#;CB4`RypsP+0ASqU$xi%m~4Ed>9 zWf0joBt?Z(+=0)sz%y0mF@_M@`ssCN2y^}YL8$R&2nnH)XV%vVXUO2F zU(^eP5P^Y@FPP)h1Y6szme-#*H`je)oBGDBCz)xx`ph2&&M$q&*PJf79tUcvkfZ>} z&AzvdlMJRe1&ZkmEF{3lg#R>o(yjf7B%>?|f3SZyJ$w)qYQ5}EBU#CRzvw1`QYg6^ zwlysLcV~&MeImiao&95)1;Bre!kYk;1RZ;Zt`_-UkN)oNmekhdY4t860YLF!4>%9@Mo&Gv88n>V8Ug&aX?W2H2*M^Dz)=-No5fF<@@SXfUnKvPbfH3Ts#Hnul!KEt#%e_>N`;~~KyQ8S1+4MC zH}ST53xnVl+{d8E&+O6BjW8d2&~FyE11D0HtB^_{>Q_|SyAV?^Gc)|_00tb9ZSA8; z5T3pzxzgCXyBiBcU;-LH=tkyJ5)uB^^_5H#?m+Z9dQx zOI0#8STw7pwxz5YtpcZlNdl6Al}XUVhn!Hw*V{aQMDcd`NpBP{jY>ujulttbj&6$> za>k3(bNnX%$9EtT24F}NCNl^1VlL_;n`)1Sisglmei!r*O1nVD=4*+fy(Rls?{Hg? zCA;A)lmo?ID&2cj^=Qy;{vv7SZg0r@n%YgRKX1m%=}?CWi;ojEXFn*CnWN#)|%TcgZ8{xOHGcrbhc-(MT@S1LS*Cm4ucCUjZq z&%pklD!2bzs~pF8LCTY)BkaM znaKXk*IU~p_i}+t$P9Wq?;CV>ml9Y-oOEMZz`(!(T11{V{qju|Lf-n+7#CppBkd&R zD!d*yoC#v$J~tHd_z`w*!HNk?ddR&Pn_^H>X8*BWL|L$lGK@?3CEQWZ+x zOHI?C|2gYAh~2q3b+01{=t5!LEjxp|0PPj2s@;z@NQ7g}6M=nlt(Qg!)DR zL>+s>*|HlGuQ4X0sUQ1b4=IbVHioINSU%+tgR9T=v3)^g}q55`VO)y6x0 zUdAaws>2wHt;e76%YG|MCIY!<*zq8RK_8ao!G+C##++s+!)?1iJd~TR!8V|`2 z7hD#VYGy(*@3%pLP&5sXlB+IEuKP)}Fp~`|4pG!Dk8Jorl6BTb`Ov&(#7{E*nLV)I|@{TT>!zK^i z`B`~$9PSBeRj#zw;U=K;fwuW(7pa%mZ3))ANK-M}laElH`yJzJU!WdFE+ZDC84HpSe!0Y2v&*7;mm| z%jx!z35&-sjLlyBI7*G?_wFOuc)o;3RoIJmsoi8dwp7)b=yfuN3LQ3a^JzQtsBE$s z(3e2u!(E}$JW9c9IDtK%=-FT;8X+9u=%-zyMI};Fq7qz$f|>G{4St=Oa=y&ktQQ3^ zgc8P6TUF|dwU#n8Xbep+6U3E>hhOVR%V^LvsEi(#8_n4}io=FDc_z-*tHQ2lEBSz^ zmO$41v1CWg6po;8qQWIt=$G!-WDW};QaRt+Mv&Afa~0ME8sxv?8lN(LZUN;0q2gc@ znM3m90FCXExoBRd`_Rv-B-D;bs{DGJS#HsZhjRhK zpfFhFGIX~6?jDg^i(9jiES@GV?8dvx2SJAABm`@VaL;DzaO*v5#zJhCjUqVN7dc& zvIw)euP%l7GGkR>$y|kY6RCI%?o5dfkM2Y0&y2g$Z^V@vEm{v{pXDUtu!*qnW_)T@ ztCUs?b~B*6dn1XGLELO)Q>!h`Sd6Rbd~N}6K3j3@+oK6wiS^bi3Q8TxN?~p%8|wTY zMCRX-u!?T=qxd1#{M#kFf7qLCvigCehZ0W_E@oQ=ylL+x}B9TJ!<5B31SbD zOoVXRd!=GQ3o0|Kw_3CtO+^Ya>GDiAd*6GLy|oJJlGz+i&}6sX_}vwE>Zd01jyVvY zGj{^%YXXhouFqelDLb`ht!A@cZ<+y%chzD*W;Wh`Szx%XRr_Ov+2$Ac>gGx#&D^~<)$B@fJ_G2=bM)datT%kH9>NU z&xxiFkDJ_cwKo>t;Z?k!=|ej>H{T9`<~=~AQbSHwt9V`$`mYe~P6%v`6Q!FioLF}_ zd3ve6%Yn9e`4G72e_1pM*!|3!D}z;~QD^KTc)cSieTV&6Ym@gQP%lx@Q!0~`d2=;5 zT}+=Zle+#j1#!IUdJ(^wT$M~QTHa*TqI7diL2$gosFrbiEBxLOa~t@)nfM#-^k@^V z9DYrCb3S%Xs^=PoVn5?rUn=lvipMO!!}o-Pr(Fu%oAQ88qkMH+Ca;=V2)DeNK^4X5 zsxCJ+BuQ@AsVXkTux+O5Sxul?11tPIQLqA)6zX+|!|>0RJ{2l(>}8bS735)n<>PM+ z%feosc8?dZ{SxW|GuyuA-n{e^xeyT2Hs?z!8ubb_8VHX4i*>J=QrR{{eh`a8Q;%$1dFxuknC&uc;XQFPsh0*TplL?IUd(Fu;&Xufgf^Y0#AbI49~Ky z_h%JnFgYhzw;1$CtlO%U(UA={o02~9Z$%7l7i(m)=}jsuz(PDe_vajfueySH?j)x; zwQiT2+;2I4AM^UWlE{#0CqK-o*}Csy-rMbN^1=~UTsBJOk14p{a59`jeI>4#H{ zXAovh4zmUJ&UNf0LmUgiKc6AtaVj2`A7)(wHK(Rd#t6-0tWRJ{>v4JCVx3R<1Z_*X z4Rd4MXXg^ih_-)zuJ2jgyykcn24g>^(5VW?6!#nkOK##UAEN=;xB|!2dTaA7Bzjs^wM`1ymxqmyXVDDHTP;V-}&so@>dz z{-nyy`rfgb#{-33iL1y+5A4qlgX5L7762PP#DaM2J4M&$eN(A2+4)&0b6x?elKxpC zKjm*5!ye$kq^eU6+heCUdp^%Q2^u}{xmtFJHG6CAd{nVv<9l#~gN3ZwEd}#}&+-IMsPdEgZ7Y(ZAHD&Z$mNS6YN_O+ncZ@yB=ujs)0vs?LI zJ4mR5MlzE!KKpICm^=}7x!R)arE}ay=Y*saLq3PAh}@2Ux@+KRVvrOL4235Zl~#wb zt38b^idMPHG+e*MNRzL5C^*^$H}rB!i2yZ;wM2R3M)h>g?aU(!f!vpo%UovV?BiYj ze)Y2mzR&%*yQj}nN4ZW*H3DO?rZ)I11Z^jE%?Hzik;@yR^@nf);@QiO^%~qOa|9_^ z$05H@tx@gwO7adh@$s`=YX#PsKazQt+$Sanzb6g>;S2{?;ZN11N+3DZAN4*p$o*1UDV6Kfbx=*tGNu zE7z=*xZW8kP{<2ZYLv0yo6Y(TXS*HUVfBdroGW^Lc+HUz6=- zA>?*Y=aK28?V36?YA1jxceAilbeh7a)h*#{8L*s#Oi6b-kI~UqsJ;lFz%1rCQmEzy z0gqSMwPr;*0P84973ufUp-+PubE0^jFXf2_VfYmPEHa^Gh)9rb0B*57+MwkDO_Q%| z>C&%Wab-Y8)TpFVL+yNggBB*1N2j)cHB*?holkL;gk;@qua$Nrq*+|57EkB_*nuSL zMJ#vhMld}K7xL_bC^vz{Vbfb&))-;jxsmt_D`yJ>(VQB3Ab@-CGD?Dm0Q0-2f>Gy_ zCoqPG8NfEES8OD!Uo$!YK0CZ`QeNn~M}x2N!Y>`()VAD^XfUer{T<_&O`)HK-m~?U z%cetFEA_Q$2np1m6|`KADrKAzjyEM`v`i;%S|8>`)<{0_lLY2E5(c7<3Q{v)f4Qlr z;2h?5{BU#M*Jmq9CV9>HjWAGry=kCA-O=g-8=rK~FOwGsVIcs8L~{qj>1|cVvJa-$ z^iX65LTSmU9a;AY=BLzP}uGk3j%93Q*#>yGpG*ifShp;5_sPMbo&i=<8M{#XZ6Hj?1rr(o*=8VVn z^MW#c%u9OSgc^)oMXeB|6T28@Z|(WkRVrkcmAe-5^UEdz(#{ps!2&1Z0@Vo0!E#l# z+D5QL3Cj(@PGZ^mbr7b@NEYJa4i_{Z!g_Xb3Od?cr`o^f%32{rTlwCnIdI zx-lDAPfRw~CUx6RRbk3)+uvPnO|#==CX3V7swF~Uu?n0^5{Q1StWp_OTZC8H#pz4? z`}w!1RyDv4wix1@7C5=GW~XsF>~cmMSwo)Xqr!R+)z7Fq|zDgH|m!CCQ6SZcd`76Wm`=$l8XxnM9ADN{w*kP z=U03%If4MVkyN5AL8#-oi|0!R3QayYoO74^*AE>K3J%ZJ3I)t*+;unBLB<^(9;12l zwscGyz4|XudU-IBj|L~>X&5+0JvpPX>%P8aw3!2T3p3zFc z`lCjfK2FYt2Ioq2f^y|C^95lSjVIgPqYSu&iJOtR0kJ4bmP}12_gY*Ve_Q-mt zaZyZUqlpT+G-CTLh!5nX*XKsjrK#7gtPkr4uLApP3|J5gt;NNv-*D!2H^#&$8_u1a z;1hFD_cpf=qd+Q?Av>s<>;-!M8^@dhs&oq?EU8^F8Z4!qA({x6khk&oB9(|=EA23k z)uy!8Pc^LxYhJ+HbgjE!n6gW{=5HYn?S~EbHEfgW zM=vlQqs?&*Q>Xi!tj0$*cW*QQ&TRtxxApy-0p+VFT%460fjDvBGdmV!V&2;k^X9yXd zgt?K;EpP|!3%s^3;Z(iG%eBlOh1tJZp|E>+UjAO|x~l@?S2E)Ym;yjj;>cgFfAgdm zs9W_kV)?TvkFUp9k=aB5MuqTw&=Lsd@cil6QKG%TqTxK_VT`etZs?C$44vYANgHZZ z(>%RgRadrT6|qXiL&=7b%koXq!$8<6_GMEyB)Q~OLvwNGH{y&UAMh#5M@FD zD3xw00KZ)*t*1jPXI@z!)?+UvB+A+SD{S2{Syfa0Nq0FVEzy#*S*s3kl2Ysun#I+J_5!wiWg(rFmY7!}XHU>{Zd>iR-OU zW_(NF_5epy#PqIC%cFb2PXQH~OeQyF;@bF!E)%nVf%?=`L}HOkCeElHS{Zt1H!C!I z%~K6;zS$8nsYGtp%};yWYwzAFh}Xtl+=s5EY3M`LzpCO_Dj*I?;6O60KE2hTsNPdo z+EI@z!Kudx+0_H^a=S@(#Ha|tB-v%EFzWo(&zn<4@3h@;A6TIy_KK#y!4ty2r?QLU zja*tfSD=)lq9S&4zX|y@NS30EL0a*?I<=EPkxOb2F_K3eeAH@8spCk20bs{%a2CU( z;N+^?NC&tAzR{qi^0`tfpw10_Uh{c4Z>K)3x1k%&_b}Sjf)n~kRDfvD@_$v|-!Al& zvv6j3N#uG>kU1={BT`6zN@0wp$>pKphuu3m%;H6Ye~`%G3aEMBo#ne3_r^&2yxt|0 z%$oD+qm20xp81-hB?ehCTO-F0acue)Wi=Us%-2hjr12Xk>v0Kk_mzXwKjE0J74P*5L9gV{xWycYP2HS3cP&dTa>Y}P$0MVhrP{Pb+ zF<;vv^WjkHieL4jm~;q;lm`Scn5AC-mQAhct|_Zo!Jn@_y`0HwkWpeNcy_UfLl72b zyE>h2$-3Ey_;>?8@7!_CkF-6Ohm4EQ#I_0>Lf3IQ*L}u~>p(>$0(Bu`yw*t3A3=S*t~fns z=#6Ns1@&4=VM+l6w48(6FdJz3tqrB8O?|Oylg@~5PX_E`)9isu@u!dtXv90e=$EqH zws%Yq9>p*UQw?xL%Cik1Ya8h@Qrp~!F4smsAy`BezF&cY^w3j;g0^+`2>ez0-!cTd z@(EAb+?Jh<*-#Cj>%9)x@>+YDQSmO4d;OrP6Kl?9v@CYr8Du{id=wylyPD8o!S~In zIIae7bNIQQ9sOuH_f$}doeO!g$w9&-Q`L1eulkCy1G91!CF-tz&x4zuo5h$m_(azZ zQkUO=(^#BE>^B2HBN2-R$|-KV4|bD%W7F?GfcW;ik9SRhIr^11loU~#?p9m;fk-4v zCy%6NqCiA(d3)Q#{y24ww^@%0N?oPi9^+bP!2$Vq+}vd$?m4D ze6g|0{Px0m+es5)cHc;0H6Y;p{JIiaouP~6rl%BK&o>OJGWF_)r+t=M+4C_KApWBe zB*^vc4>v~ub1SiDY_YN^8uZ*89u4~NB3_W6+g?N~_Kr-RASXoNLy!;&UJT}NixMRw zq|d%}cZB`8mWDaKJp=cE>T9nOg1F02)SM2K*}ZW>^NBt$NepJ25tz2VcQ5fIKrxlw za1iPe>{^M2QG>4U1o1(!3B$_|?$uuJFRsd;0;$^)oIRr(%tF-cim!yO@+9!VkZo|< zvZ2g*fsqn&DFAvDWZWu)t<)}E80(txI$|F7Yy}LZIyF}T)5d9$Ntgz$7}oWT$;?Sa z`KFZ4 za#Oi25zMCQv`5u^{x7PBPzJ2phkZNK5O_-SNFVXkh%?lI z457@vQFKm`#6bP#-TV6cta|QKH44SB5G2>YGFUgaclQ*I;!ccIikn~BjhzcC^SVIxpXRdsbIV<6uwRLW4qcI0Hpvr^FDF~N(GF+3#tEE z!Z^V@l<=>&C%tFi7^q=VumDOxxiSXxy$aN#Y5XNPT3p|3{CsK10kH@ne-mQ=k!@@Z zAbxxP-SBMzc;k90nW_9&5=1%el232GOfD@TqL%6(&hu|BoFPUKK#5k&uXlltKR{e7 z`(2YV@jqPS|FGp(Mo^+FZzEbhur~xH^I;URNwFb%A^s7G>Js9%PYQ(N@mr0N2rQKE zOuNNwurB%($__iFkBasoy}^Ab|Kh%Zk=^saeyNlQN;57!szG-fH`FO-f{ln;pb`O~ zt2iCVT)p>m;ryVbXM<#ZwGWI@#P`brltAX>6n+H^l(26Z8c`W!IJLC=%tb4Z96wntyaQq-+htT8TTMFe8C;H z)3k^;HkZle@7*arVELb}}^V?^WAMMu-M^Y(w~bUt0LG|) z6|-jVkbZ|H-s%)KL3>88y953P3vd^dT?1cvlT;9b9>eD^uqvjae=Oqf(-ka7s3m@` zfUH;W3z0jz5 z%p|&R|9bq!Zv^+>1p5E={{Lc4+=16o5D^J0R^1YqcDtaRj>{IxlSY((kK_-<_GLmK zk)+e>gtQyQlg#*4VG^_d%`pD8JzHobf`z@ZczVB$&_nd~OyaQEHxhx*T7}6h9=ky(X>Jx~`Cd^%;&+VS(fOP+zbCZs zZ?B%N6DcRtmgE1|VaEwdfYgZS-B6c8Em=1n*2-k_rk$;|3VH85^J1W<;b=G7q(JG* zCVa}KQ)020h0CbPoCf;0y-bP4BSEM0J2jrI+_rsDW^&%%J7SawMO=rcSY1t|-00;aVTGJ1;|*Xmuo`((7qKz~YmSsh1~6sg{i$(5;Sha&kF8h08^` zlMF>Cj-^x`T}5nq)6|(w%^#9{|CwY~v2xyfR9M-ALrBimOUB;6>L@u`dPDa_CT;mG&O}dh%yc~%f(q5HZ!oc zMB|~)#JoOnsR(8?=k)1a^3kj|w+Rg|-%sTecI(nHVccMC^WDdf4*5l3m!8P!;u;j0 zI;P+N8i+Mrue${g!mg{i>CR8HPgeOuC}oyC2_0W~KiB=_-k2mn+KZR}Z;%P@BcVuAi2q7Rd^`*&r%*& zTLEhwkJgMozh$-ECq!e-jO|j$o1ENf?i@aEb{@f@PgVT&1Jr2`%9Pfv zGLu_BHS2t>L;PaBjUi2cij>FoBH?RTYMw@G#Eh4)rxbv2c;|O)%CI_s<4ymRd?*|~ zVbN6P{JBgVVSq}hh8|7gRgYxBOXG!k!(E+XogC=8ohEY^JyxPg+!q6VatKVLdgNCy zz!L1lOYk?I{U4>JJqB>4N^_Jo=S>!$TXxKPn>mMOgLTbYjZ+jhFS&E7NJjF6>GBSE zt;M+%UQCabpkQvj<+O~Yua@lQ>7UAmXo7ds@Vm(wA&;KiX5kj*THAfQ7v&Noa36%a zBg1%5NQ9FgU(7u5dYf~?v4AhbrVK6(BT679{4tHMcERcW?0Ct`N3^EX-So@hXi}M0 zp8HpMex}R|BaP7&@OE{+s3b^*h)DS@F5sO%#iOohY_zb;a=_qt#x$_!18~gZ@^skY zIUMLwXSzDrpp~ZC<&bCq-j4{|>{bf=@k{>z8W8(s^TT}~OA1tM(&!(wbw!`WLAjLH z>&@RcY1Hb&%{-5;3|X)s-hSZBlxud1Zsv3u#4C|rMl_|Q*uM7W91f@SL5yW$DUaJ{ zV!o?A_d&m;32C3sRZ@k;;7-5&`BX?HL>Y{pn+ogPBKRkRum}BXU4bp#>-w#^!9Yd7 znI(^<(2+2f)nLGW5RD{|&9ki8PYzm&^aewn<%c%ACQoAiNTJh}YVYW9Nv}1PvY>Fx z*CS$fw7(X*-g&t|5Rgl3IAxcJ*csR}ZXk|7%xbz9#11l-*!&2(W9k+VB*5E`)$}P} zcx!OWL1j?Ylk!#283j;N?FX&C$EKf$<;EaBK?^c5?)}Nj74lkUCcCGjG&hw>0f0xL zm!YL3CXhN9`E;!e5sX+>70w+hl=do%=hMC#M;M9SSDY$lLSh2Y)^ng=OtaUuW&Yc{rPizq z4sT9B>>A;pyhW4)m>O+bc1_HK@fu3jAjU+(J<~3$(Cg){WO9<@gXI=;(?A)EWSkPg z7qXbd8@fEO=Q)&Ztbt%4R5rtOA+zO$#d}z=&;Sgsp2>eQj=v+VeERgBR8e@SjOq4p zGJ(sHm-~sL{e)Nak{)@}1R( zbp>jc?q4Z!`wx(*@2m{16_vt_%FiOS>Yj z=kSgv1J7RtfX&7EQeaZmtLV;}(i%IzxRykC6~C=HpU)3an@TXJ{F213S)nd^&C(eN zxX>EpKNU`p$D2(}cjxQMrD`RlXrn1?wT!X$!=a##71;9)X^RK)#s~L7j z&0U|*Mf|;cP#;L|5b)Cn@>9K^GusNmjn?bGr{Hpsd<>saHn_DCzMr6$!HdV@&3bh2 zLB%Y`IUmoA8ktUl2T);olcuv;=2ra_(3lR;c!it1;(PVZd&au9XH_#Se{`4o2Vp2m z`hTgKBxi;6biE#?&1kX2;G0qRY2V?(Sny}ceifTZqVrs)DHloDfa@zaD z)cL-N;Hb*Q*krxI3P82QplAT`7vjPVtcS-I~<`#IwUCw4iP8Q!~wt|3N zZM4`H>ptL4=jXSVmZ2sOAmQ;7lt(3TKEN-FV--29^0q7T*=$Uv%G9bV&?wuRz(ADg zIO`9+X^I(&CNc{%r#_jIVDsB!0ZO61K@n?((kWy%E37ZL=Q26`Sux2xZuA|XDqTV% zY#e=Hlfyyc|M-z`Y7m{0aOwoPn;rv)iIw|whoc9p)2Y?!s{yo%ItahI-uZ_$ml&%YlIcw&QJFuB(gKBeAPzClcD*+;v3^hW zh$ji4dS5B=aU%B}ua@3*e}YE#m9UuKu~(dOSkLgF-XE=KHsvXYHF_$*?1QfU?zIED z&fu^?{`Ir@FHSBRi%TbgT#~q>3zIN^Fj~EhKsBz4B6_|P^ap`Aq{4^%?ZMdQ>q8k{ z(TP;lZy_FjGzK-%@343UeR-65GN}f#FP`lqi2MCYpXJEe+$(jgm|g?W&zmEjHUM*3 z7;qrmt;+z_C}Hs1u|EMqzDNYBl?6W5jW(MlS;I7HltoE!`IBF|h?3XgBaV)WiXB$( zBf;C}(&uA}6#;M_`3kL$L5b17DZT#zCZb#c8l$}Z==&|5dsx{W_F`Xv1ka|zU-_nH zcc@Sy;BCba1KfB_m6DXD=z~x!3Kd>m*8n+KuVLjbj0`k&Ys-N#GwpoVz)R)Hp*vtH zO{dF21i`l=$ee`Uo5^;gl?)RxOw0f}q(T8NDa`X%`QAtZj}1j{p7&EJT1>T+`zhvv zcg1zJjcxWL^zl3jXWHxQ>ljLdShcDm%MOWc=28t=;|nK>eIyP?<*e&3Sp{$0`-nPT zkDUL7M1%)O_J^2@&k|$@4tGoFWa>Oz+hRX3Rg!6xr7{EzL-AM^#Q-ge?gOb@2zK1Y zMa1UE<&zVWI|loGX$Ed3)zqlNeQAzCjIePcV`HkiabYA-c6fOENE==Xn3+^?hKk`j z4#K+8x*fgR41*N^$>sv8k#m)h)Rb(aQ^=&v-sQmC`MhFpK>c%sR@LahF4?!vYH@nH za?5v1ld?NIyn;(nJ91BIH2$&SZydns0$hRzlZ8Z(1J~&P`2VkcY0mQJ8EPitlV0CI zMgJcR|DRvP*>4IZ;TC+O5C4TM1zAzP<}=Z)iTht8^|OB63o+7)t>ybKgo!u-R;(h* zVvqi>63ABvrk_Abk*N#$*8d7eUtjssb$>-9*vx@{@WvPL5R~}>HpIX0DOW}Q@p4~- zOw0I7iLrv3JN)+-|8Lq?7@9hQg8p9$&oG%RerbF@-#6m2-r7E)ZnL_Pv^w+sFT}Sk zvCn&wFCJ_15h%9p3vu<9i~l7(hK+xoFI(JO9FPXTFZj>4x2Jz?VsqZ+`}_NPoYh_( zF1Hb&{`imP$3~a_TaKq09?9ltYEoY%Uzc<|vT4Cum@1iHJVJqF6uaYIj-Lw&2CWwD zvg5wX)3ej%rks#WHn*-?xei??Kfm1W5+p9CyM$CyRl%uZv%7`GdS@aox3{RcbOLFO z%+rCkW{u@fALhU7q<>@3cH+P1l0T+aP8{W{yAzs8lCqz$nLKvb(K=QTu}iA>D>!~E zx@28$>n^Qsrr1LIx!TA>v&K38Y^hx@fkK{KxAk$R4E9UU5qDpf67uA1xv92DWBhUl ze;Sq)D!KVixZdXKhaq0lZibl6FQ$~543+t!eD-qkx#eM>$i3Ml z5kKyZmGKCkRVwtP50#6AjGuR>y2ic#LdKgPcD2su)7oKu&X&_VWHT+=@@dRAp#Vx{ zp`!IKXvtnys@jxifbKQ^R3!{c-*mY|hS%!6=uvT74cW}SB*ikD?r3qYwvli zSAW7jb8DrTWc2AhR$$_4C^ly}T+!G?SHblw<=xxpcr|Oj2Rq=Z^Gm1rL9z2dz^X#8 z({A#;#rrwZ`D8_T5oKFrxh6fr@PWW$nxc5B{TW6kk-%PPCBkwL--5|LOk-B1OjDfQ zZrLn3tW{*%$!vku}kgraUV={&q$vDJ2weg8jMdFoyaNOPzqf%^76*TlXZvZ3^ ziFlTLNUE-XT?~S(3Ecs$E5>VkSfV&&`(H2CO(WlE864oeB{hMl?QaA$=H)znKSET>738ql zifI&(*=;sTZ@aC%W|M75g#y8tJ^K|TTfywp;e`fpz>qZ_9aBc?t zdx%ydPvxDJp{6v-gCZ$ zwg93&@zEgy)p&+5xMULXk9Z^!@zWo^kzO5x6JXFvZ3}b-lKBu)0b0}oRQs?Xg_X8J zVDL}>GAf{#ZOHN9^&CEuWXzeZRdA-$TXSA zTyAg7UTgD`XM$p`sNI+XFy-H=pXDiF)+*1xSKuy_eJQwK&}B-LNaZVYbV=an#_j+toS=8{40wmH@x< zE*pN#Zm6|8dt`L^`D2N5d7ReHm3MZ!-Rx2MyzkaynPrB`YAv-szUb(GUBnu4z4~Lw zg3uuRYfK@?0WXFb8jZ$O*^Kq&+Af*D$DHF@wq z618lY`Y=9PH8y$twtsm{abj_qwcE0s0JXTBCfj@l1ig6gpRjSScfLd3v3->5)lL2e zu=rGo#YnK!O@Y+;W7qWg@DJZTs12Y!kJbpBt7P|+#hB)Uck+8bY`E` z_>zUcIjGPOrc3sb#%-}r{0D`m6NFxE-E z4^CKbiTr?Kkjm>8pn0<|3qJ|)xXe1Vys>nzZo%S(n z3BL1nQ^u2>GcBDcv?Nw_GMmp%FZm_6+2i7y{TnYZpC80lJKz`!SR2q-G95qVCmq5?5F0IW{Z7q(u zP&zV2T>*>nx+nUP%vAWGI3^W|fXO*duJ!Pdkfpd{wK3{zgbwYdKsCYYl|;P?a*e{7 zFtqw*wQHsQ{lwTZiRW5V($Ck+W6HlYq|uv`Sz|JRNf_RQ8g$fZ1#x=YOvI>PIXK{k zgwZ(AA7(rrHh@Yyv)SR!GR^^E2!%raSi0PR)mCE@`)-X(rnb>WhsI*2V8M`U(^L9; z3|7%8XdlEet|K%q$6!pqtdD8+S* zuBSvDIUG1Yo|`+-SvP>)zbf`M&+&M19a-LIWvLV zp7C_7v2g#ipSA9fzobbA^@K_i=>5JlJF7OjMW)gQdmP{HPmnYYHyR-;%KNpnOrbSp z1bm9mjQ39im z>7zHOT>4$F63z?hF8aFy(ADd}{sdiF?4v>~=EEE`S?362PXhkoTgk*WdAcp#DA(z4 zAZfZDh)@VvkBmO3HIYP4YgKT*v-2Y@L`H!S>X=gx|Nem|Z7tk_2rl9)X`s!Jmd$ol zRZak3{WpJhb?<&P8hF`%7Y-{hKxQZBD=@+et*r-*Wxi~tpsuTu%STF>&yoz~cD3ZY zjq)~`qYlJR_lbTyG&8Yc7dAwAYVTLNIuY9)(H&dYfBs;yWi(KQ5iv1$51|!r@D)o3 z?6->R*OnPlZZ4;T^t=anoCC@SdwrS|wsE*I-3YuA=WZ+(rxtQuFrK79!te2!=eiy_ zIJs(jw`>kdrC6+1Sth5)Ophml6pH(s2bv1sv+Hh8Jvi|#gEg~0W0v7`*kTRUlgAd< z;Wk6!q!QFx%#8WE(fef6J9Vl717V;AblGDyS&9$qa@ypKi=;&pXTYLFN`L;aSSji<%iiF zT$zap$e#XcDG6VwJuF^8_#u0YC3$`)^$GGMSzAJJGW|7R9J7Wp5&Q-Sw} zIkQF??gERH;J7ZM&rqhTwM^_SREGja(;j?1#Xv4hS8}6QXrKn(@g`|x(i`V;bVacD zT5^t*A#wMY+x((^nK{<_R!>S!p7ar)!@Y9foLUBwTLZx$IhyX1>OxBC7U z=46F5`OCacraqW4(PcS|8{NT%Gws0_9335~*S<0Ss3F0dqN%*NhGOCJi??u5C#??$ z=KTG4#+`G!((wpE@#!$=wBo~@A00lI{+50)Pt$q+6`pAm2A?uF{obi2BPRQ4FfMid z0$M{dY75P@0yUr>lg6sC;CNJw3cWO#PE`N^^1SbepjzF6W)`RcD$w0tuglKckNrwQ zrJifNK;zBOg4+9CRqx2*%wJkWU74MflU`3cYg%?s?XOqI?yi}u#WPr$yMI=kA(o%o zDCB(T9j|d6!%`V1P(A%3iNA$`ebOB*{T*{3tM;&eqzs$bCx_R578g^1_x5BNIZ3%uiheQNyPoMaXl%fLe)1XOHob0oPImUp{%btG@$Gt`-YJoO zZexi$vHT12*U!$x^6!yIB)6ECct5^`avX|PI;%!R!$0Ycr1Mp{^SWO(Q>IabiMp$a zP37&FTu(zwdEDokqpB%%p|Vf@bWFi;S&4{d@q0wIm3(`wUHCbd6`Z87dxkTe&@jNaZF21ynXoR=+wkv& z40@+ViQ|yZRm%?1#6D;T&moGXx=i)IXtVWVTCCqGB zAp9`;dNK(2zmo{^_a``;ibY??rcXdmez33nyPWvOPwJ8X^*>sl^8OFF!I1jKAC}uK zasOG6zrHXvL>oCz-Jz|!S>tZ#J2)5-z&~^{FB|3`n|;|Zvm>##iAW1_mR|F0e-2)Q3)t< z6V6s>&z`quT=Cs|UMMrsZ0Iz&^tVLxlgVZB#v_I-H-m*rP9M}>RoT1>T_~(=7ELr!L>)X?KoJTa`#Fy)DOmZ3RlnfqC_8zXWC#s~JU8i3%o1o~b z|CzTS^Coif{C=XA{La=*Y#=Zh9r`J~A9JyLVx?H9_d>5yX;NSQ^E5IYiO=(CcY`j8 zP0U#K`B%$o^(eX>;91^rl;*|cTBk||YWYt#6T?6ZKw<75>qrQ1n`^o9`0D+_DTSi`k^ay+Rw~gblPpy$6Sun`lys%)UgH9vERS*<3?Zt z=48(*x7K7@w-(_or% znRavG8xE6aw)<^Yq}|DqD@9Hm)9-gG@Ke`-PT{Nov~neB^vp3=fBkDJ&PIl9mFpmD zb*tV(K^`BiZX02G)6EpM{Ad=DsJ}Yu9e~7Az}OA9^)j&hd_HW=oxeIfduq2R&lU;9 z?mV=y$ke7df^K8`w1EV2IiFzE13hamb%Jjd;DLAw8ep(POs7Azcy;eb$HrM7HM?^1 zyp3yV%xN;EpvZ~}o1m6bbQc}om%^XK;!*soIyElAZ%zo~kr#=2kYj8{_uOxA?}xt3 zIL*~BhoQI|9LxE_WN74e*|Qrhz=8mpk;`;Oo#hwP2X9QIhkKwid;)MP;I4>2w^yZ% zu50*o`tL&8_Q^bH!s7kz(%|F#7J$I#bTpUJ*OKY=i{y{t>=X1GOMV|T@^h|r*STo6 zvWWGx@!RqI_eBl&B|eXxQf8}pQTTUakrCOGei}j9-umZ8~6*xr53yP!AmIU}o zF~oqlFDk~B%)wOA75{>|*?g^A1~!9?f(KxFw^FmhiJpOKwPVkGLKMOXYR^HhTsNapvbir`2BHM`GQGV8plfY2|CHOj zA(7;nDp0uIn~dnzH6ynTvf1xc&3mnQ*8i}&h}+LHc6c1YUHl~!Xa$T2;w)DLwRF8i zuPXXg+D5CL^|Q>i#L1W!9-rrD5h}>y36Iy@R9G49ZJ!y_&{vipHP?H~v-#jTb$}aV z@9b4JjlK)O8_yw( zYRpzObc?4Es)r^`M)qm|g+HY)>HU)eABHFyPOIA5*R|E@8QcRt?bC0VZHh-1rU={Y zxR1EENAva%30U6`ZkF1;R=)sY9E7OJ>N>@db^9=@BL00=qt>O!^Dl^>uWJP9^^EqD zvv)ah^$3m%Os@}`6yBU8z7X)1Ovc0G9{Qv|(+cAW(s%2CIQE(Y`?ah33%%)Fu7BGJ z0Hb8?GfYPy)jVku(lXLF?)ljljDr+8uUmoPDVX&RynrEUd^W3gS^A%AoCPw&@G9~; z`T6FtKZT|<{7zTq0WMK94k`bzAdb!>+~jT|&*YWNWH`CxQuxQYG}ob6chyA~%a_qp zuf4}RKB2#pFmE+~{x5wE>tierEeJ^!rDA2r`-M z{v*&|y0z7>wBRnBispgwrC6~=+4X*Flk!_F(Hm(gjQ{M1>8cTVR$VEXtVmMp_5E5$ zcti??DqqcZszZyn_$jMYr&+dG?NJ@Tu9<`7U)OzN2wm;07%z){=FnXW*cQo`mqx&# z(L;O#>cC9eJji8p{NungsU4pA-{o;0%|NB0Faw0_UU|X+l}6FW5XR8M>|1= z06nlA+pdR)rSVMQ%!Eo?sZul&hhgD@za&T!5|9rZh^LfHUtqa*1Sm`%|9A|eRC3w& zV5-<^^$P>e+GMVTRco25jiOWnb@H&=Y5Q%t*#Vm7qKErN@_t%+%j-!q=kLWusfiL5 zJd5R@Up%$>CTD7TLQoj!RtKWBZ-@6MO|Dvj#_DLEfo_|fw8nv|*Mz5nhaJGG#YtV` z$;?#C^@|c|D@0R(dE_|zEauPdiUZV8{646D$u{`9QY55Bp;-81(6W-p1V}W%K2Rl* z84X40u!1daLx3+{_Jw25>f-^|D(gAWbqL4i)GU!no1TofOTQ{G!QpRs4l z$Bbp@8@*pa!D|K+sg)y|UN&-uidtH5wBGql+gr)s3%sW`)~RYSNH!vp5zH%8zh_k* z83%?Bhw_7H-d*C%KR@Cxn7sR7OYSC5`8N5(WLz?tN`pi;ja~Ads!V2~^fvsmTLS4& zsrIMg(u7!L74#D1HC+trIXKY7XmQ9bjR$B*8I(>x1*^NuPZ(? zluROj?}@|ipe_bqh9Ay-9O)bv11Ax~F0N4x|L?*9{~aVZwuB!Yu3=zwhlM%)9Kh*$ z*x{vOjHpx(HMa6XcMd^L0cKkx!#QE_Dae})r_o~eN!bK~?C>52?I3v#RuaVfNU;q^ zWBQ2pZP3wA#n#NV9Ljj<=7-2)_ri2{L^!fJE^`)8U&lKuV%a$LS#(ELW15A&Y_VC{ zpaVu}vC60UY?Lh7XK9E>E6$RNHnqp<9QGqDc;MVHxK{AX z`L6DS*V-6@geJ2MA>f=<87VQmj^N0N6QahcVmKQDN&|293@%+Ef{tdw%1JmJyI4{Q zN~7s8R`8^+R{VB9tDVwc$${Dia3-!*xjAPDU zph}55iZEms=jO>$ja3I%I=~cy~00q^=FNbxw zoRlc7(Axv1XkR}CN+9br%+?tK_M4lu2&HD@k5<`}d2V!N zD|sl~+4UA9G!8M9+7fj#Zzr3<1%8ohWGBHM-8M!n>KwQDVlJgPV(lFw^@I-OR!o&k zm^Uk`>Gw*v*$Qv)@-!a`Tt9&g54TWJwZlRVY3bE{V3@!Vt5Db3;qfzc&OX~9K%6)D!r|*7mQO|=Nh^vOqmudlUn>) zfF=36URfPZqCtFeKk`>DmJU3uB`@`8)0y08%#zjJ)*6kO-DmJ*Pbl1l+62mh(avNP zRbvCM<7EqQHx22!uvAlsIUykTitc6Ce4Io?C(5vOQ1%?PPOr5KohBjw?V=`evDNPo z1EPBs&2X?CLJ!kDfZ{~kHHb#A#|9)<>=7ARfZ7CvA{1f^3$7RE}wV^w_nYl1P38J+-{CEBvM(+Sf#2*Kg{ky)ZojGm9Hpx zH_&HMDQ1URYBnpvB6BRkhx*kY>-SG$K~QM|2JF~$EjIB16pA!EAuR4{`LJ{e(`Qp| zA^M^L-K`JL7yL_f{H**U$%ajEH^T5{TG1! zF)#fW+W{OCP8S5g&51yk(GOgoor(0|eLD5}IhONvJmB^82Pr$iMAO%Fc8b|@ZRh0u zH)9aMFK9Q_KO|OsrZE{!uU-j>u}>W)1>{FGP@NQ&@H`pW0-(sn;YOa%H>E#4K^cfY z6wkv%;?nOQAH~=!QtPcH0UycMH*(+3AhfTCGk~=ddh|PEwvh;Ndd`Cf&%W2i^HvdJ z|4F=Lwdpxhe|p7tJ#N~WE zGx`PSBoNv{MMd8`T_@`MT|9l5P1sC6x(SdIP{}d^pwyoL1@wVXsg^NVpZ_(P<_+Th z006|-E<^b`15}NxLnO8>oO^19#ihqk5WCl3zQ0X&0&sAqZGN?IFxnr$Ta(l5d}-95 zG~U1L;oqOh%Q;%8Q%IoE`2>Yb9G6eC7?iao$kFl6Q~tHhPVW@&-fTx%qw!lAEH1+U zMNolU<^a&pkZah?`qnT}a88I7#qG*C-)sVi{z=AcjRuxw01kb5>b4nT+~^4d0RPPQMKyu!Z}yB3{$_LnxdAeVJh^u7iAuo91o(&$Dc1oSF#%YG`qh3EX4vvN z+=&Odd%hTDWkidzZ=`cNF2MF3nZF7M7Qp2i#RV~w?DqD1%I4_xg~8$8xnwiqm1g{BWgC z2wkJu+7~E~&I7qn(PCv0Twm!u;OlXSs(uK^l&LC=sGFNQ@Jz-Mjk=G`P}!Hs?yUqJw#b8mrARH03*pzGJ{G8 z8U;1rDU0D!lck7y<;m0IrRpLdWIy5iD!;4Hs0g;1ptFBxbq{@c-(6h;=x`>S<0rc- z^=Heqd&||LlFuy;{jl|XW^*wNQUa0*VKb7xc;@v1waAURI*(0Ucs;b@S@mM$V!PkZSBalbZ0f4D|SdCu1 zPY;f15SP%m}_VzxTN{bvexv6p+biW}5}^UGMpsluT874KmoU_Lm#4 zpLg{}6HOvau3T!g$!FI9TDXzpTsAa{^#X_`X11}_U>q$8O)6VFfHMx}&q1M(e(xa) zk%)wBP3TPb6>`O8jvlbh%oe(x8;F2?5Q3FVpFSO!F0tF}mC?>Gxe7v1VCA$Q#8<-! zw`=KUalY8?M640Du2fO1E0e6 zcZ@cK`->gGv2bbO^*s9F ziJApEvWCt;4(9zBH4l7|nx28=tKe}dxqo?`=ryVO#bu`#Aep=VyPHovp^88g&%N1- zyV-bYa7#K@;*K#nMHWv^7Eh#$PTKV*psXZ%E{sTkwxx**IXVjUH4kk%@-Gd6CYk?i z?ZgPu{2VxnN04b8Uh^liceJZR$$iGJu8+$;b%56{FlBt7$;3gEWsP~WI9by}Zocc# zEssjM&hdD@4_3{W`!8=%Dpt$krEcPralq^4X?qyTl?bC0(nJ0XL}(2g@O=k1qk! z?$Got;P)%ES-;<#u5U-QYzBOPaB3ygJFWihv@^FOx{#8-78V)C!j3;Jdnj?8FXGHt zr@+kQ`Qn(42FGU#>+o714;~?vcH{{LqSc0E8Hz4~lks3+Sl}Dpv?DrK*Uck*Pn1)kKSv_s4Qw z#tDt%uFP4v40~bi6o)Si>(H5U`(ud5cNmO`9qn$8TqUwk)X3FDN558lZWWbGAg9Gw zE8CdD!=nNP%!HN3L_!cZiHamrdu~0-Je3D}kg8~U0hfE@L_p-RqNj}^mZkPAQldbP zcWr8OK3$ov9&o} z(o0={)p{j6qbm5p*I}=VZS^e__6& zm*#&d&rnbsH3Sm#sbAv^9uHTvGEF#n>pcG?s{O!zCaiS21N7DHPi2*%P$<(&lRVr# z_yr~M&v$hZ_nn!D-)l;;j7gjH?oU*;ZVg{Xw7q=mWUnXo`cdo9DT|(mINf{ z&|NPSc!QND_`M*;?awc+%k0y2w>$ov=hxTMiE=btpL;Gsj70Bm_Y}5G4S z*I<7j>VO5;<3Of6rIe=@ZL6CD%i-A+ohd>nh=~Uaf+Hy~9&^H1VD3oB>1ZhrB6Gy) zy=I+3trLb$lWCjYkCLHM!MR1l%4YLnPAEu7W{73-4G=vkE_JESof5Q;F4C9t4obd+To9kU)99ygalO2hE{lhVF>l zVAz~!VD|wNWaBpo@4MkeUA8W-nANB?&J$+#kYElKF2m{BA{X|9c2W)I`rlTNYgh_f zbR83v|J~bT#r(a*B9eQF0*S{NewE?=3r}-wsl2$9K>{HgUVnVTGsH)wa5l3g`BvxS z2?Y+~>Syj?j+aC|%K0v*L0^)R%+A^dwI#woz*kxLk^jRdYHJI&#E>X+pSP#7$}@#MQ8OXqe>>6J^vqO*CdD~ta@HL_K3XH4Cw zAZ8sD;)z&xK{D0uH$WB`t}lkXF;`*&m`iSowIW_PoHI}h=;p^C?DNj1jV_rOys#L| z(-=_=y%1Bn$VBZVx3}XnrNIyzNS+5>FXKPdvd_Td@`l(T9Oj|m%x@=R=eKFMO(@%< z&G7_1c`zFXjnD&HD;$DB>xf zm6|R41yOBI@{B@_C4a=Naz%}WJu^MHFG=wfRF5a~%y+j*<)%g8=qM3(2aDmcQSsRKN}Lh(6Syu{~PJ zi@k6~=#jPNj6n`*v^Q3aP~VpER1+!}i9b{(39<>@u}56=^ORoLo!6D#ED0hTt3>2{ z1y^9)@nT8cqEtWEM-;f>r3%UytSi+}dAz&}oUbX7D^V*Ev^SQSH94KsLWv_!Y2U+s zmQkKKg)$V~zr^7CU0aPrdoGcjwV%O?A7gw+ooOQT8rCbFlnq;K~2u^^|2anQuY;wAS*yHBcA;4Sa1TFfesu2AGKvDJ*p z5PQFa{0=-iw1hfDTN+kV#!2mdIJ-ahGs#5=@6=hd&f^YuJDv9Wq}EV!4?o7M9ySW8K=9oPJs1bvU}25@ljwpIUB?7<;6heoPqgAVq;w5rcO{sP0bvr zYoaXIsR2Hhv#6QUMSiY&E6E>h;@}_%?|_GQedxQmka~-@Smp&dtHrSr$Q}uE&}1cU z5C#dZt3|p4@PfFR`PXmZ#v@0ffj7^k<<_!h)MNO6{W1uGODw3~%NL(EmdF5U=0Nhl zUwu0;2tp+%h{Zrd!*l<^WFYvzLg3%epRAC-D@NDtqUU`0*Sq-pxdeDw-?7{DuWCM%p_3M;m9_FeqI_FhH$@IhDA)NnH zI0A5P_y@bcJT;=4i^L1+4L6+@d2YfNT6g6uH(82caT+i$w;q-RA|oOSDDh?rY*VTf zfXk#P#bY^|_Lg#>;NfMB8Xl~V=Jc5^byIBZ>=;vWXxMr^dn6c-Bm*i#TSAp*hSL3d z)#B>V=J9%(i-SWUGLn}7KePFPo%=0Z{e3-?yqBk>9(V1j;rzH#P`2pQVqk{CB*yPF zDx$wPyKBhlyaRgtXha5ckhnjoV0a~hMyDaV==3CRp+=JvY9cr8Q_-Fa^h}q;zV{%_ z2`nrmPhob%`ndN9Wn^c|ASv@AcDlf_KUK9_WZ-|%UoiyDg7dNo>A~UL*0v)E1|vU+ zHf*|pUVUxr@~_M6uV*RHAIj?4mmy}iCy(QWw#Y?Nk&$~_`%Dt$+iO{?MXMbIS0ipJ zg|BP+4g){n=55jSE1H}=ij~V)0d5oQ<12J!wRC%_WRzRF72T3BT7txHE^sVAwYyA`|aXR@+# z*s3>Kf{BP7O2>V%ocE%keFZ0#Q(_r7rbrWijE&;F|9QP1 z05b{b=Mn~JY|P2l$J^`u0C+Z=m25OC&N09

pNdpE=GZl{k5(V5qXJ?VA4EY;fTk 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 He!%|%Lb4(X literal 0 HcmV?d00001 diff --git a/packages/independent/lighthouse-impact/docs/comment-links-highlighted.png b/packages/independent/lighthouse-impact/docs/comment-links-highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..137673ff3059bf7b4d825f421911d9d2ec80afd7 GIT binary patch literal 51091 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

Lighthouse impact

`, + ...(impactAnalysisEnabled + ? [ + ` +${renderBody({ + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + pullRequestBase, + pullRequestHead, +})} +`, + ] + : []), + ...(beforeMergeGist + ? [ + renderGistLinks({ + pullRequestBase, + beforeMergeGist, + afterMergeGist, + }), + ] + : []), + ]; + + const body = bodyLines.join(` +`); + + return { warnings, body }; +}; + +const renderBody = ({ + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + pullRequestBase, + pullRequestHead, +}) => { + return Object.keys(afterMergeLighthouseReport.categories).map( + (categoryName) => { + return renderCategory(categoryName, { + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + pullRequestBase, + pullRequestHead, + }); + }, + ).join(` + +`); +}; + +const renderCategory = ( + category, + { beforeMergeLighthouseReport, afterMergeLighthouseReport }, +) => { + const beforeMergeDisplayedScore = scoreToDisplayedScore( + beforeMergeLighthouseReport.categories[category].score, + ); + const afterMergeDisplayedScore = scoreToDisplayedScore( + afterMergeLighthouseReport.categories[category].score, + ); + const diff = afterMergeDisplayedScore - beforeMergeDisplayedScore; + const diffDisplayValue = diff === 0 ? "no impact" : formatNumericDiff(diff); + + const summaryText = `${category} score: ${afterMergeDisplayedScore} (${diffDisplayValue})`; + + return `
+ ${summaryText} + ${ + category === "performance" + ? `
Keep in mind performance score variation may be caused by external factors. Learn more.
` + : "" + } + ${renderCategoryAudits(category, { + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + })} +
`; +}; + +const scoreToDisplayedScore = (floatingNumber) => + Math.round(floatingNumber * 100); + +const renderCategoryAudits = ( + category, + { beforeMergeLighthouseReport, afterMergeLighthouseReport }, +) => { + const { auditRefs } = afterMergeLighthouseReport.categories[category]; + const audits = []; + auditRefs.forEach((auditRef) => { + const auditId = auditRef.id; + const beforeMergeAudit = beforeMergeLighthouseReport.audits[auditId]; + const afterMergeAudit = afterMergeLighthouseReport.audits[auditId]; + const beforeMergeAuditOutput = renderAudit(beforeMergeAudit); + const afterMergeAuditOutput = renderAudit(afterMergeAudit); + + // both are not applicable + if (beforeMergeAuditOutput === null && afterMergeAuditOutput === null) { + return; + } + + // becomes applicable + if (beforeMergeAuditOutput === null && afterMergeAuditOutput !== null) { + audits.push([ + `${auditId}`, + `---`, + `---`, + `${afterMergeAuditOutput}`, + ]); + return; + } + + // becomes unapplicable + if (beforeMergeAuditOutput !== null && afterMergeAuditOutput === null) { + audits.push([ + `${auditId}`, + `---`, + `${beforeMergeAuditOutput}`, + `---`, + ]); + return; + } + + if ( + typeof beforeMergeAuditOutput === "number" && + typeof afterMergeAuditOutput === "number" + ) { + const diff = afterMergeAuditOutput - beforeMergeAuditOutput; + + audits.push([ + `${auditId}`, + `${diff === 0 ? "none" : formatNumericDiff(diff)}`, + `${beforeMergeAuditOutput}`, + `${afterMergeAuditOutput}`, + ]); + return; + } + + audits.push([ + `${auditId}`, + `${ + beforeMergeAuditOutput === afterMergeAuditOutput ? "none" : "---" + }`, + `${beforeMergeAuditOutput}`, + `${afterMergeAuditOutput}`, + ]); + }); + + return ` + + + + + + + + + + + ${audits.map( + (cells) => ` + ${cells.join(` + `)}`, + ).join(` + + `)} + + +
${category} auditimpactbefore mergeafter merge
`; +}; + +const renderAudit = (audit) => { + const { scoreDisplayMode } = audit; + + if (scoreDisplayMode === "manual") { + return null; + } + + if (scoreDisplayMode === "notApplicable") { + return null; + } + + if (scoreDisplayMode === "informative") { + const { displayValue } = audit; + if (typeof displayValue !== "undefined") return displayValue; + + const { numericValue } = audit; + if (typeof numericValue !== "undefined") return numericValue; + + return null; + } + + if (scoreDisplayMode === "binary") { + const { score } = audit; + return score ? "✔" : "☓"; + } + + if (scoreDisplayMode === "numeric") { + const { score } = audit; + return scoreToDisplayedScore(score); + } + + if (scoreDisplayMode === "error") { + return "error"; + } + + return null; +}; + +const renderGistLinks = ({ + beforeMergeGist, + afterMergeGist, + pullRequestBase, +}) => { + return ` + Impact analyzed comparing ${pullRequestBase} report and report after merge +
`; +}; + +const gistIdToReportUrl = (gistId) => { + return `https://googlechrome.github.io/lighthouse/viewer/?gist=${gistId}`; +}; diff --git a/packages/independent/lighthouse-impact/src/pr_impact/format_numeric_diff.js b/packages/independent/lighthouse-impact/src/pr_impact/format_numeric_diff.js new file mode 100644 index 0000000000..c1f165efc6 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/pr_impact/format_numeric_diff.js @@ -0,0 +1,14 @@ +const enDecimalFormatter = new Intl.NumberFormat("en", { style: "decimal" }); + +export const formatNumericDiff = (valueAsNumber) => { + const valueAsAbsoluteNumber = Math.abs(valueAsNumber); + const valueAsString = enDecimalFormatter.format(valueAsAbsoluteNumber); + + if (valueAsNumber < 0) { + return `-${valueAsString}`; + } + if (valueAsNumber > 0) { + return `+${valueAsString}`; + } + return valueAsString; +}; diff --git a/packages/independent/lighthouse-impact/src/pr_impact/jsenv_comment_parameters.js b/packages/independent/lighthouse-impact/src/pr_impact/jsenv_comment_parameters.js new file mode 100644 index 0000000000..697c41d342 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/pr_impact/jsenv_comment_parameters.js @@ -0,0 +1 @@ +export const jsenvCommentParameters = {}; diff --git a/packages/independent/lighthouse-impact/src/pr_impact/patch_or_post_gists.js b/packages/independent/lighthouse-impact/src/pr_impact/patch_or_post_gists.js new file mode 100644 index 0000000000..a14a1ebd84 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/pr_impact/patch_or_post_gists.js @@ -0,0 +1,153 @@ +import * as githubRESTAPI from "@jsenv/github-pull-request-impact/src/internal/github_rest_api.js"; +import { createDetailedMessage } from "@jsenv/humanize"; + +// https://developer.github.com/v3/gists/#create-a-gist + +export const patchOrPostGists = async ({ + logger, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + existingComment, +}) => { + let beforeMergeGistId; + let afterMergeGistId; + + if (existingComment) { + const gistIds = gistIdsFromComment(existingComment); + if (gistIds) { + beforeMergeGistId = gistIds.beforeMergeGistId; + afterMergeGistId = gistIds.afterMergeGistId; + logger.debug( + createDetailedMessage(`gists found in comment body`, { + "before merging gist with lighthouse report": + gistIdToUrl(beforeMergeGistId), + "after merging gist with lighthouse report": + gistIdToUrl(afterMergeGistId), + }), + ); + } else { + logger.debug(`cannot find gist id in comment body`); + } + } + + logger.debug(`update or create both gists.`); + let [beforeMergeGist, afterMergeGist] = await Promise.all([ + beforeMergeGistId + ? githubRESTAPI.GET({ + url: `https://api.github.com/gists/${beforeMergeGistId}`, + githubToken, + }) + : null, + afterMergeGistId + ? githubRESTAPI.GET({ + url: `https://api.github.com/gists/${afterMergeGistId}`, + githubToken, + }) + : null, + ]); + + const beforeMergeGistBody = createGistBody(beforeMergeLighthouseReport, { + repositoryOwner, + repositoryName, + pullRequestNumber, + beforeMerge: true, + }); + if (beforeMergeGist) { + logger.info(`updating base gist at ${gistIdToUrl(beforeMergeGist.id)}`); + beforeMergeGist = await githubRESTAPI.PATCH({ + url: `https://api.github.com/gists/${beforeMergeGist.id}`, + githubToken, + body: beforeMergeGistBody, + }); + logger.info(`base gist updated`); + } else { + logger.info(`creating base gist`); + beforeMergeGist = await githubRESTAPI.POST({ + url: `https://api.github.com/gists`, + githubToken, + body: beforeMergeGistBody, + }); + logger.info(`base gist created at ${gistIdToUrl(beforeMergeGist.id)}`); + } + + const afterMergeGistBody = createGistBody(afterMergeLighthouseReport, { + repositoryOwner, + repositoryName, + pullRequestNumber, + beforeMerge: false, + }); + if (afterMergeGist) { + logger.info( + `updating after merge gist at ${gistIdToUrl(afterMergeGist.id)}`, + ); + afterMergeGist = await githubRESTAPI.PATCH({ + url: `https://api.github.com/gists/${afterMergeGist.id}`, + githubToken, + body: afterMergeGistBody, + }); + logger.info(`after merge gist updated`); + } else { + logger.info(`creating after merge gist`); + afterMergeGist = await githubRESTAPI.POST({ + url: `https://api.github.com/gists`, + githubToken, + body: afterMergeGistBody, + }); + logger.info( + `after merge gist created at ${gistIdToUrl(afterMergeGist.id)}`, + ); + } + + return { + beforeMergeGist, + afterMergeGist, + }; +}; + +const createGistBody = ( + lighthouseReport, + { repositoryOwner, repositoryName, pullRequestNumber, beforeMerge }, +) => { + return { + files: { + [`${repositoryOwner}_${repositoryName}_pr_${pullRequestNumber}_${ + beforeMerge ? "before_merge" : "after_merge" + }_lighthouse_report.json`]: { + content: JSON.stringify(lighthouseReport, null, " "), + }, + }, + public: false, + }; +}; + +const beforeMergeGistIdRegex = //; +const afterMergeGistIdRegex = //; + +const gistIdsFromComment = (comment) => { + const beforeMergeGistIdMatch = comment.body.match(beforeMergeGistIdRegex); + if (!beforeMergeGistIdMatch) { + return null; + } + + const afterMergeGistIdMatch = comment.body.match(afterMergeGistIdRegex); + if (!afterMergeGistIdMatch) { + return null; + } + + const beforeMergeGistId = beforeMergeGistIdMatch[1]; + const afterMergeGistId = afterMergeGistIdMatch[1]; + return { + beforeMergeGistId, + afterMergeGistId, + }; +}; + +const gistIdToUrl = (gistId) => { + return `https://gist.github.com/${gistId}`; +}; diff --git a/packages/independent/lighthouse-impact/src/report_lighthouse_impact_in_github_pr.js b/packages/independent/lighthouse-impact/src/report_lighthouse_impact_in_github_pr.js new file mode 100644 index 0000000000..ee16c2d075 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/report_lighthouse_impact_in_github_pr.js @@ -0,0 +1,117 @@ +import { importOneExportFromFile } from "@jsenv/dynamic-import-worker"; +import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem"; +import { commentGitHubPullRequestImpact } from "@jsenv/github-pull-request-impact"; +import { createLighthouseImpactComment } from "./pr_impact/createLighthouseImpactComment.js"; +import { patchOrPostGists } from "./pr_impact/patchOrPostGists.js"; + +export const reportLighthouseImpactInGithubPullRequest = async ({ + logLevel, + commandLogs = false, + cancelOnSIGINT, + rootDirectoryUrl, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + installCommand = "npm install", + lighthouseReportUrl, + + runLink, + commitInGeneratedByInfo, + catchError, + skipGistWarning = false, +}) => { + rootDirectoryUrl = assertAndNormalizeDirectoryUrl(rootDirectoryUrl); + if (typeof lighthouseReportUrl === "string") { + lighthouseReportUrl = new URL(lighthouseReportUrl, rootDirectoryUrl).href; + } else if (lighthouseReportUrl instanceof URL) { + } else { + throw new TypeError( + `lighthouseReportUrl must be a string or an url but received ${lighthouseReportUrl}`, + ); + } + + return commentGitHubPullRequestImpact({ + logLevel, + commandLogs, + // lighthouse report are super verbose, do not log them + infoLogs: false, + cancelOnSIGINT, + rootDirectoryUrl, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + + collectInfo: async ({ execCommandInRootDirectory }) => { + await execCommandInRootDirectory(installCommand); + const lighthouseReport = + await importOneExportFromFile(lighthouseReportUrl); + return { version: 1, data: lighthouseReport }; + }, + commentIdentifier: ``, + createCommentForComparison: async ({ + logger, + + pullRequestBase, + pullRequestHead, + beforeMergeData, + afterMergeData, + existingComment, + }) => { + const gistWarnings = []; + + let beforeMergeGist; + let afterMergeGist; + try { + const gistResult = await patchOrPostGists({ + logger, + + githubToken, + repositoryOwner, + repositoryName, + pullRequestNumber, + + beforeMergeLighthouseReport: beforeMergeData, + afterMergeLighthouseReport: afterMergeData, + existingComment, + }); + beforeMergeGist = gistResult.beforeMergeGist; + afterMergeGist = gistResult.afterMergeGist; + } catch (e) { + if (e.responseStatus === 403) { + if (!skipGistWarning) { + gistWarnings.push( + `**Warning:** Link to lighthouse reports cannot be generated because github token is not allowed to create gists.`, + ); + } + } else { + throw e; + } + } + + const comment = createLighthouseImpactComment({ + pullRequestBase, + pullRequestHead, + beforeMergeLighthouseReport: beforeMergeData, + afterMergeLighthouseReport: afterMergeData, + beforeMergeGist, + afterMergeGist, + }); + + return { + warnings: [...gistWarnings, ...comment.warnings], + body: comment.body, + }; + }, + generatedByLink: { + url: "https://github.com/jsenv/workflow/tree/main/packages/lighthouse-impact", + text: "@jsenv/lighthouse-impact", + }, + runLink, + commitInGeneratedByInfo, + catchError, + }); +}; diff --git a/packages/independent/lighthouse-impact/src/run_lighthouse_on_playwright_page.js b/packages/independent/lighthouse-impact/src/run_lighthouse_on_playwright_page.js new file mode 100644 index 0000000000..993ec08e0f --- /dev/null +++ b/packages/independent/lighthouse-impact/src/run_lighthouse_on_playwright_page.js @@ -0,0 +1,70 @@ +import { generateLighthouseReport } from "./generate/generate_lighthouse_report.js"; + +export const runLighthouseOnPlaywrightPage = async ( + page, + { chromiumDebuggingPort, ...options }, +) => { + const url = page.url(); + const userAgent = await page.evaluate(() => { + /* eslint-disable no-undef */ + return navigator.userAgent; + /* eslint-enable no-undef */ + }); + const deviceScaleFactor = await page.evaluate(() => { + /* eslint-disable no-undef */ + return window.devicePixelRatio; + /* eslint-enable no-undef */ + }); + const { screenWidth, screenHeight } = await page.evaluate(() => { + /* eslint-disable no-undef */ + return { + screenWidth: window.screen.width, + screenHeight: window.screen.height, + }; + /* eslint-enable no-undef */ + }); + // see "isMobile" into https://playwright.dev/docs/api/class-browser#browser-new-context + const viewportMetaTakenIntoAccount = await page.evaluate(() => { + /* eslint-disable no-undef */ + let mutate; + const viewportWidthNow = window.innerWidth; + const viewportMeta = document.head.querySelector("meta[name=viewport]"); + if (viewportMeta) { + mutate = () => { + const content = viewportMeta.content; + viewportMeta.setAttribute("content", `width=${viewportWidthNow + 1}`); + return () => { + viewportMeta.setAttribute("content", content); + }; + }; + } else { + mutate = () => { + const viewportMeta = document.createElement("meta"); + viewportMeta.name = "viewport"; + viewportMeta.setAttribute("content", `width=${viewportWidthNow + 1}`); + document.head.appendChild(viewportMeta); + return () => { + document.head.removeChild(viewportMeta); + }; + }; + } + const cleanup = mutate(); + const viewportWidthAfter = window.innerWidth; + cleanup(); + const viewportMetaTakenIntoAccount = + viewportWidthAfter !== viewportWidthNow; + return viewportMetaTakenIntoAccount; + /* eslint-enable no-undef */ + }); + + const report = await generateLighthouseReport(url, { + chromiumDebuggingPort, + emulatedScreenWidth: screenWidth, + emulatedScreenHeight: screenHeight, + emulatedDeviceScaleFactor: deviceScaleFactor, + emulatedMobile: viewportMetaTakenIntoAccount, + emulatedUserAgent: userAgent, + ...options, + }); + return report; +}; diff --git a/packages/independent/lighthouse-impact/tests/comment/comment.test.mjs b/packages/independent/lighthouse-impact/tests/comment/comment.test.mjs new file mode 100644 index 0000000000..39ab842f0b --- /dev/null +++ b/packages/independent/lighthouse-impact/tests/comment/comment.test.mjs @@ -0,0 +1,31 @@ +/** + +This test is meant to work like this: + +It reads comment_snapshot.md and ensure regenerating it gives the same output. +The goal is to force dev to regenerate comment_snapshot.md and ensure it looks correct +before commiting it. + +-> This is snapshot testing to force a human review when comment is modified. + +*/ + +import { readFileSync } from "node:fs"; +import { assert } from "@jsenv/assert"; + +const commentSnapshotFileUrl = new URL( + "./comment_snapshot.md", + import.meta.url, +); +const readCommentSnapshotFile = () => { + const fileContent = String(readFileSync(commentSnapshotFileUrl)); + return fileContent; +}; + +// disable on windows because it would fails due to line endings (CRLF) +if (process.platform !== "win32") { + const expect = readCommentSnapshotFile(); + await import("./generate_comment_snapshot_file.mjs"); + const actual = readCommentSnapshotFile(); + assert({ actual, expect }); +} diff --git a/packages/independent/lighthouse-impact/tests/comment/comment_snapshot.md b/packages/independent/lighthouse-impact/tests/comment/comment_snapshot.md new file mode 100644 index 0000000000..6c14447de0 --- /dev/null +++ b/packages/independent/lighthouse-impact/tests/comment/comment_snapshot.md @@ -0,0 +1,616 @@ +# basic + + + +

Lighthouse impact

+ +
+ perf score: 90 (+10) + + + + + + + + + + + + + + + + + + + + + + + + + +
perf auditimpactbefore mergeafter merge
whatever+205070
foo---
+
+ + + Impact analyzed comparing base report and report after merge +
+ +# version mismatch + +--- + +**Warning:** Impact analysis skipped because lighthouse version are different on `base` (1.0.0) and `head` (1.0.1). + +--- + + + +

Lighthouse impact

+ + Impact analyzed comparing base report and report after merge +
+ +# real + + + +

Lighthouse impact

+ +
+ performance score: 99 (no impact) +
Keep in mind performance score variation may be caused by external factors. Learn more.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
performance auditimpactbefore mergeafter merge
first-contentful-paintnone9696
first-meaningful-paintnone9696
speed-indexnone100100
interactivenone100100
first-cpu-idlenone100100
max-potential-fidnone9999
estimated-input-latencynone100100
total-blocking-timenone100100
render-blocking-resourcesnone8787
uses-responsive-imagesnone100100
offscreen-imagesnone100100
unminified-cssnone100100
unminified-javascriptnone7575
unused-css-rulesnone100100
uses-optimized-imagesnone100100
uses-webp-imagesnone100100
uses-text-compressionnone5858
uses-rel-preconnectnone100100
time-to-first-bytenone
redirectsnone100100
uses-rel-preloadnone100100
efficient-animated-contentnone100100
total-byte-weightnone100100
uses-long-cache-ttlnone3737
dom-sizenone100100
critical-request-chainsnone2 chains found2 chains found
bootup-timenone100100
mainthread-work-breakdownnone100100
font-displaynone
resource-summarynone3 requests • 190 KB3 requests • 190 KB
network-requestsnone33
network-rttnone0 ms0 ms
network-server-latencynone0 ms0 ms
main-thread-tasksnone33
metricsnone1946.99751946.9975
+
+ +
+ accessibility score: 84 (no impact) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
accessibility auditimpactbefore mergeafter merge
button-namenone
color-contrastnone
document-titlenone
html-has-langnone
+
+ +
+ best-practices score: 86 (no impact) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
best-practices auditimpactbefore mergeafter merge
appcache-manifestnone
is-on-httpsnone
uses-http2none
uses-passive-event-listenersnone
no-document-writenone
external-anchors-use-rel-noopenernone
geolocation-on-startnone
doctypenone
no-vulnerable-librariesnone
js-librariesnone
notification-on-startnone
deprecationsnone
password-inputs-can-be-pasted-intonone
errors-in-consolenone
image-aspect-rationone
+
+ +
+ seo score: 60 (no impact) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
seo auditimpactbefore mergeafter merge
viewportnone
document-titlenone
meta-descriptionnone
http-status-codenone
link-textnone
is-crawlablenone
hreflangnone
font-sizenone
pluginsnone
tap-targetsnone
+
+ +
+ pwa score: 33 (no impact) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pwa auditimpactbefore mergeafter merge
load-fast-enough-for-pwanone
works-offlinenone
offline-start-urlnone
is-on-httpsnone
service-workernone
installable-manifestnone
redirects-httpnone
splash-screennone
themed-omniboxnone
content-widthnone
viewportnone
without-javascriptnone
apple-touch-iconnone
+
+ + + Impact analyzed comparing base report and report after merge +
diff --git a/packages/independent/lighthouse-impact/tests/comment/generate_comment_snapshot_file.mjs b/packages/independent/lighthouse-impact/tests/comment/generate_comment_snapshot_file.mjs new file mode 100644 index 0000000000..60522239fe --- /dev/null +++ b/packages/independent/lighthouse-impact/tests/comment/generate_comment_snapshot_file.mjs @@ -0,0 +1,108 @@ +/** + +https://github.com/actions/toolkit/tree/master/packages/exec +https://github.com/actions/toolkit/tree/master/packages/core + +*/ + +import { createGitHubPullRequestCommentText } from "@jsenv/github-pull-request-impact"; +import { createLighthouseImpactComment } from "@jsenv/lighthouse-impact/src/pr_impact/create_lighthouse_impact_comment.js"; +import { jsenvCommentParameters } from "@jsenv/lighthouse-impact/src/pr_impact/jsenv_comment_parameters.js"; +import { readFileSync, writeFileSync } from "node:fs"; + +const generateComment = (data) => { + return createGitHubPullRequestCommentText( + createLighthouseImpactComment({ + pullRequestBase: "base", + pullRequestHead: "head", + ...jsenvCommentParameters, + ...data, + }), + ); +}; + +const normalReport = JSON.parse( + String( + readFileSync( + new URL("./lighthouse_report_examples/normal.json", import.meta.url), + ), + ), +); + +const examples = { + "basic": generateComment({ + beforeMergeLighthouseReport: { + audits: { + whatever: { + score: 0.5, + scoreDisplayMode: "numeric", + description: "whatever description", + }, + foo: { + score: 0, + scoreDisplayMode: "binary", + description: "foo description", + }, + }, + categories: { + perf: { + score: 0.8, + description: "Total perf score", + }, + }, + }, + afterMergeLighthouseReport: { + audits: { + whatever: { + score: 0.7, + scoreDisplayMode: "numeric", + description: "whatever description", + }, + foo: { + score: 1, + scoreDisplayMode: "binary", + description: "foo description", + }, + }, + categories: { + perf: { + score: 0.9, + auditRefs: [{ id: "whatever" }, { id: "foo" }], + description: "Total perf score", + }, + }, + }, + beforeMergeGist: { id: "base" }, + afterMergeGist: { id: "head" }, + }), + "version mismatch": generateComment({ + beforeMergeLighthouseReport: { + lighthouseVersion: "1.0.0", + }, + afterMergeLighthouseReport: { + lighthouseVersion: "1.0.1", + }, + beforeMergeGist: { id: "base" }, + afterMergeGist: { id: "head" }, + }), + "real": generateComment({ + beforeMergeLighthouseReport: normalReport, + afterMergeLighthouseReport: normalReport, + beforeMergeGist: { id: "base" }, + afterMergeGist: { id: "head" }, + }), +}; + +const exampleFileUrl = new URL("./comment_snapshot.md", import.meta.url); +const exampleFileContent = Object.keys(examples).map((exampleName) => { + return `# ${exampleName} + +${examples[exampleName]}`; +}).join(` + +`); +writeFileSync( + exampleFileUrl, + `${exampleFileContent} +`, +); diff --git a/packages/independent/lighthouse-impact/tests/comment/lighthouse_report_examples/fcp-error.json b/packages/independent/lighthouse-impact/tests/comment/lighthouse_report_examples/fcp-error.json new file mode 100644 index 0000000000..ab6772e248 --- /dev/null +++ b/packages/independent/lighthouse-impact/tests/comment/lighthouse_report_examples/fcp-error.json @@ -0,0 +1,5081 @@ +{ + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/84.0.4147.89 Safari/537.36", + "environment": { + "networkUserAgent": "", + "hostUserAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/84.0.4147.89 Safari/537.36", + "benchmarkIndex": 1204 + }, + "lighthouseVersion": "6.1.1", + "fetchTime": "2020-07-24T11:56:57.743Z", + "requestedUrl": "https://127.0.0.1:36625/", + "finalUrl": "https://127.0.0.1:36625/", + "runWarnings": [ + "Something went wrong with recording the trace over your page load. Please run Lighthouse again. (NO_FCP)" + ], + "runtimeError": { + "code": "NO_FCP", + "message": "Something went wrong with recording the trace over your page load. Please run Lighthouse again. (NO_FCP)" + }, + "audits": { + "is-on-https": { + "id": "is-on-https", + "title": "Uses HTTPS", + "description": "All sites should be protected with HTTPS, even ones that don't handle sensitive data. This includes avoiding [mixed content](https://developers.google.com/web/fundamentals/security/prevent-mixed-content/what-is-mixed-content), where some resources are loaded over HTTP despite the initial request being servedover HTTPS. HTTPS prevents intruders from tampering with or passively listening in on the communications between your app and your users, and is a prerequisite for HTTP/2 and many new web platform APIs. [Learn more](https://web.dev/is-on-https/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "redirects-http": { + "id": "redirects-http", + "title": "Redirects HTTP traffic to HTTPS", + "description": "If you've already set up HTTPS, make sure that you redirect all HTTP traffic to HTTPS in order to enable secure web features for all your users. [Learn more](https://web.dev/redirects-http/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required HTTPRedirect gatherer did not run." + }, + "service-worker": { + "id": "service-worker", + "title": "Registers a service worker that controls page and `start_url`", + "description": "The service worker is the technology that enables your app to use many Progressive Web App features, such as offline, add to homescreen, and push notifications. [Learn more](https://web.dev/service-worker/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ServiceWorker gatherer did not run." + }, + "works-offline": { + "id": "works-offline", + "title": "Current page responds with a 200 when offline", + "description": "If you're building a Progressive Web App, consider using a service worker so that your app can work offline. [Learn more](https://web.dev/works-offline/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Offline gatherer did not run." + }, + "viewport": { + "id": "viewport", + "title": "Has a `` tag with `width` or `initial-scale`", + "description": "Add a `` tag to optimize your app for mobile screens. [Learn more](https://web.dev/viewport/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required MetaElements gatherer did not run." + }, + "without-javascript": { + "id": "without-javascript", + "title": "Contains some content when JavaScript is not available", + "description": "Your app should display some content when JavaScript is disabled, even if it's just a warning to the user that JavaScript is required to use the app. [Learn more](https://web.dev/without-javascript/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required HTMLWithoutJavaScript gatherer did not run." + }, + "first-contentful-paint": { + "id": "first-contentful-paint", + "title": "First Contentful Paint", + "description": "First Contentful Paint marks the time at which the first text or image is painted. [Learn more](https://web.dev/first-contentful-paint/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "largest-contentful-paint": { + "id": "largest-contentful-paint", + "title": "Largest Contentful Paint", + "description": "Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn More](https://web.dev/lighthouse-largest-contentful-paint/)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "first-meaningful-paint": { + "id": "first-meaningful-paint", + "title": "First Meaningful Paint", + "description": "First Meaningful Paint measures when the primary content of a page is visible. [Learn more](https://web.dev/first-meaningful-paint/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "load-fast-enough-for-pwa": { + "id": "load-fast-enough-for-pwa", + "title": "Page load is fast enough on mobile networks", + "description": "A fast page load over a cellular network ensures a good mobile user experience. [Learn more](https://web.dev/load-fast-enough-for-pwa/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "speed-index": { + "id": "speed-index", + "title": "Speed Index", + "description": "Speed Index shows how quickly the contents of a page are visibly populated. [Learn more](https://web.dev/speed-index/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "screenshot-thumbnails": { + "id": "screenshot-thumbnails", + "title": "Screenshot Thumbnails", + "description": "This is what the load of your site looked like.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "final-screenshot": { + "id": "final-screenshot", + "title": "Final Screenshot", + "description": "The last screenshot captured of the pageload.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "estimated-input-latency": { + "id": "estimated-input-latency", + "title": "Estimated Input Latency", + "description": "Estimated Input Latency is an estimate of how long your app takes to respond to user input, in milliseconds, during the busiest 5s window of page load. If your latency is higher than 50 ms, users may perceive your app as laggy. [Learn more](https://web.dev/estimated-input-latency/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "total-blocking-time": { + "id": "total-blocking-time", + "title": "Total Blocking Time", + "description": "Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "max-potential-fid": { + "id": "max-potential-fid", + "title": "Max Potential First Input Delay", + "description": "The maximum potential First Input Delay that your users could experience is the duration of the longest task. [Learn more](https://web.dev/lighthouse-max-potential-fid/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "cumulative-layout-shift": { + "id": "cumulative-layout-shift", + "title": "Cumulative Layout Shift", + "description": "Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more](https://web.dev/cls/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "errors-in-console": { + "id": "errors-in-console", + "title": "No browser errors logged to the console", + "description": "Errors logged to the console indicate unresolved problems. They can come from network request failures and other browser concerns. [Learn more](https://web.dev/errors-in-console/)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ConsoleMessages gatherer did not run." + }, + "server-response-time": { + "id": "server-response-time", + "title": "Initial server response time was short", + "description": "Keep the server response time for the main document short because all other requests depend on it. [Learn more](https://web.dev/time-to-first-byte/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "first-cpu-idle": { + "id": "first-cpu-idle", + "title": "First CPU Idle", + "description": "First CPU Idle marks the first time at which the page's main thread is quiet enough to handle input. [Learn more](https://web.dev/first-cpu-idle/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "interactive": { + "id": "interactive", + "title": "Time to Interactive", + "description": "Time to interactive is the amount of time it takes for the page to become fully interactive. [Learn more](https://web.dev/interactive/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "user-timings": { + "id": "user-timings", + "title": "User Timing marks and measures", + "description": "Consider instrumenting your app with the User Timing API to measure your app's real-world performance during key user experiences. [Learn more](https://web.dev/user-timings/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "critical-request-chains": { + "id": "critical-request-chains", + "title": "Avoid chaining critical requests", + "description": "The Critical Request Chains below show you what resources are loaded with a high priority. Consider reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load. [Learn more](https://web.dev/critical-request-chains/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "redirects": { + "id": "redirects", + "title": "Avoid multiple page redirects", + "description": "Redirects introduce additional delays before the page can be loaded. [Learn more](https://web.dev/redirects/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "installable-manifest": { + "id": "installable-manifest", + "title": "Web app manifest does not meet the installability requirements", + "description": "Browsers can proactively prompt users to add your app to their homescreen, which can lead to higher engagement. [Learn more](https://web.dev/installable-manifest/).", + "score": 0, + "scoreDisplayMode": "binary", + "explanation": "Failures: No manifest was fetched.", + "details": { + "type": "debugdata", + "items": [ + { + "failures": ["No manifest was fetched"], + "isParseFailure": true, + "parseFailureReason": "No manifest was fetched" + } + ] + } + }, + "apple-touch-icon": { + "id": "apple-touch-icon", + "title": "Provides a valid `apple-touch-icon`", + "description": "For ideal appearance on iOS when users add a progressive web app to the home screen, define an `apple-touch-icon`. It must point to a non-transparent 192px (or 180px) square PNG. [Learn More](https://web.dev/apple-touch-icon/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required LinkElements gatherer did not run." + }, + "splash-screen": { + "id": "splash-screen", + "title": "Is not configured for a custom splash screen", + "description": "A themed splash screen ensures a high-quality experience when users launch your app from their homescreens. [Learn more](https://web.dev/splash-screen/).", + "score": 0, + "scoreDisplayMode": "binary", + "explanation": "Failures: No manifest was fetched.", + "details": { + "type": "debugdata", + "items": [ + { + "failures": ["No manifest was fetched"], + "isParseFailure": true, + "parseFailureReason": "No manifest was fetched" + } + ] + } + }, + "themed-omnibox": { + "id": "themed-omnibox", + "title": "Sets a theme color for the address bar.", + "description": "The browser address bar can be themed to match your site. [Learn more](https://web.dev/themed-omnibox/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required MetaElements gatherer did not run." + }, + "maskable-icon": { + "id": "maskable-icon", + "title": "Manifest doesn't have a maskable icon", + "description": "A maskable icon ensures that the image fills the entire shape without being letterboxed when installing the app on a device. [Learn more](https://web.dev/maskable-icon-audit/).", + "score": 0, + "scoreDisplayMode": "binary", + "explanation": "No manifest was fetched" + }, + "content-width": { + "id": "content-width", + "title": "Content is sized correctly for the viewport", + "description": "If the width of your app's content doesn't match the width of the viewport, your app might not be optimized for mobile screens. [Learn more](https://web.dev/content-width/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ViewportDimensions gatherer did not run." + }, + "image-aspect-ratio": { + "id": "image-aspect-ratio", + "title": "Displays images with correct aspect ratio", + "description": "Image display dimensions should match natural aspect ratio. [Learn more](https://web.dev/image-aspect-ratio/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ImageElements gatherer did not run." + }, + "image-size-responsive": { + "id": "image-size-responsive", + "title": "Serves images with appropriate resolution", + "description": "Image natural dimensions should be proportional to the display size and the pixel ratio to maximize image clarity. [Learn more](https://web.dev/serve-responsive-images/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ImageElements gatherer did not run." + }, + "deprecations": { + "id": "deprecations", + "title": "Avoids deprecated APIs", + "description": "Deprecated APIs will eventually be removed from the browser. [Learn more](https://web.dev/deprecations/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required ConsoleMessages gatherer did not run." + }, + "mainthread-work-breakdown": { + "id": "mainthread-work-breakdown", + "title": "Minimizes main-thread work", + "description": "Consider reducing the time spent parsing, compiling and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://web.dev/mainthread-work-breakdown/)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "bootup-time": { + "id": "bootup-time", + "title": "JavaScript execution time", + "description": "Consider reducing the time spent parsing, compiling, and executing JS. You may find delivering smaller JS payloads helps with this. [Learn more](https://web.dev/bootup-time/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "uses-rel-preload": { + "id": "uses-rel-preload", + "title": "Preload key requests", + "description": "Consider using `` to prioritize fetching resources that are currently requested later in page load. [Learn more](https://web.dev/uses-rel-preload/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "uses-rel-preconnect": { + "id": "uses-rel-preconnect", + "title": "Preconnect to required origins", + "description": "Consider adding `preconnect` or `dns-prefetch` resource hints to establish early connections to important third-party origins. [Learn more](https://web.dev/uses-rel-preconnect/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "font-display": { + "id": "font-display", + "title": "All text remains visible during webfont loads", + "description": "Leverage the font-display CSS feature to ensure text is user-visible while webfonts are loading. [Learn more](https://web.dev/font-display/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "diagnostics": { + "id": "diagnostics", + "title": "Diagnostics", + "description": "Collection of useful page vitals.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "network-requests": { + "id": "network-requests", + "title": "Network Requests", + "description": "Lists the network requests that were made during page load.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "network-rtt": { + "id": "network-rtt", + "title": "Network Round Trip Times", + "description": "Network round trip times (RTT) have a large impact on performance. If the RTT to an origin is high, it's an indication that servers closer to the user could improve performance. [Learn more](https://hpbn.co/primer-on-latency-and-bandwidth/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "network-server-latency": { + "id": "network-server-latency", + "title": "Server Backend Latencies", + "description": "Server latencies can impact web performance. If the server latency of an origin is high, it's an indication the server is overloaded or has poor backend performance. [Learn more](https://hpbn.co/primer-on-web-performance/#analyzing-the-resource-waterfall).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "main-thread-tasks": { + "id": "main-thread-tasks", + "title": "Tasks", + "description": "Lists the toplevel main thread tasks that executed during page load.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "metrics": { + "id": "metrics", + "title": "Metrics", + "description": "Collects all available metrics.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "offline-start-url": { + "id": "offline-start-url", + "title": "`start_url` responds with a 200 when offline", + "description": "A service worker enables your web app to be reliable in unpredictable network conditions. [Learn more](https://web.dev/offline-start-url/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required StartUrl gatherer did not run." + }, + "performance-budget": { + "id": "performance-budget", + "title": "Performance budget", + "description": "Keep the quantity and size of network requests under the targets set by the provided performance budget. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "timing-budget": { + "id": "timing-budget", + "title": "Timing budget", + "description": "Set a timing budget to help you keep an eye on the performance of your site. Performant sites load fast and respond to user input events quickly. [Learn more](https://developers.google.com/web/tools/lighthouse/audits/budgets).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "resource-summary": { + "id": "resource-summary", + "title": "Keep request counts low and transfer sizes small", + "description": "To set budgets for the quantity and size of page resources, add a budget.json file. [Learn more](https://web.dev/use-lighthouse-for-performance-budgets/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required devtoolsLogs gatherer did not run." + }, + "third-party-summary": { + "id": "third-party-summary", + "title": "Minimize third-party usage", + "description": "Third-party code can significantly impact load performance. Limit the number of redundant third-party providers and try to load third-party code after your page has primarily finished loading. [Learn more](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "largest-contentful-paint-element": { + "id": "largest-contentful-paint-element", + "title": "Largest Contentful Paint element", + "description": "This is the largest contentful element painted within the viewport. [Learn More](https://web.dev/lighthouse-largest-contentful-paint/)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "layout-shift-elements": { + "id": "layout-shift-elements", + "title": "Avoid large layout shifts", + "description": "These DOM elements contribute most to the CLS of the page.", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required TraceElements gatherer did not run." + }, + "long-tasks": { + "id": "long-tasks", + "title": "Avoid long main-thread tasks", + "description": "Lists the longest tasks on the main thread, useful for identifying worst contributors to input delay. [Learn more](https://web.dev/long-tasks-devtools/)", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required traces gatherer did not run." + }, + "pwa-cross-browser": { + "id": "pwa-cross-browser", + "title": "Site works cross-browser", + "description": "To reach the most number of users, sites should work across every major browser. [Learn more](https://web.dev/pwa-cross-browser/).", + "score": null, + "scoreDisplayMode": "manual" + }, + "pwa-page-transitions": { + "id": "pwa-page-transitions", + "title": "Page transitions don't feel like they block on the network", + "description": "Transitions should feel snappy as you tap around, even on a slow network. This experience is key to a user's perception of performance. [Learn more](https://web.dev/pwa-page-transitions/).", + "score": null, + "scoreDisplayMode": "manual" + }, + "pwa-each-page-has-url": { + "id": "pwa-each-page-has-url", + "title": "Each page has a URL", + "description": "Ensure individual pages are deep linkable via URL and that URLs are unique for the purpose of shareability on social media. [Learn more](https://web.dev/pwa-each-page-has-url/).", + "score": null, + "scoreDisplayMode": "manual" + }, + "accesskeys": { + "id": "accesskeys", + "title": "`[accesskey]` values are unique", + "description": "Access keys let users quickly focus a part of the page. For proper navigation, each access key must be unique. [Learn more](https://web.dev/accesskeys/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-allowed-attr": { + "id": "aria-allowed-attr", + "title": "`[aria-*]` attributes match their roles", + "description": "Each ARIA `role` supports a specific subset of `aria-*` attributes. Mismatching these invalidates the `aria-*` attributes. [Learn more](https://web.dev/aria-allowed-attr/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-hidden-body": { + "id": "aria-hidden-body", + "title": "`[aria-hidden=\"true\"]` is not present on the document ``", + "description": "Assistive technologies, like screen readers, work inconsistently when `aria-hidden=\"true\"` is set on the document ``. [Learn more](https://web.dev/aria-hidden-body/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-hidden-focus": { + "id": "aria-hidden-focus", + "title": "`[aria-hidden=\"true\"]` elements do not contain focusable descendents", + "description": "Focusable descendents within an `[aria-hidden=\"true\"]` element prevent those interactive elements from being available to users of assistive technologies like screen readers. [Learn more](https://web.dev/aria-hidden-focus/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-input-field-name": { + "id": "aria-input-field-name", + "title": "ARIA input fields have accessible names", + "description": "When an input field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-input-field-name/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-required-attr": { + "id": "aria-required-attr", + "title": "`[role]`s have all required `[aria-*]` attributes", + "description": "Some ARIA roles have required attributes that describe the state of the element to screen readers. [Learn more](https://web.dev/aria-required-attr/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-required-children": { + "id": "aria-required-children", + "title": "Elements with an ARIA `[role]` that require children to contain a specific `[role]` have all required children.", + "description": "Some ARIA parent roles must contain specific child roles to perform their intended accessibility functions. [Learn more](https://web.dev/aria-required-children/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-required-parent": { + "id": "aria-required-parent", + "title": "`[role]`s are contained by their required parent element", + "description": "Some ARIA child roles must be contained by specific parent roles to properly perform their intended accessibility functions. [Learn more](https://web.dev/aria-required-parent/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-roles": { + "id": "aria-roles", + "title": "`[role]` values are valid", + "description": "ARIA roles must have valid values in order to perform their intended accessibility functions. [Learn more](https://web.dev/aria-roles/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-toggle-field-name": { + "id": "aria-toggle-field-name", + "title": "ARIA toggle fields have accessible names", + "description": "When a toggle field doesn't have an accessible name, screen readers announce it with a generic name, making it unusable for users who rely on screen readers. [Learn more](https://web.dev/aria-toggle-field-name/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-valid-attr-value": { + "id": "aria-valid-attr-value", + "title": "`[aria-*]` attributes have valid values", + "description": "Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid values. [Learn more](https://web.dev/aria-valid-attr-value/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "aria-valid-attr": { + "id": "aria-valid-attr", + "title": "`[aria-*]` attributes are valid and not misspelled", + "description": "Assistive technologies, like screen readers, can't interpret ARIA attributes with invalid names. [Learn more](https://web.dev/aria-valid-attr/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "button-name": { + "id": "button-name", + "title": "Buttons have an accessible name", + "description": "When a button doesn't have an accessible name, screen readers announce it as \"button\", making it unusable for users who rely on screen readers. [Learn more](https://web.dev/button-name/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "bypass": { + "id": "bypass", + "title": "The page contains a heading, skip link, or landmark region", + "description": "Adding ways to bypass repetitive content lets keyboard users navigate the page more efficiently. [Learn more](https://web.dev/bypass/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "color-contrast": { + "id": "color-contrast", + "title": "Background and foreground colors have a sufficient contrast ratio", + "description": "Low-contrast text is difficult or impossible for many users to read. [Learn more](https://web.dev/color-contrast/).", + "score": null, + "scoreDisplayMode": "error", + "errorMessage": "Required Accessibility gatherer did not run." + }, + "definition-list": { + "id": "definition-list", + "title": "`
`'s contain only properly-ordered `
` and `
` groups, `

YdxiOHNB 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.0.0" + }, + "publishConfig": { + "access": "public" + }, + "type": "module", + "exports": { + ".": { + "import": "./src/main.js" + }, + "./*": "./*" + }, + "main": "./src/main.js", + "files": [ + "/src/" + ], + "scripts": { + "snapshot": "node ./tests/comment/generate_comment_snapshot_file.mjs", + "test": "node ./scripts/test.mjs" + }, + "dependencies": { + "@jsenv/abort": "4.3.0", + "@jsenv/dynamic-import-worker": "1.2.1", + "@jsenv/filesystem": "4.10.2", + "@jsenv/github-pull-request-impact": "1.7.7", + "@jsenv/humanize": "1.2.8", + "lighthouse": "12.2.0" + } +} diff --git a/packages/independent/lighthouse-impact/scripts/test.mjs b/packages/independent/lighthouse-impact/scripts/test.mjs new file mode 100644 index 0000000000..7e7ef2d738 --- /dev/null +++ b/packages/independent/lighthouse-impact/scripts/test.mjs @@ -0,0 +1,12 @@ +import { executeTestPlan, nodeWorkerThread } from "@jsenv/test"; + +await executeTestPlan({ + rootDirectoryUrl: new URL("../", import.meta.url), + testPlan: { + "tests/**/*.test.mjs": { + node: { + runtime: nodeWorkerThread(), + }, + }, + }, +}); diff --git a/packages/independent/lighthouse-impact/src/generate/generate_lighthouse_report.js b/packages/independent/lighthouse-impact/src/generate/generate_lighthouse_report.js new file mode 100644 index 0000000000..6e98528de8 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/generate/generate_lighthouse_report.js @@ -0,0 +1,140 @@ +// https://github.com/GoogleChrome/lighthouse/blob/5a14deb5c4e0ec4e8e58f50ff72b53851b021bcf/docs/readme.md#using-programmatically + +import { Abort, raceProcessTeardownEvents } from "@jsenv/abort"; +import { assertAndNormalizeFileUrl, writeFileSync } from "@jsenv/filesystem"; +import { createLogger } from "@jsenv/humanize"; +import { + formatReportAsHtml, + formatReportAsJson, + formatReportAsSummaryText, + reduceToMedianReport, + runLighthouse, +} from "./lighthouse_api.js"; + +export const generateLighthouseReport = async ( + url, + { + signal = new AbortController().signal, + handleSIGINT = true, + logLevel, + + chromiumDebuggingPort, + // I'm pretty sure these options are given to lighthouse + // so that it knows how chrome is currently configured + // lighthouse won't actually enable the emulated screen width + // this should be done when starting chrome (with chrome-launcher) + // in that case I think pupeteer might be better with something like + // https://github.com/GoogleChrome/lighthouse/issues/14134#issuecomment-1158091067 + // see https://github.com/GoogleChrome/lighthouse/blob/78b93aacacb12ae10f14049c5a16bc48a431f5a6/core/config/constants.js#L70 + // and https://github.com/GoogleChrome/lighthouse/blob/78b93aacacb12ae10f14049c5a16bc48a431f5a6/core/config/desktop-config.js#L10 + emulatedScreenWidth, + emulatedScreenHeight, + emulatedDeviceScaleFactor, + emulatedMobile = true, + emulatedUserAgent, + throttling, + lighthouseSettings = {}, + + runCount = 1, + delayBetweenEachRun = 1_000, + + log = false, + jsonFileUrl, + jsonFileLog = true, + htmlFileUrl, + htmlFileLog = true, + } = {}, +) => { + if (chromiumDebuggingPort === undefined) { + throw new Error( + `"chromiumDebuggingPort" is required, got ${chromiumDebuggingPort}`, + ); + } + + const generateReportOperation = Abort.startOperation(); + generateReportOperation.addAbortSignal(signal); + if (handleSIGINT) { + generateReportOperation.addAbortSource((abort) => { + return raceProcessTeardownEvents( + { + SIGINT: true, + }, + abort, + ); + }); + } + + const jsenvGenerateLighthouseReport = async () => { + const logger = createLogger({ logLevel }); + if (generateReportOperation.signal.aborted) { + return { aborted: true }; + } + const lighthouseOptions = { + extends: "lighthouse:default", + port: chromiumDebuggingPort, + settings: { + formFactor: emulatedMobile ? "mobile" : "desktop", + throttling, + screenEmulation: { + mobile: emulatedMobile, + width: emulatedScreenWidth, + height: emulatedScreenHeight, + deviceScaleFactor: emulatedDeviceScaleFactor, + disabled: false, + }, + emulatedUserAgent, + ...lighthouseSettings, + }, + }; + const reports = []; + try { + await Array(runCount) + .fill() + .reduce(async (previous, _, index) => { + generateReportOperation.throwIfAborted(); + await previous; + if (index > 0 && delayBetweenEachRun) { + await new Promise((resolve) => + setTimeout(resolve, delayBetweenEachRun), + ); + } + generateReportOperation.throwIfAborted(); + const report = await runLighthouse(url, lighthouseOptions); + reports.push(report); + }, Promise.resolve()); + } catch (e) { + if (Abort.isAbortError(e)) { + return { aborted: true }; + } + throw e; + } + + const lighthouseReport = await reduceToMedianReport(reports); + if (log) { + logger.info(formatReportAsSummaryText(lighthouseReport)); + } + if (jsonFileUrl) { + assertAndNormalizeFileUrl(jsonFileUrl); + const json = formatReportAsJson(lighthouseReport); + writeFileSync(jsonFileUrl, json); + if (jsonFileLog) { + logger.info(`-> ${jsonFileUrl}`); + } + } + if (htmlFileUrl) { + assertAndNormalizeFileUrl(htmlFileUrl); + const html = await formatReportAsHtml(lighthouseReport); + writeFileSync(htmlFileUrl, html); + if (htmlFileLog) { + logger.info(`-> ${htmlFileUrl}`); + } + } + return lighthouseReport; + }; + + try { + return await jsenvGenerateLighthouseReport(); + } finally { + await generateReportOperation.end(); + } +}; diff --git a/packages/independent/lighthouse-impact/src/generate/lighthouse_api.js b/packages/independent/lighthouse-impact/src/generate/lighthouse_api.js new file mode 100644 index 0000000000..ca572ffd92 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/generate/lighthouse_api.js @@ -0,0 +1,44 @@ +export const runLighthouse = async (url, lighthouseOptions) => { + const { default: lighthouse } = await import("lighthouse"); + const results = await lighthouse(url, undefined, lighthouseOptions); + // use results.lhr for the JS-consumeable output + // https://github.com/GoogleChrome/lighthouse/blob/master/types/lhr.d.ts + // use results.report for the HTML/JSON/CSV output as a string + // use results.artifacts for the trace/screenshots/other specific case you need (rarer) + const { lhr } = results; + const { runtimeError } = lhr; + if (runtimeError) { + const error = new Error(runtimeError.message); + Object.assign(error, runtimeError); + throw error; + } + return lhr; +}; + +export const reduceToMedianReport = async (lighthouseReports) => { + const { computeMedianRun } = await import( + "lighthouse/core/lib/median-run.js" + ); + return computeMedianRun(lighthouseReports); +}; + +export const formatReportAsSummaryText = (lighthouseReport) => { + const scores = {}; + Object.keys(lighthouseReport.categories).forEach((name) => { + scores[name] = lighthouseReport.categories[name].score; + }); + return JSON.stringify(scores, null, " "); +}; + +export const formatReportAsJson = (lighthouseReport) => { + const json = JSON.stringify(lighthouseReport, null, " "); + return json; +}; + +export const formatReportAsHtml = async (lighthouseReport) => { + const { ReportGenerator } = await import( + "lighthouse/report/generator/report-generator.js" + ); + const html = ReportGenerator.generateReportHtml(lighthouseReport); + return html; +}; diff --git a/packages/independent/lighthouse-impact/src/main.js b/packages/independent/lighthouse-impact/src/main.js new file mode 100644 index 0000000000..3e7ad713e9 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/main.js @@ -0,0 +1,3 @@ +export { readGitHubWorkflowEnv } from "@jsenv/github-pull-request-impact"; +export { reportLighthouseImpactInGithubPullRequest } from "./report_lighthouse_impact_in_github_pr.js"; +export { runLighthouseOnPlaywrightPage } from "./run_lighthouse_on_playwright_page.js"; diff --git a/packages/independent/lighthouse-impact/src/pr_impact/create_lighthouse_impact_comment.js b/packages/independent/lighthouse-impact/src/pr_impact/create_lighthouse_impact_comment.js new file mode 100644 index 0000000000..ac5c1e9207 --- /dev/null +++ b/packages/independent/lighthouse-impact/src/pr_impact/create_lighthouse_impact_comment.js @@ -0,0 +1,271 @@ +import { formatNumericDiff } from "./format_numeric_diff.js"; + +export const createLighthouseImpactComment = ({ + pullRequestBase, + pullRequestHead, + beforeMergeLighthouseReport, + afterMergeLighthouseReport, + beforeMergeGist, + afterMergeGist, +}) => { + const warnings = []; + + const beforeMergeVersion = beforeMergeLighthouseReport.lighthouseVersion; + const afterMergeVersion = afterMergeLighthouseReport.lighthouseVersion; + let impactAnalysisEnabled = true; + if (beforeMergeVersion !== afterMergeVersion) { + impactAnalysisEnabled = false; + warnings.push( + `**Warning:** Impact analysis skipped because lighthouse version are different on \`${pullRequestBase}\` (${beforeMergeVersion}) and \`${pullRequestHead}\` (${afterMergeVersion}).`, + ); + } + + const beforeMergeWarnings = beforeMergeLighthouseReport.runWarnings; + if (beforeMergeWarnings && beforeMergeWarnings.length) { + warnings.push( + `**Warning**: warnings produced while generating lighthouse report on \`${pullRequestBase}\`: +- ${beforeMergeWarnings.join(` +- `)}`, + ); + } + const afterMergeWarnings = afterMergeLighthouseReport.runWarnings; + if (afterMergeWarnings && afterMergeWarnings.length) { + warnings.push( + `**Warning**: warnings produced while generating lighthouse report after merge: +- ${afterMergeWarnings.join(` +- `)}`, + ); + } + + const bodyLines = [ + ...(beforeMergeGist + ? [``] + : []), + ...(afterMergeGist + ? [``] + : []), + `