Compare commits
4 Commits
deps/25.11
...
ts-runQuer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f515ed81a0 | ||
|
|
4fe5834532 | ||
|
|
999ed1cb3d | ||
|
|
ab40428ed3 |
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"setup-worktree": ["yarn"]
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml",
|
||||
"docker-compose.yml"
|
||||
],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
}
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,14 +1,15 @@
|
||||
name: Bug Report
|
||||
description: File a bug report also known as an issue or problem.
|
||||
title: '[Bug]: '
|
||||
labels: ['needs triage', 'bug']
|
||||
type: Bug
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
|
||||
@@ -22,6 +23,8 @@ body:
|
||||
options:
|
||||
- label: 'I have searched and found no existing issue'
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -40,6 +43,7 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: env-info
|
||||
attributes:
|
||||
value: '## Environment Details'
|
||||
- type: dropdown
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -2,9 +2,9 @@ name: Feature request
|
||||
description: Request a missing feature
|
||||
title: '[Feature] '
|
||||
labels: ['feature']
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! Please ensure you provide as much information as possible so we can better understand what you’re proposing so we can come up with the best solution for everyone.
|
||||
@@ -16,6 +16,8 @@ body:
|
||||
options:
|
||||
- label: 'I have searched and found no existing issue'
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: '💻'
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes -->
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
const commentId = process.env.GITHUB_EVENT_COMMENT_ID;
|
||||
|
||||
if (!token || !repo || !issueNumber || !commentId) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
function setOutput(name, value) {
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
||||
}
|
||||
|
||||
async function checkFirstComment() {
|
||||
try {
|
||||
console.log('Fetching comments with Octokit...');
|
||||
|
||||
// Get all comments with automatic pagination
|
||||
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
|
||||
owner,
|
||||
repo: repoName,
|
||||
issue_number: issueNumber,
|
||||
});
|
||||
|
||||
console.log(`Total comments found: ${comments.length}`);
|
||||
|
||||
// Filter for CodeRabbit summary comments (containing the specific marker)
|
||||
const coderabbitSummaryComments = comments.filter(comment => {
|
||||
const isCodeRabbit = comment.user.login === 'coderabbitai[bot]';
|
||||
const hasSummaryMarker = comment.body.includes(
|
||||
'<!-- This is an auto-generated comment: summarize by coderabbit.ai -->',
|
||||
);
|
||||
|
||||
if (isCodeRabbit) {
|
||||
console.log(
|
||||
`CodeRabbit comment found (ID: ${comment.id}), has summary marker: ${hasSummaryMarker}`,
|
||||
);
|
||||
}
|
||||
|
||||
return isCodeRabbit && hasSummaryMarker;
|
||||
});
|
||||
|
||||
const isFirstSummaryComment =
|
||||
coderabbitSummaryComments.length === 1 &&
|
||||
coderabbitSummaryComments[0].id == commentId;
|
||||
|
||||
console.log(
|
||||
`CodeRabbit summary comments found: ${coderabbitSummaryComments.length}`,
|
||||
);
|
||||
console.log(`Current comment ID: ${commentId}`);
|
||||
console.log(`Is first summary comment: ${isFirstSummaryComment}`);
|
||||
setOutput('result', isFirstSummaryComment);
|
||||
} catch (error) {
|
||||
console.log('Error checking CodeRabbit comment:', error.message);
|
||||
console.log('Stack:', error.stack);
|
||||
setOutput('result', 'false');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkFirstComment().catch(error => {
|
||||
console.log('Unhandled error:', error.message);
|
||||
console.log('Stack:', error.stack);
|
||||
setOutput('result', 'false');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
const prDetailsJson = process.env.PR_DETAILS;
|
||||
|
||||
if (!token || !repo || !issueNumber || !prDetailsJson) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
function setOutput(name, value) {
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
||||
}
|
||||
|
||||
async function checkReleaseNotesExists() {
|
||||
try {
|
||||
const prDetails = JSON.parse(prDetailsJson);
|
||||
if (!prDetails) {
|
||||
console.log('No PR details available, skipping file check');
|
||||
setOutput('result', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = `upcoming-release-notes/${prDetails.number}.md`;
|
||||
|
||||
// Get PR info to get head SHA
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo: repoName,
|
||||
pull_number: issueNumber,
|
||||
});
|
||||
|
||||
const prHeadSha = pr.head.sha;
|
||||
console.log(
|
||||
`Checking for file on PR branch: ${pr.head.ref} (${prHeadSha})`,
|
||||
);
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
await octokit.rest.repos.getContent({
|
||||
owner,
|
||||
repo: repoName,
|
||||
path: fileName,
|
||||
ref: prHeadSha,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Release notes file already exists on PR branch: ${fileName}`,
|
||||
);
|
||||
setOutput('result', 'true');
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
console.log(
|
||||
`No existing release notes file found on PR branch: ${fileName}`,
|
||||
);
|
||||
setOutput('result', 'false');
|
||||
} else {
|
||||
console.log('Error checking file existence:', error.message);
|
||||
setOutput('result', 'false');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error in file existence check:', error.message);
|
||||
setOutput('result', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
checkReleaseNotesExists();
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
const summaryDataJson = process.env.SUMMARY_DATA;
|
||||
const category = process.env.CATEGORY;
|
||||
|
||||
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
async function commentOnPR() {
|
||||
try {
|
||||
const summaryData = JSON.parse(summaryDataJson);
|
||||
|
||||
if (!summaryData) {
|
||||
console.log('No summary data available, skipping comment');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!category || category === 'null') {
|
||||
console.log('No valid category available, skipping comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean category for display
|
||||
const cleanCategory =
|
||||
typeof category === 'string'
|
||||
? category.replace(/^["']|["']$/g, '')
|
||||
: category;
|
||||
|
||||
// Get PR info for the file URL
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo: repoName,
|
||||
pull_number: issueNumber,
|
||||
});
|
||||
|
||||
const prBranch = pr.head.ref;
|
||||
const headOwner = pr.head.repo.owner.login;
|
||||
const headRepo = pr.head.repo.name;
|
||||
const fileUrl = `https://github.com/${headOwner}/${headRepo}/blob/${prBranch}/upcoming-release-notes/${summaryData.prNumber}.md`;
|
||||
|
||||
const commentBody = [
|
||||
'🤖 **Auto-generated Release Notes**',
|
||||
'',
|
||||
`Hey @${summaryData.author}! I've automatically created a release notes file based on CodeRabbit's analysis:`,
|
||||
'',
|
||||
`**Category:** ${cleanCategory}`,
|
||||
`**Summary:** ${summaryData.summary}`,
|
||||
`**File:** [upcoming-release-notes/${summaryData.prNumber}.md](${fileUrl})`,
|
||||
'',
|
||||
// 'The release notes file has been committed to the repository. You can edit it if needed before merging.',
|
||||
"If you're happy with this release note, you can add it to your pull request. If not, you'll need to add your own before a maintainer can review your change.",
|
||||
].join('\n');
|
||||
|
||||
await octokit.rest.issues.createComment({
|
||||
owner,
|
||||
repo: repoName,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody,
|
||||
});
|
||||
|
||||
console.log('✅ Successfully commented on PR');
|
||||
} catch (error) {
|
||||
console.log('Error commenting on PR:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
commentOnPR();
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
const summaryDataJson = process.env.SUMMARY_DATA;
|
||||
const category = process.env.CATEGORY;
|
||||
|
||||
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
async function createReleaseNotesFile() {
|
||||
try {
|
||||
const summaryData = JSON.parse(summaryDataJson);
|
||||
|
||||
console.log('Debug - Category value:', category);
|
||||
console.log('Debug - Category type:', typeof category);
|
||||
console.log('Debug - Category JSON stringified:', JSON.stringify(category));
|
||||
|
||||
if (!summaryData) {
|
||||
console.log('No summary data available, cannot create file');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!category || category === 'null') {
|
||||
console.log('No valid category available, cannot create file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create file content - ensure category is not quoted
|
||||
const cleanCategory =
|
||||
typeof category === 'string'
|
||||
? category.replace(/^["']|["']$/g, '')
|
||||
: category;
|
||||
console.log('Debug - Clean category:', cleanCategory);
|
||||
|
||||
const fileContent = `---
|
||||
category: ${cleanCategory}
|
||||
authors: [${summaryData.author}]
|
||||
---
|
||||
|
||||
${summaryData.summary}`;
|
||||
|
||||
const fileName = `upcoming-release-notes/${summaryData.prNumber}.md`;
|
||||
|
||||
console.log(`Creating release notes file: ${fileName}`);
|
||||
console.log('File content:');
|
||||
console.log(fileContent);
|
||||
|
||||
// Get PR info
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo: repoName,
|
||||
pull_number: issueNumber,
|
||||
});
|
||||
|
||||
const prBranch = pr.head.ref;
|
||||
const headOwner = pr.head.repo.owner.login;
|
||||
const headRepo = pr.head.repo.name;
|
||||
|
||||
console.log(
|
||||
`Committing to PR branch: ${headOwner}/${headRepo}:${prBranch}`,
|
||||
);
|
||||
|
||||
// Create the file via GitHub API on the PR branch
|
||||
await octokit.rest.repos.createOrUpdateFileContents({
|
||||
owner: headOwner,
|
||||
repo: headRepo,
|
||||
path: fileName,
|
||||
message: `Add release notes for PR #${summaryData.prNumber}`,
|
||||
content: Buffer.from(`${fileContent}\n\n`).toString('base64'),
|
||||
branch: prBranch,
|
||||
committer: {
|
||||
name: 'github-actions[bot]',
|
||||
email: 'github-actions[bot]@users.noreply.github.com',
|
||||
},
|
||||
author: {
|
||||
name: 'github-actions[bot]',
|
||||
email: 'github-actions[bot]@users.noreply.github.com',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Successfully created release notes file: ${fileName}`);
|
||||
} catch (error) {
|
||||
console.log('Error creating release notes file:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
createReleaseNotesFile();
|
||||
@@ -1,118 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
|
||||
const prDetailsJson = process.env.PR_DETAILS;
|
||||
const summaryDataJson = process.env.SUMMARY_DATA;
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!commentBody || !prDetailsJson || !summaryDataJson || !openaiApiKey) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function setOutput(name, value) {
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
const prDetails = JSON.parse(prDetailsJson);
|
||||
const summaryData = JSON.parse(summaryDataJson);
|
||||
|
||||
if (!summaryData || !prDetails) {
|
||||
console.log('Missing data for categorization');
|
||||
setOutput('result', 'null');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const data = JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
|
||||
},
|
||||
],
|
||||
max_tokens: 10,
|
||||
temperature: 0.1,
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.openai.com',
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${openaiApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, res => {
|
||||
let responseData = '';
|
||||
res.on('data', chunk => (responseData += chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.log('OpenAI API error for categorization');
|
||||
setOutput('result', 'null');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(responseData);
|
||||
console.log('OpenAI raw response:', JSON.stringify(response, null, 2));
|
||||
|
||||
const rawContent = response.choices[0].message.content.trim();
|
||||
console.log('Raw content from OpenAI:', rawContent);
|
||||
|
||||
let category;
|
||||
try {
|
||||
category = JSON.parse(rawContent);
|
||||
console.log('Parsed category:', category);
|
||||
} catch (parseError) {
|
||||
console.log(
|
||||
'JSON parse error, using raw content:',
|
||||
parseError.message,
|
||||
);
|
||||
category = rawContent;
|
||||
}
|
||||
|
||||
// Validate the category response
|
||||
const validCategories = [
|
||||
'Features',
|
||||
'Bugfix',
|
||||
'Enhancements',
|
||||
'Maintenance',
|
||||
];
|
||||
if (validCategories.includes(category)) {
|
||||
console.log('OpenAI categorized as:', category);
|
||||
setOutput('result', category);
|
||||
} else {
|
||||
console.log('Invalid category from OpenAI:', category);
|
||||
console.log('Valid categories are:', validCategories);
|
||||
setOutput('result', 'null');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error parsing OpenAI response:', error.message);
|
||||
setOutput('result', 'null');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', error => {
|
||||
console.log('Error in categorization:', error.message);
|
||||
setOutput('result', 'null');
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
} catch (error) {
|
||||
console.log('Error in categorization:', error.message);
|
||||
setOutput('result', 'null');
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
|
||||
const prDetailsJson = process.env.PR_DETAILS;
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!commentBody || !prDetailsJson || !openaiApiKey) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function setOutput(name, value) {
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
const prDetails = JSON.parse(prDetailsJson);
|
||||
|
||||
if (!prDetails) {
|
||||
console.log('No PR details available, cannot generate summary');
|
||||
setOutput('result', 'null');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('CodeRabbit comment body:', commentBody);
|
||||
|
||||
const data = JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content:
|
||||
'You are a technical writer helping to create concise release notes. Generate a maximum 15-word summary that describes what this PR does. Focus on the user-facing changes or bug fixes. Do not include "This PR" or similar phrases - just describe the change directly. Start with a base form verb (e.g., "Add" not "Adds", "Fix" not "Fixes", "Introduce" not "Introduces").',
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `PR Title: ${prDetails.title}\n\nCodeRabbit Analysis:\n${commentBody}\n\nPlease provide a concise summary (max 15 words) of what this PR accomplishes.`,
|
||||
},
|
||||
],
|
||||
max_tokens: 50,
|
||||
temperature: 0.3,
|
||||
});
|
||||
|
||||
const options = {
|
||||
hostname: 'api.openai.com',
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${openaiApiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
const req = https.request(options, res => {
|
||||
let responseData = '';
|
||||
res.on('data', chunk => (responseData += chunk));
|
||||
res.on('end', () => {
|
||||
if (res.statusCode !== 200) {
|
||||
console.log(`OpenAI API error: ${res.statusCode} ${res.statusMessage}`);
|
||||
setOutput('result', 'null');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = JSON.parse(responseData);
|
||||
const summary = response.choices[0].message.content.trim();
|
||||
|
||||
console.log('Generated summary:', summary);
|
||||
|
||||
const result = {
|
||||
summary: summary,
|
||||
prNumber: prDetails.number,
|
||||
author: prDetails.author,
|
||||
};
|
||||
|
||||
setOutput('result', JSON.stringify(result));
|
||||
} catch (error) {
|
||||
console.log('Error parsing OpenAI response:', error.message);
|
||||
setOutput('result', 'null');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', error => {
|
||||
console.log('Error generating summary:', error.message);
|
||||
setOutput('result', 'null');
|
||||
});
|
||||
|
||||
req.write(data);
|
||||
req.end();
|
||||
} catch (error) {
|
||||
console.log('Error generating summary:', error.message);
|
||||
setOutput('result', 'null');
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import fs from 'fs';
|
||||
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
const repo = process.env.GITHUB_REPOSITORY;
|
||||
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
|
||||
|
||||
if (!token || !repo || !issueNumber) {
|
||||
console.log('Missing required environment variables');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [owner, repoName] = repo.split('/');
|
||||
const octokit = new Octokit({ auth: token });
|
||||
|
||||
function setOutput(name, value) {
|
||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
|
||||
}
|
||||
|
||||
async function getPRDetails() {
|
||||
try {
|
||||
console.log(
|
||||
`Fetching PR details for ${owner}/${repoName}#${issueNumber}...`,
|
||||
);
|
||||
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo: repoName,
|
||||
pull_number: issueNumber,
|
||||
});
|
||||
|
||||
console.log('PR details fetched successfully');
|
||||
console.log('- PR Number:', pr.number);
|
||||
console.log('- PR Author:', pr.user.login);
|
||||
console.log('- PR Title:', pr.title);
|
||||
|
||||
const result = {
|
||||
number: pr.number,
|
||||
author: pr.user.login,
|
||||
title: pr.title,
|
||||
};
|
||||
|
||||
setOutput('result', JSON.stringify(result));
|
||||
} catch (error) {
|
||||
console.log('Error getting PR details:', error.message);
|
||||
console.log('Stack:', error.stack);
|
||||
setOutput('result', 'null');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
getPRDetails().catch(error => {
|
||||
console.log('Unhandled error:', error.message);
|
||||
console.log('Stack:', error.stack);
|
||||
setOutput('result', 'null');
|
||||
process.exit(1);
|
||||
});
|
||||
19
.github/actions/setup/action.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Setup
|
||||
description: Setup the environment for the project
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
@@ -17,31 +16,17 @@ runs:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 18.16.0
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
if: ${{ env.ACT }}
|
||||
- name: Get Node version
|
||||
id: get-node
|
||||
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Ensure Lage cache directory exists
|
||||
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
shell: bash
|
||||
- name: Cache Lage
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
lage-${{ runner.os }}-
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
|
||||
363
.github/scripts/count-points.mjs
vendored
@@ -1,363 +0,0 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { minimatch } from 'minimatch';
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
const limit = pLimit(50);
|
||||
|
||||
/** Repository-specific configuration for points calculation */
|
||||
const REPOSITORY_CONFIG = new Map([
|
||||
[
|
||||
'actual',
|
||||
{
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 0,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 500, points: 8 },
|
||||
{ minChanges: 100, points: 6 },
|
||||
{ minChanges: 10, points: 2 },
|
||||
{ minChanges: 0, points: 1 },
|
||||
],
|
||||
EXCLUDED_FILES: [
|
||||
'yarn.lock',
|
||||
'.yarn/**/*',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'release-notes/**/*',
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'docs',
|
||||
{
|
||||
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 2000, points: 6 },
|
||||
{ minChanges: 200, points: 4 },
|
||||
{ minChanges: 0, points: 2 },
|
||||
],
|
||||
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
/**
|
||||
* Get the start and end dates for the last month.
|
||||
* @returns {Object} An object containing the start and end dates.
|
||||
*/
|
||||
function getLastMonthDates() {
|
||||
// Get data relating to the last month
|
||||
const now = new Date();
|
||||
// Always use UTC for calculations
|
||||
const firstDayOfLastMonth = new Date(
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0, 0),
|
||||
);
|
||||
const since = process.env.START_DATE
|
||||
? new Date(Date.parse(process.env.START_DATE))
|
||||
: firstDayOfLastMonth;
|
||||
|
||||
// Calculate the end of the month for the since date in UTC
|
||||
const until = new Date(
|
||||
Date.UTC(
|
||||
since.getUTCFullYear(),
|
||||
since.getUTCMonth() + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
999,
|
||||
),
|
||||
);
|
||||
|
||||
return { since, until };
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for calculating the monthly points each core contributor has earned.
|
||||
* These are used for payouts depending.
|
||||
* @param {string} repo - The repository to analyze ('actual' or 'docs')
|
||||
* @returns {number} The total points earned for the repository
|
||||
*/
|
||||
async function countContributorPoints(repo) {
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
const owner = 'actualbudget';
|
||||
const config = REPOSITORY_CONFIG.get(repo);
|
||||
|
||||
const { since, until } = getLastMonthDates();
|
||||
|
||||
// Get organization members
|
||||
const { data: orgMembers } = await octokit.orgs.listMembers({
|
||||
org: owner,
|
||||
});
|
||||
const orgMemberLogins = new Set(orgMembers.map(member => member.login));
|
||||
|
||||
// Initialize stats map with all org members
|
||||
const stats = new Map(
|
||||
Array.from(orgMemberLogins).map(login => [
|
||||
login,
|
||||
{
|
||||
reviews: [], // Will store objects with PR number and points
|
||||
labelRemovals: [],
|
||||
issueClosings: [],
|
||||
points: 0,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
// Helper function to print statistics
|
||||
const printStats = (title, getValue, formatLine) => {
|
||||
console.log(`\n${title}:`);
|
||||
console.log('='.repeat(title.length + 1));
|
||||
|
||||
const entries = Array.from(stats.entries())
|
||||
.map(([user, userStats]) => [user, getValue(userStats)])
|
||||
.filter(([, count]) => count > 0)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log(`No ${title.toLowerCase()} found in the last month.`);
|
||||
} else {
|
||||
entries.forEach(([user, count]) => {
|
||||
console.log(formatLine(user, count));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Get all PRs using search
|
||||
const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
|
||||
const recentPRs = await octokit.paginate(
|
||||
'GET /search/issues',
|
||||
{
|
||||
q: searchQuery,
|
||||
per_page: 100,
|
||||
advanced_search: true,
|
||||
},
|
||||
response => response.data.filter(pr => pr.number),
|
||||
);
|
||||
|
||||
// Get reviews and PR details for each PR
|
||||
await Promise.all(
|
||||
recentPRs.map(pr =>
|
||||
limit(async () => {
|
||||
const [reviews, modifiedFiles] = await Promise.all([
|
||||
octokit.pulls.listReviews({ owner, repo, pull_number: pr.number }),
|
||||
octokit.paginate(
|
||||
octokit.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
per_page: 100,
|
||||
},
|
||||
res => res.data,
|
||||
),
|
||||
]);
|
||||
|
||||
const totalChanges = modifiedFiles
|
||||
.filter(
|
||||
file =>
|
||||
!config.EXCLUDED_FILES.some(pattern =>
|
||||
minimatch(file.filename, pattern),
|
||||
),
|
||||
)
|
||||
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
|
||||
|
||||
const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
|
||||
const prPoints =
|
||||
config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
|
||||
?.points ?? 0;
|
||||
|
||||
if (isReleasePR) {
|
||||
if (stats.has(pr.user.login)) {
|
||||
const creatorStats = stats.get(pr.user.login);
|
||||
creatorStats.reviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: config.POINTS_PER_RELEASE_PR,
|
||||
isReleaseCreator: true,
|
||||
});
|
||||
creatorStats.points += config.POINTS_PER_RELEASE_PR;
|
||||
}
|
||||
} else {
|
||||
const uniqueReviewers = new Set();
|
||||
reviews.data
|
||||
.filter(
|
||||
review =>
|
||||
stats.has(review.user?.login) &&
|
||||
review.state === 'APPROVED' &&
|
||||
!uniqueReviewers.has(review.user?.login),
|
||||
)
|
||||
.forEach(({ user: { login: reviewer } }) => {
|
||||
uniqueReviewers.add(reviewer);
|
||||
const userStats = stats.get(reviewer);
|
||||
userStats.reviews.push({
|
||||
pr: pr.number.toString(),
|
||||
points: prPoints,
|
||||
});
|
||||
userStats.points += prPoints;
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Get all issues with label events in the last month
|
||||
const issues = await octokit.paginate(octokit.issues.listForRepo, {
|
||||
owner,
|
||||
repo,
|
||||
state: 'all',
|
||||
sort: 'updated',
|
||||
direction: 'desc',
|
||||
per_page: 100,
|
||||
since: since.toISOString(),
|
||||
});
|
||||
|
||||
// Get label events for each issue
|
||||
await Promise.all(
|
||||
issues.map(issue =>
|
||||
limit(async () => {
|
||||
const { data: events } = await octokit.issues.listEventsForTimeline({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
|
||||
events
|
||||
.filter(event => {
|
||||
const createdAt = new Date(event.created_at);
|
||||
return (
|
||||
createdAt.getTime() > since.getTime() &&
|
||||
createdAt.getTime() <= until.getTime() &&
|
||||
stats.has(event.actor?.login)
|
||||
);
|
||||
})
|
||||
.forEach(event => {
|
||||
if (
|
||||
event.event === 'unlabeled' &&
|
||||
event.label?.name.toLowerCase() === 'needs triage'
|
||||
) {
|
||||
const remover = event.actor.login;
|
||||
const userStats = stats.get(remover);
|
||||
userStats.labelRemovals.push(issue.number.toString());
|
||||
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
|
||||
}
|
||||
|
||||
if (
|
||||
event.event === 'closed' &&
|
||||
event.state_reason === 'not_planned'
|
||||
) {
|
||||
const closer = event.actor.login;
|
||||
const userStats = stats.get(closer);
|
||||
userStats.issueClosings.push(issue.number.toString());
|
||||
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
|
||||
}
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Print all statistics
|
||||
printStats(
|
||||
`PR Review Statistics (${repo})`,
|
||||
stats => stats.reviews.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (PRs: ${stats
|
||||
.get(user)
|
||||
.reviews.map(r => {
|
||||
if (r.isReleaseCreator) {
|
||||
return `#${r.pr} (${r.points}pts - Release Creator)`;
|
||||
}
|
||||
return `#${r.pr} (${r.points}pts)`;
|
||||
})
|
||||
.join(', ')})`,
|
||||
);
|
||||
printStats(
|
||||
`"Needs Triage" Label Removal Statistics (${repo})`,
|
||||
stats => stats.labelRemovals.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
|
||||
);
|
||||
printStats(
|
||||
`Issue Closing Statistics (${repo})`,
|
||||
stats => stats.issueClosings.length,
|
||||
(user, count) =>
|
||||
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
|
||||
);
|
||||
|
||||
// Print points summary
|
||||
printStats(
|
||||
`Points Summary (${repo})`,
|
||||
stats => stats.points,
|
||||
(user, userPoints) => `${user}: ${userPoints}`,
|
||||
);
|
||||
|
||||
// Calculate and print total points
|
||||
const totalPoints = Array.from(stats.values()).reduce(
|
||||
(sum, userStats) => sum + userStats.points,
|
||||
0,
|
||||
);
|
||||
console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);
|
||||
|
||||
// Return the points
|
||||
return new Map(
|
||||
Array.from(stats.entries()).map(([login, userStats]) => [
|
||||
login,
|
||||
userStats.points,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the points for both repositories and print cumulative results
|
||||
*/
|
||||
async function calculateCumulativePoints() {
|
||||
// Get stats for each repository
|
||||
const repoPointsResults = await Promise.all(
|
||||
Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
|
||||
);
|
||||
|
||||
// Calculate cumulative stats
|
||||
const cumulativeStats = new Map(repoPointsResults[0]);
|
||||
|
||||
// Combine stats from all repositories
|
||||
for (let i = 1; i < repoPointsResults.length; i++) {
|
||||
for (const [login, points] of repoPointsResults[i].entries()) {
|
||||
if (!cumulativeStats.has(login)) {
|
||||
cumulativeStats.set(login, 0);
|
||||
}
|
||||
|
||||
cumulativeStats.set(login, cumulativeStats.get(login) + points);
|
||||
}
|
||||
}
|
||||
|
||||
// Print cumulative statistics
|
||||
console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
console.log('\nCumulative Points Summary:');
|
||||
console.log('='.repeat('Cumulative Points Summary'.length + 1));
|
||||
|
||||
const entries = Array.from(cumulativeStats.entries())
|
||||
.filter(([, count]) => count > 0)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log('No cumulative points summary found.');
|
||||
} else {
|
||||
entries.forEach(([user, points]) => {
|
||||
console.log(`${user}: ${points}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate and print total cumulative points
|
||||
const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
|
||||
(sum, points) => sum + points,
|
||||
0,
|
||||
);
|
||||
console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
|
||||
}
|
||||
|
||||
// Run the calculations
|
||||
calculateCumulativePoints().catch(console.error);
|
||||
89
.github/workflows/ai-generated-release-notes.yml
vendored
@@ -1,89 +0,0 @@
|
||||
name: Generate Release Notes from CodeRabbit summary
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
generate-release-notes:
|
||||
# Only run on PR comments from CodeRabbit bot
|
||||
if: github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Check if this is CodeRabbit's first comment
|
||||
id: check-first-comment
|
||||
run: node .github/actions/ai-generated-release-notes/check-first-comment.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
|
||||
|
||||
- name: Get PR details
|
||||
if: steps.check-first-comment.outputs.result == 'true'
|
||||
id: pr-details
|
||||
run: node .github/actions/ai-generated-release-notes/pr-details.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
- name: Check if release notes file already exists
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
|
||||
id: check-release-notes-exists
|
||||
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
|
||||
|
||||
- name: Generate summary with OpenAI
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false'
|
||||
id: generate-summary
|
||||
run: node .github/actions/ai-generated-release-notes/generate-summary.js
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
|
||||
|
||||
- name: Determine category with OpenAI
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null'
|
||||
id: determine-category
|
||||
run: node .github/actions/ai-generated-release-notes/determine-category.js
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
|
||||
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
|
||||
|
||||
- name: Create and commit release notes file via GitHub API
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
|
||||
run: node .github/actions/ai-generated-release-notes/create-release-notes-file.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
|
||||
CATEGORY: ${{ steps.determine-category.outputs.result }}
|
||||
|
||||
- name: Comment on PR
|
||||
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
|
||||
run: node .github/actions/ai-generated-release-notes/comment-on-pr.js
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
|
||||
CATEGORY: ${{ steps.determine-category.outputs.result }}
|
||||
23
.github/workflows/autofix.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: autofix.ci
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
autofix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Format code
|
||||
run: yarn lint:fix
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
22
.github/workflows/build.yml
vendored
@@ -24,8 +24,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build API
|
||||
run: cd packages/api && yarn build
|
||||
- name: Create package tgz
|
||||
@@ -42,8 +40,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build CRDT
|
||||
run: cd packages/crdt && yarn build
|
||||
- name: Create package tgz
|
||||
@@ -61,7 +57,7 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:browser
|
||||
run: ./bin/package-browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -72,19 +68,3 @@ jobs:
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build Server
|
||||
run: yarn workspace @actual-app/sync-server build
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sync-server
|
||||
path: packages/sync-server/build
|
||||
|
||||
20
.github/workflows/check.yml
vendored
@@ -17,8 +17,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
typecheck:
|
||||
@@ -27,30 +25,14 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Typecheck
|
||||
run: yarn typecheck
|
||||
validate-cli:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
- name: Check that the built CLI works
|
||||
run: node packages/sync-server/build/bin/actual-server.js --version
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
@@ -61,6 +43,6 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
26
.github/workflows/count-points.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Count points
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 00:00 on the first day of every month
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
startDate:
|
||||
description: 'Start date for point counter (YYYY-MM-DD)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
count-points:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Count points
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
START_DATE: ${{ inputs.startDate }}
|
||||
run: node .github/scripts/count-points.mjs
|
||||
104
.github/workflows/docker-edge.yml
vendored
@@ -1,104 +0,0 @@
|
||||
name: Build Edge Docker Image
|
||||
|
||||
# Edge Docker images are built for every push to master
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: docker-edge-build
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
${{ !github.event.repository.fork && 'actualbudget/actual-server' || '' }}
|
||||
ghcr.io/${{ github.repository_owner }}/actual-server
|
||||
ghcr.io/${{ github.repository_owner }}/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:edge
|
||||
TAGS: |
|
||||
type=edge,value=edge
|
||||
type=sha
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu, alpine]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
flavor: ${{ matrix.os != 'ubuntu' && format('suffix=-{0}', matrix.os) || '' }}
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request' && !github.event.repository.fork
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build image for testing
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
|
||||
tags: actualbudget/actual-server-testing
|
||||
|
||||
- name: Test that the docker image boots
|
||||
run: |
|
||||
docker run --detach --network=host actualbudget/actual-server-testing
|
||||
sleep 5
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
|
||||
|
||||
# This will use the cache from the earlier build step and not rebuild the image
|
||||
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
||||
- name: Build and push images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7${{ matrix.os == 'alpine' && ',linux/arm/v6' || '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
96
.github/workflows/docker-release.yml
vendored
@@ -1,96 +0,0 @@
|
||||
name: Build Stable Docker Image
|
||||
|
||||
# Stable Docker images are built for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- LICENSE.txt
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:latest (see docker/metadata-action flavor inputs, below)
|
||||
# - actual-server:1.3
|
||||
# - actual-server:1.3.7
|
||||
# - actual-server:sha-90dd603
|
||||
TAGS: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: latest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Docker meta for Alpine image
|
||||
id: alpine-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: |
|
||||
latest=true
|
||||
suffix=-alpine,onlatest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/ubuntu.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Build and push alpine image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/alpine.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
tags: ${{ steps.alpine-meta.outputs.tags }}
|
||||
32
.github/workflows/e2e-test.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
|
||||
@@ -32,13 +31,11 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Run E2E Tests on Netlify URL
|
||||
run: yarn e2e
|
||||
env:
|
||||
@@ -50,35 +47,12 @@ jobs:
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
functional-desktop-app:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Trust the repository directory
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Run Desktop app E2E Tests
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-app-test-results
|
||||
path: packages/desktop-electron/e2e/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
vrt:
|
||||
name: Visual regression
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
|
||||
66
.github/workflows/electron-master.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-22.04
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron for Mac
|
||||
@@ -57,7 +57,6 @@ jobs:
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
@@ -78,68 +77,13 @@ jobs:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Process release version
|
||||
id: process_version
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Add to new release
|
||||
- name: Add to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
|
||||
|
||||
## Desktop releases
|
||||
Please note: Microsoft store updates can sometimes lag behind the main release by a couple of days while they verify the new version.
|
||||
|
||||
<a href="https://apps.microsoft.com/detail/9p2hmlhsdbrm?cid=Github+Releases&mode=direct">
|
||||
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
|
||||
</a>
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
|
||||
publish-microsoft-store:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Install StoreBroker
|
||||
shell: powershell
|
||||
run: |
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: actual-electron-windows-latest-appx
|
||||
|
||||
- name: Submit to Microsoft Store
|
||||
shell: powershell
|
||||
run: |
|
||||
# Disable telemetry
|
||||
$global:SBDisableTelemetry = $true
|
||||
|
||||
# Authenticate against the store
|
||||
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
|
||||
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
|
||||
|
||||
# Zip and create metadata files
|
||||
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
|
||||
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
|
||||
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
|
||||
|
||||
# Submit the app
|
||||
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
|
||||
Update-ApplicationSubmission `
|
||||
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
|
||||
-SubmissionDataPath "submission.json" `
|
||||
-PackagePath "submission.zip" `
|
||||
-ReplacePackages `
|
||||
-NoStatus `
|
||||
-AutoCommit `
|
||||
-Force
|
||||
|
||||
8
.github/workflows/electron-pr.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-22.04
|
||||
- ubuntu-latest
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -39,9 +39,9 @@ jobs:
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform//24.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp//24.08 -y
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
|
||||
56
.github/workflows/generate-release-pr.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: Generate release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Commit or branch to release'
|
||||
required: true
|
||||
default: 'master'
|
||||
version:
|
||||
description: 'Version number for the release (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
generate-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Bump package versions
|
||||
id: bump_package_versions
|
||||
shell: bash
|
||||
run: |
|
||||
declare -A packages=(
|
||||
[web]="desktop-client"
|
||||
[electron]="desktop-electron"
|
||||
[sync]="sync-server"
|
||||
[api]="api"
|
||||
)
|
||||
|
||||
for key in "${!packages[@]}"; do
|
||||
pkg="${packages[$key]}"
|
||||
|
||||
if [[ -n "${{ github.event.inputs.version }}" ]]; then
|
||||
version="${{ github.event.inputs.version }}"
|
||||
else
|
||||
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
|
||||
--package-json "./packages/$pkg/package.json" \
|
||||
--type auto \
|
||||
--update)
|
||||
fi
|
||||
|
||||
eval "NEW_${key^^}_VERSION=\"$version\""
|
||||
done
|
||||
|
||||
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'
|
||||
branch: 'release/v${{ steps.bump_package_versions.outputs.version }}'
|
||||
@@ -3,7 +3,7 @@ name: Extract and upload i18n strings
|
||||
on:
|
||||
schedule:
|
||||
# 4am UTC
|
||||
- cron: '0 4 * * *'
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
uses: ./actual/.github/actions/setup
|
||||
with:
|
||||
working-directory: actual
|
||||
download-translations: false # As we'll manually clone instead
|
||||
download-translations: false # As we'll manually clone instead
|
||||
- name: Configure Git config
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
actualbudget/actual
|
||||
|
||||
- name: Unlock translations
|
||||
if: always() # Clean up even on failure
|
||||
if: always() # Clean up even on failure
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
body: |
|
||||
:sparkles: Thanks for sharing your idea! :sparkles:
|
||||
|
||||
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
|
||||
This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesn’t mean we don’t accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).
|
||||
|
||||
The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
run: node .github/actions/handle-feature-requests.js
|
||||
env:
|
||||
|
||||
10
.github/workflows/netlify-release.yml
vendored
@@ -22,15 +22,15 @@ jobs:
|
||||
steps:
|
||||
- name: Repository Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
|
||||
- name: Install Netlify
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
|
||||
- name: Build Actual
|
||||
run: yarn build:browser
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify_deploy
|
||||
@@ -40,4 +40,4 @@ jobs:
|
||||
--site ${{ secrets.NETLIFY_SITE_ID }} \
|
||||
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
|
||||
--filter @actual-app/web \
|
||||
--prod
|
||||
--prod
|
||||
@@ -1,95 +0,0 @@
|
||||
name: Publish nightly npm packages
|
||||
|
||||
# Nightly npm packages are built daily
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and pack npm packages
|
||||
if: github.event.repository.fork == false
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Update package versions
|
||||
run: |
|
||||
# Get new nightly versions
|
||||
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
|
||||
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
|
||||
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
|
||||
|
||||
# Set package versions
|
||||
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
|
||||
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
|
||||
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
|
||||
|
||||
- name: Yarn install
|
||||
run: |
|
||||
yarn install
|
||||
|
||||
- name: Build Server & Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Pack the web and server packages
|
||||
run: |
|
||||
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
|
||||
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
|
||||
|
||||
- name: Build API
|
||||
run: yarn build:api
|
||||
|
||||
- name: Pack the api package
|
||||
run: |
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish Nightly npm packages
|
||||
needs: build-and-pack
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public --tag nightly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Sync-Server
|
||||
run: |
|
||||
npm publish sync-server/@actual-app/sync-server.tgz --access public --tag nightly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish API
|
||||
run: |
|
||||
npm publish api/@actual-app/api.tgz --access public --tag nightly
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
78
.github/workflows/publish-npm-packages.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Publish npm packages
|
||||
|
||||
# # Npm packages are published for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and pack npm packages
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Pack the web and server packages
|
||||
run: |
|
||||
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
|
||||
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
|
||||
|
||||
- name: Build API
|
||||
run: yarn build:api
|
||||
|
||||
- name: Pack the api package
|
||||
run: |
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish npm packages
|
||||
needs: build-and-pack
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Sync-Server
|
||||
run: |
|
||||
npm publish sync-server/@actual-app/sync-server.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish API
|
||||
run: |
|
||||
npm publish api/@actual-app/api.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
7
.github/workflows/size-compare.yml
vendored
@@ -13,9 +13,6 @@ name: Compare Sizes
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '!packages/sync-server/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -54,7 +51,6 @@ jobs:
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: base
|
||||
|
||||
@@ -63,7 +59,6 @@ jobs:
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
@@ -83,7 +78,7 @@ jobs:
|
||||
base-stats-json-path: ./base/web-stats.json
|
||||
title: desktop-client
|
||||
|
||||
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||
- uses: github/webpack-bundlesize-compare-action@v2.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/loot-core-stats.json
|
||||
|
||||
16
.github/workflows/stale.yml
vendored
@@ -2,7 +2,6 @@ name: 'Close stale PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
@@ -25,18 +24,3 @@ jobs:
|
||||
any-of-labels: ':construction: WIP'
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: -1
|
||||
|
||||
stale-needs-info:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-label: 'needs info'
|
||||
days-before-stale: -1
|
||||
days-before-close: 7
|
||||
close-issue-message: 'This issue has been automatically closed because there have been no comments for 7 days after the "needs info" label was added. If you still need help, please feel free to reopen the issue with the requested information.'
|
||||
remove-stale-when-updated: false
|
||||
stale-pr-message: '' # Disable PR processing
|
||||
close-pr-message: '' # Disable PR processing
|
||||
days-before-pr-stale: -1 # Disable PR processing
|
||||
days-before-pr-close: -1 # Disable PR processing
|
||||
|
||||
115
.github/workflows/update-vrt.yml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: /update-vrt
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-vrt:
|
||||
name: Update VRT
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
- name: Create patch
|
||||
run: |
|
||||
git config --system --add safe.directory "*"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git reset
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git format-patch -1 HEAD --stdout > Update-VRT.patch
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch
|
||||
path: Update-VRT.patch
|
||||
|
||||
push-patch:
|
||||
runs-on: ubuntu-latest
|
||||
needs: update-vrt
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: patch
|
||||
- name: Apply patch and push
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git apply Update-VRT.patch
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git push origin HEAD:${BRANCH_NAME}
|
||||
- name: Add finished reaction
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "rocket"
|
||||
|
||||
add-starting-reaction:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: React to comment
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "+1"
|
||||
156
.github/workflows/vrt-update-apply.yml
vendored
@@ -1,156 +0,0 @@
|
||||
name: VRT Update - Apply
|
||||
# SECURITY: This workflow runs in trusted base repo context.
|
||||
# It treats the patch artifact as untrusted data, validates it contains only PNGs,
|
||||
# and safely applies it to the contributor's fork branch.
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['VRT Update - Generate']
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
apply-vrt-updates:
|
||||
name: Apply VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download patch artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-patch-*
|
||||
path: /tmp/artifacts
|
||||
|
||||
- name: Download metadata artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: vrt-metadata-*
|
||||
path: /tmp/metadata
|
||||
|
||||
- name: Extract metadata
|
||||
id: metadata
|
||||
run: |
|
||||
# Find the metadata directory (will be vrt-metadata-{PR_NUMBER})
|
||||
METADATA_DIR=$(find /tmp/metadata -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
|
||||
if [ -z "$METADATA_DIR" ]; then
|
||||
echo "No metadata found, skipping..."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PR_NUMBER=$(cat "$METADATA_DIR/pr-number.txt")
|
||||
HEAD_REF=$(cat "$METADATA_DIR/head-ref.txt")
|
||||
HEAD_REPO=$(cat "$METADATA_DIR/head-repo.txt")
|
||||
|
||||
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
|
||||
echo "head_repo=$HEAD_REPO" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Found PR #$PR_NUMBER: $HEAD_REPO @ $HEAD_REF"
|
||||
|
||||
- name: Checkout fork branch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.metadata.outputs.head_repo }}
|
||||
ref: ${{ steps.metadata.outputs.head_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate and apply patch
|
||||
if: steps.metadata.outputs.pr_number != ''
|
||||
id: apply
|
||||
run: |
|
||||
# Find the patch file
|
||||
PATCH_DIR=$(find /tmp/artifacts -mindepth 1 -maxdepth 1 -type d | head -n 1)
|
||||
PATCH_FILE="$PATCH_DIR/vrt-update.patch"
|
||||
|
||||
if [ ! -f "$PATCH_FILE" ]; then
|
||||
echo "No patch file found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found patch file: $PATCH_FILE"
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
echo "Validating patch contains only PNG files..."
|
||||
if grep -E '^(\+\+\+|---) [ab]/' "$PATCH_FILE" | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files! Rejecting for security."
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch validation failed: contains non-PNG files" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract file list for verification
|
||||
FILES_CHANGED=$(grep -E '^\+\+\+ b/' "$PATCH_FILE" | sed 's/^+++ b\///' | wc -l)
|
||||
echo "Patch modifies $FILES_CHANGED PNG file(s)"
|
||||
|
||||
# Configure git
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Apply patch
|
||||
echo "Applying patch..."
|
||||
if git apply --check "$PATCH_FILE" 2>&1; then
|
||||
git apply "$PATCH_FILE"
|
||||
|
||||
# Stage only PNG files (extra safety)
|
||||
git add "**/*.png"
|
||||
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes after applying patch"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit
|
||||
git commit -m "Update VRT screenshots" -m "Auto-generated by VRT workflow" -m "PR: #${{ steps.metadata.outputs.pr_number }}"
|
||||
|
||||
echo "applied=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Patch could not be applied cleanly"
|
||||
echo "applied=false" >> "$GITHUB_OUTPUT"
|
||||
echo "error=Patch conflicts with current branch state" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Push changes
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
env:
|
||||
HEAD_REF: ${{ steps.metadata.outputs.head_ref }}
|
||||
HEAD_REPO: ${{ steps.metadata.outputs.head_repo }}
|
||||
run: |
|
||||
git push origin "HEAD:refs/heads/$HEAD_REF"
|
||||
echo "Successfully pushed VRT updates to $HEAD_REPO@$HEAD_REF"
|
||||
|
||||
- name: Comment on PR - Success
|
||||
if: steps.apply.outputs.applied == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: '✅ VRT screenshots have been automatically updated.'
|
||||
});
|
||||
|
||||
- name: Comment on PR - Failure
|
||||
if: failure() && steps.metadata.outputs.pr_number != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const error = `${{ steps.apply.outputs.error }}` || 'Unknown error occurred';
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: ${{ steps.metadata.outputs.pr_number }},
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `❌ Failed to apply VRT updates: ${error}\n\nPlease check the workflow logs for details.`
|
||||
});
|
||||
105
.github/workflows/vrt-update-generate.yml
vendored
@@ -1,105 +0,0 @@
|
||||
name: VRT Update - Generate
|
||||
# SECURITY: This workflow runs in untrusted fork context with no write permissions.
|
||||
# It only generates VRT patch artifacts that are later applied by vrt-update-apply.yml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '.github/workflows/vrt-update-generate.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
generate-vrt-updates:
|
||||
name: Generate VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
continue-on-error: true
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
|
||||
- name: Create patch with PNG changes only
|
||||
id: create-patch
|
||||
run: |
|
||||
# Trust the repository directory (required for container environments)
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Stage only PNG files
|
||||
git add "**/*.png"
|
||||
|
||||
# Check if there are any changes
|
||||
if git diff --staged --quiet; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No VRT changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Create commit and patch
|
||||
git commit -m "Update VRT screenshots"
|
||||
git format-patch -1 HEAD --stdout > vrt-update.patch
|
||||
|
||||
# Validate patch only contains PNG files
|
||||
if grep -E '^(\+\+\+|---) [ab]/' vrt-update.patch | grep -v '\.png$'; then
|
||||
echo "ERROR: Patch contains non-PNG files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Patch created successfully with PNG changes only"
|
||||
|
||||
- name: Upload patch artifact
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-patch-${{ github.event.pull_request.number }}
|
||||
path: vrt-update.patch
|
||||
retention-days: 5
|
||||
|
||||
- name: Save PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
run: |
|
||||
mkdir -p pr-metadata
|
||||
echo "${{ github.event.pull_request.number }}" > pr-metadata/pr-number.txt
|
||||
echo "${{ github.event.pull_request.head.ref }}" > pr-metadata/head-ref.txt
|
||||
echo "${{ github.event.pull_request.head.repo.full_name }}" > pr-metadata/head-repo.txt
|
||||
|
||||
- name: Upload PR metadata
|
||||
if: steps.create-patch.outputs.has_changes == 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vrt-metadata-${{ github.event.pull_request.number }}
|
||||
path: pr-metadata/
|
||||
retention-days: 5
|
||||
18
.gitignore
vendored
@@ -3,10 +3,12 @@
|
||||
!data/.gitkeep
|
||||
/data2
|
||||
Actual-*
|
||||
!actual-server.js
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
# Secrets
|
||||
.secret-tokens
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
@@ -23,8 +25,6 @@ packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
packages/desktop-client/service-worker
|
||||
packages/plugins-service/dist
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
@@ -53,15 +53,3 @@ bundle.mobile.js.map
|
||||
|
||||
# build output
|
||||
package.tgz
|
||||
|
||||
# Fly.io configuration
|
||||
fly.toml
|
||||
|
||||
# TypeScript cache
|
||||
build/
|
||||
|
||||
# .d.ts files aren't type-checked with skipLibCheck set to true
|
||||
*.d.ts
|
||||
|
||||
# Lage cache
|
||||
.lage/
|
||||
|
||||
@@ -1,30 +1 @@
|
||||
sync_pb.*
|
||||
packages/api/app/bundle.api.js
|
||||
packages/api/app/stats.json
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/api/migrations
|
||||
packages/crdt/dist
|
||||
packages/component-library/src/icons/**/*
|
||||
packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/stats.json
|
||||
packages/desktop-client/.swc/
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/locale/
|
||||
packages/desktop-client/build-electron/
|
||||
packages/desktop-client/build-stats/
|
||||
packages/desktop-client/public/kcab/
|
||||
packages/desktop-client/public/data/
|
||||
packages/desktop-client/**/node_modules/*
|
||||
packages/desktop-client/node_modules/
|
||||
packages/desktop-client/test-results/
|
||||
packages/desktop-client/playwright-report/
|
||||
packages/desktop-electron/client-build/
|
||||
packages/desktop-electron/build/
|
||||
packages/desktop-electron/dist/
|
||||
packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
packages/sync-server/coverage/
|
||||
.yarn/*
|
||||
upcoming-release-notes/*
|
||||
|
||||
2
.secret-tokens.example
Normal file
@@ -0,0 +1,2 @@
|
||||
export APPLE_ID=example@email.com
|
||||
export APPLE_APP_SPECIFIC_PASSWORD=password
|
||||
@@ -1,10 +0,0 @@
|
||||
diff --git a/methods/inflater.js b/methods/inflater.js
|
||||
index 8769e66e82b25541aba80b1ac6429199c9a8179f..1d4402402f0e1aaf64062c1f004c3d6e6fe93e76 100644
|
||||
--- a/methods/inflater.js
|
||||
+++ b/methods/inflater.js
|
||||
@@ -1,4 +1,4 @@
|
||||
-const version = +(process.versions ? process.versions.node : "").split(".")[0] || 0;
|
||||
+const version = +(process?.versions?.node ?? "").split(".")[0] || 0;
|
||||
|
||||
module.exports = function (/*Buffer*/ inbuf, /*number*/ expectedLength) {
|
||||
var zlib = require("zlib");
|
||||
942
.yarn/releases/yarn-4.10.3.cjs
vendored
894
.yarn/releases/yarn-4.3.1.cjs
vendored
Executable file
@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.10.3.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
|
||||
585
AGENTS.md
@@ -1,585 +0,0 @@
|
||||
# AGENTS.md - Guide for AI Agents Working with Actual Budget
|
||||
|
||||
This guide provides comprehensive information for AI agents (like Cursor) working with the Actual Budget codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Actual Budget** is a local-first personal finance tool written in TypeScript/JavaScript. It's 100% free and open-source with synchronization capabilities across devices.
|
||||
|
||||
- **Repository**: https://github.com/actualbudget/actual
|
||||
- **Community Docs**: https://github.com/actualbudget/docs or https://actualbudget.org/docs
|
||||
- **License**: MIT
|
||||
- **Primary Language**: TypeScript (with React)
|
||||
- **Build System**: Yarn 4 workspaces (monorepo)
|
||||
|
||||
## Quick Start Commands
|
||||
|
||||
### Essential Commands (Run from Root)
|
||||
|
||||
```bash
|
||||
# Type checking (ALWAYS run before committing)
|
||||
yarn typecheck
|
||||
|
||||
# Linting and formatting (with auto-fix)
|
||||
yarn lint:fix
|
||||
|
||||
# Run all tests
|
||||
yarn test
|
||||
|
||||
# Start development server (browser)
|
||||
yarn start
|
||||
|
||||
# Start with sync server
|
||||
yarn start:server-dev
|
||||
|
||||
# Start desktop app development
|
||||
yarn start:desktop
|
||||
```
|
||||
|
||||
### Important Rules
|
||||
|
||||
- **ALWAYS run yarn commands from the root directory** - never run them in child workspaces
|
||||
- Use `yarn workspace <workspace-name> run <command>` for workspace-specific tasks
|
||||
- Tests run once and exit by default (using `vitest --run`)
|
||||
|
||||
### Task Orchestration with Lage
|
||||
|
||||
The project uses **[lage](https://microsoft.github.io/lage/)** (a task runner for JavaScript monorepos) to efficiently run tests and other tasks across multiple workspaces:
|
||||
|
||||
- **Parallel execution**: Runs tests in parallel across workspaces for faster feedback
|
||||
- **Smart caching**: Caches test results to skip unchanged packages (cached in `.lage/` directory)
|
||||
- **Dependency awareness**: Understands workspace dependencies and execution order
|
||||
- **Continues on error**: Uses `--continue` flag to run all packages even if one fails
|
||||
|
||||
**Lage Commands:**
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages
|
||||
yarn test # Equivalent to: lage test --continue
|
||||
|
||||
# Run tests without cache (for debugging/CI)
|
||||
yarn test:debug # Equivalent to: lage test --no-cache --continue
|
||||
```
|
||||
|
||||
Configuration is in `lage.config.js` at the project root.
|
||||
|
||||
## Architecture & Package Structure
|
||||
|
||||
### Core Packages
|
||||
|
||||
#### 1. **loot-core** (`packages/loot-core/`)
|
||||
|
||||
The core application logic that runs on any platform.
|
||||
|
||||
- Business logic, database operations, and calculations
|
||||
- Platform-agnostic code
|
||||
- Exports for both browser and node environments
|
||||
- Test commands:
|
||||
|
||||
```bash
|
||||
# Run all loot-core tests
|
||||
yarn workspace loot-core run test
|
||||
|
||||
# Or run tests across all packages using lage
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### 2. **desktop-client** (`packages/desktop-client/` - aliased as `@actual-app/web`)
|
||||
|
||||
The React-based UI for web and desktop.
|
||||
|
||||
- React components using functional programming patterns
|
||||
- E2E tests using Playwright
|
||||
- Vite for bundling
|
||||
- Commands:
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn workspace @actual-app/web start:browser
|
||||
|
||||
# Build
|
||||
yarn workspace @actual-app/web build
|
||||
|
||||
# E2E tests
|
||||
yarn workspace @actual-app/web e2e
|
||||
|
||||
# Visual regression tests
|
||||
yarn workspace @actual-app/web vrt
|
||||
```
|
||||
|
||||
#### 3. **desktop-electron** (`packages/desktop-electron/`)
|
||||
|
||||
Electron wrapper for the desktop application.
|
||||
|
||||
- Window management and native OS integration
|
||||
- E2E tests for Electron-specific features
|
||||
|
||||
#### 4. **api** (`packages/api/` - aliased as `@actual-app/api`)
|
||||
|
||||
Public API for programmatic access to Actual.
|
||||
|
||||
- Node.js API
|
||||
- Designed for integrations and automation
|
||||
- Commands:
|
||||
|
||||
```bash
|
||||
# Build
|
||||
yarn workspace @actual-app/api build
|
||||
|
||||
# Run tests
|
||||
yarn workspace @actual-app/api test
|
||||
|
||||
# Or use lage to run all tests
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### 5. **sync-server** (`packages/sync-server/` - aliased as `@actual-app/sync-server`)
|
||||
|
||||
Synchronization server for multi-device support.
|
||||
|
||||
- Express-based server
|
||||
- Currently transitioning to TypeScript (mostly JavaScript)
|
||||
- Commands:
|
||||
```bash
|
||||
yarn workspace @actual-app/sync-server start
|
||||
```
|
||||
|
||||
#### 6. **component-library** (`packages/component-library/` - aliased as `@actual-app/components`)
|
||||
|
||||
Reusable React UI components.
|
||||
|
||||
- Shared components like Button, Input, Menu, etc.
|
||||
- Theme system and design tokens
|
||||
- Icons (375+ icons in SVG/TSX format)
|
||||
|
||||
#### 7. **crdt** (`packages/crdt/` - aliased as `@actual-app/crdt`)
|
||||
|
||||
CRDT (Conflict-free Replicated Data Type) implementation for data synchronization.
|
||||
|
||||
- Protocol buffers for serialization
|
||||
- Core sync logic
|
||||
|
||||
#### 8. **plugins-service** (`packages/plugins-service/`)
|
||||
|
||||
Service for handling plugins/extensions.
|
||||
|
||||
#### 9. **eslint-plugin-actual** (`packages/eslint-plugin-actual/`)
|
||||
|
||||
Custom ESLint rules specific to Actual.
|
||||
|
||||
- `no-untranslated-strings`: Enforces i18n usage
|
||||
- `prefer-trans-over-t`: Prefers Trans component over t() function
|
||||
- `prefer-logger-over-console`: Enforces using logger instead of console
|
||||
- `typography`: Typography rules
|
||||
- `prefer-if-statement`: Prefers explicit if statements
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### 1. Making Changes
|
||||
|
||||
When implementing changes:
|
||||
|
||||
1. Read relevant files to understand current implementation
|
||||
2. Make focused, incremental changes
|
||||
3. Run type checking: `yarn typecheck`
|
||||
4. Run linting: `yarn lint:fix`
|
||||
5. Run relevant tests
|
||||
6. Fix any linter errors that are introduced
|
||||
|
||||
### 2. Testing Strategy
|
||||
|
||||
**Unit Tests (Vitest)**
|
||||
|
||||
The project uses **lage** for running tests across all workspaces efficiently.
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages (using lage)
|
||||
yarn test
|
||||
|
||||
# Run tests without cache (for debugging)
|
||||
yarn test:debug
|
||||
|
||||
# Run tests for a specific package
|
||||
yarn workspace loot-core run test
|
||||
|
||||
# Run a specific test file (watch mode)
|
||||
yarn workspace loot-core run test path/to/test.test.ts
|
||||
```
|
||||
|
||||
**E2E Tests (Playwright)**
|
||||
|
||||
```bash
|
||||
# Run E2E tests for web
|
||||
yarn e2e
|
||||
|
||||
# Desktop Electron E2E (includes full build)
|
||||
yarn e2e:desktop
|
||||
|
||||
# Visual regression tests
|
||||
yarn vrt
|
||||
|
||||
# Visual regression in Docker (consistent environment)
|
||||
yarn vrt:docker
|
||||
|
||||
# Run E2E tests for a specific package
|
||||
yarn workspace @actual-app/web e2e
|
||||
```
|
||||
|
||||
**Testing Best Practices:**
|
||||
|
||||
- Minimize mocked dependencies - prefer real implementations
|
||||
- Use descriptive test names
|
||||
- Vitest globals are available: `describe`, `it`, `expect`, `beforeEach`, etc.
|
||||
- For sync-server tests, globals are explicitly defined in config
|
||||
|
||||
### 3. Type Checking
|
||||
|
||||
TypeScript configuration uses:
|
||||
|
||||
- Incremental compilation
|
||||
- Strict type checking with `typescript-strict-plugin`
|
||||
- Platform-specific exports in `loot-core` (node vs browser)
|
||||
|
||||
Always run `yarn typecheck` before committing.
|
||||
|
||||
### 4. Internationalization (i18n)
|
||||
|
||||
- Use `Trans` component instead of `t()` function when possible
|
||||
- All user-facing strings must be translated
|
||||
- Generate i18n files: `yarn generate:i18n`
|
||||
- Custom ESLint rules enforce translation usage
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
**Type Usage:**
|
||||
|
||||
- Use TypeScript for all code
|
||||
- Prefer `type` over `interface`
|
||||
- Avoid `enum` - use objects or maps
|
||||
- Avoid `any` or `unknown` unless absolutely necessary
|
||||
- Look for existing type definitions in the codebase
|
||||
- Avoid type assertions (`as`, `!`) - prefer `satisfies`
|
||||
- Use inline type imports: `import { type MyType } from '...'`
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Use descriptive variable names with auxiliary verbs (e.g., `isLoaded`, `hasError`)
|
||||
- Named exports for components and utilities (avoid default exports except in specific cases)
|
||||
|
||||
**Code Structure:**
|
||||
|
||||
- Functional and declarative programming patterns - avoid classes
|
||||
- Use the `function` keyword for pure functions
|
||||
- Prefer iteration and modularization over code duplication
|
||||
- Structure files: exported component/page, helpers, static content, types
|
||||
- Create new components in their own files
|
||||
|
||||
**React Patterns:**
|
||||
|
||||
- Don't use `React.FunctionComponent` or `React.FC` - type props directly
|
||||
- Don't use `React.*` patterns - use named imports instead
|
||||
- Use `<Link>` instead of `<a>` tags
|
||||
- Use custom hooks from `src/hooks` (not react-router directly):
|
||||
- `useNavigate()` from `src/hooks` (not react-router)
|
||||
- `useDispatch()`, `useSelector()`, `useStore()` from `src/redux` (not react-redux)
|
||||
- Avoid unstable nested components
|
||||
- Use `satisfies` for type narrowing
|
||||
|
||||
**JSX Style:**
|
||||
|
||||
- Declarative JSX, minimal and readable
|
||||
- Avoid unnecessary curly braces in conditionals
|
||||
- Use concise syntax for simple statements
|
||||
- Prefer explicit expressions (`condition && <Component />`)
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports are automatically organized by ESLint with the following order:
|
||||
|
||||
1. React imports (first)
|
||||
2. Built-in Node.js modules
|
||||
3. External packages
|
||||
4. Actual packages (`loot-core`, `@actual-app/components` - legacy pattern `loot-design` may appear in old code)
|
||||
5. Parent imports
|
||||
6. Sibling imports
|
||||
7. Index imports
|
||||
|
||||
Always maintain newlines between import groups.
|
||||
|
||||
### Platform-Specific Code
|
||||
|
||||
- Don't directly reference platform-specific imports (`.api`, `.web`, `.electron`)
|
||||
- Use conditional exports in `loot-core` for platform-specific code
|
||||
- Platform resolution happens at build time via package.json exports
|
||||
|
||||
### Restricted Patterns
|
||||
|
||||
**Never:**
|
||||
|
||||
- Use `console.*` (use logger instead - enforced by ESLint)
|
||||
- Import from `uuid` without destructuring: use `import { v4 as uuidv4 } from 'uuid'`
|
||||
- Import colors directly - use theme instead
|
||||
- Import `@actual-app/web/*` in `loot-core`
|
||||
|
||||
**Git Commands:**
|
||||
|
||||
- Never update git config
|
||||
- Never run destructive git operations (force push, hard reset) unless explicitly requested
|
||||
- Never skip hooks (--no-verify, --no-gpg-sign)
|
||||
- Never force push to main/master
|
||||
- Never commit unless explicitly asked
|
||||
|
||||
## File Structure Patterns
|
||||
|
||||
### Typical Component File
|
||||
|
||||
```typescript
|
||||
import { type ComponentType } from 'react';
|
||||
// ... other imports
|
||||
|
||||
type MyComponentProps = {
|
||||
// Props definition
|
||||
};
|
||||
|
||||
export function MyComponent({ prop1, prop2 }: MyComponentProps) {
|
||||
// Component logic
|
||||
return (
|
||||
// JSX
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Test File
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
// ... imports
|
||||
|
||||
describe('ComponentName', () => {
|
||||
it('should behave as expected', () => {
|
||||
// Test logic
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Important Directories & Files
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- `/package.json` - Root workspace configuration, scripts
|
||||
- `/lage.config.js` - Lage task runner configuration
|
||||
- `/eslint.config.mjs` - ESLint configuration (flat config format)
|
||||
- `/tsconfig.json` - Root TypeScript configuration
|
||||
- `/.cursorignore`, `/.gitignore` - Ignored files
|
||||
- `/yarn.lock` - Dependency lockfile (Yarn 4)
|
||||
|
||||
### Documentation
|
||||
|
||||
- `/README.md` - Project overview
|
||||
- `/CONTRIBUTING.md` - Points to community docs
|
||||
- `/upcoming-release-notes/` - Release notes for next version
|
||||
- `/CODEOWNERS` - Code ownership definitions
|
||||
|
||||
### Build Artifacts (Don't Edit)
|
||||
|
||||
- `packages/*/lib-dist/` - Built output
|
||||
- `packages/*/dist/` - Built output
|
||||
- `packages/*/build/` - Built output
|
||||
- `packages/desktop-client/playwright-report/` - Test reports
|
||||
- `packages/desktop-client/test-results/` - Test results
|
||||
- `.lage/` - Lage task runner cache (improves test performance)
|
||||
|
||||
### Key Source Directories
|
||||
|
||||
- `packages/loot-core/src/client/` - Client-side core logic
|
||||
- `packages/loot-core/src/server/` - Server-side core logic
|
||||
- `packages/loot-core/src/shared/` - Shared utilities
|
||||
- `packages/loot-core/src/types/` - Type definitions
|
||||
- `packages/desktop-client/src/components/` - React components
|
||||
- `packages/desktop-client/src/hooks/` - Custom React hooks
|
||||
- `packages/desktop-client/e2e/` - End-to-end tests
|
||||
- `packages/component-library/src/` - Reusable components
|
||||
- `packages/component-library/src/icons/` - Icon components (auto-generated, don't edit)
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Running Specific Tests
|
||||
|
||||
```bash
|
||||
# Run all tests across all packages (recommended)
|
||||
yarn test
|
||||
|
||||
# Unit test for a specific file in loot-core (watch mode)
|
||||
yarn workspace loot-core run test src/path/to/file.test.ts
|
||||
|
||||
# E2E test for a specific file
|
||||
yarn workspace @actual-app/web run playwright test accounts.test.ts --browser=chromium
|
||||
```
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
# Browser build
|
||||
yarn build:browser
|
||||
|
||||
# Desktop build
|
||||
yarn build:desktop
|
||||
|
||||
# API build
|
||||
yarn build:api
|
||||
|
||||
# Sync server build
|
||||
yarn build:server
|
||||
```
|
||||
|
||||
### Type Checking Specific Packages
|
||||
|
||||
TypeScript uses project references. Run `yarn typecheck` from root to check all packages.
|
||||
|
||||
### Debugging Tests
|
||||
|
||||
```bash
|
||||
# Run tests in debug mode (without parallelization)
|
||||
yarn test:debug
|
||||
|
||||
# Run specific E2E test with headed browser
|
||||
yarn workspace @actual-app/web run playwright test --headed --debug accounts.test.ts
|
||||
```
|
||||
|
||||
### Working with Icons
|
||||
|
||||
Icons in `packages/component-library/src/icons/` are auto-generated. Don't manually edit them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Type Errors
|
||||
|
||||
1. Run `yarn typecheck` to see all type errors
|
||||
2. Check if types are imported correctly
|
||||
3. Look for existing type definitions in `packages/loot-core/src/types/`
|
||||
4. Use `satisfies` instead of `as` for type narrowing
|
||||
|
||||
### Linter Errors
|
||||
|
||||
1. Run `yarn lint:fix` to auto-fix many issues
|
||||
2. Check ESLint output for specific rule violations
|
||||
3. Custom rules:
|
||||
- `actual/no-untranslated-strings` - Add i18n
|
||||
- `actual/prefer-trans-over-t` - Use Trans component
|
||||
- `actual/prefer-logger-over-console` - Use logger
|
||||
- Check `eslint.config.mjs` for complete rules
|
||||
|
||||
### Test Failures
|
||||
|
||||
1. Check if test is running in correct environment (node vs web)
|
||||
2. For Vitest: check `vitest.config.ts` or `vitest.web.config.ts`
|
||||
3. For Playwright: check `playwright.config.ts`
|
||||
4. Ensure mock minimization - prefer real implementations
|
||||
5. **Lage cache issues**: Clear cache with `rm -rf .lage` if tests behave unexpectedly
|
||||
6. **Tests continue on error**: With `--continue` flag, all packages run even if one fails
|
||||
|
||||
### Import Resolution Issues
|
||||
|
||||
1. Check `tsconfig.json` for path mappings
|
||||
2. Check package.json `exports` field (especially for loot-core)
|
||||
3. Verify platform-specific imports (`.web`, `.electron`, `.api`)
|
||||
4. Use absolute imports in `desktop-client` (enforced by ESLint)
|
||||
|
||||
### Build Failures
|
||||
|
||||
1. Clean build artifacts: `rm -rf packages/*/dist packages/*/lib-dist packages/*/build`
|
||||
2. Reinstall dependencies: `yarn install`
|
||||
3. Check Node.js version (requires >=20)
|
||||
4. Check Yarn version (requires ^4.9.1)
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- Located alongside source files or in `__tests__` directories
|
||||
- Use `.test.ts`, `.test.tsx`, `.spec.js` extensions
|
||||
- Vitest is the test runner
|
||||
- Minimize mocking - prefer real implementations
|
||||
|
||||
### E2E Tests
|
||||
|
||||
- Located in `packages/desktop-client/e2e/`
|
||||
- Use Playwright test runner
|
||||
- Visual regression snapshots in `*-snapshots/` directories
|
||||
- Page models in `e2e/page-models/` for reusable page interactions
|
||||
- Mobile tests have `.mobile.test.ts` suffix
|
||||
|
||||
### Visual Regression Tests (VRT)
|
||||
|
||||
- Snapshots stored per test file in `*-snapshots/` directories
|
||||
- Use Docker for consistent environment: `yarn vrt:docker`
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Community Documentation**: https://actualbudget.org/docs/contributing/
|
||||
- **Discord Community**: https://discord.gg/pRYNYr4W5A
|
||||
- **GitHub Issues**: https://github.com/actualbudget/actual/issues
|
||||
- **Feature Requests**: Label "needs votes" sorted by reactions
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
Before committing changes, ensure:
|
||||
|
||||
- [ ] `yarn typecheck` passes
|
||||
- [ ] `yarn lint:fix` has been run
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] No new console.\* usage (use logger)
|
||||
- [ ] User-facing strings are translated
|
||||
- [ ] Prefer `type` over `interface`
|
||||
- [ ] Named exports used (not default exports)
|
||||
- [ ] Imports are properly ordered
|
||||
- [ ] Platform-specific code uses proper exports
|
||||
- [ ] No unnecessary type assertions
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
When creating pull requests:
|
||||
|
||||
- **AI-Generated PRs**: If you create a PR using AI assistance, add the **"AI generated"** label to the pull request. This helps maintainers understand the nature of the contribution.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Bundle Size**: Check with rollup-plugin-visualizer
|
||||
- **Type Checking**: Uses incremental compilation
|
||||
- **Testing**: Tests run in parallel by default
|
||||
- **Linting**: ESLint caches results for faster subsequent runs
|
||||
|
||||
## Workspace Commands Reference
|
||||
|
||||
```bash
|
||||
# List all workspaces
|
||||
yarn workspaces list
|
||||
|
||||
# Run command in specific workspace
|
||||
yarn workspace <workspace-name> run <command>
|
||||
|
||||
# Run command in all workspaces
|
||||
yarn workspaces foreach --all run <command>
|
||||
|
||||
# Install production dependencies only (for server deployment)
|
||||
yarn install:server
|
||||
```
|
||||
|
||||
## Environment Requirements
|
||||
|
||||
- **Node.js**: >=20
|
||||
- **Yarn**: ^4.9.1 (managed by packageManager field)
|
||||
- **Browser Targets**: Electron >= 35.0, modern browsers (see browserslist)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The codebase is actively being migrated:
|
||||
|
||||
- **JavaScript → TypeScript**: sync-server is in progress
|
||||
- **Classes → Functions**: Prefer functional patterns
|
||||
- **React.\* → Named Imports**: Legacy React.\* patterns being removed
|
||||
|
||||
When working with older code, follow the newer patterns described in this guide.
|
||||
10
CODEOWNERS
@@ -1,10 +0,0 @@
|
||||
# CODEOWNERS file for Actual Budget
|
||||
# Please add your name to code-paths that you feel especially
|
||||
# passionate about. You will be notified for any PRs there.
|
||||
|
||||
/packages/api/ @MatissJanis
|
||||
/packages/component-library/ @MatissJanis
|
||||
/packages/desktop-client/src/components/mobile @joel-jeremy
|
||||
/packages/desktop-electron/ @MikesGlitch
|
||||
/packages/loot-core/src/server/budget @youngcw
|
||||
/packages/sync-server/ @matt-fidd
|
||||
@@ -5,7 +5,7 @@
|
||||
# you are doing.
|
||||
###################################################
|
||||
|
||||
FROM node:22-bookworm as dev
|
||||
FROM node:18-bullseye as dev
|
||||
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||
WORKDIR /app
|
||||
CMD ["sh", "./bin/docker-start"]
|
||||
|
||||
@@ -66,11 +66,7 @@ To add new feature requests, open a new Issue of the "Feature Request" type.
|
||||
|
||||
### Translation
|
||||
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/actualbudget/">
|
||||
<img src="https://hosted.weblate.org/widget/actualbudget/actual/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
## Repo Activity
|
||||
|
||||
|
||||
@@ -9,14 +9,10 @@ if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git checkout .
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ RELEASE=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
SKIP_EXE_BUILD=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
@@ -16,39 +16,27 @@ while [[ $# -gt 0 ]]; do
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
--skip-exe-build)
|
||||
SKIP_EXE_BUILD=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
# Get translations
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
if [ "$OSTYPE" == "msys" ]; then
|
||||
if [ $CI != true ]; then
|
||||
read -s -p "Windows certificate password: " -r CSC_KEY_PASSWORD
|
||||
export CSC_KEY_PASSWORD
|
||||
elif [ -n "$CIRCLE_TAG" ]; then
|
||||
# We only want to run this on CircleCI as Github doesn't have the CSC_KEY_PASSWORD secret set.
|
||||
certutil -f -p ${CSC_KEY_PASSWORD} -importPfx ~/windows-shift-reset-llc.p12
|
||||
fi
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
# required for running the sync-server server
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
@@ -56,17 +44,14 @@ yarn workspace desktop-electron update-client
|
||||
cd packages/desktop-electron;
|
||||
yarn clean;
|
||||
|
||||
if [ $SKIP_EXE_BUILD == true ]; then
|
||||
echo "Building the dist"
|
||||
yarn build:dist
|
||||
echo "Skipping exe build"
|
||||
else
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
yarn build
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "Created release"
|
||||
else
|
||||
yarn build
|
||||
fi
|
||||
echo "\nCreated release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
fi
|
||||
)
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import { existsSync, writeFile } from 'node:fs';
|
||||
import { exit } from 'node:process';
|
||||
|
||||
import prompts from 'prompts';
|
||||
|
||||
async function run() {
|
||||
const username = await execAsync(
|
||||
// eslint-disable-next-line actual/typography
|
||||
"gh api user --jq '.login'",
|
||||
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
|
||||
);
|
||||
const activePr = await getActivePr(username);
|
||||
if (activePr) {
|
||||
console.log(
|
||||
`Found potentially matching PR ${activePr.number}: ${activePr.title}`,
|
||||
);
|
||||
}
|
||||
const initialPrNumber = activePr?.number ?? (await getNextPrNumber());
|
||||
|
||||
const result = await prompts([
|
||||
{
|
||||
name: 'githubUsername',
|
||||
message: 'Comma-separated GitHub username(s)',
|
||||
type: 'text',
|
||||
initial: username,
|
||||
},
|
||||
{
|
||||
name: 'pullRequestNumber',
|
||||
message: 'PR Number',
|
||||
type: 'number',
|
||||
initial: initialPrNumber,
|
||||
},
|
||||
{
|
||||
name: 'releaseNoteType',
|
||||
message: 'Release Note Type',
|
||||
type: 'select',
|
||||
choices: [
|
||||
{ title: '✨ Features', value: 'Features' },
|
||||
{ title: '👍 Enhancements', value: 'Enhancements' },
|
||||
{ title: '🐛 Bugfix', value: 'Bugfix' },
|
||||
{ title: '⚙️ Maintenance', value: 'Maintenance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'oneLineSummary',
|
||||
message: 'Brief Summary',
|
||||
type: 'text',
|
||||
initial: activePr?.title,
|
||||
},
|
||||
]);
|
||||
|
||||
if (
|
||||
!result.githubUsername ||
|
||||
!result.oneLineSummary ||
|
||||
!result.releaseNoteType ||
|
||||
!result.pullRequestNumber
|
||||
) {
|
||||
console.log('All questions must be answered. Exiting');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const fileContents = getFileContents(
|
||||
result.releaseNoteType,
|
||||
result.githubUsername,
|
||||
result.oneLineSummary,
|
||||
);
|
||||
const prNumber = result.pullRequestNumber;
|
||||
|
||||
const filepath = `./upcoming-release-notes/${prNumber}.md`;
|
||||
if (existsSync(filepath)) {
|
||||
const { confirm } = await prompts({
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
message: `This will overwrite the existing release note ${filepath} Are you sure?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
console.log('Exiting');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filepath, fileContents, err => {
|
||||
if (err) {
|
||||
console.error('Failed to write release note file:', err);
|
||||
exit(1);
|
||||
} else {
|
||||
console.log(`Release note generated successfully: ${filepath}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// makes an attempt to find an existing open PR from <username>:<branch>
|
||||
async function getActivePr(
|
||||
username: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
if (!username) {
|
||||
return undefined;
|
||||
}
|
||||
const branchName = await execAsync('git rev-parse --abbrev-ref HEAD');
|
||||
if (!branchName) {
|
||||
return undefined;
|
||||
}
|
||||
const forkHead = `${username}:${branchName}`;
|
||||
return getPrNumberFromHead(forkHead);
|
||||
}
|
||||
|
||||
async function getPrNumberFromHead(
|
||||
head: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
try {
|
||||
// head is a weird query parameter in this API call. If nothing matches, it
|
||||
// will return as if the head query parameter doesn't exist. To get around
|
||||
// this, we make the page size 2 and only return the number if the length.
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/pulls?state=open&per_page=2&head=' +
|
||||
head,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn('error fetching from github pulls api:', resp.status);
|
||||
return undefined;
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
if (ghResponse?.length === 1) {
|
||||
return ghResponse[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('error fetching from github pulls api:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextPrNumber(): Promise<number> {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/issues?state=all&per_page=1',
|
||||
);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`API responded with status: ${resp.status}`);
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
const latestPrNumber = ghResponse?.[0]?.number;
|
||||
if (!latestPrNumber) {
|
||||
console.error(
|
||||
'Could not find latest issue number in GitHub API response',
|
||||
ghResponse,
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
return latestPrNumber + 1;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch next PR number:', error);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileContents(type: string, username: string, summary: string) {
|
||||
return `---
|
||||
category: ${type}
|
||||
authors: [${username}]
|
||||
---
|
||||
|
||||
${summary}
|
||||
`;
|
||||
}
|
||||
|
||||
// simple exec that fails silently and returns an empty string on failure
|
||||
async function execAsync(cmd: string, errorLog?: string): Promise<string> {
|
||||
return new Promise<string>(res => {
|
||||
exec(cmd, (error, stdout) => {
|
||||
if (error) {
|
||||
console.log(errorLog);
|
||||
res('');
|
||||
} else {
|
||||
res(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -28,5 +28,5 @@ echo "Running VRT tests with the following parameters:"
|
||||
echo "E2E_START_URL: $E2E_START_URL"
|
||||
echo "VRT_ARGS: $VRT_ARGS"
|
||||
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash \
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
|
||||
@@ -15,3 +15,4 @@ services:
|
||||
volumes:
|
||||
- '.:/app'
|
||||
restart: 'no'
|
||||
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import globals from 'globals';
|
||||
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginPrettier from 'eslint-plugin-prettier/recommended';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginRulesDir from 'eslint-plugin-rulesdir';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
|
||||
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
pluginRulesDir.RULES_DIR = path.join(
|
||||
__dirname,
|
||||
'packages',
|
||||
'eslint-plugin-actual',
|
||||
'lib',
|
||||
'rules',
|
||||
);
|
||||
|
||||
const confusingBrowserGlobals = [
|
||||
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
|
||||
'addEventListener',
|
||||
@@ -72,59 +85,39 @@ const confusingBrowserGlobals = [
|
||||
'top',
|
||||
];
|
||||
|
||||
export default defineConfig(
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
'packages/api/app/stats.json',
|
||||
'packages/api/dist',
|
||||
'packages/api/@types',
|
||||
'packages/api/migrations',
|
||||
'packages/crdt/dist',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/service-worker/*',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
'packages/desktop-client/public/data/',
|
||||
'packages/desktop-client/**/node_modules/*',
|
||||
'packages/desktop-client/node_modules/',
|
||||
'packages/desktop-client/src/icons/**/*',
|
||||
'packages/desktop-client/test-results/',
|
||||
'packages/desktop-client/playwright-report/',
|
||||
'packages/desktop-electron/client-build/',
|
||||
'packages/desktop-electron/build/',
|
||||
'packages/desktop-electron/dist/',
|
||||
'packages/import-ynab4/**/node_modules/*',
|
||||
'packages/import-ynab5/**/node_modules/*',
|
||||
'packages/loot-core/**/node_modules/*',
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'packages/sync-server/build/',
|
||||
'packages/plugins-service/dist/',
|
||||
'packages/sync-server',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
],
|
||||
},
|
||||
{
|
||||
// Temporary until the sync-server is migrated to TypeScript
|
||||
files: [
|
||||
'packages/sync-server/**/*.spec.{js,jsx}',
|
||||
'packages/sync-server/**/*.test.{js,jsx}',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
vi: true,
|
||||
describe: true,
|
||||
expect: true,
|
||||
it: true,
|
||||
beforeAll: true,
|
||||
beforeEach: true,
|
||||
afterAll: true,
|
||||
afterEach: true,
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
@@ -133,6 +126,7 @@ export default defineConfig(
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.commonjs,
|
||||
...globals.jest,
|
||||
...globals.node,
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
@@ -152,23 +146,18 @@ export default defineConfig(
|
||||
},
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginTypescript.configs.recommended,
|
||||
pluginPrettier,
|
||||
...pluginTypescript.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
actual: pluginActual,
|
||||
},
|
||||
rules: {
|
||||
'actual/no-untranslated-strings': 'error',
|
||||
'actual/prefer-trans-over-t': 'error',
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
rulesdir: pluginRulesDir,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
plugins: {
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
'react-hooks': pluginReactHooks,
|
||||
},
|
||||
rules: {
|
||||
// http://eslint.org/docs/rules/
|
||||
'array-callback-return': 'warn',
|
||||
@@ -452,9 +441,8 @@ export default defineConfig(
|
||||
},
|
||||
],
|
||||
|
||||
'actual/typography': 'warn',
|
||||
'actual/prefer-if-statement': 'warn',
|
||||
'actual/prefer-logger-over-console': 'error',
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
@@ -462,7 +450,6 @@ export default defineConfig(
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
argsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
@@ -499,32 +486,6 @@ export default defineConfig(
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-router',
|
||||
importNames: ['useNavigate'],
|
||||
message:
|
||||
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch'],
|
||||
message:
|
||||
"Please import Actual's useDispatch() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useSelector'],
|
||||
message:
|
||||
"Please import Actual's useSelector() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useStore'],
|
||||
message:
|
||||
"Please import Actual's useStore() hook from `src/redux` instead.",
|
||||
},
|
||||
],
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
@@ -540,10 +501,6 @@ export default defineConfig(
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
{
|
||||
group: ['@actual-app/web/*'],
|
||||
message: 'Please do not import `@actual-app/web` in `loot-core`',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -565,7 +522,7 @@ export default defineConfig(
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
files: ['**/*.ts?(x)'],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
@@ -573,7 +530,7 @@ export default defineConfig(
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
project: [path.join(__dirname, './tsconfig.json')],
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
@@ -593,13 +550,6 @@ export default defineConfig(
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
||||
'no-undef': 'off',
|
||||
|
||||
// TypeScript already handles these (https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import)
|
||||
'import/named': 'off',
|
||||
'import/namespace': 'off',
|
||||
'import/default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
|
||||
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
'no-array-constructor': 'off',
|
||||
@@ -633,19 +583,6 @@ export default defineConfig(
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
|
||||
plugins: {
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
rules: {
|
||||
'typescript-paths/absolute-parent-import': [
|
||||
'error',
|
||||
{ preferPathOverBaseUrl: true },
|
||||
],
|
||||
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
@@ -680,6 +617,85 @@ export default defineConfig(
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*'],
|
||||
ignores: ['packages/desktop-client/src/hooks/useNavigate.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-router-dom',
|
||||
importNames: ['useNavigate'],
|
||||
message:
|
||||
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*', 'packages/loot-core/**/*'],
|
||||
ignores: ['packages/desktop-client/src/redux/index.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch'],
|
||||
message:
|
||||
"Please import Actual's useDispatch() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useSelector'],
|
||||
message:
|
||||
"Please import Actual's useSelector() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useStore'],
|
||||
message:
|
||||
"Please import Actual's useStore() hook from `src/redux` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/loot-core/src/**/*'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: "Don't directly reference imports from other platforms",
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
{
|
||||
group: ['loot-core/**'],
|
||||
message:
|
||||
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/loot-core/src/types/**/*',
|
||||
@@ -694,6 +710,27 @@ export default defineConfig(
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/src/style/index.*',
|
||||
'packages/desktop-client/src/style/palette.*',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'off',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
|
||||
|
||||
@@ -707,16 +744,6 @@ export default defineConfig(
|
||||
'import/no-unresolved': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Allow configuring vitest with default exports (recommended as per vitest docs)
|
||||
{
|
||||
files: ['**/vitest.config.ts', '**/vitest.web.config.ts'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
@@ -731,7 +758,7 @@ export default defineConfig(
|
||||
'packages/desktop-client/src/components/budget/MobileBudget.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
|
||||
'packages/component-library/src/Menu.tsx',
|
||||
'packages/desktop-client/src/components/common/Menu.tsx',
|
||||
'packages/desktop-client/src/components/FinancesApp.tsx',
|
||||
'packages/desktop-client/src/components/GlobalKeys.ts',
|
||||
'packages/desktop-client/src/components/LoggedInUser.tsx',
|
||||
@@ -760,6 +787,21 @@ export default defineConfig(
|
||||
'packages/desktop-client/src/components/select/DateSelect.tsx',
|
||||
'packages/desktop-client/src/components/sidebar/Tools.tsx',
|
||||
'packages/desktop-client/src/components/sort.tsx',
|
||||
'packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
|
||||
'packages/desktop-client/src/components/table.tsx',
|
||||
'packages/desktop-client/src/components/Titlebar.tsx',
|
||||
'packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
|
||||
'packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
|
||||
'packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
|
||||
'packages/desktop-client/src/components/transactions/TransactionList.jsx',
|
||||
'packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
|
||||
'packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
|
||||
'packages/desktop-client/src/hooks/useAccounts.ts',
|
||||
'packages/desktop-client/src/hooks/useCategories.ts',
|
||||
'packages/desktop-client/src/hooks/usePayees.ts',
|
||||
'packages/desktop-client/src/hooks/useProperFocus.tsx',
|
||||
'packages/desktop-client/src/hooks/useSelected.tsx',
|
||||
'packages/loot-core/src/client/query-hooks.tsx',
|
||||
],
|
||||
|
||||
rules: {
|
||||
@@ -773,13 +815,10 @@ export default defineConfig(
|
||||
'**/*.test.ts',
|
||||
'**/*.test.jsx',
|
||||
'**/*.test.tsx',
|
||||
'**/*.spec.js',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'actual/typography': 'off',
|
||||
'actual/no-untranslated-strings': 'off',
|
||||
'actual/prefer-logger-over-console': 'off',
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -793,19 +832,4 @@ export default defineConfig(
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/**/*'],
|
||||
// TODO: fix the issues in these files
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'actual/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/src/app-gocardless/banks/*.js'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
];
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/** @type {import('lage').ConfigOptions} */
|
||||
module.exports = {
|
||||
pipeline: {
|
||||
test: {
|
||||
type: 'npmScript',
|
||||
options: {
|
||||
outputGlob: [
|
||||
'coverage/**',
|
||||
'**/test-results/**',
|
||||
'**/playwright-report/**',
|
||||
],
|
||||
},
|
||||
},
|
||||
build: {
|
||||
type: 'npmScript',
|
||||
cache: true,
|
||||
options: {
|
||||
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
||||
},
|
||||
},
|
||||
},
|
||||
cacheOptions: {
|
||||
cacheStorageConfig: {
|
||||
provider: 'local',
|
||||
outputGlob: ['lib-dist/**', 'dist/**', 'build/**'],
|
||||
},
|
||||
},
|
||||
npmClient: 'yarn',
|
||||
concurrency: 2,
|
||||
};
|
||||
91
package.json
@@ -19,91 +19,66 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn start:browser",
|
||||
"start:server": "yarn workspace @actual-app/sync-server start",
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"desktop-dependencies": "npm-run-all --parallel rebuild-electron build:browser-backend build:plugins-service",
|
||||
"start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:browser": "yarn workspace plugins-service build-dev && npm-run-all --parallel 'start:browser-*'",
|
||||
"start:service-plugins": "yarn workspace plugins-service watch",
|
||||
"start:electron": "yarn start:desktop",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:browser-backend": "yarn workspace loot-core build:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:plugins-service": "yarn workspace plugins-service build",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
"test": "lage test --continue",
|
||||
"test:debug": "lage test --no-cache --continue",
|
||||
"e2e": "yarn workspace @actual-app/web run e2e",
|
||||
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
|
||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
||||
"vrt": "yarn workspace @actual-app/web run vrt",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/loot-core",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "prettier --check . && eslint . --max-warnings 0",
|
||||
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "yarn tsc --incremental && tsc-strict",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.18.11",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.0",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^16.4.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lage": "^2.14.14",
|
||||
"lint-staged": "^16.2.3",
|
||||
"minimatch": "^10.0.3",
|
||||
"node-jq": "^6.3.1",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.9",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-limit": "^7.1.1",
|
||||
"prettier": "^3.6.2",
|
||||
"prompts": "^2.4.2",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.40.1",
|
||||
"socks": ">=2.8.3"
|
||||
"rollup": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"yarn": "^4.9.1"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,mjs,jsx,ts,tsx,md,json,yml}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
|
||||
},
|
||||
"packageManager": "yarn@4.10.3",
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"browserslist": [
|
||||
"electron >= 35.0",
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`API setup and teardown > successfully loads budget 1`] = `
|
||||
[
|
||||
exports[`API setup and teardown successfully loads budget 1`] = `
|
||||
Array [
|
||||
"2016-10",
|
||||
"2016-11",
|
||||
"2016-12",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type {
|
||||
RequestInfo as FetchInfo,
|
||||
RequestInit as FetchInit,
|
||||
} from 'node-fetch';
|
||||
// @ts-ignore: false-positive commonjs module error on build until typescript 5.3
|
||||
} from 'node-fetch'; // with { 'resolution-mode': 'import' };
|
||||
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
@@ -15,6 +16,9 @@ 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';
|
||||
|
||||
@@ -42,11 +46,7 @@ export async function init(config: InitConfig = {}) {
|
||||
|
||||
export async function shutdown() {
|
||||
if (actualApp) {
|
||||
try {
|
||||
await actualApp.send('sync');
|
||||
} catch (e) {
|
||||
// most likely that no budget is loaded, so the sync failed
|
||||
}
|
||||
await actualApp.send('sync');
|
||||
await actualApp.send('close-budget');
|
||||
actualApp = null;
|
||||
}
|
||||
|
||||
24
packages/api/jest.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'testing.js',
|
||||
'testing.ts',
|
||||
'api.js',
|
||||
'api.ts',
|
||||
'api.tsx',
|
||||
'electron.js',
|
||||
'electron.ts',
|
||||
'mjs',
|
||||
'js',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'],
|
||||
setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'],
|
||||
transformIgnorePatterns: ['/node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -6,9 +6,10 @@ import * as api from './index';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
global.IS_TESTING = true;
|
||||
|
||||
beforeEach(async () => {
|
||||
// we need real datetime if we are going to mix new timestamps with our mock data
|
||||
global.restoreDateNow();
|
||||
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
@@ -568,20 +569,8 @@ describe('API CRUD operations', () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
let newTransaction = [
|
||||
{
|
||||
account: accountId,
|
||||
date: '2023-11-03',
|
||||
imported_id: '11',
|
||||
amount: 100,
|
||||
notes: 'notes',
|
||||
},
|
||||
{
|
||||
account: accountId,
|
||||
date: '2023-11-03',
|
||||
imported_id: '12',
|
||||
amount: 100,
|
||||
notes: '',
|
||||
},
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
|
||||
];
|
||||
|
||||
const addResult = await api.addTransactions(accountId, newTransaction, {
|
||||
@@ -609,27 +598,9 @@ describe('API CRUD operations', () => {
|
||||
expect(transactions).toHaveLength(2);
|
||||
|
||||
newTransaction = [
|
||||
{
|
||||
account: accountId,
|
||||
date: '2023-12-03',
|
||||
imported_id: '11',
|
||||
amount: 100,
|
||||
notes: 'notes',
|
||||
},
|
||||
{
|
||||
account: accountId,
|
||||
date: '2023-12-03',
|
||||
imported_id: '12',
|
||||
amount: 100,
|
||||
notes: 'notes',
|
||||
},
|
||||
{
|
||||
account: accountId,
|
||||
date: '2023-12-03',
|
||||
imported_id: '22',
|
||||
amount: 200,
|
||||
notes: '',
|
||||
},
|
||||
{ date: '2023-12-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '12', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '22', amount: 200, notes: '' },
|
||||
];
|
||||
|
||||
const reconciled = await api.importTransactions(accountId, newTransaction);
|
||||
@@ -683,179 +654,4 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Transactions: import notes are preserved when importing', async () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
// Test with notes
|
||||
const transactionsWithNotes = [
|
||||
{
|
||||
date: '2023-11-03',
|
||||
imported_id: '11',
|
||||
amount: 100,
|
||||
notes: 'test note',
|
||||
},
|
||||
];
|
||||
|
||||
const addResultWithNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithNotes).toBe('ok');
|
||||
|
||||
let transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBe('test note');
|
||||
|
||||
// Clear transactions
|
||||
await api.deleteTransaction(transactions[0].id);
|
||||
|
||||
// Test without notes
|
||||
const transactionsWithoutNotes = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: null },
|
||||
];
|
||||
|
||||
const addResultWithoutNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithoutNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithoutNotes).toBe('ok');
|
||||
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
|
||||
test('Schedules: successfully complete schedules operations', async () => {
|
||||
await api.loadBudget(budgetName);
|
||||
//test a schedule with a recuring configuration
|
||||
const ScheduleId1 = await api.createSchedule({
|
||||
name: 'test-schedule 1',
|
||||
posts_transaction: true,
|
||||
// amount: -5000,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
});
|
||||
//test the creation of non recurring schedule
|
||||
const ScheduleId2 = await api.createSchedule({
|
||||
name: 'test-schedule 2',
|
||||
posts_transaction: false,
|
||||
amount: 4000,
|
||||
amountOp: 'is',
|
||||
date: '2025-06-13',
|
||||
});
|
||||
let schedules = await api.getSchedules();
|
||||
|
||||
// Schedules successfully created
|
||||
expect(schedules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'test-schedule 1',
|
||||
posts_transaction: true,
|
||||
// amount: -5000,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'test-schedule 2',
|
||||
posts_transaction: false,
|
||||
amount: 4000,
|
||||
amountOp: 'is',
|
||||
date: '2025-06-13',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
//check getIDByName works on schedules
|
||||
expect(await api.getIDByName('schedules', 'test-schedule 1')).toEqual(
|
||||
ScheduleId1,
|
||||
);
|
||||
expect(await api.getIDByName('schedules', 'test-schedule 2')).toEqual(
|
||||
ScheduleId2,
|
||||
);
|
||||
|
||||
//check getIDByName works on accounts
|
||||
const schedAccountId1 = await api.createAccount(
|
||||
{ name: 'sched-test-account1', offbudget: true },
|
||||
1000,
|
||||
);
|
||||
|
||||
expect(await api.getIDByName('accounts', 'sched-test-account1')).toEqual(
|
||||
schedAccountId1,
|
||||
);
|
||||
|
||||
//check getIDByName works on payees
|
||||
const schedPayeeId1 = await api.createPayee({ name: 'sched-test-payee1' });
|
||||
|
||||
expect(await api.getIDByName('payees', 'sched-test-payee1')).toEqual(
|
||||
schedPayeeId1,
|
||||
);
|
||||
await api.updateSchedule(ScheduleId1, {
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
});
|
||||
await api.deleteSchedule(ScheduleId2);
|
||||
|
||||
// schedules successfully updated, and one of them deleted
|
||||
await api.updateSchedule(ScheduleId1, {
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
payee: schedPayeeId1,
|
||||
});
|
||||
await api.deleteSchedule(ScheduleId2);
|
||||
|
||||
schedules = await api.getSchedules();
|
||||
expect(schedules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: ScheduleId1,
|
||||
posts_transaction: true,
|
||||
amount: -10000,
|
||||
account: schedAccountId1,
|
||||
payee: schedPayeeId1,
|
||||
amountOp: 'is',
|
||||
date: {
|
||||
frequency: 'monthly',
|
||||
interval: 1,
|
||||
start: '2025-06-13',
|
||||
patterns: [],
|
||||
skipWeekend: false,
|
||||
weekendSolveMode: 'after',
|
||||
endMode: 'never',
|
||||
},
|
||||
}),
|
||||
expect.not.objectContaining({ id: ScheduleId2 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type { ImportTransactionEntity } from 'loot-core/types/models/import-transaction';
|
||||
import type { Handlers } from 'loot-core/src/types/handlers';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
@@ -53,18 +52,10 @@ export async function batchBudgetUpdates(func) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `aqlQuery` instead.
|
||||
* This function will be removed in a future release.
|
||||
*/
|
||||
export function runQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function aqlQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
@@ -96,21 +87,18 @@ export function addTransactions(
|
||||
|
||||
export interface ImportTransactionsOpts {
|
||||
defaultCleared?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
export function importTransactions(
|
||||
accountId: string,
|
||||
transactions: ImportTransactionEntity[],
|
||||
accountId,
|
||||
transactions,
|
||||
opts: ImportTransactionsOpts = {
|
||||
defaultCleared: true,
|
||||
dryRun: false,
|
||||
},
|
||||
) {
|
||||
return send('api/transactions-import', {
|
||||
accountId,
|
||||
transactions,
|
||||
isPreview: opts.dryRun,
|
||||
opts,
|
||||
});
|
||||
}
|
||||
@@ -242,31 +230,3 @@ export function holdBudgetForNextMonth(month, amount) {
|
||||
export function resetBudgetHold(month) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
|
||||
export function createSchedule(schedule) {
|
||||
return send('api/schedule-create', schedule);
|
||||
}
|
||||
|
||||
export function updateSchedule(id, fields, resetNextDate?: boolean) {
|
||||
return send('api/schedule-update', {
|
||||
id,
|
||||
fields,
|
||||
resetNextDate,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSchedule(scheduleId) {
|
||||
return send('api/schedule-delete', scheduleId);
|
||||
}
|
||||
|
||||
export function getSchedules() {
|
||||
return send('api/schedules-get');
|
||||
}
|
||||
|
||||
export function getIDByName(type, name) {
|
||||
return send('api/get-id-by-name', { type, name });
|
||||
}
|
||||
|
||||
export function getServerVersion() {
|
||||
return send('api/get-server-version');
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.10.0",
|
||||
"version": "25.1.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
@@ -14,24 +14,27 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"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": "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 && yarn run build:crdt && vitest --run",
|
||||
"test": "yarn run build:app && jest -c jest.config.js",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
"outDir": "dist",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/src/*"]
|
||||
"loot-core/src/*": ["./loot-core/*"],
|
||||
"loot-core/*": ["./@types/loot-core/*"]
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
}
|
||||
|
||||
7
packages/api/utils.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function amountToInteger(n) {
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
export function integerToAmount(n) {
|
||||
return parseFloat((n / 100).toFixed(2));
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// @ts-ignore: bundle not available until we build it
|
||||
// eslint-disable-next-line import/extensions
|
||||
import * as bundle from './app/bundle.api.js';
|
||||
|
||||
export const amountToInteger = bundle.lib.amountToInteger;
|
||||
export const integerToAmount = bundle.lib.integerToAmount;
|
||||
@@ -1,15 +0,0 @@
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// This script is used in GitHub Actions to get the next version based on the current package.json version.
|
||||
// It supports three types of versioning: nightly, hotfix, and monthly.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { getNextVersion } from '../src/versions/get-next-package-version.js';
|
||||
|
||||
const args = process.argv;
|
||||
|
||||
const options = {
|
||||
'package-json': {
|
||||
type: 'string',
|
||||
short: 'p',
|
||||
},
|
||||
type: {
|
||||
type: 'string', // nightly, hotfix, monthly, auto
|
||||
short: 't',
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
short: 'u',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { values } = parseArgs({
|
||||
args,
|
||||
options,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (!values['package-json']) {
|
||||
console.error(
|
||||
'Please specify the path to package.json using --package-json or -p option.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJsonPath = values['package-json'];
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
|
||||
let newVersion;
|
||||
try {
|
||||
newVersion = getNextVersion({
|
||||
currentVersion,
|
||||
type: values.type,
|
||||
currentDate: new Date(),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(newVersion);
|
||||
|
||||
if (values.update) {
|
||||
packageJson.version = newVersion;
|
||||
fs.writeFileSync(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2) + '\n',
|
||||
'utf8',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"name": "@actual-app/ci-actions",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest --run"
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
function parseVersion(version) {
|
||||
const [y, m, p] = version.split('.');
|
||||
return {
|
||||
versionYear: parseInt(y, 10),
|
||||
versionMonth: parseInt(m, 10),
|
||||
versionHotfix: parseInt(p, 10),
|
||||
};
|
||||
}
|
||||
|
||||
function computeNextMonth(versionYear, versionMonth) {
|
||||
// Create date and add 1 month
|
||||
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
|
||||
const nextVersionMonthDate = new Date(
|
||||
versionDate.getFullYear(),
|
||||
versionDate.getMonth() + 1,
|
||||
1,
|
||||
);
|
||||
|
||||
// Format back to YY.M format
|
||||
const fullYear = nextVersionMonthDate.getFullYear();
|
||||
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
|
||||
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
|
||||
return { nextVersionYear, nextVersionMonth };
|
||||
}
|
||||
|
||||
// Determine logical type from 'auto' based on the current date and version
|
||||
function resolveType(type, currentDate, versionYear, versionMonth) {
|
||||
if (type !== 'auto') return type;
|
||||
const inPatchMonth =
|
||||
currentDate.getFullYear() === 2000 + versionYear &&
|
||||
currentDate.getMonth() + 1 === versionMonth;
|
||||
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
|
||||
return 'monthly';
|
||||
}
|
||||
|
||||
export function getNextVersion({
|
||||
currentVersion,
|
||||
type,
|
||||
currentDate = new Date(),
|
||||
}) {
|
||||
const { versionYear, versionMonth, versionHotfix } =
|
||||
parseVersion(currentVersion);
|
||||
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
|
||||
versionYear,
|
||||
versionMonth,
|
||||
);
|
||||
const resolvedType = resolveType(
|
||||
type,
|
||||
currentDate,
|
||||
versionYear,
|
||||
versionMonth,
|
||||
);
|
||||
|
||||
// Format date stamp once for nightly
|
||||
const currentDateString = currentDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
.replaceAll('-', '');
|
||||
|
||||
switch (resolvedType) {
|
||||
case 'nightly':
|
||||
return `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDateString}`;
|
||||
case 'hotfix':
|
||||
return `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
|
||||
case 'monthly':
|
||||
return `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||
default:
|
||||
throw new Error(
|
||||
'Invalid type specified. Use “auto”, “nightly”, “hotfix”, or “monthly”.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { getNextVersion } from './get-next-package-version';
|
||||
|
||||
describe('getNextVersion (lib)', () => {
|
||||
it('hotfix increments patch', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.1',
|
||||
type: 'hotfix',
|
||||
currentDate: new Date('2025-08-10'),
|
||||
}),
|
||||
).toBe('25.8.2');
|
||||
});
|
||||
|
||||
it('monthly advances month same year', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.3',
|
||||
type: 'monthly',
|
||||
currentDate: new Date('2025-08-15'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('monthly wraps year December -> January', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.12.3',
|
||||
type: 'monthly',
|
||||
currentDate: new Date('2025-12-05'),
|
||||
}),
|
||||
).toBe('26.1.0');
|
||||
});
|
||||
|
||||
it('nightly format with date stamp', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.1',
|
||||
type: 'nightly',
|
||||
currentDate: new Date('2025-08-22'),
|
||||
}),
|
||||
).toBe('25.9.0-nightly.20250822');
|
||||
});
|
||||
|
||||
it('auto before 25th -> hotfix', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-08-20'),
|
||||
}),
|
||||
).toBe('25.8.5');
|
||||
});
|
||||
|
||||
it('auto after 25th (same month) -> monthly', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-08-27'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('auto after 25th (next month) -> monthly', () => {
|
||||
expect(
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'auto',
|
||||
currentDate: new Date('2025-09-02'),
|
||||
}),
|
||||
).toBe('25.9.0');
|
||||
});
|
||||
|
||||
it('invalid type throws', () => {
|
||||
expect(() =>
|
||||
getNextVersion({
|
||||
currentVersion: '25.8.4',
|
||||
type: 'unknown',
|
||||
currentDate: new Date('2025-08-10'),
|
||||
}),
|
||||
).toThrow(/Invalid type/);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
|
||||
environment: 'node',
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
{
|
||||
"name": "@actual-app/components",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2",
|
||||
"react-dom": ">=18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.13.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
"./icons/logo": "./src/icons/logo/index.ts",
|
||||
"./icons/v0": "./src/icons/v0/index.ts",
|
||||
"./icons/v1": "./src/icons/v1/index.ts",
|
||||
"./icons/v2": "./src/icons/v2/index.ts",
|
||||
"./icons/AnimatedLoading": "./src/icons/AnimatedLoading.tsx",
|
||||
"./icons/Loading": "./src/icons/Loading.tsx",
|
||||
"./aligned-text": "./src/AlignedText.tsx",
|
||||
"./block": "./src/Block.tsx",
|
||||
"./button": "./src/Button.tsx",
|
||||
"./card": "./src/Card.tsx",
|
||||
"./form-error": "./src/FormError.tsx",
|
||||
"./initial-focus": "./src/InitialFocus.ts",
|
||||
"./inline-field": "./src/InlineField.tsx",
|
||||
"./input": "./src/Input.tsx",
|
||||
"./label": "./src/Label.tsx",
|
||||
"./menu": "./src/Menu.tsx",
|
||||
"./paragraph": "./src/Paragraph.tsx",
|
||||
"./popover": "./src/Popover.tsx",
|
||||
"./select": "./src/Select.tsx",
|
||||
"./space-between": "./src/SpaceBetween.tsx",
|
||||
"./stack": "./src/Stack.tsx",
|
||||
"./styles": "./src/styles.ts",
|
||||
"./text": "./src/Text.tsx",
|
||||
"./text-one-line": "./src/TextOneLine.tsx",
|
||||
"./theme": "./src/theme.ts",
|
||||
"./tokens": "./src/tokens.ts",
|
||||
"./toggle": "./src/Toggle.tsx",
|
||||
"./tooltip": "./src/Tooltip.tsx",
|
||||
"./view": "./src/View.tsx",
|
||||
"./color-picker": "./src/ColorPicker.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:web": "ENV=web vitest --run -c vitest.web.config.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { ChangeEvent, ReactNode } from 'react';
|
||||
import {
|
||||
ColorPicker as AriaColorPicker,
|
||||
ColorPickerProps as AriaColorPickerProps,
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
ColorSwatch as AriaColorSwatch,
|
||||
ColorSwatchProps,
|
||||
ColorSwatchPicker as AriaColorSwatchPicker,
|
||||
ColorSwatchPickerItem,
|
||||
ColorField,
|
||||
parseColor,
|
||||
} from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { Input } from './Input';
|
||||
import { Popover } from './Popover';
|
||||
|
||||
function ColorSwatch(props: ColorSwatchProps) {
|
||||
return (
|
||||
<AriaColorSwatch
|
||||
{...props}
|
||||
style={({ color }) => ({
|
||||
background: color.toString('hex'),
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// colors from https://materialui.co/colors
|
||||
const DEFAULT_COLOR_SET = [
|
||||
'#690CB0',
|
||||
'#D32F2F',
|
||||
'#C2185B',
|
||||
'#7B1FA2',
|
||||
'#512DA8',
|
||||
'#303F9F',
|
||||
'#1976D2',
|
||||
'#0288D1',
|
||||
'#0097A7',
|
||||
'#00796B',
|
||||
'#388E3C',
|
||||
'#689F38',
|
||||
'#AFB42B',
|
||||
'#FBC02D',
|
||||
'#FFA000',
|
||||
'#F57C00',
|
||||
'#E64A19',
|
||||
'#5D4037',
|
||||
'#616161',
|
||||
'#455A64',
|
||||
];
|
||||
|
||||
interface ColorSwatchPickerProps {
|
||||
columns?: number;
|
||||
colorset?: string[];
|
||||
}
|
||||
|
||||
function ColorSwatchPicker({
|
||||
columns = 5,
|
||||
colorset = DEFAULT_COLOR_SET,
|
||||
}: ColorSwatchPickerProps) {
|
||||
const pickers = [];
|
||||
|
||||
for (let l = 0; l < colorset.length / columns; l++) {
|
||||
const pickerItems = [];
|
||||
|
||||
for (let c = 0; c < columns; c++) {
|
||||
const color = colorset[columns * l + c];
|
||||
if (!color) {
|
||||
break;
|
||||
}
|
||||
|
||||
pickerItems.push(
|
||||
<ColorSwatchPickerItem
|
||||
key={color}
|
||||
color={color}
|
||||
className={css({
|
||||
position: 'relative',
|
||||
outline: 'none',
|
||||
borderRadius: '4px',
|
||||
width: 'fit-content',
|
||||
forcedColorAdjust: 'none',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&[data-selected]::after': {
|
||||
// eslint-disable-next-line actual/typography
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
border: '2px solid black',
|
||||
outline: '2px solid white',
|
||||
outlineOffset: '-4px',
|
||||
borderRadius: 'inherit',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ColorSwatch />
|
||||
</ColorSwatchPickerItem>,
|
||||
);
|
||||
}
|
||||
|
||||
pickers.push(
|
||||
<AriaColorSwatchPicker
|
||||
key={`colorset-${l}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{pickerItems}
|
||||
</AriaColorSwatchPicker>,
|
||||
);
|
||||
}
|
||||
|
||||
return pickers;
|
||||
}
|
||||
const isColor = (value: string) => /^#[0-9a-fA-F]{6}$/.test(value);
|
||||
|
||||
interface ColorPickerProps extends AriaColorPickerProps {
|
||||
children?: ReactNode;
|
||||
columns?: number;
|
||||
colorset?: string[];
|
||||
}
|
||||
|
||||
export function ColorPicker({
|
||||
children,
|
||||
columns,
|
||||
colorset,
|
||||
...props
|
||||
}: ColorPickerProps) {
|
||||
const onInput = (value: string) => {
|
||||
if (!isColor(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const color = parseColor(value);
|
||||
if (color) {
|
||||
props.onChange?.(color);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AriaColorPicker defaultValue={props.defaultValue ?? '#690CB0'} {...props}>
|
||||
<DialogTrigger>
|
||||
{children}
|
||||
<Popover>
|
||||
<Dialog
|
||||
style={{
|
||||
outline: 'none',
|
||||
padding: '15px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
minWidth: '192px',
|
||||
maxHeight: 'inherit',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<ColorSwatchPicker columns={columns} colorset={colorset} />
|
||||
<ColorField
|
||||
onInput={({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
|
||||
onInput(value)
|
||||
}
|
||||
>
|
||||
<Input placeholder="#RRGGBB" style={{ width: '100px' }} />
|
||||
</ColorField>
|
||||
</Dialog>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
</AriaColorPicker>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
|
||||
*/
|
||||
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* InitialFocus sets focus on its child element
|
||||
* when it mounts.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
|
||||
*/
|
||||
export function InitialFocus<T extends HTMLElement = HTMLElement>({
|
||||
children,
|
||||
}: InitialFocusProps<T>) {
|
||||
const ref = useRef<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// This is needed to avoid a strange interaction with
|
||||
// `ScopeTab`, which doesn't allow it to be focused at first for
|
||||
// some reason. Need to look into it.
|
||||
setTimeout(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
if (
|
||||
ref.current instanceof HTMLInputElement ||
|
||||
ref.current instanceof HTMLTextAreaElement
|
||||
) {
|
||||
ref.current.setSelectionRange(0, 10000);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children(ref);
|
||||
}
|
||||
|
||||
const child = Children.only(children);
|
||||
if (isValidElement(child)) {
|
||||
return cloneElement(child, { ref });
|
||||
}
|
||||
throw new Error(
|
||||
'InitialFocus expects a single valid React element as its child.',
|
||||
);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { InitialFocus } from './InitialFocus';
|
||||
import { View } from './View';
|
||||
|
||||
describe('InitialFocus', () => {
|
||||
it('should focus a text input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
|
||||
it('should focus a textarea', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<textarea title="focused" />
|
||||
</InitialFocus>
|
||||
<textarea title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const textarea = component.getByTitle('focused') as HTMLTextAreaElement;
|
||||
const unfocusedTextarea = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
expect(document.activeElement).not.toBe(unfocusedTextarea);
|
||||
});
|
||||
|
||||
it('should select text in an input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" defaultValue="Hello World" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(input.selectionStart).toBe(0);
|
||||
expect(input.selectionEnd).toBe(11); // Length of "Hello World"
|
||||
});
|
||||
|
||||
it('should focus a button', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<button title="focused">Click me</button>
|
||||
</InitialFocus>
|
||||
<button title="unfocused">Do not click me</button>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const button = component.getByTitle('focused') as HTMLButtonElement;
|
||||
const unfocusedButton = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLButtonElement;
|
||||
expect(document.activeElement).toBe(button);
|
||||
expect(document.activeElement).not.toBe(unfocusedButton);
|
||||
});
|
||||
|
||||
it('should focus a custom component with ref forwarding', async () => {
|
||||
const CustomInput = forwardRef<HTMLInputElement>((props, ref) => (
|
||||
<input type="text" ref={ref} {...props} title="focused" />
|
||||
));
|
||||
CustomInput.displayName = 'CustomInput';
|
||||
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
{node => <CustomInput ref={node as Ref<HTMLInputElement>} />}
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentPropsWithRef,
|
||||
type KeyboardEvent,
|
||||
type FocusEvent,
|
||||
} from 'react';
|
||||
import { Input as ReactAriaInput } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { useResponsive } from './hooks/useResponsive';
|
||||
import { styles } from './styles';
|
||||
import { theme } from './theme';
|
||||
|
||||
export const baseInputStyle = {
|
||||
outline: 0,
|
||||
backgroundColor: theme.tableBackground,
|
||||
color: theme.formInputText,
|
||||
margin: 0,
|
||||
padding: 5,
|
||||
borderRadius: 4,
|
||||
border: '1px solid ' + theme.formInputBorder,
|
||||
};
|
||||
|
||||
const defaultInputClassName = css({
|
||||
...baseInputStyle,
|
||||
color: theme.formInputText,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
'&[data-focused]': {
|
||||
border: '1px solid ' + theme.formInputBorderSelected,
|
||||
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
|
||||
},
|
||||
'&[data-disabled]': {
|
||||
color: theme.formInputTextPlaceholder,
|
||||
},
|
||||
'::placeholder': { color: theme.formInputTextPlaceholder },
|
||||
...styles.smallText,
|
||||
});
|
||||
|
||||
export type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
|
||||
onEnter?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEscape?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onChangeValue?: (
|
||||
newValue: string,
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => void;
|
||||
onUpdate?: (newValue: string, event: FocusEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
export function Input({
|
||||
ref,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onChangeValue,
|
||||
onUpdate,
|
||||
className,
|
||||
...props
|
||||
}: InputProps) {
|
||||
return (
|
||||
<ReactAriaInput
|
||||
ref={ref}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultInputClassName, className(renderProps))
|
||||
: cx(defaultInputClassName, className)
|
||||
}
|
||||
{...props}
|
||||
onKeyUp={e => {
|
||||
props.onKeyUp?.(e);
|
||||
|
||||
if (e.key === 'Enter' && onEnter) {
|
||||
onEnter(e.currentTarget.value, e);
|
||||
}
|
||||
|
||||
if (e.key === 'Escape' && onEscape) {
|
||||
onEscape(e.currentTarget.value, e);
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
onUpdate?.(e.currentTarget.value, e);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
onChange={e => {
|
||||
onChangeValue?.(e.currentTarget.value, e);
|
||||
props.onChange?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultBigInputClassName = css({
|
||||
padding: 10,
|
||||
fontSize: 15,
|
||||
border: 'none',
|
||||
...styles.shadow,
|
||||
'&[data-focused]': { border: 'none', ...styles.shadow },
|
||||
});
|
||||
|
||||
export function BigInput({ className, ...props }: InputProps) {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultBigInputClassName, className(renderProps))
|
||||
: cx(defaultBigInputClassName, className)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResponsiveInput(props: InputProps) {
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
return isNarrowWidth ? <BigInput {...props} /> : <Input {...props} />;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useWindowSize } from 'usehooks-ts';
|
||||
|
||||
import { breakpoints } from '../tokens';
|
||||
|
||||
export function useResponsive() {
|
||||
const { height, width } = useWindowSize({
|
||||
debounceDelay: 250,
|
||||
});
|
||||
|
||||
// Possible view modes: narrow, small, medium, wide
|
||||
// To check if we're at least small width, check !isNarrowWidth
|
||||
return {
|
||||
// atLeastMediumWidth is provided to avoid checking (isMediumWidth || isWideWidth)
|
||||
atLeastMediumWidth: width >= breakpoints.medium,
|
||||
isNarrowWidth: width < breakpoints.small,
|
||||
isSmallWidth: width >= breakpoints.small && width < breakpoints.medium,
|
||||
isMediumWidth: width >= breakpoints.medium && width < breakpoints.wide,
|
||||
// No atLeastWideWidth because that's identical to isWideWidth
|
||||
isWideWidth: width >= breakpoints.wide,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgArrowButtonSingleLeft1 = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M.25 12a2.643 2.643 0 0 1 .775-1.875L10.566.584a1.768 1.768 0 0 1 2.5 2.5l-8.739 8.739a.25.25 0 0 0 0 .354l8.739 8.739a1.768 1.768 0 0 1-2.5 2.5l-9.541-9.541A2.643 2.643 0 0 1 .25 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Bold" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-button-single-left-1</title><path d="M.25,12a2.643,2.643,0,0,1,.775-1.875L10.566.584a1.768,1.768,0,0,1,2.5,2.5L4.327,11.823a.25.25,0,0,0,0,.354l8.739,8.739a1.768,1.768,0,0,1-2.5,2.5L1.025,13.875A2.643,2.643,0,0,1,.25,12Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 312 B |
@@ -1,163 +0,0 @@
|
||||
import { keyframes } from '@emotion/css';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { tokens } from './tokens';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CSSProperties = Record<string, any>;
|
||||
|
||||
const MOBILE_MIN_HEIGHT = 40;
|
||||
|
||||
const shadowLarge = {
|
||||
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const styles: Record<string, any> = {
|
||||
incomeHeaderHeight: 70,
|
||||
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
|
||||
monthRightPadding: 5,
|
||||
menuBorderRadius: 4,
|
||||
mobileMinHeight: MOBILE_MIN_HEIGHT,
|
||||
mobileMenuItem: {
|
||||
fontSize: 17,
|
||||
fontWeight: 400,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
height: MOBILE_MIN_HEIGHT,
|
||||
minHeight: MOBILE_MIN_HEIGHT,
|
||||
},
|
||||
mobileEditingPadding: 12,
|
||||
altMenuMaxHeight: 250,
|
||||
altMenuText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
altMenuHeaderText: {
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
},
|
||||
veryLargeText: {
|
||||
fontSize: 30,
|
||||
fontWeight: 600,
|
||||
},
|
||||
largeText: {
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
mediumText: {
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
smallText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
verySmallText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
tinyText: {
|
||||
fontSize: 10,
|
||||
},
|
||||
page: {
|
||||
flex: 1,
|
||||
'@media (max-height: 550px)': {
|
||||
minHeight: 700, // ensure we can scroll on small screens
|
||||
},
|
||||
paddingTop: 8, // height of the titlebar
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingTop: 36,
|
||||
},
|
||||
},
|
||||
pageContent: {
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
},
|
||||
},
|
||||
settingsPageContent: {
|
||||
padding: 20,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
padding: 'inherit',
|
||||
},
|
||||
},
|
||||
staticText: {
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
},
|
||||
shadow: {
|
||||
boxShadow: '0 2px 4px 0 rgba(0,0,0,0.1)',
|
||||
},
|
||||
shadowLarge,
|
||||
tnum: {
|
||||
// eslint-disable-next-line actual/typography
|
||||
fontFeatureSettings: '"tnum"',
|
||||
},
|
||||
notFixed: { fontFeatureSettings: '' },
|
||||
text: {
|
||||
fontSize: 16,
|
||||
// lineHeight: 22.4 // TODO: This seems like trouble, but what's the right value?
|
||||
},
|
||||
delayedFadeIn: {
|
||||
animationName: keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
}),
|
||||
animationDuration: '1s',
|
||||
animationFillMode: 'both',
|
||||
animationDelay: '0.5s',
|
||||
},
|
||||
underlinedText: {
|
||||
borderBottom: `2px solid`,
|
||||
},
|
||||
noTapHighlight: {
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
lineClamp: (lines: number) => {
|
||||
return {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: lines,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
};
|
||||
},
|
||||
tooltip: {
|
||||
padding: 5,
|
||||
...shadowLarge,
|
||||
borderWidth: 2,
|
||||
borderRadius: 4,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.tooltipBorder,
|
||||
backgroundColor: theme.tooltipBackground,
|
||||
color: theme.tooltipText,
|
||||
overflow: 'auto',
|
||||
},
|
||||
popover: {
|
||||
border: 'none',
|
||||
backgroundColor: theme.menuBackground,
|
||||
color: theme.menuItemText,
|
||||
},
|
||||
// Dynamically set
|
||||
horizontalScrollbar: null as CSSProperties | null,
|
||||
lightScrollbar: null as CSSProperties | null,
|
||||
darkScrollbar: null as CSSProperties | null,
|
||||
scrollbarWidth: null as number | null,
|
||||
editorPill: {
|
||||
color: theme.pillText,
|
||||
backgroundColor: theme.pillBackground,
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
mobileListItem: {
|
||||
borderBottom: `1px solid ${theme.tableBorder}`,
|
||||
backgroundColor: theme.tableBackground,
|
||||
padding: 16,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
export const theme = {
|
||||
pageBackground: 'var(--color-pageBackground)',
|
||||
pageBackgroundModalActive: 'var(--color-pageBackgroundModalActive)',
|
||||
pageBackgroundTopLeft: 'var(--color-pageBackgroundTopLeft)',
|
||||
pageBackgroundBottomRight: 'var(--color-pageBackgroundBottomRight)',
|
||||
pageBackgroundLineTop: 'var(--color-pageBackgroundLineTop)',
|
||||
pageBackgroundLineMid: 'var(--color-pageBackgroundLineMid)',
|
||||
pageBackgroundLineBottom: 'var(--color-pageBackgroundLineBottom)',
|
||||
pageText: 'var(--color-pageText)',
|
||||
pageTextLight: 'var(--color-pageTextLight)',
|
||||
pageTextSubdued: 'var(--color-pageTextSubdued)',
|
||||
pageTextDark: 'var(--color-pageTextDark)',
|
||||
pageTextPositive: 'var(--color-pageTextPositive)',
|
||||
pageTextLink: 'var(--color-pageTextLink)',
|
||||
pageTextLinkLight: 'var(--color-pageTextLinkLight)',
|
||||
cardBackground: 'var(--color-cardBackground)',
|
||||
cardBorder: 'var(--color-cardBorder)',
|
||||
cardShadow: 'var(--color-cardShadow)',
|
||||
tableBackground: 'var(--color-tableBackground)',
|
||||
tableRowBackgroundHover: 'var(--color-tableRowBackgroundHover)',
|
||||
tableText: 'var(--color-tableText)',
|
||||
tableTextLight: 'var(--color-tableTextLight)',
|
||||
tableTextSubdued: 'var(--color-tableTextSubdued)',
|
||||
tableTextSelected: 'var(--color-tableTextSelected)',
|
||||
tableTextHover: 'var(--color-tableTextHover)',
|
||||
tableTextInactive: 'var(--color-tableTextInactive)',
|
||||
tableHeaderText: 'var(--color-tableHeaderText)',
|
||||
tableHeaderBackground: 'var(--color-tableHeaderBackground)',
|
||||
tableBorder: 'var(--color-tableBorder)',
|
||||
tableBorderSelected: 'var(--color-tableBorderSelected)',
|
||||
tableBorderHover: 'var(--color-tableBorderHover)',
|
||||
tableBorderSeparator: 'var(--color-tableBorderSeparator)',
|
||||
tableRowBackgroundHighlight: 'var(--color-tableRowBackgroundHighlight)',
|
||||
tableRowBackgroundHighlightText:
|
||||
'var(--color-tableRowBackgroundHighlightText)',
|
||||
tableRowHeaderBackground: 'var(--color-tableRowHeaderBackground)',
|
||||
tableRowHeaderText: 'var(--color-tableRowHeaderText)',
|
||||
sidebarBackground: 'var(--color-sidebarBackground)',
|
||||
sidebarItemBackgroundPending: 'var(--color-sidebarItemBackgroundPending)',
|
||||
sidebarItemBackgroundPositive: 'var(--color-sidebarItemBackgroundPositive)',
|
||||
sidebarItemBackgroundFailed: 'var(--color-sidebarItemBackgroundFailed)',
|
||||
sidebarItemAccentSelected: 'var(--color-sidebarItemAccentSelected)',
|
||||
sidebarItemBackgroundHover: 'var(--color-sidebarItemBackgroundHover)',
|
||||
sidebarItemText: 'var(--color-sidebarItemText)',
|
||||
sidebarItemTextSelected: 'var(--color-sidebarItemTextSelected)',
|
||||
menuBackground: 'var(--color-menuBackground)',
|
||||
menuItemBackground: 'var(--color-menuItemBackground)',
|
||||
menuItemBackgroundHover: 'var(--color-menuItemBackgroundHover)',
|
||||
menuItemText: 'var(--color-menuItemText)',
|
||||
menuItemTextHover: 'var(--color-menuItemTextHover)',
|
||||
menuItemTextSelected: 'var(--color-menuItemTextSelected)',
|
||||
menuItemTextHeader: 'var(--color-menuItemTextHeader)',
|
||||
menuBorder: 'var(--color-menuBorder)',
|
||||
menuBorderHover: 'var(--color-menuBorderHover)',
|
||||
menuKeybindingText: 'var(--color-menuKeybindingText)',
|
||||
menuAutoCompleteBackground: 'var(--color-menuAutoCompleteBackground)',
|
||||
menuAutoCompleteBackgroundHover:
|
||||
'var(--color-menuAutoCompleteBackgroundHover)',
|
||||
menuAutoCompleteText: 'var(--color-menuAutoCompleteText)',
|
||||
menuAutoCompleteTextHover: 'var(--color-menuAutoCompleteTextHover)',
|
||||
menuAutoCompleteTextHeader: 'var(--color-menuAutoCompleteTextHeader)',
|
||||
menuAutoCompleteItemTextHover: 'var(--color-menuAutoCompleteItemTextHover)',
|
||||
menuAutoCompleteItemText: 'var(--color-menuAutoCompleteItemText)',
|
||||
modalBackground: 'var(--color-modalBackground)',
|
||||
modalBorder: 'var(--color-modalBorder)',
|
||||
mobileHeaderBackground: 'var(--color-mobileHeaderBackground)',
|
||||
mobileHeaderText: 'var(--color-mobileHeaderText)',
|
||||
mobileHeaderTextSubdued: 'var(--color-mobileHeaderTextSubdued)',
|
||||
mobileHeaderTextHover: 'var(--color-mobileHeaderTextHover)',
|
||||
mobilePageBackground: 'var(--color-mobilePageBackground)',
|
||||
mobileNavBackground: 'var(--color-mobileNavBackground)',
|
||||
mobileNavItem: 'var(--color-mobileNavItem)',
|
||||
mobileNavItemSelected: 'var(--color-mobileNavItemSelected)',
|
||||
mobileAccountShadow: 'var(--color-mobileAccountShadow)',
|
||||
mobileAccountText: 'var(--color-mobileAccountText)',
|
||||
mobileTransactionSelected: 'var(--color-mobileTransactionSelected)',
|
||||
mobileViewTheme: 'var(--color-mobileViewTheme)',
|
||||
mobileConfigServerViewTheme: 'var(--color-mobileConfigServerViewTheme)',
|
||||
markdownNormal: 'var(--color-markdownNormal)',
|
||||
markdownDark: 'var(--color-markdownDark)',
|
||||
markdownLight: 'var(--color-markdownLight)',
|
||||
buttonMenuText: 'var(--color-buttonMenuText)',
|
||||
buttonMenuTextHover: 'var(--color-buttonMenuTextHover)',
|
||||
buttonMenuBackground: 'var(--color-buttonMenuBackground)',
|
||||
buttonMenuBackgroundHover: 'var(--color-buttonMenuBackgroundHover)',
|
||||
buttonMenuBorder: 'var(--color-buttonMenuBorder)',
|
||||
buttonMenuSelectedText: 'var(--color-buttonMenuSelectedText)',
|
||||
buttonMenuSelectedTextHover: 'var(--color-buttonMenuSelectedTextHover)',
|
||||
buttonMenuSelectedBackground: 'var(--color-buttonMenuSelectedBackground)',
|
||||
buttonMenuSelectedBackgroundHover:
|
||||
'var(--color-buttonMenuSelectedBackgroundHover)',
|
||||
buttonMenuSelectedBorder: 'var(--color-buttonMenuSelectedBorder)',
|
||||
buttonPrimaryText: 'var(--color-buttonPrimaryText)',
|
||||
buttonPrimaryTextHover: 'var(--color-buttonPrimaryTextHover)',
|
||||
buttonPrimaryBackground: 'var(--color-buttonPrimaryBackground)',
|
||||
buttonPrimaryBackgroundHover: 'var(--color-buttonPrimaryBackgroundHover)',
|
||||
buttonPrimaryBorder: 'var(--color-buttonPrimaryBorder)',
|
||||
buttonPrimaryShadow: 'var(--color-buttonPrimaryShadow)',
|
||||
buttonPrimaryDisabledText: 'var(--color-buttonPrimaryDisabledText)',
|
||||
buttonPrimaryDisabledBackground:
|
||||
'var(--color-buttonPrimaryDisabledBackground)',
|
||||
buttonPrimaryDisabledBorder: 'var(--color-buttonPrimaryDisabledBorder)',
|
||||
buttonNormalText: 'var(--color-buttonNormalText)',
|
||||
buttonNormalTextHover: 'var(--color-buttonNormalTextHover)',
|
||||
buttonNormalBackground: 'var(--color-buttonNormalBackground)',
|
||||
buttonNormalBackgroundHover: 'var(--color-buttonNormalBackgroundHover)',
|
||||
buttonNormalBorder: 'var(--color-buttonNormalBorder)',
|
||||
buttonNormalShadow: 'var(--color-buttonNormalShadow)',
|
||||
buttonNormalSelectedText: 'var(--color-buttonNormalSelectedText)',
|
||||
buttonNormalSelectedBackground: 'var(--color-buttonNormalSelectedBackground)',
|
||||
buttonNormalDisabledText: 'var(--color-buttonNormalDisabledText)',
|
||||
buttonNormalDisabledBackground: 'var(--color-buttonNormalDisabledBackground)',
|
||||
buttonNormalDisabledBorder: 'var(--color-buttonNormalDisabledBorder)',
|
||||
buttonBareText: 'var(--color-buttonBareText)',
|
||||
buttonBareTextHover: 'var(--color-buttonBareTextHover)',
|
||||
buttonBareBackground: 'var(--color-buttonBareBackground)',
|
||||
buttonBareBackgroundHover: 'var(--color-buttonBareBackgroundHover)',
|
||||
buttonBareBackgroundActive: 'var(--color-buttonBareBackgroundActive)',
|
||||
buttonBareDisabledText: 'var(--color-buttonBareDisabledText)',
|
||||
buttonBareDisabledBackground: 'var(--color-buttonBareDisabledBackground)',
|
||||
calendarText: 'var(--color-calendarText)',
|
||||
calendarBackground: 'var(--color-calendarBackground)',
|
||||
calendarItemText: 'var(--color-calendarItemText)',
|
||||
calendarItemBackground: 'var(--color-calendarItemBackground)',
|
||||
calendarSelectedBackground: 'var(--color-calendarSelectedBackground)',
|
||||
noticeBackground: 'var(--color-noticeBackground)',
|
||||
noticeBackgroundLight: 'var(--color-noticeBackgroundLight)',
|
||||
noticeBackgroundDark: 'var(--color-noticeBackgroundDark)',
|
||||
noticeText: 'var(--color-noticeText)',
|
||||
noticeTextLight: 'var(--color-noticeTextLight)',
|
||||
noticeTextDark: 'var(--color-noticeTextDark)',
|
||||
noticeTextMenu: 'var(--color-noticeTextMenu)',
|
||||
noticeTextMenuHover: 'var(--color-noticeTextMenuHover)',
|
||||
noticeBorder: 'var(--color-noticeBorder)',
|
||||
warningBackground: 'var(--color-warningBackground)',
|
||||
warningText: 'var(--color-warningText)',
|
||||
warningTextLight: 'var(--color-warningTextLight)',
|
||||
warningTextDark: 'var(--color-warningTextDark)',
|
||||
warningBorder: 'var(--color-warningBorder)',
|
||||
errorBackground: 'var(--color-errorBackground)',
|
||||
errorText: 'var(--color-errorText)',
|
||||
errorTextDark: 'var(--color-errorTextDark)',
|
||||
errorTextDarker: 'var(--color-errorTextDarker)',
|
||||
errorTextMenu: 'var(--color-errorTextMenu)',
|
||||
errorBorder: 'var(--color-errorBorder)',
|
||||
upcomingBackground: 'var(--color-upcomingBackground)',
|
||||
upcomingText: 'var(--color-upcomingText)',
|
||||
upcomingBorder: 'var(--color-upcomingBorder)',
|
||||
formLabelText: 'var(--color-formLabelText)',
|
||||
formLabelBackground: 'var(--color-formLabelBackground)',
|
||||
formInputBackground: 'var(--color-formInputBackground)',
|
||||
formInputBackgroundSelected: 'var(--color-formInputBackgroundSelected)',
|
||||
formInputBackgroundSelection: 'var(--color-formInputBackgroundSelection)',
|
||||
formInputBorder: 'var(--color-formInputBorder)',
|
||||
formInputTextReadOnlySelection: 'var(--color-formInputTextReadOnlySelection)',
|
||||
formInputBorderSelected: 'var(--color-formInputBorderSelected)',
|
||||
formInputText: 'var(--color-formInputText)',
|
||||
formInputTextSelected: 'var(--color-formInputTextSelected)',
|
||||
formInputTextPlaceholder: 'var(--color-formInputTextPlaceholder)',
|
||||
formInputTextPlaceholderSelected:
|
||||
'var(--color-formInputTextPlaceholderSelected)',
|
||||
formInputTextSelection: 'var(--color-formInputTextSelection)',
|
||||
formInputShadowSelected: 'var(--color-formInputShadowSelected)',
|
||||
formInputTextHighlight: 'var(--color-formInputTextHighlight)',
|
||||
checkboxText: 'var(--color-checkboxText)',
|
||||
checkboxBackgroundSelected: 'var(--color-checkboxBackgroundSelected)',
|
||||
checkboxBorderSelected: 'var(--color-checkboxBorderSelected)',
|
||||
checkboxShadowSelected: 'var(--color-checkboxShadowSelected)',
|
||||
checkboxToggleBackground: 'var(--color-checkboxToggleBackground)',
|
||||
checkboxToggleBackgroundSelected:
|
||||
'var(--color-checkboxToggleBackgroundSelected)',
|
||||
checkboxToggleDisabled: 'var(--color-checkboxToggleDisabled)',
|
||||
pillBackground: 'var(--color-pillBackground)',
|
||||
pillBackgroundLight: 'var(--color-pillBackgroundLight)',
|
||||
pillText: 'var(--color-pillText)',
|
||||
pillTextHighlighted: 'var(--color-pillTextHighlighted)',
|
||||
pillBorder: 'var(--color-pillBorder)',
|
||||
pillBorderDark: 'var(--color-pillBorderDark)',
|
||||
pillBackgroundSelected: 'var(--color-pillBackgroundSelected)',
|
||||
pillTextSelected: 'var(--color-pillTextSelected)',
|
||||
pillBorderSelected: 'var(--color-pillBorderSelected)',
|
||||
pillTextSubdued: 'var(--color-pillTextSubdued)',
|
||||
reportsRed: 'var(--color-reportsRed)',
|
||||
reportsBlue: 'var(--color-reportsBlue)',
|
||||
reportsGreen: 'var(--color-reportsGreen)',
|
||||
reportsGray: 'var(--color-reportsGray)',
|
||||
reportsLabel: 'var(--color-reportsLabel)',
|
||||
reportsInnerLabel: 'var(--color-reportsInnerLabel)',
|
||||
noteTagBackground: 'var(--color-noteTagBackground)',
|
||||
noteTagBackgroundHover: 'var(--color-noteTagBackgroundHover)',
|
||||
noteTagDefault: 'var(--color-noteTagDefault)',
|
||||
noteTagText: 'var(--color-noteTagText)',
|
||||
budgetOtherMonth: 'var(--color-budgetOtherMonth)',
|
||||
budgetCurrentMonth: 'var(--color-budgetCurrentMonth)',
|
||||
budgetHeaderOtherMonth: 'var(--color-budgetHeaderOtherMonth)',
|
||||
budgetHeaderCurrentMonth: 'var(--color-budgetHeaderCurrentMonth)',
|
||||
floatingActionBarBackground: 'var(--color-floatingActionBarBackground)',
|
||||
floatingActionBarBorder: 'var(--color-floatingActionBarBorder)',
|
||||
floatingActionBarText: 'var(--color-floatingActionBarText)',
|
||||
tooltipText: 'var(--color-tooltipText)',
|
||||
tooltipBackground: 'var(--color-tooltipBackground)',
|
||||
tooltipBorder: 'var(--color-tooltipBorder)',
|
||||
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const resolveExtensions = [
|
||||
'.testing.ts',
|
||||
'.web.ts',
|
||||
'.mjs',
|
||||
'.js',
|
||||
'.mts',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.json',
|
||||
'.wasm',
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
poolOptions: {
|
||||
threads: {
|
||||
maxThreads: 2,
|
||||
minThreads: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve('../../../crdt/src$1'),
|
||||
},
|
||||
],
|
||||
extensions: resolveExtensions,
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
});
|
||||
@@ -18,9 +18,4 @@ protoc --plugin="protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts" \
|
||||
|
||||
../../node_modules/.bin/prettier --write src/proto/*.d.ts
|
||||
|
||||
for file in src/proto/*.d.ts; do
|
||||
{ echo "/* eslint-disable @typescript-eslint/no-namespace */"; sed 's/export class/export declare class/g' "$file"; } > "${file%.d.ts}.ts"
|
||||
rm "$file"
|
||||
done
|
||||
|
||||
echo 'One more step! Find the `var global = ...` declaration in src/proto/sync_pb.js and change it to `var global = globalThis;`'
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './src';
|
||||
export * from './src/main';
|
||||
|
||||
6
packages/crdt/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -11,19 +11,21 @@
|
||||
"scripts": {
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "rm -rf dist && yarn run build:node",
|
||||
"test": "vitest --run --globals"
|
||||
"build": "rm -rf dist && yarn run build:node && cp src/proto/sync_pb.d.ts dist/src/proto/",
|
||||
"test": "jest -c jest.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"google-protobuf": "^3.12.0-rc.1",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^13.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-protobuf": "^3.15.12",
|
||||
"protoc-gen-js": "^3.21.4-4",
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^3.2.4"
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`merkle trie > adding an item works 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
exports[`merkle trie adding an item works 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"hash": 1983295247,
|
||||
@@ -34,14 +34,14 @@ exports[`merkle trie > adding an item works 1`] = `
|
||||
},
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"hash": 1469038940,
|
||||
},
|
||||
"hash": 1469038940,
|
||||
@@ -78,33 +78,33 @@ exports[`merkle trie > adding an item works 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
exports[`merkle trie pruning works and keeps correct hashes 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1100,
|
||||
},
|
||||
"hash": 1100,
|
||||
@@ -113,28 +113,28 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1956,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -143,28 +143,28 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -175,29 +175,29 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -206,10 +206,10 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
@@ -246,33 +246,33 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
exports[`merkle trie pruning works and keeps correct hashes 2`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -281,19 +281,19 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -304,20 +304,20 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -326,10 +326,10 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import './proto/sync_pb.js'; // Import for side effects
|
||||
|
||||
export {
|
||||
merkle,
|
||||
getClock,
|
||||
setClock,
|
||||
makeClock,
|
||||
makeClientId,
|
||||
serializeClock,
|
||||
deserializeClock,
|
||||
type Clock,
|
||||
Timestamp,
|
||||
} from './crdt';
|
||||
|
||||
// Access global proto namespace
|
||||
export const SyncRequest = (globalThis as any).proto.SyncRequest;
|
||||
export const SyncResponse = (globalThis as any).proto.SyncResponse;
|
||||
export const Message = (globalThis as any).proto.Message;
|
||||
export const MessageEnvelope = (globalThis as any).proto.MessageEnvelope;
|
||||
export const EncryptedData = (globalThis as any).proto.EncryptedData;
|
||||
|
||||
export const SyncProtoBuf = (globalThis as any).proto;
|
||||
14
packages/crdt/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as SyncPb from './proto/sync_pb';
|
||||
export {
|
||||
merkle,
|
||||
getClock,
|
||||
setClock,
|
||||
makeClock,
|
||||
makeClientId,
|
||||
serializeClock,
|
||||
deserializeClock,
|
||||
type Clock,
|
||||
Timestamp,
|
||||
} from './crdt';
|
||||
|
||||
export const SyncProtoBuf = SyncPb;
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
// package:
|
||||
// file: sync.proto
|
||||
|
||||
import * as jspb from 'google-protobuf';
|
||||
|
||||
export declare class EncryptedData extends jspb.Message {
|
||||
export class EncryptedData extends jspb.Message {
|
||||
getIv(): Uint8Array | string;
|
||||
getIv_asU8(): Uint8Array;
|
||||
getIv_asB64(): string;
|
||||
@@ -49,7 +48,7 @@ export namespace EncryptedData {
|
||||
};
|
||||
}
|
||||
|
||||
export declare class Message extends jspb.Message {
|
||||
export class Message extends jspb.Message {
|
||||
getDataset(): string;
|
||||
setDataset(value: string): void;
|
||||
|
||||
@@ -89,7 +88,7 @@ export namespace Message {
|
||||
};
|
||||
}
|
||||
|
||||
export declare class MessageEnvelope extends jspb.Message {
|
||||
export class MessageEnvelope extends jspb.Message {
|
||||
getTimestamp(): string;
|
||||
setTimestamp(value: string): void;
|
||||
|
||||
@@ -130,7 +129,7 @@ export namespace MessageEnvelope {
|
||||
};
|
||||
}
|
||||
|
||||
export declare class SyncRequest extends jspb.Message {
|
||||
export class SyncRequest extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
@@ -179,7 +178,7 @@ export namespace SyncRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export declare class SyncResponse extends jspb.Message {
|
||||
export class SyncResponse extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
@@ -157,9 +157,9 @@ proto.EncryptedData.prototype.toObject = function(opt_includeInstance) {
|
||||
*/
|
||||
proto.EncryptedData.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
iv: msg.getIv_asB64(),
|
||||
authtag: msg.getAuthtag_asB64(),
|
||||
data: msg.getData_asB64()
|
||||
iv: msg.getIv_asB64(),
|
||||
authtag: msg.getAuthtag_asB64(),
|
||||
data: msg.getData_asB64()
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -419,10 +419,10 @@ proto.Message.prototype.toObject = function(opt_includeInstance) {
|
||||
*/
|
||||
proto.Message.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
dataset: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
row: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
column: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
value: jspb.Message.getFieldWithDefault(msg, 4, "")
|
||||
dataset: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
row: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
column: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
value: jspb.Message.getFieldWithDefault(msg, 4, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -639,9 +639,9 @@ proto.MessageEnvelope.prototype.toObject = function(opt_includeInstance) {
|
||||
*/
|
||||
proto.MessageEnvelope.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
timestamp: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
isencrypted: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
|
||||
content: msg.getContent_asB64()
|
||||
timestamp: jspb.Message.getFieldWithDefault(msg, 1, ""),
|
||||
isencrypted: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
|
||||
content: msg.getContent_asB64()
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -860,12 +860,12 @@ proto.SyncRequest.prototype.toObject = function(opt_includeInstance) {
|
||||
*/
|
||||
proto.SyncRequest.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
|
||||
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
|
||||
proto.MessageEnvelope.toObject, includeInstance),
|
||||
fileid: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
groupid: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
keyid: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
||||
since: jspb.Message.getFieldWithDefault(msg, 6, "")
|
||||
fileid: jspb.Message.getFieldWithDefault(msg, 2, ""),
|
||||
groupid: jspb.Message.getFieldWithDefault(msg, 3, ""),
|
||||
keyid: jspb.Message.getFieldWithDefault(msg, 5, ""),
|
||||
since: jspb.Message.getFieldWithDefault(msg, 6, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
@@ -1140,9 +1140,9 @@ proto.SyncResponse.prototype.toObject = function(opt_includeInstance) {
|
||||
*/
|
||||
proto.SyncResponse.toObject = function(includeInstance, msg) {
|
||||
var f, obj = {
|
||||
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
|
||||
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
|
||||
proto.MessageEnvelope.toObject, includeInstance),
|
||||
merkle: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
merkle: jspb.Message.getFieldWithDefault(msg, 2, "")
|
||||
};
|
||||
|
||||
if (includeInstance) {
|
||||
|
||||
3
packages/desktop-client/.gitignore
vendored
@@ -14,9 +14,6 @@ build-electron
|
||||
build-stats
|
||||
stats.json
|
||||
|
||||
# generated service worker
|
||||
service-worker/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
|
||||
@@ -39,7 +39,6 @@ HTTPS=true yarn start
|
||||
```
|
||||
|
||||
or using the dev container:
|
||||
|
||||
```
|
||||
HTTPS=true docker compose up --build
|
||||
```
|
||||
@@ -65,10 +64,10 @@ Run manually:
|
||||
|
||||
```sh
|
||||
# Run docker container
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.56.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
|
||||
# Use the ip and port noted earlier
|
||||
@@ -83,7 +82,6 @@ E2E_START_URL=https://ip:port yarn vrt
|
||||
You can also run the tests against a remote server by passing the URL:
|
||||
|
||||
Run in standardized docker container:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
|
||||
@@ -92,7 +90,6 @@ E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
```
|
||||
|
||||
Run locally:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt
|
||||
```
|
||||
|
||||
@@ -20,23 +20,16 @@ const processTranslations = () => {
|
||||
|
||||
console.log(`en.json has ${enKeysCount} keys.`);
|
||||
|
||||
files.forEach(file => {
|
||||
files.forEach((file) => {
|
||||
if (file === 'en.json' || path.extname(file) !== '.json') return;
|
||||
|
||||
if (file.startsWith('en-')) {
|
||||
console.log(`Keeping ${file} as it's an English language.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join(localRepoPath, file);
|
||||
const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
const fileKeysCount = Object.keys(jsonData).length;
|
||||
|
||||
// Calculate the percentage of keys present compared to en.json
|
||||
const percentage = (fileKeysCount / enKeysCount) * 100;
|
||||
console.log(
|
||||
`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`,
|
||||
);
|
||||
console.log(`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`);
|
||||
|
||||
if (percentage < 50) {
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |