snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
This commit is contained in:
25
webapp/scripts/CLAUDE.OPTIONAL.md
Normal file
25
webapp/scripts/CLAUDE.OPTIONAL.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# CLAUDE: `webapp/scripts/`
|
||||
|
||||
## Purpose
|
||||
- Node-based helpers for building, running, and maintaining the webapp (e.g., dev server orchestration, build pipelines, localization tooling).
|
||||
- Invoked via `npm`/`make` targets—direct execution should be rare.
|
||||
|
||||
## Key Scripts
|
||||
- `dev-server.mjs` – webpack-dev-server bootstrap; shares config with `make dev`.
|
||||
- `run.mjs`, `build.mjs`, `dist` helpers – orchestrate multi-workspace builds and env wiring.
|
||||
- `gen_lang_imports.mjs` – regenerates locale import lists.
|
||||
- `update-versions.sh` - updates version of published platform packages.
|
||||
|
||||
## Guidelines
|
||||
- Scripts should be idempotent and safe to run on CI and macOS/Linux dev machines.
|
||||
- Prefer ES modules + top-level `await` already used in existing scripts.
|
||||
- Keep configuration (ports, paths) sourced from `config.mk` or env vars instead of hard-coding.
|
||||
- Log actionable errors; exit with non-zero codes so CI fails fast.
|
||||
- When script behavior changes, update associated `Makefile` targets and `webapp/CLAUDE.md` command docs.
|
||||
|
||||
## References
|
||||
- `config.mk`, root `Makefile`, and workspace `package.json` scripts to understand entry points.
|
||||
- `webapp/STYLE_GUIDE.md → Automated style checking` for how scripts integrate with lint/test tooling.
|
||||
|
||||
|
||||
|
||||
48
webapp/scripts/build.mjs
Normal file
48
webapp/scripts/build.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import chalk from 'chalk';
|
||||
import concurrently from 'concurrently';
|
||||
|
||||
import {getExitCode, getPlatformCommands} from './utils.mjs';
|
||||
|
||||
async function buildAll() {
|
||||
console.log(chalk.inverse.bold('Building subpackages...') + '\n');
|
||||
|
||||
try {
|
||||
const {result} = concurrently(
|
||||
getPlatformCommands('build'),
|
||||
{
|
||||
killOthers: 'failure',
|
||||
},
|
||||
);
|
||||
|
||||
await result;
|
||||
} catch (closeEvents) {
|
||||
console.error(chalk.inverse.bold.red('Failed to build subpackages'), closeEvents);
|
||||
return getExitCode(closeEvents);
|
||||
}
|
||||
|
||||
console.log('\n' + chalk.inverse.bold('Subpackages built! Building web app...') + '\n');
|
||||
|
||||
// It's not necessary to run these commands through concurrently, but it makes the output consistent
|
||||
|
||||
try {
|
||||
const {result} = concurrently([
|
||||
{command: 'npm:build --workspace=channels', name: 'webapp', prefixColor: 'cyan'},
|
||||
]);
|
||||
await result;
|
||||
} catch (closeEvents) {
|
||||
console.error(chalk.inverse.bold.red('Failed to build web app'), closeEvents);
|
||||
return getExitCode(closeEvents);
|
||||
}
|
||||
|
||||
console.log('\n' + chalk.inverse.bold('Web app built!'));
|
||||
return 0;
|
||||
}
|
||||
|
||||
buildAll().then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
});
|
||||
470
webapp/scripts/check-external-links.mjs
Normal file
470
webapp/scripts/check-external-links.mjs
Normal file
@@ -0,0 +1,470 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
const MATTERMOST_URL_PATTERN = /https?:\/\/(?:[a-z0-9-]+\.)*mattermost\.com(?:[/?#][^"'\s<>()]*)?/gi;
|
||||
|
||||
const PERMALINK_PATTERN = /^https?:\/\/(www\.)?mattermost\.com\/pl\//;
|
||||
|
||||
const PUSH_SERVER_PATTERN = /^https?:\/\/(([a-z0-9-]+\.)?push|hpns-[a-z]+)\.mattermost\.com(\/|$)/;
|
||||
const ROOT_DOMAIN_PATTERN = /^https?:\/\/(www\.)?mattermost\.com\/?$/;
|
||||
/** Marketing forms; often slow or block bot User-Agents — not reliable to verify in CI. */
|
||||
const FORMS_MATTERMOST_PATTERN = /^https?:\/\/forms\.mattermost\.com\//i;
|
||||
|
||||
// Hosts whose bot-management (Cloudflare TLS fingerprint checks etc.) rejects
|
||||
// non-browser clients regardless of User-Agent spoofing, so we can't validate
|
||||
// them from CI.
|
||||
const UNCRAWLABLE_HOSTS = new Set([
|
||||
'support.mattermost.com', // Zendesk fronted by Cloudflare bot mgmt
|
||||
]);
|
||||
|
||||
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
||||
|
||||
const DIRECTORIES_TO_SCAN = [
|
||||
'channels/src',
|
||||
'platform/client/src',
|
||||
'platform/components/src',
|
||||
'platform/mattermost-redux/src',
|
||||
];
|
||||
|
||||
function getAllSourceFiles(dir, excludeTests = true) {
|
||||
const files = [];
|
||||
|
||||
function walk(currentDir) {
|
||||
if (!fs.existsSync(currentDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(currentDir, {withFileTypes: true});
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'coverage') {
|
||||
continue;
|
||||
}
|
||||
walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name);
|
||||
if (!SOURCE_EXTENSIONS.includes(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excludeTests && (entry.name.includes('.test.') || entry.name.includes('.spec.'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
function extractUrls(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const matches = content.match(MATTERMOST_URL_PATTERN) || [];
|
||||
|
||||
return matches.map((url) => {
|
||||
let cleanUrl = url;
|
||||
cleanUrl = cleanUrl.replace(/[',;)}\]]+$/, '');
|
||||
cleanUrl = cleanUrl.replace(/\\n$/, '');
|
||||
return cleanUrl;
|
||||
});
|
||||
}
|
||||
|
||||
function findAllMattermostUrls(rootDir, excludeTests = true) {
|
||||
const urlMap = new Map();
|
||||
|
||||
for (const dir of DIRECTORIES_TO_SCAN) {
|
||||
const fullDir = path.join(rootDir, dir);
|
||||
const files = getAllSourceFiles(fullDir, excludeTests);
|
||||
|
||||
for (const file of files) {
|
||||
const urls = extractUrls(file);
|
||||
for (const url of urls) {
|
||||
if (!urlMap.has(url)) {
|
||||
urlMap.set(url, []);
|
||||
}
|
||||
urlMap.get(url).push(path.relative(rootDir, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlMap;
|
||||
}
|
||||
|
||||
function isValidPermalink(url) {
|
||||
return PERMALINK_PATTERN.test(url) ||
|
||||
ROOT_DOMAIN_PATTERN.test(url) ||
|
||||
PUSH_SERVER_PATTERN.test(url);
|
||||
}
|
||||
|
||||
function findNonPermalinkUrls(urlMap) {
|
||||
const invalid = [];
|
||||
for (const [url, files] of urlMap) {
|
||||
if (!isValidPermalink(url)) {
|
||||
invalid.push({url, files});
|
||||
}
|
||||
}
|
||||
return invalid;
|
||||
}
|
||||
|
||||
const FETCH_HEADERS = {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; MattermostLinkChecker/1.0; +https://github.com/mattermost/mattermost)',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
};
|
||||
|
||||
const MAX_REDIRECTS = 10;
|
||||
|
||||
async function checkUrl(url, retries = 2) {
|
||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||
try {
|
||||
return await checkUrlWithRedirects(url);
|
||||
} catch (error) {
|
||||
if (attempt === retries) {
|
||||
return {
|
||||
url,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUrlWithRedirects(originalUrl) {
|
||||
let currentUrl = originalUrl;
|
||||
|
||||
for (let redirectCount = 0; redirectCount <= MAX_REDIRECTS; redirectCount++) {
|
||||
let response = await fetch(currentUrl, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
headers: FETCH_HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
// Some origins reject HEAD; retry with GET and let the outer loop
|
||||
// handle any 3xx the GET returns so we still follow the chain.
|
||||
if (response.status === 405 || response.status === 403) {
|
||||
await response.body?.cancel();
|
||||
response = await fetch(currentUrl, {
|
||||
method: 'GET',
|
||||
redirect: 'manual',
|
||||
headers: FETCH_HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
}
|
||||
|
||||
// Always release the response body — HEAD leaves it null (safe no-op),
|
||||
// but the GET fallback returns real bodies we never read. Leaving them
|
||||
// open pins sockets in undici's pool until finalization.
|
||||
try {
|
||||
const result = processResponse(originalUrl, response);
|
||||
|
||||
if (!result.redirect) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const location = response.headers.get('location');
|
||||
if (!location) {
|
||||
return {
|
||||
url: originalUrl,
|
||||
status: response.status,
|
||||
ok: false,
|
||||
error: 'Redirect without Location header',
|
||||
};
|
||||
}
|
||||
|
||||
currentUrl = new URL(location, currentUrl).href;
|
||||
} finally {
|
||||
await response.body?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: originalUrl,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: 'Too many redirects',
|
||||
};
|
||||
}
|
||||
|
||||
function processResponse(originalUrl, response) {
|
||||
const {status} = response;
|
||||
const cfHeader = response.headers.get('cf-mitigated');
|
||||
const isRedirect = status >= 300 && status < 400;
|
||||
|
||||
if (cfHeader && (isRedirect || status === 403 || status === 503)) {
|
||||
return {url: originalUrl, status, ok: true};
|
||||
}
|
||||
|
||||
// Follow every 3xx (including 301) to its final destination. Short-circuiting
|
||||
// 301s as "OK" hides real 404s at the chain's end
|
||||
if (isRedirect) {
|
||||
return {url: originalUrl, status, redirect: true};
|
||||
}
|
||||
|
||||
if (status === 429) {
|
||||
return {url: originalUrl, status, ok: true, rateLimited: true};
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
return {url: originalUrl, status, ok: true};
|
||||
}
|
||||
|
||||
return {url: originalUrl, status, ok: false};
|
||||
}
|
||||
|
||||
function shouldSkipUrlCheck(url) {
|
||||
if (PUSH_SERVER_PATTERN.test(url) || FORMS_MATTERMOST_PATTERN.test(url)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return UNCRAWLABLE_HOSTS.has(new URL(url).hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUrls(urls, concurrency = 5, silentProgress = false) {
|
||||
const results = [];
|
||||
const urlList = Array.from(urls.keys()).filter((url) => !shouldSkipUrlCheck(url));
|
||||
|
||||
for (let i = 0; i < urlList.length; i += concurrency) {
|
||||
const batch = urlList.slice(i, i + concurrency);
|
||||
const batchResults = await Promise.all(batch.map((url) => checkUrl(url)));
|
||||
results.push(...batchResults);
|
||||
|
||||
if (!silentProgress) {
|
||||
const completed = Math.min(i + concurrency, urlList.length);
|
||||
process.stderr.write(`\rChecking URLs: ${completed}/${urlList.length}`);
|
||||
}
|
||||
}
|
||||
if (!silentProgress) {
|
||||
process.stderr.write('\n');
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function printResults(results, urlMap, nonPermalinkUrls) {
|
||||
const broken = results.filter((r) => !r.ok);
|
||||
const rateLimited = results.filter((r) => r.ok && r.rateLimited);
|
||||
const working = results.filter((r) => r.ok && !r.rateLimited);
|
||||
|
||||
console.log('\n' + chalk.bold('=== External Link Check Results ===\n'));
|
||||
|
||||
console.log(chalk.green(`✓ ${working.length} URLs are accessible`));
|
||||
if (rateLimited.length > 0) {
|
||||
console.log(chalk.yellow(`⚠ ${rateLimited.length} URLs returned 429 (rate limited, skipped)`));
|
||||
}
|
||||
if (broken.length > 0) {
|
||||
console.log(chalk.red(`✗ ${broken.length} URLs are broken`));
|
||||
}
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
console.log(chalk.yellow(`⚠ ${nonPermalinkUrls.length} URLs are not using permalink format (warning)`));
|
||||
}
|
||||
console.log();
|
||||
|
||||
if (rateLimited.length > 0) {
|
||||
console.log(chalk.yellow.bold('Rate-limited URLs (warning only):\n'));
|
||||
for (const result of rateLimited) {
|
||||
console.log(chalk.yellow(` ${result.url}`));
|
||||
console.log(chalk.gray(` Status: HTTP 429 (Too Many Requests)`));
|
||||
console.log(chalk.gray(` Found in:`));
|
||||
for (const file of urlMap.get(result.url)) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
if (broken.length > 0) {
|
||||
console.log(chalk.red.bold('Broken URLs:\n'));
|
||||
for (const result of broken) {
|
||||
const statusText = result.error ? `Error: ${result.error}` : `HTTP ${result.status}`;
|
||||
console.log(chalk.red(` ${result.url}`));
|
||||
console.log(chalk.gray(` Status: ${statusText}`));
|
||||
console.log(chalk.gray(` Found in:`));
|
||||
for (const file of urlMap.get(result.url)) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
console.log(chalk.yellow.bold('URLs not using permalink format (warning):\n'));
|
||||
console.log(chalk.gray(' All mattermost.com links should route via https://mattermost.com/pl/\n'));
|
||||
for (const item of nonPermalinkUrls) {
|
||||
console.log(chalk.yellow(` ${item.url}`));
|
||||
console.log(chalk.gray(` Found in:`));
|
||||
for (const file of item.files) {
|
||||
console.log(chalk.gray(` - ${file}`));
|
||||
}
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
if (broken.length === 0 && nonPermalinkUrls.length === 0) {
|
||||
console.log(chalk.green.bold(`✓ All URLs are valid and accessible\n`));
|
||||
}
|
||||
|
||||
return broken.length > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
function generateMarkdownSummary(results, urlMap, nonPermalinkUrls) {
|
||||
const broken = results.filter((r) => !r.ok);
|
||||
const rateLimited = results.filter((r) => r.ok && r.rateLimited);
|
||||
const working = results.filter((r) => r.ok && !r.rateLimited);
|
||||
|
||||
const lines = [];
|
||||
|
||||
lines.push('## External Link Check Results\n');
|
||||
|
||||
if (broken.length === 0 && nonPermalinkUrls.length === 0 && rateLimited.length === 0) {
|
||||
lines.push(`✅ **All ${working.length} mattermost.com URLs are valid and accessible**\n`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
lines.push(`| Status | Count |`);
|
||||
lines.push(`|--------|-------|`);
|
||||
lines.push(`| ✅ Working | ${working.length} |`);
|
||||
if (rateLimited.length > 0) {
|
||||
lines.push(`| ⚠️ Rate limited / 429 (warning) | ${rateLimited.length} |`);
|
||||
}
|
||||
if (broken.length > 0) {
|
||||
lines.push(`| ❌ Broken | ${broken.length} |`);
|
||||
}
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
lines.push(`| ⚠️ Missing /pl/ prefix (warning) | ${nonPermalinkUrls.length} |`);
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
if (rateLimited.length > 0) {
|
||||
lines.push('### ⚠️ Rate-limited URLs (warning only)\n');
|
||||
lines.push('> These URLs returned HTTP 429 (Too Many Requests) and could not be verified. They are not treated as failures.\n');
|
||||
lines.push('| URL | Files |');
|
||||
lines.push('|-----|-------|');
|
||||
|
||||
for (const result of rateLimited) {
|
||||
const files = urlMap.get(result.url).map((f) => `\`${f}\``).join(', ');
|
||||
lines.push(`| ${result.url} | ${files} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (broken.length > 0) {
|
||||
lines.push('### Broken URLs\n');
|
||||
lines.push('| URL | Status | Files |');
|
||||
lines.push('|-----|--------|-------|');
|
||||
|
||||
for (const result of broken) {
|
||||
const statusText = result.error ? `Error: ${result.error}` : `HTTP ${result.status}`;
|
||||
const files = urlMap.get(result.url).map((f) => `\`${f}\``).join(', ');
|
||||
lines.push(`| ${result.url} | ${statusText} | ${files} |`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (nonPermalinkUrls.length > 0) {
|
||||
lines.push('### ⚠️ URLs Missing Permalink Format (warning)\n');
|
||||
lines.push('> All mattermost.com links should route via `https://mattermost.com/pl/`\n');
|
||||
lines.push('<details>\n<summary>Show URLs</summary>\n');
|
||||
lines.push('| URL | Files |');
|
||||
lines.push('|-----|-------|');
|
||||
|
||||
for (const item of nonPermalinkUrls) {
|
||||
const files = item.files.map((f) => `\`${f}\``).join(', ');
|
||||
lines.push(`| ${item.url} | ${files} |`);
|
||||
}
|
||||
lines.push('\n</details>');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const includeTests = args.includes('--include-tests');
|
||||
const jsonOutput = args.includes('--json');
|
||||
const markdownOutput = args.includes('--markdown');
|
||||
|
||||
const rootDir = process.cwd();
|
||||
|
||||
if (!markdownOutput) {
|
||||
console.log(chalk.inverse.bold(' Checking mattermost.com links in webapp... ') + '\n');
|
||||
|
||||
if (includeTests) {
|
||||
console.log(chalk.yellow('Including test files in scan\n'));
|
||||
}
|
||||
}
|
||||
|
||||
const urlMap = findAllMattermostUrls(rootDir, !includeTests);
|
||||
|
||||
if (!markdownOutput) {
|
||||
console.log(`Found ${chalk.bold(urlMap.size)} unique mattermost.com URLs\n`);
|
||||
}
|
||||
|
||||
if (urlMap.size === 0) {
|
||||
if (markdownOutput) {
|
||||
console.log('## External Link Check Results\n\n⚠️ No URLs found to check');
|
||||
} else {
|
||||
console.log(chalk.yellow('No URLs found to check'));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const nonPermalinkUrls = findNonPermalinkUrls(urlMap);
|
||||
const results = await checkUrls(urlMap, 5, markdownOutput);
|
||||
const brokenCount = results.filter((r) => !r.ok).length;
|
||||
|
||||
if (markdownOutput) {
|
||||
console.log(generateMarkdownSummary(results, urlMap, nonPermalinkUrls));
|
||||
return brokenCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (jsonOutput) {
|
||||
const output = {
|
||||
total: results.length,
|
||||
working: results.filter((r) => r.ok && !r.rateLimited).length,
|
||||
rateLimited: results.filter((r) => r.rateLimited).map((r) => ({
|
||||
url: r.url,
|
||||
status: r.status,
|
||||
files: urlMap.get(r.url),
|
||||
})),
|
||||
broken: results.filter((r) => !r.ok).map((r) => ({
|
||||
url: r.url,
|
||||
status: r.status,
|
||||
error: r.error,
|
||||
files: urlMap.get(r.url),
|
||||
})),
|
||||
nonPermalink: nonPermalinkUrls,
|
||||
};
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
return brokenCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
return printResults(results, urlMap, nonPermalinkUrls);
|
||||
}
|
||||
|
||||
main().then((exitCode) => {
|
||||
process.exitCode = exitCode;
|
||||
}).catch((error) => {
|
||||
console.error(chalk.red('Error:'), error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
40
webapp/scripts/dev-server.mjs
Normal file
40
webapp/scripts/dev-server.mjs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import chalk from 'chalk';
|
||||
import concurrently from 'concurrently';
|
||||
|
||||
import {getPlatformCommands} from './utils.mjs';
|
||||
|
||||
async function watchAllWithDevServer() {
|
||||
console.log(chalk.inverse.bold('Watching web app and all subpackages...'));
|
||||
|
||||
const commands = [
|
||||
{command: 'npm:dev-server --workspace=channels', name: 'webapp', prefixColor: 'cyan'},
|
||||
];
|
||||
|
||||
commands.push(...getPlatformCommands('run'));
|
||||
|
||||
console.log('\n');
|
||||
|
||||
const {result} = concurrently(
|
||||
commands,
|
||||
{
|
||||
killOthers: 'failure',
|
||||
},
|
||||
);
|
||||
|
||||
let exitCode = 0;
|
||||
try {
|
||||
await result;
|
||||
} catch (closeEvents) {
|
||||
exitCode = getExitCode(closeEvents, 0);
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
watchAllWithDevServer().then((exitCode) => {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
55
webapp/scripts/gen_lang_imports.mjs
Normal file
55
webapp/scripts/gen_lang_imports.mjs
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/bin/node
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
let lines = '';
|
||||
const langIDs = [];
|
||||
const langFiles = {};
|
||||
|
||||
fs.readdirSync('./channels/src/i18n').forEach(file => {
|
||||
if (file.endsWith('.json')) {
|
||||
const langID = file.substr(0, file.indexOf('.json'));
|
||||
|
||||
// Skip default language
|
||||
if (langID === 'en') {
|
||||
return
|
||||
}
|
||||
|
||||
langIDs.push(langID);
|
||||
lines += `import ${langID.replace('-', '')} from './${file}';\n`;
|
||||
langFiles[langID] = langID.replace('-', '');
|
||||
}
|
||||
});
|
||||
|
||||
// To generate the file exports we need to do a bit more work to handle ids with dashes and also output a map of literals rather than strings.
|
||||
lines += `
|
||||
// TypeScript thinks it's importing these language files' contents directly, but Webpack rewrites the above imports
|
||||
// to the URL the file for lazy loading. That's the reason for the ugly type assertions below.
|
||||
export const langFiles: {
|
||||
[langID: string]: string;
|
||||
} = {
|
||||
` + Object.keys(langFiles).reduce((out, id, idx) => {
|
||||
out += ' ';
|
||||
|
||||
if (id.includes('-')) {
|
||||
out += `'${id}': ${langFiles[id]}`;
|
||||
} else {
|
||||
out += `${id}: ${langFiles[id]}`;
|
||||
}
|
||||
|
||||
out += ' as unknown as string';
|
||||
|
||||
if (idx !== (langIDs.length - 1)) {
|
||||
out += ',';
|
||||
}
|
||||
|
||||
out += '\n';
|
||||
|
||||
return out;
|
||||
}, '') + '};';
|
||||
|
||||
const header = `// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.\n\n// DO NOT EDIT - This file is generated by gen_lang_imports through the gen-lang-imports script\n\n/* eslint-disable */\n\n`;
|
||||
|
||||
fs.writeFileSync('./channels/src/i18n/imports.ts', header+lines);
|
||||
|
||||
58
webapp/scripts/run.mjs
Normal file
58
webapp/scripts/run.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable no-console, no-process-env */
|
||||
|
||||
import chalk from 'chalk';
|
||||
import concurrently from 'concurrently';
|
||||
|
||||
import {makeRunner} from './runner.mjs';
|
||||
import {getExitCode, getPlatformCommands} from './utils.mjs';
|
||||
|
||||
async function watchAll(useRunner) {
|
||||
if (!useRunner) {
|
||||
console.log(chalk.inverse.bold('Watching web app and all subpackages...'));
|
||||
}
|
||||
|
||||
const commands = [
|
||||
{command: 'npm:run --workspace=channels', name: 'webapp', prefixColor: 'cyan'},
|
||||
];
|
||||
|
||||
commands.push(...getPlatformCommands('run'));
|
||||
|
||||
let runner;
|
||||
if (useRunner) {
|
||||
runner = makeRunner(commands);
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
|
||||
const {result, commands: runningCommands} = concurrently(
|
||||
commands,
|
||||
{
|
||||
killOthers: 'failure',
|
||||
outputStream: runner?.getOutputStream(),
|
||||
},
|
||||
);
|
||||
|
||||
runner?.addCloseListener(() => {
|
||||
for (const command of runningCommands) {
|
||||
command.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
|
||||
let exitCode = 0;
|
||||
try {
|
||||
await result;
|
||||
} catch (closeEvents) {
|
||||
exitCode = getExitCode(closeEvents, 0);
|
||||
}
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
const useRunner = process.argv[2] === '--runner' || process.env.MM_USE_WEBAPP_RUNNER;
|
||||
|
||||
watchAll(useRunner).then((exitCode) => {
|
||||
process.exit(exitCode);
|
||||
});
|
||||
|
||||
237
webapp/scripts/runner.mjs
Normal file
237
webapp/scripts/runner.mjs
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Writable} from 'node:stream';
|
||||
|
||||
import blessed from 'blessed';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
||||
class Runner {
|
||||
commands;
|
||||
filter = '';
|
||||
ui;
|
||||
|
||||
scrollLocked = true;
|
||||
|
||||
buffer = [];
|
||||
partialBuffer = '';
|
||||
|
||||
outputStream;
|
||||
|
||||
closeListeners = new Set();
|
||||
|
||||
constructor(commands) {
|
||||
this.commands = commands;
|
||||
this.outputStream = new Writable({
|
||||
write: (chunk, encoding, callback) => this.writeToStream(chunk, encoding, callback),
|
||||
});
|
||||
|
||||
this.makeUi(commands.map((command) => command.name), this.onFilter);
|
||||
this.registerHotkeys();
|
||||
}
|
||||
|
||||
// Initialization
|
||||
|
||||
makeUi(commandNames) {
|
||||
// Set up screen and output panes
|
||||
const screen = blessed.screen({
|
||||
smartCSR: true,
|
||||
dockBorders: true,
|
||||
});
|
||||
|
||||
const output = blessed.box({
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%-3',
|
||||
content: 'THE END IS NEVER '.repeat(1000),
|
||||
tags: true,
|
||||
alwaysScroll: true,
|
||||
scrollable: true,
|
||||
scrollbar: {
|
||||
ch: '#',
|
||||
style: {},
|
||||
track: {
|
||||
ch: '|',
|
||||
},
|
||||
},
|
||||
style: {},
|
||||
});
|
||||
|
||||
screen.append(output);
|
||||
|
||||
// Set up the menu bar
|
||||
const menu = blessed.listbar({
|
||||
top: '100%-3',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: 3,
|
||||
border: {
|
||||
type: 'line',
|
||||
},
|
||||
style: {
|
||||
item: {
|
||||
bg: 'red',
|
||||
hover: {
|
||||
bg: 'green',
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
bg: 'blue',
|
||||
},
|
||||
},
|
||||
tags: true,
|
||||
autoCommandKeys: true,
|
||||
mouse: true,
|
||||
});
|
||||
|
||||
menu.add('All', () => this.onFilter(''));
|
||||
for (const name of commandNames) {
|
||||
menu.add(name, () => this.onFilter(name));
|
||||
}
|
||||
|
||||
screen.append(menu);
|
||||
|
||||
this.ui = {
|
||||
menu,
|
||||
output,
|
||||
screen,
|
||||
};
|
||||
}
|
||||
|
||||
registerHotkeys() {
|
||||
this.ui.screen.key(['escape', 'q', 'C-c'], () => {
|
||||
for (const listener of this.closeListeners) {
|
||||
listener();
|
||||
}
|
||||
});
|
||||
|
||||
this.ui.screen.key(['up', 'down'], (char, key) => {
|
||||
this.scrollDelta(key.name === 'up' ? -1 : 1);
|
||||
});
|
||||
this.ui.screen.on('wheelup', () => {
|
||||
this.scrollDelta(-3);
|
||||
});
|
||||
this.ui.screen.on('wheeldown', () => {
|
||||
this.scrollDelta(3);
|
||||
});
|
||||
this.ui.screen.key('end', () => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
// Rendering and internal logic
|
||||
|
||||
renderUi() {
|
||||
const filtered = this.buffer.filter((line) => this.filter === '' || line.tag === this.filter);
|
||||
|
||||
this.ui.output.setContent(filtered.map((line) => this.formatLine(line)).join('\n'));
|
||||
|
||||
if (this.scrollLocked) {
|
||||
this.ui.output.scrollbar.style.inverse = true;
|
||||
this.ui.output.setScrollPerc(100);
|
||||
} else {
|
||||
this.ui.output.scrollbar.style.inverse = false;
|
||||
}
|
||||
|
||||
this.ui.screen.render();
|
||||
}
|
||||
|
||||
formatLine(line) {
|
||||
const color = this.commands.find((command) => command.name === line.tag)?.prefixColor;
|
||||
|
||||
return color ? `{bold}{${color}-fg}[${line.tag}]{/} ${line.text}` : `[${line.tag}] ${line.text}`;
|
||||
}
|
||||
|
||||
onFilter(newFilter) {
|
||||
this.filter = newFilter;
|
||||
|
||||
this.scrollLocked = true;
|
||||
this.renderUi();
|
||||
}
|
||||
|
||||
scrollDelta(delta) {
|
||||
this.ui.output.scroll(delta);
|
||||
|
||||
if (this.ui.output.getScrollPerc() >= 100 || this.ui.output.getScrollHeight() <= this.ui.output.height) {
|
||||
this.scrollLocked = true;
|
||||
} else {
|
||||
this.scrollLocked = false;
|
||||
}
|
||||
|
||||
this.renderUi();
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.scrollLocked = true;
|
||||
this.renderUi();
|
||||
}
|
||||
|
||||
// Terminal output handling
|
||||
|
||||
getOutputStream() {
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
writeToStream(chunk, encoding, callback) {
|
||||
const str = String(chunk);
|
||||
|
||||
if (str.includes('\n')) {
|
||||
const parts = str.split('\n');
|
||||
|
||||
// Add completed lines to buffer
|
||||
this.appendToBuffer(this.partialBuffer + parts[0]);
|
||||
|
||||
for (let i = 1; i < parts.length - 1; i++) {
|
||||
this.appendToBuffer(parts[i]);
|
||||
}
|
||||
|
||||
// Track partial line
|
||||
this.partialBuffer = parts[parts.length - 1];
|
||||
} else {
|
||||
// Track partial line
|
||||
this.partialBuffer += str;
|
||||
}
|
||||
|
||||
this.renderUi();
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
appendToBuffer(line) {
|
||||
// This regex is more complicated than expected because it
|
||||
const match = (/^\[([^\]]*)\]\s*(.*)$/).exec(stripAnsi(line));
|
||||
|
||||
if (match) {
|
||||
this.buffer.push({tag: match[1], text: match[2]});
|
||||
} else {
|
||||
this.buffer.push({tag: '', text: 'Line not recognized correctly: ' + line});
|
||||
}
|
||||
|
||||
// Keep the buffer from using too much memory by removing the oldest chunk of it every time it goes over 5000 lines
|
||||
const bufferCapacity = 5000;
|
||||
const capacityReduction = 1000;
|
||||
|
||||
if (this.buffer.length > bufferCapacity) {
|
||||
this.buffer = this.buffer.slice(this.buffer.length - capacityReduction);
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
|
||||
addCloseListener(listener) {
|
||||
this.closeListeners.add(listener);
|
||||
}
|
||||
|
||||
removeCloseListener(listener) {
|
||||
this.closeListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
export function makeRunner(commands) {
|
||||
const runner = new Runner(commands);
|
||||
|
||||
runner.renderUi();
|
||||
|
||||
return runner;
|
||||
}
|
||||
65
webapp/scripts/update-versions.sh
Executable file
65
webapp/scripts/update-versions.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
# See LICENSE.txt for license information.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: scripts/update-versions.sh monorepo_version"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
version="$1"
|
||||
if ! echo "$version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$'; then
|
||||
# The version provided appears to be invalid
|
||||
usage
|
||||
fi
|
||||
|
||||
# These workspaces and packages within the monorepo all synchronize their versions with the web app and server
|
||||
workspaces=(
|
||||
channels
|
||||
platform/client
|
||||
platform/components
|
||||
platform/mattermost-redux
|
||||
platform/shared
|
||||
platform/types
|
||||
)
|
||||
packages=(
|
||||
@mattermost/client
|
||||
@mattermost/shared
|
||||
@mattermost/types
|
||||
)
|
||||
|
||||
packages_json=$(printf '%s\n' "${packages[@]}" | jq -R . | jq -s .)
|
||||
workspaces_json=$(printf '%s\n' "${workspaces[@]}" | jq -R . | jq -s .)
|
||||
|
||||
# In each package's package.json, update the version of that package and any of the other workspace packages to match
|
||||
for workspace in "${workspaces[@]}"; do
|
||||
pkg_json="${workspace}/package.json"
|
||||
jq --arg version "$version" --argjson packages "$packages_json" '
|
||||
.version = $version |
|
||||
reduce $packages[] as $pkg (.;
|
||||
|
||||
reduce ["dependencies", "devDependencies", "peerDependencies"][] as $section (.;
|
||||
if .[$section][$pkg] then .[$section][$pkg] = $version else . end
|
||||
)
|
||||
)
|
||||
' "$pkg_json" > "${pkg_json}.tmp" && mv "${pkg_json}.tmp" "$pkg_json"
|
||||
done
|
||||
|
||||
# Then go through the package-lock.json and make those same changes
|
||||
jq --arg version "$version" --argjson packages "$packages_json" --argjson workspaces "$workspaces_json" '
|
||||
reduce $workspaces[] as $ws (.;
|
||||
.packages[$ws].version = $version |
|
||||
reduce $packages[] as $pkg (.;
|
||||
reduce ["dependencies", "devDependencies", "peerDependencies"][] as $section (.;
|
||||
if .packages[$ws][$section][$pkg] then .packages[$ws][$section][$pkg] = $version else . end
|
||||
)
|
||||
)
|
||||
)
|
||||
' package-lock.json > package-lock.json.tmp && mv package-lock.json.tmp package-lock.json
|
||||
59
webapp/scripts/utils.mjs
Normal file
59
webapp/scripts/utils.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
function getWorkspaces() {
|
||||
const packageFile = fs.readFileSync('package.json');
|
||||
const packageJson = JSON.parse(packageFile, 'utf8');
|
||||
|
||||
return packageJson.workspaces;
|
||||
}
|
||||
|
||||
function getPlatformPackagesContainingCommand(scriptName) {
|
||||
return getWorkspaces().filter((workspace) => {
|
||||
if (!workspace.startsWith('platform/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const workspacePackageFile = fs.readFileSync(path.join(workspace, 'package.json'));
|
||||
const workspacePackageJson = JSON.parse(workspacePackageFile, 'utf8');
|
||||
|
||||
return workspacePackageJson?.scripts?.[scriptName];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of concurrently commands to run a given script on every platform workspace that contains it.
|
||||
*/
|
||||
export function getPlatformCommands(scriptName) {
|
||||
return getPlatformPackagesContainingCommand(scriptName).map((workspace) => ({
|
||||
command: `npm:${scriptName} --workspace=${workspace}`,
|
||||
name: workspace.substring(workspace.lastIndexOf('/') + 1),
|
||||
prefixColor: getColorForWorkspace(workspace),
|
||||
}));
|
||||
}
|
||||
|
||||
const workspaceColors = ['green', 'magenta', 'yellow', 'red', 'blue'];
|
||||
function getColorForWorkspace(workspace) {
|
||||
const index = getWorkspaces().indexOf(workspace);
|
||||
|
||||
return index === -1 ? chalk.white : workspaceColors[index % workspaceColors.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("concurrently").CloseEvent[]} closeEvents - An array of CloseEvents thrown by concurrently when waiting on a result
|
||||
* @param {number} codeOnSignal - Which error code to return when the process is interrupted
|
||||
*/
|
||||
export function getExitCode(closeEvents, codeOnSignal = 1) {
|
||||
const exitCode = closeEvents.find((event) => !event.killed && event.exitCode > 0)?.exitCode;
|
||||
|
||||
if (typeof exitCode === 'string') {
|
||||
return codeOnSignal
|
||||
} else {
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user