diff --git a/package.json b/package.json index 6b5837ec02..93d80d3eb0 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,15 @@ "test:e2e:headless": "pnpm --filter=web-mapviewer run test:e2e:headless", "test:unit": "pnpm run --recursive --parallel --no-bail --if-present test:unit", "test:unit:watch": "pnpm run --recursive --if-present test:unit:watch", - "update:workspace": "node ./scripts/update-pnpm-workspace.js" + "update:workspace": "tsx ./scripts/update-pnpm-workspace.ts" }, "engines": { "node": ">=22.18", "pnpm": ">=10.15" }, "devDependencies": { + "@types/node": "catalog:", + "tsx": "catalog:", "yaml": "catalog:" }, "pnpm": { diff --git a/packages/viewer/package.json b/packages/viewer/package.json index 8d168d2073..887e21fab0 100644 --- a/packages/viewer/package.json +++ b/packages/viewer/package.json @@ -8,7 +8,7 @@ "build:int": "pnpm run build --mode integration", "build:prod": "pnpm run build --mode production", "build:test": "pnpm run build --mode test", - "check:external": "pnpx vite-node scripts/check-external-layers-providers.js", + "check:external": "pnpx vite-node scripts/check-external-layers-providers.ts", "delete:reports": "rimraf tests/results/ || true", "delete:reports:unit": "rimraf tests/results/unit/ || true", "dev": "vite --port 8080 --host --cors", @@ -114,11 +114,13 @@ "@tailwindcss/vite": "catalog:", "@types/bootstrap": "catalog:", "@types/geojson": "catalog:", + "@types/jsdom": "catalog:", "@types/lodash": "catalog:", "@types/luxon": "catalog:", "@types/node": "catalog:", "@types/pako": "catalog:", "@types/sortablejs": "^1.15.8", + "@types/yargs": "^17.0.34", "@vite-pwa/assets-generator": "catalog:", "@vitejs/plugin-basic-ssl": "catalog:", "@vitejs/plugin-vue": "catalog:", diff --git a/packages/viewer/scripts/check-external-layers-providers.js b/packages/viewer/scripts/check-external-layers-providers.ts similarity index 66% rename from packages/viewer/scripts/check-external-layers-providers.js rename to packages/viewer/scripts/check-external-layers-providers.ts index 527ec3ae04..c410ebf0e9 100644 --- a/packages/viewer/scripts/check-external-layers-providers.js +++ b/packages/viewer/scripts/check-external-layers-providers.ts @@ -1,5 +1,5 @@ #!./node_modules/.bin/vite-node --script - + import { JSDOM } from 'jsdom' @@ -9,7 +9,12 @@ global.DOMParser = dom.window.DOMParser global.Node = dom.window.Node import { LV95 } from '@swissgeo/coordinates' -import axios, { AxiosError } from 'axios' +import { + EXTERNAL_SERVER_TIMEOUT, + setWmsGetMapParams, +} from '@swissgeo/layers/api' +import { wmsCapabilitiesParser, wmtsCapabilitiesParser, type WMSCapabilitiesResponse, type WMTSCapabilitiesResponse } from '@swissgeo/layers/parsers' +import axios, { AxiosError, type AxiosResponse } from 'axios' import axiosRetry from 'axios-retry' import { promises as fs } from 'fs' import { exit } from 'process' @@ -18,12 +23,6 @@ import writeYamlFile from 'write-yaml-file' import yargs from 'yargs' import { hideBin } from 'yargs/helpers' -import { - EXTERNAL_SERVER_TIMEOUT, - parseWmsCapabilities, - parseWmtsCapabilities, - setWmsGetMapParams, -} from '@/api/layers/layers-external.api' import { guessExternalLayerUrl, isWmsGetCap, @@ -31,8 +30,46 @@ import { isWmtsGetCap, isWmtsUrl, } from '@/modules/menu/components/advancedTools/ImportCatalogue/utils' +import path from 'path' + +// constants + const SIZE_OF_CONTENT_DISPLAY = 150 +// types and interfaces + +interface ProviderObject { + provider: string, + url: string, + status?: number | undefined, + error?: string, + headers?: Record[] + content? : string +} + +type InvalidProviderArrayKeys = 'invalid_providers' | 'invalid_cors' | 'invalid_wms' | 'invalid_wmts' | 'invalid_content' +interface Result { + valid_providers: string[], + invalid_providers: ProviderObject[], + invalid_cors: ProviderObject[], + invalid_wms: ProviderObject[], + invalid_wmts: ProviderObject[], + invalid_content: ProviderObject[], +} + +interface HasProvider { + provider: string +} + +interface Options { + input?: string, + url?: string, + inplace?: boolean, + datetime?: boolean, + _: (string | number)[], + $0: string, +} + const options = yargs(hideBin(process.argv)) .usage('Usage: $0 [options]') .version('1.0.0') @@ -54,27 +91,27 @@ const options = yargs(hideBin(process.argv)) type: 'boolean', }) .help('h') - .alias('h', 'help').argv + .alias('h', 'help').argv as Options -function setupAxiosRetry() { +function setupAxiosRetry(): void { axiosRetry(axios, { retries: 8, // number of retries - retryDelay: (retryCount) => { + retryDelay: (retryCount: number): number => { console.log(`retry attempt: ${retryCount}`) return retryCount * 2000 // time interval between retries }, - retryCondition: (error) => { + retryCondition: (error): boolean => { // if retry condition is not specified, by default idempotent requests are retried return !error?.response || error?.response?.status >= 500 }, }) } -function compareResultByProvider(a, b) { - return compareCaseInsensitive(a['provider'], b['provider']) +function compareResultByProvider(a: HasProvider, b: HasProvider): number { + return compareCaseInsensitive(a.provider, b.provider) } -function compareCaseInsensitive(a, b) { +function compareCaseInsensitive(a: string, b: string): number { if (a.toLowerCase() > b.toLowerCase()) { return 1 } else if (a.toLowerCase() < b.toLowerCase()) { @@ -88,7 +125,7 @@ const requestHeaders = { 'Sec-Fetch-Site': 'cross-site', } -async function checkProviderGetMapTile(provider, capabilitiesResponse, result) { +async function checkProviderGetMapTile(provider: string, capabilitiesResponse: AxiosResponse, result: Result): Promise { const content = capabilitiesResponse.data let isProviderMapTileValid = true if (isWmsGetCap(content)) { @@ -100,63 +137,63 @@ async function checkProviderGetMapTile(provider, capabilitiesResponse, result) { return isProviderMapTileValid } -async function handleWms(provider, content, result) { +async function handleWms(provider: string, content: string, result: Result): Promise { let isProviderMapValid = true - const capabilities = parseWmsCapabilities(content, provider) - const layers = capabilities.getAllExternalLayerObjects( - LV95, - 1, // opacity - true, // visible - false // throw error in case of an error - ) + const capabilities: WMSCapabilitiesResponse = wmsCapabilitiesParser.parse(content, new URL(provider)) + const layers = wmsCapabilitiesParser.getAllExternalLayers(capabilities, { + outputProjection: LV95, + initialValues: { opacity: 1, isVisible: true }, + }) const firstLeaf = findFirstLeaf(layers) - const finding = capabilities.findLayer(firstLeaf.id) - const crs = capabilities.Capability.Layer.CRS[0] - const style = finding.layer?.Style ? finding.layer?.Style[0]?.Name : 'default' + const capabilitiesLayer = wmsCapabilitiesParser.getCapabilitiesLayer(capabilities, firstLeaf.id) + const crs = capabilities?.Capability?.Layer?.CRS[0] + const style = capabilitiesLayer?.Style ? capabilitiesLayer.Style[0]?.Identifier : 'default' const getCapabilitiesUrl = - capabilities.Capability.Request.GetCapabilities.DCPType[0].HTTP.Get.OnlineResource + capabilities.Capability?.Request?.GetCapabilities?.DCPType[0]?.HTTP?.Get?.OnlineResource // If the GetMap URL is the same as the GetCapabilities URL, we skip it because it's already checked - const getMapUrls = capabilities.Capability.Request.GetMap.DCPType.map( - (d) => d.HTTP.Get.OnlineResource + const getMapUrls = capabilities.Capability?.Request.GetMap.DCPType.map( + (d) => d.HTTP.Get?.OnlineResource ) .filter(Boolean) .filter((url) => getCapabilitiesUrl !== url) + if (!getMapUrls || !crs) { + isProviderMapValid = false + return isProviderMapValid + } for (const getMapUrl of getMapUrls) { - const url = setWmsGetMapParams(new URL(getMapUrl), firstLeaf.id, crs, style).toString() + const url = setWmsGetMapParams(new URL(`${getMapUrl}`), firstLeaf.id, crs, style!).toString() try { - const { responseGetMap, redirectHeaders } = await fetchMapTile(url, provider) + const { response, redirectHeaders } = await fetchMapTile(url, provider) isProviderMapValid = isProviderMapValid && (await checkProviderResponse( provider, url, - responseGetMap, + response, result, redirectHeaders, checkProviderResponseContentGetMap )) } catch (error) { isProviderMapValid = false - result.invalid_wms.push(createErrorEntry(provider, url, error, content)) + result.invalid_wms.push(createErrorEntry(provider, url, error as Error, content)) } } return isProviderMapValid } -async function handleWmts(provider, content, result) { +async function handleWmts(provider: string, content: string, result: Result): Promise { let isProviderGetTileValid = true - const capabilities = parseWmtsCapabilities(content, provider) - const layers = capabilities.getAllExternalLayerObjects( - LV95, - 1, // opacity - true, // visible - false // throw error in case of an error - ) + const capabilities: WMTSCapabilitiesResponse = wmtsCapabilitiesParser.parse(content, new URL(provider)) + const layers = wmtsCapabilitiesParser.getAllExternalLayers(capabilities, { + outputProjection: LV95, + initialValues: { opacity: 1, isVisible: true }, + }) const exampleLayer = findFirstLeaf(layers) const getTileUrlWithPlaceholders = exampleLayer.urlTemplate @@ -175,46 +212,48 @@ async function handleWmts(provider, content, result) { // Find the default value for each dimension const placeHolderParams = - exampleLayer[dimensionKey]?.reduce((acc, curr) => { + dimensionKey && exampleLayer[dimensionKey]?.reduce((acc: any, curr: any) => { // Find the key for the default value const defaultKey = Object.keys(curr).find((key) => key.toLowerCase() === 'default') // Find the key for the identifier const identifierKey = Object.keys(curr).find( (key) => key.toLowerCase() === 'identifier' || key.toLowerCase() === 'id' ) - if (curr[defaultKey]) { + if (defaultKey && identifierKey && curr[defaultKey]) { acc[curr[identifierKey]] = curr[defaultKey] } return acc }, params) || params - let getTileUrl + let getTileUrl: string | undefined try { getTileUrl = replaceUrlPlaceholders(getTileUrlWithPlaceholders, placeHolderParams) - const { response, redirectHeaders } = await fetchMapTile(getTileUrl, provider) - isProviderGetTileValid = await checkProviderResponse( - provider, - getTileUrl, - response, - result, - redirectHeaders, - checkProviderResponseContentGetMap - ) + if (getTileUrl) { + const { response, redirectHeaders } = await fetchMapTile(getTileUrl, provider) + isProviderGetTileValid = await checkProviderResponse( + provider, + getTileUrl, + response, + result, + redirectHeaders, + checkProviderResponseContentGetMap + ) + } } catch (error) { isProviderGetTileValid = false - result.invalid_wmts.push(createErrorEntry(provider, getTileUrl, error, content)) + result.invalid_wmts.push(createErrorEntry(provider, getTileUrl || '', error as Error, content)) } return isProviderGetTileValid } -async function fetchMapTile(url, provider) { - let redirectHeaders = [] +async function fetchMapTile(url: string, provider: string): Promise { + const redirectHeaders: any[] = [] const response = await axios.get(url, { headers: requestHeaders, timeout: EXTERNAL_SERVER_TIMEOUT, responseType: 'arraybuffer', - beforeRedirect: (options_, response) => { + beforeRedirect: (options_: any, response: any) => { redirectHeaders.push(response.headers) }, }) @@ -228,7 +267,7 @@ async function fetchMapTile(url, provider) { return { response, redirectHeaders } } -function createErrorEntry(provider, url, error, content) { +function createErrorEntry(provider: string, url: string, error: Error, content: string): any { return { provider, url, @@ -239,20 +278,15 @@ function createErrorEntry(provider, url, error, content) { /** * Replace placeholders in a URL template with values from a params object - * - * @param {string} urlTemplate URL template with placeholders - * @param {Object} params Object with placeholder values - * @returns {string} URL with placeholders replaced - * @throws {Error} If a placeholder is missing in the params object */ -function replaceUrlPlaceholders(urlTemplate, params) { - const normalizedParams = Object.entries(params).reduce((acc, [key, _]) => { +function replaceUrlPlaceholders(urlTemplate: string, params: any): string { + const normalizedParams = Object.entries(params).reduce((acc: any, [key, _]) => { acc[key.toLowerCase()] = params[key] return acc }, {}) - return urlTemplate.replace(/{(\w+)}/g, (_, key) => { + return urlTemplate.replace(/{(\w+)}/g, (_: any, key: string) => { const lowerKey = key.toLowerCase() - + if (normalizedParams.hasOwnProperty(lowerKey)) { return normalizedParams[lowerKey] } else { @@ -263,11 +297,8 @@ function replaceUrlPlaceholders(urlTemplate, params) { /** * Find the first leaf layer in a layer tree - * - * @param {Array} layers Array of layers - * @returns {Object} First leaf layer */ -function findFirstLeaf(layers) { +function findFirstLeaf(layers: any[]): any { if (!layers || layers.length === 0) { return null } @@ -285,12 +316,12 @@ function findFirstLeaf(layers) { return null } -async function checkProvider(provider, result) { +async function checkProvider(provider: string, result: Result): Promise { const url = guessExternalLayerUrl(provider, 'en').toString() - let capabilitiesResponse - const redirectHeaders = [] + let capabilitiesResponse : AxiosResponse + const redirectHeaders: Record[] = [] try { - capabilitiesResponse = await axios.get(url, { + capabilitiesResponse = await axios.get(url, { // headers: requestHeaders, beforeRedirect: (options_, response) => { redirectHeaders.push(response.headers) @@ -299,7 +330,7 @@ async function checkProvider(provider, result) { }) } catch (error) { if (error instanceof AxiosError) { - console.error(`Provider ${provider} is not accessible: ${error}`) + console.error(`Provider ${provider} is not accessible: ${error.message}`) result.invalid_providers.push({ provider, url, @@ -332,13 +363,13 @@ async function checkProvider(provider, result) { } async function checkProviderResponse( - provider, - url, - response, - result, - redirectHeaders, - checkContentFnc -) { + provider: string, + url: string, + response: any, + result: Result, + redirectHeaders: any[], + checkContentFnc: any +): Promise { if (![200, 201].includes(response.status)) { console.error(`Provider ${provider} is not valid: status=${response.status}`) result.invalid_providers.push({ @@ -392,31 +423,30 @@ async function checkProviderResponse( return false } -function checkProviderResponseContent(provider, url, response, result) { +function checkProviderResponseContent(provider: string, url: string, response: any, result: Result): boolean { const content = response.data - const parseCapabilities = (isCapFn, parseFn, type, result, resultArray) => { + // TODO HERE: Specify it's a function that takes one string and return a boolean + const parseCapabilities = (isCapFn: Function, parser: any, type: string, result: Result, resultArrayKey: InvalidProviderArrayKeys): boolean => { if (!isCapFn(content)) { return false } try { - const capabilities = parseFn(content, url) - const layers = capabilities.getAllExternalLayerObjects( - LV95, - 1, // opacity - true, // visible - false // throw Error in case of error - ) + const capabilities: WMTSCapabilitiesResponse | WMSCapabilitiesResponse = parser.parse(content, new URL(url)) + const layers = parser.getAllExternalLayers(capabilities, { + outputProjection: LV95, + initialValues: { opacity: 1, isVisible: true }, + }) if (layers.length === 0) { throw new Error(`No valid ${type} layers found`) } } catch (error) { - console.error(`Invalid provider ${provider}, ${type} get Cap parsing failed: ${error}`) - result[resultArray].push({ + console.error(`Invalid provider ${provider}, ${type} get Cap parsing failed: ${String(error)}`) + result[resultArrayKey].push({ provider, url, - error: `${error}`, + error: `${String(error)}`, content: content.slice(0, SIZE_OF_CONTENT_DISPLAY), }) return false @@ -424,8 +454,8 @@ function checkProviderResponseContent(provider, url, response, result) { return true } if ( - parseCapabilities(isWmsGetCap, parseWmsCapabilities, 'WMS', result, 'invalid_wms') || - parseCapabilities(isWmtsGetCap, parseWmtsCapabilities, 'WMTS', result, 'invalid_wmts') + parseCapabilities(isWmsGetCap, wmsCapabilitiesParser, 'WMS', result, 'invalid_wms') || + parseCapabilities(isWmtsGetCap, wmtsCapabilitiesParser, 'WMTS', result, 'invalid_wmts') ) { return true } @@ -439,7 +469,7 @@ function checkProviderResponseContent(provider, url, response, result) { return false } -async function checkProviderResponseContentGetMap(provider, url, response, result) { +async function checkProviderResponseContentGetMap(provider: string, url: string, response: any, result: Result): Promise { const content = Buffer.from(response.data) let isValid = false try { @@ -449,21 +479,21 @@ async function checkProviderResponseContentGetMap(provider, url, response, resul } catch (error) { if (isWmsUrl(url)) { console.error( - `Invalid provider ${provider}, WMS get Map content parsing failed: ${error}` + `Invalid provider ${provider}, WMS get Map content parsing failed: ${String(error)}` ) result.invalid_wms.push({ provider: provider, url: url, - error: `${error}`, + error: `${String(error)}`, }) } else if (isWmtsUrl(url)) { console.error( - `Invalid provider ${provider}, WMTS get Tiles content parsing failed: ${error}` + `Invalid provider ${provider}, WMTS get Tiles content parsing failed: ${String(error)}` ) result.invalid_wmts.push({ provider: provider, url: url, - error: `${error}`, + error: `${String(error)}`, }) } } @@ -471,18 +501,18 @@ async function checkProviderResponseContentGetMap(provider, url, response, resul return isValid } -async function checkProviders(providers, result) { +async function checkProviders(providers: string[], result: Result): Promise { return Promise.all(providers.map(async (provider) => checkProvider(provider, result))) } -async function writeResult(result) { +async function writeResult(result: Result): Promise { const resultsFolder = 'scripts/check-layer-providers-results' let prefix = '' if (options.datetime) { prefix = `${new Date().toISOString()}_` } let valid_providers_file = `${resultsFolder}/${prefix}valid_providers.json` - if (options.inplace) { + if (options.inplace && options.input) { valid_providers_file = options.input } return Promise.all([ @@ -513,8 +543,8 @@ async function writeResult(result) { ]) } -async function main() { - const result = { +async function main(): Promise { + const result: Result = { valid_providers: [], invalid_providers: [], invalid_cors: [], @@ -524,11 +554,16 @@ async function main() { } setupAxiosRetry() - let providers = [] - if (options.url) { - providers = [options.url] - } else { - providers = JSON.parse(await fs.readFile(options.input, { encoding: 'utf-8' })) + let providers: string[] = [] + const options_url = options.url + const options_input = options.input + if (options_url) { + providers = [options_url] + } else if (options_input){ + providers = JSON.parse(await fs.readFile(path.resolve(options_input), { encoding: 'utf-8' })) + } + else { + console.error(`No sources given for providers`) } await checkProviders(providers, result) @@ -541,9 +576,8 @@ async function main() { } where invalids and ${result.invalid_cors.length} don't support CORS` ) } catch (error) { - console.error(`Failed to write results: ${error}`) + console.error(`Failed to write results: ${String(error)}`) } - return } -main().then(() => exit()) +void main().then(() => exit()) diff --git a/packages/viewer/scripts/generate-i18n-files.js b/packages/viewer/scripts/generate-i18n-files.ts similarity index 67% rename from packages/viewer/scripts/generate-i18n-files.js rename to packages/viewer/scripts/generate-i18n-files.ts index 97456b607e..edefdbec01 100644 --- a/packages/viewer/scripts/generate-i18n-files.js +++ b/packages/viewer/scripts/generate-i18n-files.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node - + import fs from 'fs' import { google } from 'googleapis' +type Lang = 'fr' | 'de' | 'en' | 'it' | 'rm' const googleApiKey = process.env.GOOGLE_API_KEY if (!googleApiKey) { @@ -11,6 +12,10 @@ if (!googleApiKey) { process.exit(1) } +function stringToLowerCaseLang(lang: string) : Lang { + return lang.toLowerCase() as Lang +} + // Reading translations from Google Spreadsheet https://docs.google.com/spreadsheets/d/1bRzdX2zwN2VG7LWEdlscrP-wGlp7O46nvrXkQNnFvVY/edit?usp=sharing const sheets = google.sheets({ version: 'v4', auth: googleApiKey }) sheets.spreadsheets.values.get( @@ -20,12 +25,19 @@ sheets.spreadsheets.values.get( }, (err, res) => { if (err) { - return console.log('The API returned an error: ' + err) + return console.log('The API returned an error: ' + err.toString()) } - const rows = res.data.values - if (rows.length) { - const translations = {} - const langByIndex = [] + const rows = res?.data.values + if (rows?.length) { + const translations: Record> = { + 'fr': {}, + 'de': {}, + 'it': {}, + 'en': {}, + 'rm': {} + } + // contains the keys to translations + const langByIndex: Lang[] = [] // creating a JSON structure with the Google spreadsheet content // structure of the JSON should be // { @@ -35,25 +47,25 @@ sheets.spreadsheets.values.get( // }, // "lang2_isoCode": { ... } // } - rows.forEach((row, rowIndex) => { + rows.forEach((row: string[], rowIndex) => { if (rowIndex === 0) { row.forEach((lang, langIndex) => { if (langIndex > 0) { - translations[lang.toLowerCase()] = {} - langByIndex[langIndex] = lang.toLowerCase() + langByIndex[langIndex] = stringToLowerCaseLang(lang) } }) } else { - langByIndex.forEach((lang, index) => { - if (index > 0) { + langByIndex.forEach((lang: Lang, index) => { + if (index > 0 && row[0]) { translations[lang][row[0]] = row[index] } }) } }) // ordering all keys alphabetically - Object.keys(translations).forEach((lang) => { - const translationForLang = translations[lang] + //@ts-expect-error we know the keys here are all Langs, but typescript doesn't like that + Object.keys(translations).forEach((lang: Lang) => { + const translationForLang: Record = translations[lang] translations[lang] = Object.keys(translationForLang) .sort() .reduce((acc, key) => ({ ...acc, [key]: translationForLang[key] }), {}) diff --git a/packages/viewer/src/modules/menu/components/advancedTools/ImportCatalogue/utils.ts b/packages/viewer/src/modules/menu/components/advancedTools/ImportCatalogue/utils.ts index cf32dc56a4..5a2b44981d 100644 --- a/packages/viewer/src/modules/menu/components/advancedTools/ImportCatalogue/utils.ts +++ b/packages/viewer/src/modules/menu/components/advancedTools/ImportCatalogue/utils.ts @@ -1,3 +1,5 @@ +import { setWmsGetCapabilitiesParams, setWmtsGetCapParams } from '@swissgeo/layers/api' + /** * Checks if file has WMS Capabilities XML content */ @@ -34,29 +36,12 @@ export function isWmtsUrl(url: string): boolean { * @returns Url object with backend parameters (eg. SERVICE=WMS, ...) */ export function guessExternalLayerUrl(provider: string, language: string): URL { - // Note: setWmsGetCapParams and setWmtsGetCapParams would need to be migrated too - // For now, just return a basic URL construction - const url = new URL(provider) - if (isWmtsUrl(provider)) { - // Add WMTS parameters - url.searchParams.set('SERVICE', 'WMTS') - url.searchParams.set('REQUEST', 'GetCapabilities') - url.searchParams.set('lang', language) - return url + return setWmtsGetCapParams(new URL(provider), language) } - if (isWmsUrl(provider)) { - // Add WMS parameters - url.searchParams.set('SERVICE', 'WMS') - url.searchParams.set('REQUEST', 'GetCapabilities') - url.searchParams.set('lang', language) - return url + return setWmsGetCapabilitiesParams(new URL(provider), language) } - // By default if the URL service type cannot be guessed we use WMS - url.searchParams.set('SERVICE', 'WMS') - url.searchParams.set('REQUEST', 'GetCapabilities') - url.searchParams.set('lang', language) - return url + return setWmsGetCapabilitiesParams(new URL(provider), language) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bd32e2728..75f12864a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,7 +91,7 @@ catalogs: specifier: ^7946.0.16 version: 7946.0.16 '@types/jsdom': - specifier: ^27.0.0 + specifier: 27.0.0 version: 27.0.0 '@types/lodash': specifier: ^4.17.20 @@ -424,6 +424,12 @@ importers: .: devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.7.2 + tsx: + specifier: 'catalog:' + version: 4.20.6 yaml: specifier: 'catalog:' version: 2.8.1 @@ -1312,6 +1318,9 @@ importers: '@types/geojson': specifier: 'catalog:' version: 7946.0.16 + '@types/jsdom': + specifier: 'catalog:' + version: 27.0.0 '@types/lodash': specifier: 'catalog:' version: 4.17.20 @@ -1327,6 +1336,9 @@ importers: '@types/sortablejs': specifier: ^1.15.8 version: 1.15.8 + '@types/yargs': + specifier: ^17.0.34 + version: 17.0.34 '@vite-pwa/assets-generator': specifier: 'catalog:' version: 1.0.2 @@ -3683,6 +3695,12 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.34': + resolution: {integrity: sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==} + '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -11184,6 +11202,12 @@ snapshots: '@types/web-bluetooth@0.0.21': {} + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.34': + dependencies: + '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': dependencies: '@types/node': 24.8.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b699eac5eb..557ad309b9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,7 @@ catalog: '@types/bootstrap': ^5.2.10 '@types/chai': ^5.2.2 '@types/geojson': ^7946.0.16 - '@types/jsdom': ^27.0.0 + '@types/jsdom': 27.0.0 '@types/lodash': ^4.17.20 '@types/luxon': ^3.7.1 '@types/node': ^24.8.1 diff --git a/scripts/update-pnpm-workspace.js b/scripts/update-pnpm-workspace.ts similarity index 71% rename from scripts/update-pnpm-workspace.js rename to scripts/update-pnpm-workspace.ts index 5fc6624528..9024309357 100644 --- a/scripts/update-pnpm-workspace.js +++ b/scripts/update-pnpm-workspace.ts @@ -1,22 +1,57 @@ -// update-pnpm-workspace.js +// update-pnpm-workspace.ts // Requires: Node 18+ (global fetch) and `yaml` package // Install: pnpm add -D yaml // // Usage: -// node scripts/update-catalog-to-latest.js pnpm-workspace.yaml [--dry-run] [--same-major] +// node scripts/update-pnpm-workspace.ts pnpm-workspace.yaml [--dry-run] [--same-major] // [--registry ] [--tag ] [--concurrency ] // // Example: -// node scripts/update-catalog-to-latest.js pnpm-workspace.yaml --dry-run -// node scripts/update-catalog-to-latest.js pnpm-workspace.yaml --same-major -// node scripts/update-catalog-to-latest.js pnpm-workspace.yaml --registry https://registry.npmjs.org --tag latest +// node scripts/update-pnpm-workspace.ts pnpm-workspace.yaml --dry-run +// node scripts/update-pnpm-workspace.ts pnpm-workspace.yaml --same-major +// node scripts/update-pnpm-workspace.ts pnpm-workspace.yaml --registry https://registry.npmjs.org --tag latest import { readFileSync, writeFileSync } from 'node:fs' import { basename } from 'node:path' import YAML from 'yaml' +import type { Document } from 'yaml' -function parseArgs(argv) { - const args = { +interface Args { + file: string + dryRun: boolean + sameMajor: boolean + registry: string + tag: string + concurrency: number +} + +interface SemverParsed { + major: number + minor: number + patch: number + pre: string +} + +interface PackageUpdate { + name: string + from: string + to: string + base: string | null + target: string +} + +interface PackageMeta { + 'dist-tags'?: Record + versions?: Record +} + +interface YAMLCatalogNode { + items?: Array<{ key: { value: string }; value: { value: string } }> + set?: (name: string, value: string) => void +} + +function parseArgs(argv: string[]): Args { + const args: Args = { file: 'pnpm-workspace.yaml', dryRun: false, sameMajor: false, @@ -24,7 +59,7 @@ function parseArgs(argv) { tag: 'latest', concurrency: 8, } - const positional = [] + const positional: string[] = [] for (let i = 2; i < argv.length; i++) { const a = argv[i] if (a === '--dry-run') { @@ -32,9 +67,9 @@ function parseArgs(argv) { } else if (a === '--same-major') { args.sameMajor = true } else if (a === '--registry') { - args.registry = argv[++i] + args.registry = argv[++i]! } else if (a === '--tag') { - args.tag = argv[++i] + args.tag = argv[++i]! } else if (a === '--concurrency') { args.concurrency = Number(argv[++i]) || args.concurrency } else if (!a.startsWith('-')) { @@ -47,19 +82,24 @@ function parseArgs(argv) { return args } -function loadYamlDocument(file) { +function loadYamlDocument(file: string): Document.Parsed { const text = readFileSync(file, 'utf8') return YAML.parseDocument(text) } -function getCatalog(doc) { +function getCatalog(doc: Document.Parsed): YAMLCatalogNode | null { if (!doc.has('catalog')) { return null } - return doc.get('catalog') + return doc.get('catalog') as YAMLCatalogNode } -function setCatalogEntry(doc, catalogNode, name, value) { +function setCatalogEntry( + doc: Document.Parsed, + catalogNode: YAMLCatalogNode | null, + name: string, + value: string +): void { if (catalogNode && catalogNode.set) { catalogNode.set(name, value) } else { @@ -67,19 +107,19 @@ function setCatalogEntry(doc, catalogNode, name, value) { } } -function extractPrefix(spec) { +function extractPrefix(spec: string): string { // Preserve ^ or ~ if present; otherwise empty (exact) const m = spec.match(/^\s*([\^~])/) return m ? m[1] : '' } -function extractBaseVersion(spec) { +function extractBaseVersion(spec: string): string | null { // Pull the first x.y.z-like token; best-effort const m = spec.match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/) return m ? m[1] : null } -function parseSemver(v) { +function parseSemver(v: string): SemverParsed | null { // Naive semver parser: returns {major, minor, patch, pre} or null if (!v) { return null @@ -91,7 +131,7 @@ function parseSemver(v) { return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] || '' } } -function cmpSemver(a, b) { +function cmpSemver(a: SemverParsed | null, b: SemverParsed | null): number { // Compare only numeric parts; treats prereleases as smaller than stable if (!a || !b) { return 0 @@ -111,32 +151,32 @@ function cmpSemver(a, b) { return aPre - bPre } -function pickHighest(versions) { +function pickHighest(versions: string[]): string | null { // Choose highest stable; if none, highest including prerelease const parsed = versions.map((v) => ({ v, p: parseSemver(v) })).filter((x) => x.p) if (parsed.length === 0) { return null } - const stable = parsed.filter((x) => !x.p.pre) + const stable = parsed.filter((x) => !x.p!.pre) const set = (stable.length ? stable : parsed).sort((x, y) => cmpSemver(x.p, y.p)) - return set[set.length - 1].v + return set[set.length - 1]!.v } -async function fetchPackageMeta(registry, pkg) { +async function fetchPackageMeta(registry: string, pkg: string): Promise { const url = `${registry.replace(/\/+$/, '')}/${encodeURIComponent(pkg)}` const res = await fetch(url, { redirect: 'follow' }) if (!res.ok) { throw new Error(`HTTP ${res.status} ${res.statusText}`) } - return res.json() + return res.json() as Promise } -function shouldSkipSpec(spec) { +function shouldSkipSpec(spec: string): boolean { // Skip non-registry specs return /^(workspace:|file:|link:|git\+|github:|bitbucket:|gitlab:)/.test(spec) } -function sameMajorTarget(base, allVersions) { +function sameMajorTarget(base: string, allVersions: string[]): string | null { const baseParsed = parseSemver(base) if (!baseParsed) { return null @@ -148,7 +188,7 @@ function sameMajorTarget(base, allVersions) { return pickHighest(same) } -async function main() { +async function main(): Promise { const args = parseArgs(process.argv) const doc = loadYamlDocument(args.file) const catalog = getCatalog(doc) @@ -157,20 +197,20 @@ async function main() { console.error('No "catalog" section found in the workspace file.') process.exit(1) } - const entries = catalog.items + const entries: Array<[string, string]> = catalog.items ? catalog.items.map((i) => [i.key.value, i.value.value]) : Object.entries(catalog) console.log( `Checking ${entries.length} catalog packages against ${args.registry} (tag: ${args.tag})...` ) - const updates = [] + const updates: PackageUpdate[] = [] let i = 0 - async function worker() { + async function worker(): Promise { while (i < entries.length) { const idx = i++ - const [name, specRaw] = entries[idx] + const [name, specRaw] = entries[idx]! const spec = String(specRaw).trim() if (shouldSkipSpec(spec)) { @@ -218,7 +258,8 @@ async function main() { }) } } catch (e) { - console.error(`Error fetching ${name}: ${e.message}`) + const error = e as Error + console.error(`Error fetching ${name}: ${error.message}`) } } } @@ -253,7 +294,7 @@ async function main() { console.log('\nPlease run `pnpm install` to update the lockfile.\n') } -main().catch((err) => { +main().catch((err: Error) => { console.error(err) process.exit(1) })