diff --git a/.eslintignore b/.eslintignore index f5f8a9e7ccd..2487b54410d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ packages/api/app/bundle.api.js packages/api/dist +packages/api/@types packages/api/migrations packages/crdt/dist diff --git a/.eslintrc.js b/.eslintrc.js index c411995fc87..2131f232a39 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -163,6 +163,11 @@ module.exports = { { patterns: [...restrictedImportPatterns, ...restrictedImportColors] }, ], + '@typescript-eslint/ban-ts-comment': [ + 'error', + { 'ts-ignore': 'allow-with-description' }, + ], + // Rules disable during TS migration '@typescript-eslint/no-var-requires': 'off', 'prefer-const': 'warn', diff --git a/.gitignore b/.gitignore index 691472708e3..9777ed2d001 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ !data/.gitkeep /data2 packages/api/dist +packages/api/@types packages/crdt/dist packages/desktop-electron/client-build packages/desktop-electron/.electron-symbols diff --git a/packages/api/index.js b/packages/api/index.js deleted file mode 100644 index 4473e540c86..00000000000 --- a/packages/api/index.js +++ /dev/null @@ -1,38 +0,0 @@ -// eslint-disable-next-line import/extensions -import * as bundle from './app/bundle.api.js'; -import * as injected from './injected'; -import { validateNodeVersion } from './validateNodeVersion'; - -let actualApp; -export const internal = bundle.lib; - -// DEPRECATED: remove the next line in @actual-app/api v7 -export * as methods from './methods'; - -export * from './methods'; -export * as utils from './utils'; - -export async function init(config = {}) { - if (actualApp) { - return; - } - - validateNodeVersion(); - - global.fetch = (...args) => - import('node-fetch').then(({ default: fetch }) => fetch(...args)); - - await bundle.init(config); - actualApp = bundle.lib; - - injected.override(bundle.lib.send); - return bundle.lib; -} - -export async function shutdown() { - if (actualApp) { - await actualApp.send('sync'); - await actualApp.send('close-budget'); - actualApp = null; - } -} diff --git a/packages/api/index.ts b/packages/api/index.ts new file mode 100644 index 00000000000..0e2c6fcd70b --- /dev/null +++ b/packages/api/index.ts @@ -0,0 +1,55 @@ +// @ts-ignore: false-positive commonjs module error on build until typescript 5.3 +import type { + RequestInfo as FetchRequestInfo, + RequestInit as FetchRequestInit, +} from 'node-fetch'; // with { 'resolution-mode': 'import' }; + +// loot-core types +import type { initConfig } from 'loot-core/server/main'; + +// eslint-disable-next-line import/extensions +import * as bundle from './app/bundle.api.js'; +import * as injected from './injected'; +import { validateNodeVersion } from './validateNodeVersion'; + +let actualApp: null | typeof bundle.lib; +export const internal = bundle.lib; + +// DEPRECATED: remove the next line in @actual-app/api v7 +export * as methods from './methods'; + +export * from './methods'; +export * as utils from './utils'; + +export async function init(config: initConfig = {}) { + if (actualApp) { + return; + } + + validateNodeVersion(); + + if (!globalThis.fetch) { + globalThis.fetch = (url: URL | RequestInfo, init?: RequestInit) => { + return import('node-fetch').then(({ default: fetch }) => + fetch( + url as unknown as FetchRequestInfo, + init as unknown as FetchRequestInit, + ), + ) as unknown as Promise; + }; + } + + await bundle.init(config); + actualApp = bundle.lib; + + injected.override(bundle.lib.send); + return bundle.lib; +} + +export async function shutdown() { + if (actualApp) { + await actualApp.send('sync'); + await actualApp.send('close-budget'); + actualApp = null; + } +} diff --git a/packages/api/methods.js b/packages/api/methods.ts similarity index 87% rename from packages/api/methods.js rename to packages/api/methods.ts index 9af541f2670..272a9b4693a 100644 --- a/packages/api/methods.js +++ b/packages/api/methods.ts @@ -1,8 +1,14 @@ +// @ts-strict-ignore +import type { Handlers } from 'loot-core/src/types/handlers'; + import * as injected from './injected'; export { q } from './app/query'; -function send(name, args) { +function send( + name: K, + args?: Parameters[0], +): Promise>> { return injected.send(name, args); } @@ -21,7 +27,7 @@ export async function loadBudget(budgetId) { return send('api/load-budget', { id: budgetId }); } -export async function downloadBudget(syncId, { password } = {}) { +export async function downloadBudget(syncId, password?) { return send('api/download-budget', { syncId, password }); } @@ -95,7 +101,7 @@ export function getAccounts() { return send('api/accounts-get'); } -export function createAccount(account, initialBalance) { +export function createAccount(account, initialBalance?) { return send('api/account-create', { account, initialBalance }); } @@ -103,7 +109,7 @@ export function updateAccount(id, fields) { return send('api/account-update', { id, fields }); } -export function closeAccount(id, transferAccountId, transferCategoryId) { +export function closeAccount(id, transferAccountId?, transferCategoryId?) { return send('api/account-close', { id, transferAccountId, @@ -127,7 +133,7 @@ export function updateCategoryGroup(id, fields) { return send('api/category-group-update', { id, fields }); } -export function deleteCategoryGroup(id, transferCategoryId) { +export function deleteCategoryGroup(id, transferCategoryId?) { return send('api/category-group-delete', { id, transferCategoryId }); } @@ -143,7 +149,7 @@ export function updateCategory(id, fields) { return send('api/category-update', { id, fields }); } -export function deleteCategory(id, transferCategoryId) { +export function deleteCategory(id, transferCategoryId?) { return send('api/category-delete', { id, transferCategoryId }); } diff --git a/packages/api/package.json b/packages/api/package.json index 7c2d9a01e5b..b0433a638e8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -7,17 +7,18 @@ "node": ">=18.12.0" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "@types/index.d.ts", "files": [ "dist" ], "scripts": { "build:app": "yarn workspace loot-core build:api", - "build:node": "tsc --p tsconfig.dist.json", + "build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json", "build:migrations": "cp migrations/*.sql dist/migrations", "build:default-db": "cp default-db.sqlite dist/", - "build": "rm -rf dist && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db", - "test": "yarn run build:app && jest -c jest.config.js" + "build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db", + "test": "yarn run build:app && jest -c jest.config.js", + "clean": "rm -rf dist @types" }, "dependencies": { "better-sqlite3": "^9.2.2", @@ -31,6 +32,7 @@ "@types/jest": "^27.5.0", "@types/uuid": "^9.0.2", "jest": "^27.0.0", + "tsc-alias": "^1.8.8", "typescript": "^5.0.2" } } diff --git a/packages/api/tsconfig.dist.json b/packages/api/tsconfig.dist.json index 6704cb1fb7c..12caac8f770 100644 --- a/packages/api/tsconfig.dist.json +++ b/packages/api/tsconfig.dist.json @@ -8,8 +8,12 @@ "moduleResolution": "Node16", "noEmit": false, "declaration": true, - "outDir": "dist" + "outDir": "dist", + "declarationDir": "@types", + "paths": { + "loot-core/*": ["./@types/loot-core/*"], + } }, "include": ["."], - "exclude": ["dist"] + "exclude": ["**/node_modules/*", "dist", "@types"] } diff --git a/packages/loot-core/bin/build-api b/packages/loot-core/bin/build-api new file mode 100755 index 00000000000..577e6342332 --- /dev/null +++ b/packages/loot-core/bin/build-api @@ -0,0 +1,21 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(dirname "$0")/.." || exit 1 +ROOT="$(pwd -P)" + +yarn tsc -p tsconfig.api.json --outDir ../api/@types/loot-core/ +# Copy existing handwritten .d.ts files, as tsc doesn't move them for us +dest="../../api/@types/loot-core" +cd src +find . -type f -name "*.d.ts" | while read -r f +do + d=$(dirname "${f}") + d="${dest}/${d}" + mkdir -p "${d}" + cp "${f}" "${d}" +done +cd "$ROOT" +yarn webpack --config ./webpack/webpack.api.config.js; +./bin/copy-migrations ../api \ No newline at end of file diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json index 48253fea564..aceb8e1f0a1 100644 --- a/packages/loot-core/package.json +++ b/packages/loot-core/package.json @@ -6,7 +6,7 @@ "scripts": { "build:node": "cross-env NODE_ENV=production webpack --config ./webpack/webpack.desktop.config.js", "watch:node": "cross-env NODE_ENV=development webpack --config ./webpack/webpack.desktop.config.js --watch", - "build:api": "cross-env NODE_ENV=development webpack --config ./webpack/webpack.api.config.js; ./bin/copy-migrations ../api", + "build:api": "cross-env NODE_ENV=development ./bin/build-api", "build:browser": "cross-env NODE_ENV=production ./bin/build-browser", "watch:browser": "cross-env NODE_ENV=development ./bin/build-browser", "test": "npm-run-all -cp 'test:*'", diff --git a/packages/loot-core/src/server/importers/ynab5.ts b/packages/loot-core/src/server/importers/ynab5.ts index f9fb1337e0e..a9e32ebfe35 100644 --- a/packages/loot-core/src/server/importers/ynab5.ts +++ b/packages/loot-core/src/server/importers/ynab5.ts @@ -146,8 +146,8 @@ async function importTransactions( ); const payeesByTransferAcct = payees - .filter((payee: YNAB5.Payee) => payee?.transfer_acct) - .map((payee: YNAB5.Payee) => [payee.transfer_acct, payee]); + .filter(payee => payee?.transfer_acct) + .map(payee => [payee.transfer_acct, payee] as [string, YNAB5.Payee]); const payeeTransferAcctHashMap = new Map( payeesByTransferAcct, ); diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts index 3a80926cdc8..c1c219f7a7f 100644 --- a/packages/loot-core/src/server/main.ts +++ b/packages/loot-core/src/server/main.ts @@ -2251,8 +2251,14 @@ export async function initApp(isDev, socketName) { } } +export type initConfig = { + dataDir?: string; + serverURL?: string; + password?: string; +}; + // eslint-disable-next-line import/no-unused-modules -export async function init(config) { +export async function init(config: initConfig) { // Get from build let dataDir, serverURL; diff --git a/packages/loot-core/src/server/mutators.ts b/packages/loot-core/src/server/mutators.ts index abd44cf829e..f5eac7b1d80 100644 --- a/packages/loot-core/src/server/mutators.ts +++ b/packages/loot-core/src/server/mutators.ts @@ -51,7 +51,9 @@ export async function runHandler( } if (mutatingMethods.has(handler)) { - return runMutator(() => handler(args), { undoTag }); + return runMutator(() => handler(args), { undoTag }) as Promise< + ReturnType + >; } // When closing a file, it clears out all global state for the file. That @@ -67,7 +69,7 @@ export async function runHandler( promise.then(() => { runningMethods.delete(promise); }); - return promise; + return promise as Promise>; } // These are useful for tests. Only use them in tests. diff --git a/packages/loot-core/src/shared/schedules.ts b/packages/loot-core/src/shared/schedules.ts index 23dbd9c4e8d..26c5c7692a7 100644 --- a/packages/loot-core/src/shared/schedules.ts +++ b/packages/loot-core/src/shared/schedules.ts @@ -1,5 +1,9 @@ // @ts-strict-ignore import type { IRuleOptions } from '@rschedule/core'; +// eslint-disable-next-line import/no-duplicates +import type { IByHourOfDayRuleRuleOptions } from '@rschedule/core/rules/ByHourOfDay'; +// eslint-disable-next-line import/no-duplicates +import type { IFrequencyRuleOptions } from '@rschedule/core/rules/Frequency'; import * as monthUtils from './months'; import { q } from './query'; @@ -183,7 +187,9 @@ export function getRecurringDescription(config, dateFormat) { } export function recurConfigToRSchedule(config) { - const base: IRuleOptions = { + const base: IRuleOptions & + IFrequencyRuleOptions & + IByHourOfDayRuleRuleOptions = { start: monthUtils.parseDate(config.start), frequency: config.frequency.toUpperCase(), byHourOfDay: [12], diff --git a/packages/loot-core/src/types/api-handlers.d.ts b/packages/loot-core/src/types/api-handlers.d.ts index c4e8c2b11b8..30e85921835 100644 --- a/packages/loot-core/src/types/api-handlers.d.ts +++ b/packages/loot-core/src/types/api-handlers.d.ts @@ -54,10 +54,11 @@ export interface ApiHandlers { payees; }) => Promise; - 'api/transactions-import': (arg: { - accountId; - transactions; - }) => Promise; + 'api/transactions-import': (arg: { accountId; transactions }) => Promise<{ + errors?: { message: string }[]; + added; + updated; + }>; 'api/transactions-add': (arg: { accountId; diff --git a/packages/loot-core/tsconfig.api.json b/packages/loot-core/tsconfig.api.json new file mode 100644 index 00000000000..5ffe224f8c9 --- /dev/null +++ b/packages/loot-core/tsconfig.api.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": false, + "noEmit": false, + }, + "include": ["./typings", "./src/server/*"], + "exclude": ["**/node_modules/*", "**/build/*", "**/lib-dist/*", "./src/server/bench.ts"], + } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b7a18248790..a421798af08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,6 +29,10 @@ "module": "ES2022", // Until/if we build using tsc "noEmit": true, + "paths": { + // until we turn on composite/references + "loot-core/*": ["./packages/loot-core/src/*"], + }, "plugins": [ { "name": "typescript-strict-plugin", diff --git a/upcoming-release-notes/2053.md b/upcoming-release-notes/2053.md new file mode 100644 index 00000000000..1c882269dd3 --- /dev/null +++ b/upcoming-release-notes/2053.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [twk3] +--- + +Bundle loot-core types into the API diff --git a/yarn.lock b/yarn.lock index 669359f25af..88390827d57 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,7 @@ __metadata: compare-versions: "npm:^6.1.0" jest: "npm:^27.0.0" node-fetch: "npm:^3.3.2" + tsc-alias: "npm:^1.8.8" typescript: "npm:^5.0.2" uuid: "npm:^9.0.0" languageName: unknown @@ -6328,7 +6329,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^9.4.1": +"commander@npm:^9.0.0, commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: 41c49b3d0f94a1fbeb0463c85b13f15aa15a9e0b4d5e10a49c0a1d58d4489b549d62262b052ae0aa6cfda53299bee487bfe337825df15e342114dde543f82906 @@ -9192,7 +9193,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": +"globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -12615,6 +12616,13 @@ __metadata: languageName: node linkType: hard +"mylas@npm:^2.1.9": + version: 2.1.13 + resolution: "mylas@npm:2.1.13" + checksum: 37f335424463c422f48d50317aa0a34fe410fabb146cbf27b453a0aa743732b5626f56deaa190bca2ce29836f809d88759007976dc78d5d22b75918a00586577 + languageName: node + linkType: hard + "nanoid@npm:^3.3.7": version: 3.3.7 resolution: "nanoid@npm:3.3.7" @@ -13569,6 +13577,15 @@ __metadata: languageName: node linkType: hard +"plimit-lit@npm:^1.2.6": + version: 1.6.1 + resolution: "plimit-lit@npm:1.6.1" + dependencies: + queue-lit: "npm:^1.5.1" + checksum: e4eaf018dc311fd4d452954c10992cd8a9eb72d168ec2274bb831d86558422703e1405a8978ffdd5c418654e6a25e10a0765a39bf3ce3a84dc799fe6268e0ea4 + languageName: node + linkType: hard + "plist@npm:^3.0.4, plist@npm:^3.0.5": version: 3.1.0 resolution: "plist@npm:3.1.0" @@ -13817,6 +13834,13 @@ __metadata: languageName: node linkType: hard +"queue-lit@npm:^1.5.1": + version: 1.5.2 + resolution: "queue-lit@npm:1.5.2" + checksum: 8dd45c79bd25b33b0c7d587391eb0b4acc4deb797bf92fef62b2d8e7c03b64083f5304f09d52a18267d34d020cc67ccde97a88185b67590eeccb194938ff1f98 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -16074,6 +16098,22 @@ __metadata: languageName: node linkType: hard +"tsc-alias@npm:^1.8.8": + version: 1.8.8 + resolution: "tsc-alias@npm:1.8.8" + dependencies: + chokidar: "npm:^3.5.3" + commander: "npm:^9.0.0" + globby: "npm:^11.0.4" + mylas: "npm:^2.1.9" + normalize-path: "npm:^3.0.0" + plimit-lit: "npm:^1.2.6" + bin: + tsc-alias: dist/bin/index.js + checksum: 145d7bb23a618e1136c8addd4b4ed23a1d503a37d3fc5b3698a993fea9331180a68853b0e78ff50fb3fb7ed95d4996a2d82f77395814bbd1c40adee8a9151d90 + languageName: node + linkType: hard + "tsconfck@npm:^2.1.0": version: 2.1.2 resolution: "tsconfck@npm:2.1.2"