diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 98966d27a..1118a7c2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,12 @@ version: 2 updates: - - package-ecosystem: 'npm' - directory: '/' + - package-ecosystem: "npm" + directory: "/" schedule: - interval: 'weekly' + interval: "weekly" open-pull-requests-limit: 100 - - package-ecosystem: 'github-actions' - directory: '/' + - package-ecosystem: "github-actions" + directory: "/" schedule: - interval: 'weekly' + interval: "weekly" open-pull-requests-limit: 100 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8248fd224..96e9e083f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,20 @@ jobs: - name: Run tests run: npm run test + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm run lint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..ae7b02b02 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +data +tmp diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b2095be81..000000000 --- a/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "semi": false, - "singleQuote": true -} diff --git a/README.md b/README.md index 7c8675e9f..c0d25049c 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,14 @@ $ npm install @neaps/tide-database The package exports an array of all tide stations in the database: ```typescript -import { constituents, stations } from '@neaps/tide-database'; +import { constituents, stations } from "@neaps/tide-database"; // Constituents is an array of all harmonic constituents used in the database with a description and speed. -console.log('Total constituents:', constituents.length); +console.log("Total constituents:", constituents.length); console.log(constituents[0]); // Stations is an array of all the files in `data/` -console.log('Total stations:', stations.length); +console.log("Total stations:", stations.length); console.log(stations[0]); ``` @@ -61,6 +61,7 @@ Subordinate stations have four kinds of offsets, two to correct for water level, ## Maintenance A GitHub Action runs monthly on the 1st of each month to automatically update NOAA tide station data. The workflow: + - Fetches the latest station list and harmonic constituents from NOAA's API - Updates existing station files with new data - Adds any newly discovered reference stations @@ -80,9 +81,9 @@ This will scan all existing NOAA station files, fetch any new stations from NOAA Releases of this database use [Semantic Versioning](https://semver.org/), with these added semantics: -* Major version changes indicate breaking changes to the data structure or APIs. However, as long as the version is "0.x", breaking changes may occur without a major version bump. -* Minor version changes indicate backward-compatible additions to the data structure or APIs, such as new fields. -* Patch version changes indicate updates to station data, and will always be the current date. For example, "0.1.20260101". +- Major version changes indicate breaking changes to the data structure or APIs. However, as long as the version is "0.x", breaking changes may occur without a major version bump. +- Minor version changes indicate backward-compatible additions to the data structure or APIs, such as new fields. +- Patch version changes indicate updates to station data, and will always be the current date. For example, "0.1.20260101". ## Releasing @@ -90,9 +91,9 @@ Releases are created by [running the Publish action](https://github.com/neaps/ti ## License -* All code in this repository is licensed under the [MIT License](./LICENSE). -* The `license` field of each station's JSON file specifies the license for that station. -* Unless otherwise noted, All other data is licensed under the [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) license. +- All code in this repository is licensed under the [MIT License](./LICENSE). +- The `license` field of each station's JSON file specifies the license for that station. +- Unless otherwise noted, All other data is licensed under the [Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) license. If using this project, please attribute it as: diff --git a/package.json b/package.json index ecf2027a7..77bff1a92 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "scripts": { "build": "tsc -b && tsc -p tsconfig.node.json && tsdown", "prepare": "npm run build", - "test": "vitest" + "test": "vitest", + "lint": "prettier --check .", + "format": "prettier --write ." }, "devDependencies": { "@neaps/tide-predictor": "^0.2.1", @@ -43,6 +45,7 @@ "geo-tz": "^8.1.4", "make-fetch-happen": "^15.0.3", "moment": "^2.30.1", + "prettier": "^3.7.4", "sort-object-keys": "^2.0.1", "tsdown": "^0.19.0-beta.3", "typescript": "^5.9.3", diff --git a/schemas/station.schema.json b/schemas/station.schema.json index eea292b09..26decca11 100644 --- a/schemas/station.schema.json +++ b/schemas/station.schema.json @@ -20,11 +20,7 @@ "offsets": { "type": "object", "description": "Offsets for subordinate stations relative to a reference station", - "required": [ - "reference", - "height", - "time" - ], + "required": ["reference", "height", "time"], "additionalProperties": false, "properties": { "reference": { @@ -34,11 +30,7 @@ "height": { "type": "object", "description": "Defines the values to add or multiply with water levels (depending on type)", - "required": [ - "high", - "low", - "type" - ], + "required": ["high", "low", "type"], "additionalProperties": false, "properties": { "high": { @@ -50,20 +42,14 @@ "description": "Height offset/multiplier for low tide - if type is 'ratio', this is a multiplier; if 'fixed', this is an offset to add (can be negative)" }, "type": { - "enum": [ - "ratio", - "fixed" - ] + "enum": ["ratio", "fixed"] } } }, "time": { "type": "object", "description": "Defines the time to add to when a high or low tide will occur", - "required": [ - "high", - "low" - ], + "required": ["high", "low"], "additionalProperties": false, "properties": { "high": { @@ -83,10 +69,7 @@ "id": { "type": "string", "description": "Unique station ID for this database only, in the format of `country/region/station-name`.", - "examples": [ - "us-ma-boston", - "ar-u-puerto-madryn" - ], + "examples": ["us-ma-boston", "ar-u-puerto-madryn"], "minLength": 4 }, "name": { @@ -117,10 +100,7 @@ }, "type": { "type": "string", - "enum": [ - "reference", - "subordinate" - ] + "enum": ["reference", "subordinate"] }, "latitude": { "type": "number", @@ -133,12 +113,7 @@ "source": { "type": "object", "description": "", - "required": [ - "name", - "id", - "published_harmonics", - "url" - ], + "required": ["name", "id", "published_harmonics", "url"], "additionalProperties": true, "properties": { "name": { @@ -146,10 +121,7 @@ "name": "Name of the source, i.e. `NOAA`" }, "id": { - "type": [ - "string", - "number" - ], + "type": ["string", "number"], "description": "The identifier used by the source for this station." }, "published_harmonics": { @@ -166,11 +138,7 @@ "license": { "type": "object", "description": "Defines the license and whatever restrictions are placed on use of the data.", - "required": [ - "type", - "commercial_use", - "url" - ], + "required": ["type", "commercial_use", "url"], "additionalProperties": true, "properties": { "type": { @@ -196,11 +164,7 @@ "type": "array", "items": { "type": "object", - "required": [ - "name", - "amplitude", - "phase" - ], + "required": ["name", "amplitude", "phase"], "additionalProperties": false, "properties": { "name": { @@ -208,11 +172,7 @@ "description": "Harmonic Constituent code (i.e. M2) - should be upper-case" }, "description": { - "type": [ - "string", - "number", - "null" - ], + "type": ["string", "number", "null"], "description": "Human-friendly description of the constituent" }, "amplitude": { diff --git a/src/index.ts b/src/index.ts index 558ac1e86..420288992 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,63 +1,63 @@ -import _constituents from './constituents.json' with { type: 'json' } +import _constituents from "./constituents.json" with { type: "json" }; export interface HarmonicConstituent { - name: string - description?: string - amplitude: number - phase: number - speed?: number + name: string; + description?: string; + amplitude: number; + phase: number; + speed?: number; } export interface Constituent { - name: string - description: string | null - speed: number + name: string; + description: string | null; + speed: number; } export interface Station { // Basic station information - id: string - name: string - continent: string - country: string - region?: string - timezone: string - disclaimers: string - type: 'reference' | 'subordinate' - latitude: number - longitude: number + id: string; + name: string; + continent: string; + country: string; + region?: string; + timezone: string; + disclaimers: string; + type: "reference" | "subordinate"; + latitude: number; + longitude: number; // Data source information source: { - name: string - id: string - published_harmonics: boolean - url: string - } + name: string; + id: string; + published_harmonics: boolean; + url: string; + }; // License information license: { - type: string - commercial_use: boolean - url: string - notes?: string - } + type: string; + commercial_use: boolean; + url: string; + notes?: string; + }; // Harmonic constituents (empty array for subordinate stations) - harmonic_constituents: HarmonicConstituent[] + harmonic_constituents: HarmonicConstituent[]; // Subordinate station offsets (empty object for reference stations) offsets?: { - reference: string - height: { high: number; low: number; type: 'ratio' | 'fixed' } - time: { high: number; low: number } - } + reference: string; + height: { high: number; low: number; type: "ratio" | "fixed" }; + time: { high: number; low: number }; + }; - datums: Record + datums: Record; } -export const constituents: Constituent[] = _constituents +export const constituents: Constituent[] = _constituents; export const stations: Station[] = Object.values( - import.meta.glob('../data/**/*.json', { eager: true, import: 'default' }) -) + import.meta.glob("../data/**/*.json", { eager: true, import: "default" }), +); diff --git a/test/validate.test.ts b/test/validate.test.ts index 054d83e1b..a0f00be24 100755 --- a/test/validate.test.ts +++ b/test/validate.test.ts @@ -1,42 +1,42 @@ #!/usr/bin/env node -import { describe, test } from 'vitest' -import { readFile } from 'fs/promises' -import { join } from 'path' -import Ajv2020 from 'ajv/dist/2020.js' -import addFormats from 'ajv-formats' -import { stations } from '../src/index.js' +import { describe, test } from "vitest"; +import { readFile } from "fs/promises"; +import { join } from "path"; +import Ajv2020 from "ajv/dist/2020.js"; +import addFormats from "ajv-formats"; +import { stations } from "../src/index.js"; -const ROOT = new URL('..', import.meta.url).pathname -const SCHEMA_PATH = join(ROOT, 'schemas', 'station.schema.json') +const ROOT = new URL("..", import.meta.url).pathname; +const SCHEMA_PATH = join(ROOT, "schemas", "station.schema.json"); -const schema = JSON.parse(await readFile(SCHEMA_PATH, 'utf-8')) -const ajv = new (Ajv2020 as any)({ allErrors: true, strict: false }) -;(addFormats as any)(ajv) -const validate = ajv.compile(schema) +const schema = JSON.parse(await readFile(SCHEMA_PATH, "utf-8")); +const ajv = new (Ajv2020 as any)({ allErrors: true, strict: false }); +(addFormats as any)(ajv); +const validate = ajv.compile(schema); -describe('schema', () => { +describe("schema", () => { stations.forEach((station) => { test(`Station ${station.id}`, () => { - const valid = validate(station) + const valid = validate(station); if (!valid) throw new Error( ajv.errorsText(validate.errors) + - `\n${JSON.stringify(validate.errors, null, 2)}` - ) - }) - }) -}) + `\n${JSON.stringify(validate.errors, null, 2)}`, + ); + }); + }); +}); -test('Does not have duplicate stations', () => { - const seen = new Map() +test("Does not have duplicate stations", () => { + const seen = new Map(); stations.forEach((station) => { - const dup = seen.get(station.source.id) + const dup = seen.get(station.source.id); if (dup) { throw new Error( - `Stations ${station.id} and ${dup.id} have the same source id ${station.source.id}` - ) + `Stations ${station.id} and ${dup.id} have the same source id ${station.source.id}`, + ); } - seen.set(station.source.id, station) - }) -}) + seen.set(station.source.id, station); + }); +}); diff --git a/tools/hawaii-converter.js b/tools/hawaii-converter.js index 172c5d141..788111c8c 100644 --- a/tools/hawaii-converter.js +++ b/tools/hawaii-converter.js @@ -1,45 +1,45 @@ -const fs = require('fs') -const moment = require('moment') +const fs = require("fs"); +const moment = require("moment"); fs.readFile(process.argv[2], (error, data) => { if (error) { - console.log(error) - return + console.log(error); + return; } - const output = [] - const rows = data.toString().split('\n') + const output = []; + const rows = data.toString().split("\n"); //The first row is always metadata, remove - rows.shift() - rows.forEach(row => { + rows.shift(); + rows.forEach((row) => { //Remove multiple spaces if (row.length < 15) { - return + return; } - row = row.replace(/\s\s+/g, ' ') - row = row.split(' ') - const date = moment(row[2].substr(0, 8), 'YYYYMMDD') - date.set('hour', 0) - if (row[2].substr(-1, 1) === '2') { - date.set('hour', 12) + row = row.replace(/\s\s+/g, " "); + row = row.split(" "); + const date = moment(row[2].substr(0, 8), "YYYYMMDD"); + date.set("hour", 0); + if (row[2].substr(-1, 1) === "2") { + date.set("hour", 12); } for (let i = 0; i < 12; i++) { //Levels are in mm, convert to meters - const level = row[i + 3] / 1000 - output.push(`${date.unix()}, ${level}`) - date.add(1, 'hour') + const level = row[i + 3] / 1000; + output.push(`${date.unix()}, ${level}`); + date.add(1, "hour"); } - }) + }); if (output.length) { - console.log('Writing file') - fs.writeFile(process.argv[3], output.join('\n'), (error, data) => { + console.log("Writing file"); + fs.writeFile(process.argv[3], output.join("\n"), (error, data) => { if (error) { - console.log(error) - return + console.log(error); + return; } - console.log('Done') - }) + console.log("Done"); + }); } else { - console.log('No rows found') + console.log("No rows found"); } -}) +}); diff --git a/tools/station.ts b/tools/station.ts index bc43cac8e..6446d5161 100644 --- a/tools/station.ts +++ b/tools/station.ts @@ -1,52 +1,52 @@ -import type { Station } from '../src/index.js' -import { find as findTz } from 'geo-tz/all' -import { slugify } from './util.ts' -import countryLookup from 'country-code-lookup' -import { join, dirname } from 'path' -import { mkdir, writeFile } from 'fs/promises' -import sortObject from 'sort-object-keys' +import type { Station } from "../src/index.js"; +import { find as findTz } from "geo-tz/all"; +import { slugify } from "./util.ts"; +import countryLookup from "country-code-lookup"; +import { join, dirname } from "path"; +import { mkdir, writeFile } from "fs/promises"; +import sortObject from "sort-object-keys"; -const __dirname = new URL('.', import.meta.url).pathname -export const DATA_DIR = join(__dirname, '..', 'data') +const __dirname = new URL(".", import.meta.url).pathname; +export const DATA_DIR = join(__dirname, "..", "data"); const sortOrder: (keyof Station)[] = [ - 'id', - 'name', - 'region', - 'country', - 'continent', - 'latitude', - 'longitude', - 'timezone', - 'source', - 'license', - 'disclaimers', - 'datums', - 'type', - 'harmonic_constituents', - 'offsets', -] + "id", + "name", + "region", + "country", + "continent", + "latitude", + "longitude", + "timezone", + "source", + "license", + "disclaimers", + "datums", + "type", + "harmonic_constituents", + "offsets", +]; export function normalize( - station: Omit + station: Omit, ): Station { const { iso2, continent, country } = countryLookup.byCountry(station.country) || countryLookup.byIso(station.country) || - {} + {}; if (!iso2 || !continent || !country) { throw new Error( - `Unable to find country info for station: ${station.name} (${station.country})` - ) + `Unable to find country info for station: ${station.name} (${station.country})`, + ); } - const timezone = findTz(station.latitude, station.longitude)[0] + const timezone = findTz(station.latitude, station.longitude)[0]; if (!timezone) { throw new Error( - `Unable to find timezone for station: ${station.name} (${station.latitude}, ${station.longitude})` - ) + `Unable to find timezone for station: ${station.name} (${station.latitude}, ${station.longitude})`, + ); } // TODO: sort keys by order of JSON schema. Mutation for now to maintain key order @@ -54,24 +54,24 @@ export function normalize( { ...station, id: [iso2, station.region, station.name] - .filter((v): v is string => typeof v === 'string' && v.length > 0) + .filter((v): v is string => typeof v === "string" && v.length > 0) .map(slugify) - .join('/'), + .join("/"), timezone, continent, country, }, - sortOrder - ) + sortOrder, + ); } export async function save(data: Station) { - const filePath = join(DATA_DIR, `${data.id}.json`) - const directory = dirname(filePath) + const filePath = join(DATA_DIR, `${data.id}.json`); + const directory = dirname(filePath); // Create directory if it doesn't exist - await mkdir(directory, { recursive: true }) + await mkdir(directory, { recursive: true }); // Write the JSON file - return writeFile(filePath, JSON.stringify(data, null, 2) + '\n') + return writeFile(filePath, JSON.stringify(data, null, 2) + "\n"); } diff --git a/tools/update-noaa-stations.ts b/tools/update-noaa-stations.ts index 0e0a64abd..3fac76207 100755 --- a/tools/update-noaa-stations.ts +++ b/tools/update-noaa-stations.ts @@ -1,68 +1,70 @@ #!/usr/bin/env node -import createFetch from 'make-fetch-happen' -import { normalize, save } from './station.ts' -import type { Station } from '../src/index.ts' +import createFetch from "make-fetch-happen"; +import { normalize, save } from "./station.ts"; +import type { Station } from "../src/index.ts"; const fetch = createFetch.defaults({ - cachePath: 'node_modules/.cache', - cache: 'force-cache', + cachePath: "node_modules/.cache", + cache: "force-cache", retry: 10, -}) +}); -const NOAA_SOURCE_NAME = 'US National Oceanic and Atmospheric Administration' +const NOAA_SOURCE_NAME = "US National Oceanic and Atmospheric Administration"; const STATIONS_URL = - 'https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json' + "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json"; -const idMap = new Map() +const idMap = new Map(); async function main() { const { stations } = await fetch( - `${STATIONS_URL}?type=tidepredictions&expand=details,tidepredoffsets&units=metric` - ).then((r) => r.json()) + `${STATIONS_URL}?type=tidepredictions&expand=details,tidepredoffsets&units=metric`, + ).then((r) => r.json()); - console.log(`Fetched metadata for ${stations.length} stations.`) + console.log(`Fetched metadata for ${stations.length} stations.`); - const referenceStations = stations.filter((s: any) => s.type === 'R') - const subordinateStations = stations.filter((s: any) => s.type === 'S') + const referenceStations = stations.filter((s: any) => s.type === "R"); + const subordinateStations = stations.filter((s: any) => s.type === "S"); - console.log('Creating reference stations:') + console.log("Creating reference stations:"); for (const meta of referenceStations) { - idMap.set(meta.id, (await saveStation(await buildStation(meta))).id) - process.stdout.write('.') + idMap.set(meta.id, (await saveStation(await buildStation(meta))).id); + process.stdout.write("."); } - console.log(`\nDone. Created ${referenceStations.length} reference stations.`) + console.log( + `\nDone. Created ${referenceStations.length} reference stations.`, + ); - console.log('Creating subordinate stations:') + console.log("Creating subordinate stations:"); for (const meta of subordinateStations) { // This should never happen, but just in case if (idMap.has(meta.id)) - throw new Error('Duplicate station ID found: ' + meta.id) + throw new Error("Duplicate station ID found: " + meta.id); // At least one station lists itself as its own reference, but doesn't have harmonic data - if (meta.id === meta.tidepredoffsets.refStationId) continue + if (meta.id === meta.tidepredoffsets.refStationId) continue; - idMap.set(meta.id, (await saveStation(await buildStation(meta))).id) - process.stdout.write('.') + idMap.set(meta.id, (await saveStation(await buildStation(meta))).id); + process.stdout.write("."); } - console.log(`\nDone. Created ${subordinateStations.length} stations.`) + console.log(`\nDone. Created ${subordinateStations.length} stations.`); } async function saveStation(data: Station) { - await save(data) - return data + await save(data); + return data; } async function buildStation(meta: any): Promise { const station = { name: meta.name, - continent: 'North America', - country: 'United States', + continent: "North America", + country: "United States", region: meta.state, - type: meta.type == 'S' ? 'subordinate' : 'reference', + type: meta.type == "S" ? "subordinate" : "reference", latitude: meta.lat, longitude: meta.lng, timezone: meta.timezone, @@ -73,18 +75,18 @@ async function buildStation(meta: any): Promise { url: `https://tidesandcurrents.noaa.gov/stationhome.html?id=${meta.id}`, }, license: { - type: 'public domain', + type: "public domain", commercial_use: true, - url: 'https://tidesandcurrents.noaa.gov/disclaimers.html', + url: "https://tidesandcurrents.noaa.gov/disclaimers.html", }, - } + }; - if (meta.type == 'S') { - const refId = idMap.get(meta.tidepredoffsets.refStationId) + if (meta.type == "S") { + const refId = idMap.get(meta.tidepredoffsets.refStationId); if (!refId) { throw new Error( - `Reference station ID ${meta.tidepredoffsets.refStationId} not found for subordinate station ${meta.id}` - ) + `Reference station ID ${meta.tidepredoffsets.refStationId} not found for subordinate station ${meta.id}`, + ); } Object.assign(station, { @@ -92,7 +94,7 @@ async function buildStation(meta: any): Promise { reference: refId, height: { type: - meta.tidepredoffsets.heightAdjustedType === 'R' ? 'ratio' : 'fixed', + meta.tidepredoffsets.heightAdjustedType === "R" ? "ratio" : "fixed", high: meta.tidepredoffsets.heightOffsetHighTide, low: meta.tidepredoffsets.heightOffsetLowTide, }, @@ -101,16 +103,16 @@ async function buildStation(meta: any): Promise { low: meta.tidepredoffsets.timeOffsetLowTide, }, }, - }) + }); } else { // Fetch full station details const res = await fetch( - `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations/${meta.id}.json?expand=details,datums,harcon,disclaimers,notices&units=metric` - ).then((r) => r.json()) - const data = res.stations[0] + `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations/${meta.id}.json?expand=details,datums,harcon,disclaimers,notices&units=metric`, + ).then((r) => r.json()); + const data = res.stations[0]; // This should never happen, but just in case - if (!data) throw new Error(`No data found for station ID: ${meta.id}`) + if (!data) throw new Error(`No data found for station ID: ${meta.id}`); Object.assign(station, { harmonic_constituents: data.harmonicConstituents.HarmonicConstituents.map( @@ -121,7 +123,7 @@ async function buildStation(meta: any): Promise { phase: h.phase_GMT, speed: h.speed, // TODO: add comments - }) + }), ), datums: { ...(data.datums.LAT ? { LAT: data.datums.LAT } : {}), @@ -129,17 +131,17 @@ async function buildStation(meta: any): Promise { // Some stations don't have all datums ...(data.datums.datums ? Object.fromEntries( - data.datums.datums.map((d: any) => [d.name, d.value]) + data.datums.datums.map((d: any) => [d.name, d.value]), ) : {}), }, disclaimers: (data.disclaimers.disclaimers ?? []) .map((d: any) => d.text) - .join('\n'), - }) + .join("\n"), + }); } - return normalize(station as Station) + return normalize(station as Station); } -main().catch(console.error) +main().catch(console.error); diff --git a/tools/util.ts b/tools/util.ts index d52650cdb..b7cabd4b2 100644 --- a/tools/util.ts +++ b/tools/util.ts @@ -4,46 +4,46 @@ export function slugify(text: string) { return text .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); } export function indexBy(data: T[], key: keyof T) { return Object.fromEntries( data.map((r) => { - return [r[key], r] - }) - ) + return [r[key], r]; + }), + ); } export function groupBy( data: T[], - keyFn: (item: T) => string + keyFn: (item: T) => string, ): Record { - const result: Record = {} + const result: Record = {}; for (const item of data) { - const key = keyFn(item) + const key = keyFn(item); if (!result[key]) { - result[key] = [] + result[key] = []; } - result[key].push(item) + result[key].push(item); } - return result + return result; } /** * Parse a CSV file */ export function parseCSV(content: string): T[] { - const lines = content.trim().split(/[\r\n]+/) - const headers = (lines.shift() ?? '').split(',') + const lines = content.trim().split(/[\r\n]+/); + const headers = (lines.shift() ?? "").split(","); return lines.map((line) => { - const values = line.split(',') + const values = line.split(","); return Object.fromEntries( - headers.map((header, index) => [header, values[index]]) - ) as T - }) + headers.map((header, index) => [header, values[index]]), + ) as T; + }); } diff --git a/tsdown.config.js b/tsdown.config.js index 45460d612..5f71df832 100644 --- a/tsdown.config.js +++ b/tsdown.config.js @@ -1,11 +1,11 @@ -import { defineConfig } from 'tsdown' +import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ['./src/index.ts'], + entry: ["./src/index.ts"], dts: true, - format: ['cjs', 'esm'], + format: ["cjs", "esm"], sourcemap: true, declarationMap: true, - target: 'es2020', - platform: 'neutral' -}) + target: "es2020", + platform: "neutral", +});