mirror of
https://github.com/zadam/trilium.git
synced 2025-10-27 16:26:31 +01:00
Compare commits
6 Commits
fix/mkdocs
...
feat/push-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53f31f9c78 | ||
|
|
22c5651eeb | ||
|
|
68a10a9813 | ||
|
|
b204964e57 | ||
|
|
b2c869d7ab | ||
|
|
307e17f9c8 |
2
.github/actions/build-electron/action.yml
vendored
2
.github/actions/build-electron/action.yml
vendored
@@ -86,7 +86,7 @@ runs:
|
||||
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
|
||||
WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
|
||||
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
|
||||
run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
||||
run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
|
||||
|
||||
# Add DMG signing step
|
||||
- name: Sign DMG
|
||||
|
||||
4
.github/actions/build-server/action.yml
vendored
4
.github/actions/build-server/action.yml
vendored
@@ -10,7 +10,7 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
@@ -23,7 +23,7 @@ runs:
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run chore:update-build-info
|
||||
pnpm run --filter server package
|
||||
pnpm nx --project=server package
|
||||
- name: Prepare artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
40
.github/instructions/nx.instructions.md
vendored
Normal file
40
.github/instructions/nx.instructions.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
applyTo: '**'
|
||||
---
|
||||
|
||||
// This file is automatically generated by Nx Console
|
||||
|
||||
You are in an nx workspace using Nx 21.3.9 and pnpm as the package manager.
|
||||
|
||||
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
|
||||
|
||||
# General Guidelines
|
||||
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
|
||||
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
|
||||
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
|
||||
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
|
||||
|
||||
# Generation Guidelines
|
||||
If the user wants to generate something, use the following flow:
|
||||
|
||||
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
|
||||
- get the available generators using the 'nx_generators' tool
|
||||
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
|
||||
- get generator details using the 'nx_generator_schema' tool
|
||||
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
|
||||
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
|
||||
- open the generator UI using the 'nx_open_generate_ui' tool
|
||||
- wait for the user to finish the generator
|
||||
- read the generator log file using the 'nx_read_generator_log' tool
|
||||
- use the information provided in the log file to answer the user's question or continue with what they were doing
|
||||
|
||||
# Running Tasks Guidelines
|
||||
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
|
||||
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
|
||||
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
|
||||
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
|
||||
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
|
||||
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.
|
||||
|
||||
|
||||
|
||||
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
453
.github/scripts/sync-docs-to-wiki-with-wiki-syntax.ts
vendored
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert markdown to GitHub Wiki format
|
||||
* - Images:  → [[image.png]]
|
||||
* - Links: [text](page.md) → [[text|page]]
|
||||
*/
|
||||
async function convertToWikiFormat(wikiDir: string): Promise<void> {
|
||||
console.log('Converting to GitHub Wiki format...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let convertedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
const originalContent = content;
|
||||
|
||||
// Convert image references to wiki format
|
||||
//  → [[image.png]]
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding
|
||||
let imagePath = src;
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
imagePath = decodeURIComponent(src);
|
||||
} catch {
|
||||
imagePath = src;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract just the filename for wiki syntax
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
// Use wiki syntax for images
|
||||
// If alt text exists, add it after pipe
|
||||
if (alt && alt.trim()) {
|
||||
return `[[${filename}|alt=${alt}]]`;
|
||||
} else {
|
||||
return `[[${filename}]]`;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert internal markdown links to wiki format
|
||||
// [text](../path/to/Page.md) → [[text|Page]]
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Check if it's a markdown file link
|
||||
if (href.endsWith('.md') || href.includes('.md#')) {
|
||||
// Decode URL encoding
|
||||
let decodedHref = href;
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
decodedHref = decodeURIComponent(href);
|
||||
} catch {
|
||||
decodedHref = href;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract page name without extension and path
|
||||
let pageName = decodedHref
|
||||
.replace(/\.md(#.*)?$/, '') // Remove .md and anchor
|
||||
.split('/') // Split by path
|
||||
.pop() || ''; // Get last part (filename)
|
||||
|
||||
// Convert spaces to hyphens (GitHub wiki convention)
|
||||
pageName = pageName.replace(/ /g, '-');
|
||||
|
||||
// Use wiki link syntax
|
||||
if (text === pageName || text === pageName.replace(/-/g, ' ')) {
|
||||
return `[[${pageName}]]`;
|
||||
} else {
|
||||
return `[[${text}|${pageName}]]`;
|
||||
}
|
||||
}
|
||||
|
||||
// For other internal links, just decode URL encoding
|
||||
if (href.includes('%') && !href.startsWith('http')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Save if modified
|
||||
if (content !== originalContent) {
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Converted: ${relativePath}`);
|
||||
convertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (convertedCount > 0) {
|
||||
console.log(`Converted ${convertedCount} files to wiki format`);
|
||||
} else {
|
||||
console.log('No files needed conversion');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten directory structure - move all files to root
|
||||
* GitHub Wiki prefers flat structure
|
||||
*/
|
||||
async function flattenStructure(wikiDir: string): Promise<void> {
|
||||
console.log('Flattening directory structure for wiki...');
|
||||
const allFiles = await getAllFiles(wikiDir);
|
||||
let movedCount = 0;
|
||||
|
||||
for (const file of allFiles) {
|
||||
// Skip if already at root
|
||||
if (!file.includes('/')) continue;
|
||||
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
const basename = path.basename(file);
|
||||
|
||||
// Create unique name if file already exists at root
|
||||
let newName = basename;
|
||||
let counter = 1;
|
||||
while (await fileExists(path.join(wikiDir, newName))) {
|
||||
const ext = path.extname(basename);
|
||||
const nameWithoutExt = basename.slice(0, -ext.length);
|
||||
newName = `${nameWithoutExt}-${counter}${ext}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
|
||||
// Move file to root
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Moved: ${file} → ${newName}`);
|
||||
movedCount++;
|
||||
}
|
||||
|
||||
if (movedCount > 0) {
|
||||
console.log(`Moved ${movedCount} files to root`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
|
||||
for (const subDir of allDirs) {
|
||||
try {
|
||||
const entries = await fs.readdir(subDir);
|
||||
if (entries.length === 0 || (entries.length === 1 && entries[0] === '.git')) {
|
||||
await fs.rmdir(subDir);
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Syncing files to wiki...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Copy file
|
||||
await fs.copyFile(file, targetPath);
|
||||
}
|
||||
|
||||
// Remove orphaned files
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
for (const wikiFile of wikiFiles) {
|
||||
if (!sourceRelativePaths.has(wikiFile) && !wikiFile.startsWith('Home')) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
const flattenWiki = process.env.FLATTEN_WIKI === 'true';
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
console.log(`Flatten structure: ${flattenWiki}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Convert to wiki format
|
||||
await convertToWikiFormat(config.wikiPath);
|
||||
|
||||
// Optionally flatten directory structure
|
||||
if (flattenWiki) {
|
||||
await flattenStructure(config.wikiPath);
|
||||
}
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
437
.github/scripts/sync-docs-to-wiki.ts
vendored
Executable file
@@ -0,0 +1,437 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const FILE_EXTENSIONS = ['.md', '.png', '.jpg', '.jpeg', '.gif', '.svg'] as const;
|
||||
const README_PATTERN = /^README(?:[-.](.+))?\.md$/;
|
||||
|
||||
interface SyncConfig {
|
||||
mainRepoPath: string;
|
||||
wikiPath: string;
|
||||
docsPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all files matching the given extensions
|
||||
*/
|
||||
async function findFiles(dir: string, extensions: readonly string[]): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
const entries: Dirent[] = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (extensions.includes(ext)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in a directory recursively
|
||||
*/
|
||||
async function getAllFiles(dir: string): Promise<Set<string>> {
|
||||
const files = new Set<string>();
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
const relativePath = path.relative(dir, fullPath);
|
||||
|
||||
// Skip .git directory
|
||||
if (entry.name === '.git' || relativePath.startsWith('.git')) continue;
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
await walk(fullPath);
|
||||
} else if (entry.isFile()) {
|
||||
files.add(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Directory might not exist yet
|
||||
if ((error as any).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync files from source to wiki, preserving directory structure and removing orphaned files
|
||||
*/
|
||||
async function syncFiles(sourceDir: string, wikiDir: string): Promise<void> {
|
||||
console.log('Analyzing files to sync...');
|
||||
|
||||
// Get all valid source files
|
||||
const sourceFiles = await findFiles(sourceDir, FILE_EXTENSIONS);
|
||||
const sourceRelativePaths = new Set<string>();
|
||||
|
||||
// Copy all source files and track their paths
|
||||
console.log(`Found ${sourceFiles.length} files to sync`);
|
||||
let copiedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const relativePath = path.relative(sourceDir, file);
|
||||
sourceRelativePaths.add(relativePath);
|
||||
|
||||
const targetPath = path.join(wikiDir, relativePath);
|
||||
const targetDir = path.dirname(targetPath);
|
||||
|
||||
// Create directory structure
|
||||
await fs.mkdir(targetDir, { recursive: true });
|
||||
|
||||
// Check if file needs updating (compare modification times)
|
||||
let needsCopy = true;
|
||||
try {
|
||||
const sourceStat = await fs.stat(file);
|
||||
const targetStat = await fs.stat(targetPath);
|
||||
// Only copy if source is newer or sizes differ
|
||||
needsCopy = sourceStat.mtime > targetStat.mtime || sourceStat.size !== targetStat.size;
|
||||
} catch {
|
||||
// Target doesn't exist, needs copy
|
||||
needsCopy = true;
|
||||
}
|
||||
|
||||
if (needsCopy) {
|
||||
await fs.copyFile(file, targetPath);
|
||||
console.log(` Updated: ${relativePath}`);
|
||||
copiedCount++;
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Updated ${copiedCount} files, ${skippedCount} unchanged`);
|
||||
|
||||
// Find and remove files that don't exist in source
|
||||
console.log('Checking for orphaned files in wiki...');
|
||||
const wikiFiles = await getAllFiles(wikiDir);
|
||||
let removedCount = 0;
|
||||
|
||||
for (const wikiFile of wikiFiles) {
|
||||
// Check if this file should exist (either as-is or will be renamed)
|
||||
let shouldExist = sourceRelativePaths.has(wikiFile);
|
||||
|
||||
// Special handling for Home files that will be created from READMEs
|
||||
if (wikiFile.startsWith('Home')) {
|
||||
const readmeVariant1 = wikiFile.replace(/^Home(-.*)?\.md$/, 'README$1.md');
|
||||
const readmeVariant2 = wikiFile.replace(/^Home-(.+)\.md$/, 'README.$1.md');
|
||||
shouldExist = sourceRelativePaths.has(readmeVariant1) || sourceRelativePaths.has(readmeVariant2) || sourceRelativePaths.has('README.md');
|
||||
}
|
||||
|
||||
if (!shouldExist) {
|
||||
const fullPath = path.join(wikiDir, wikiFile);
|
||||
await fs.unlink(fullPath);
|
||||
console.log(` Removed: ${wikiFile}`);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
console.log(`Removed ${removedCount} orphaned files`);
|
||||
|
||||
// Clean up empty directories
|
||||
await cleanEmptyDirectories(wikiDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove empty directories recursively
|
||||
*/
|
||||
async function cleanEmptyDirectories(dir: string): Promise<void> {
|
||||
async function removeEmptyDirs(currentDir: string): Promise<boolean> {
|
||||
if (currentDir === dir) return false; // Don't remove root
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
// Skip .git directory
|
||||
const filteredEntries = entries.filter(e => e.name !== '.git');
|
||||
|
||||
if (filteredEntries.length === 0) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check subdirectories
|
||||
for (const entry of filteredEntries) {
|
||||
if (entry.isDirectory()) {
|
||||
const subDir = path.join(currentDir, entry.name);
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Check again after cleaning subdirectories
|
||||
const remainingEntries = await fs.readdir(currentDir);
|
||||
const filteredRemaining = remainingEntries.filter(e => e !== '.git');
|
||||
|
||||
if (filteredRemaining.length === 0 && currentDir !== dir) {
|
||||
await fs.rmdir(currentDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all directories and process them
|
||||
const allDirs = await getAllDirectories(dir);
|
||||
for (const subDir of allDirs) {
|
||||
await removeEmptyDirs(subDir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all directories recursively
|
||||
*/
|
||||
async function getAllDirectories(dir: string): Promise<string[]> {
|
||||
const dirs: string[] = [];
|
||||
|
||||
async function walk(currentDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name !== '.git') {
|
||||
const fullPath = path.join(currentDir, entry.name);
|
||||
dirs.push(fullPath);
|
||||
await walk(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return dirs.sort((a, b) => b.length - a.length); // Sort longest first for cleanup
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix references in markdown files for wiki compatibility
|
||||
*
|
||||
* Issues fixed:
|
||||
* 1. URL-encoded image references (spaces as %20) need to match actual filenames
|
||||
* 2. Internal markdown links need to be converted to wiki syntax
|
||||
* 3. Images can optionally use wiki syntax [[image.png]] for better compatibility
|
||||
*/
|
||||
async function fixImageReferences(wikiDir: string): Promise<void> {
|
||||
console.log('Fixing references for GitHub Wiki compatibility...');
|
||||
const mdFiles = await findFiles(wikiDir, ['.md']);
|
||||
let fixedCount = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
let content = await fs.readFile(file, 'utf-8');
|
||||
let modified = false;
|
||||
const originalContent = content;
|
||||
|
||||
// Step 1: Fix URL-encoded image references
|
||||
// Convert  to 
|
||||
content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => {
|
||||
// Skip external URLs
|
||||
if (src.startsWith('http://') || src.startsWith('https://')) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding if present
|
||||
if (src.includes('%')) {
|
||||
try {
|
||||
const decodedSrc = decodeURIComponent(src);
|
||||
return ``;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Step 2: Fix internal links - decode URL encoding but keep standard markdown format
|
||||
// GitHub Wiki actually supports standard markdown links with relative paths
|
||||
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, href) => {
|
||||
// Skip external URLs, anchors, and images
|
||||
if (href.startsWith('http://') ||
|
||||
href.startsWith('https://') ||
|
||||
href.startsWith('#') ||
|
||||
href.match(/\.(png|jpg|jpeg|gif|svg)$/i)) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Decode URL encoding for all internal links
|
||||
if (href.includes('%')) {
|
||||
try {
|
||||
const decodedHref = decodeURIComponent(href);
|
||||
return `[${text}](${decodedHref})`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
// Check if content was modified
|
||||
if (content !== originalContent) {
|
||||
modified = true;
|
||||
await fs.writeFile(file, content, 'utf-8');
|
||||
const relativePath = path.relative(wikiDir, file);
|
||||
console.log(` Fixed references in: ${relativePath}`);
|
||||
fixedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (fixedCount > 0) {
|
||||
console.log(`Fixed references in ${fixedCount} files`);
|
||||
} else {
|
||||
console.log('No references needed fixing');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename README files to wiki-compatible names
|
||||
*/
|
||||
async function renameReadmeFiles(wikiDir: string): Promise<void> {
|
||||
console.log('Converting README files for wiki compatibility...');
|
||||
const files = await fs.readdir(wikiDir);
|
||||
|
||||
for (const file of files) {
|
||||
const match = file.match(README_PATTERN);
|
||||
if (match) {
|
||||
const oldPath = path.join(wikiDir, file);
|
||||
let newName: string;
|
||||
|
||||
if (match[1]) {
|
||||
// Language-specific README (e.g., README-ZH_CN.md or README.es.md)
|
||||
newName = `Home-${match[1]}.md`;
|
||||
} else {
|
||||
// Main README
|
||||
newName = 'Home.md';
|
||||
}
|
||||
|
||||
const newPath = path.join(wikiDir, newName);
|
||||
await fs.rename(oldPath, newPath);
|
||||
console.log(` Renamed: ${file} → ${newName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any changes in the wiki
|
||||
*/
|
||||
async function hasChanges(wikiDir: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd: wikiDir });
|
||||
return stdout.trim().length > 0;
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy root README.md to wiki as Home.md if it exists
|
||||
*/
|
||||
async function copyRootReadme(mainRepoPath: string, wikiPath: string): Promise<void> {
|
||||
const rootReadmePath = path.join(mainRepoPath, 'README.md');
|
||||
const wikiHomePath = path.join(wikiPath, 'Home.md');
|
||||
|
||||
try {
|
||||
await fs.access(rootReadmePath);
|
||||
await fs.copyFile(rootReadmePath, wikiHomePath);
|
||||
console.log(' Copied root README.md as Home.md');
|
||||
} catch (error) {
|
||||
// Root README doesn't exist or can't be accessed
|
||||
console.log(' No root README.md found to use as Home page');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration from environment variables
|
||||
*/
|
||||
function getConfig(): SyncConfig {
|
||||
const mainRepoPath = process.env.MAIN_REPO_PATH || 'main-repo';
|
||||
const wikiPath = process.env.WIKI_PATH || 'wiki';
|
||||
const docsPath = path.join(mainRepoPath, 'docs');
|
||||
|
||||
return { mainRepoPath, wikiPath, docsPath };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main sync function
|
||||
*/
|
||||
async function syncDocsToWiki(): Promise<void> {
|
||||
const config = getConfig();
|
||||
|
||||
console.log('Starting documentation sync to wiki...');
|
||||
console.log(`Source: ${config.docsPath}`);
|
||||
console.log(`Target: ${config.wikiPath}`);
|
||||
|
||||
try {
|
||||
// Verify paths exist
|
||||
await fs.access(config.docsPath);
|
||||
await fs.access(config.wikiPath);
|
||||
|
||||
// Sync files (copy new/updated, remove orphaned)
|
||||
await syncFiles(config.docsPath, config.wikiPath);
|
||||
|
||||
// Copy root README.md as Home.md
|
||||
await copyRootReadme(config.mainRepoPath, config.wikiPath);
|
||||
|
||||
// Fix image and link references for wiki compatibility
|
||||
await fixImageReferences(config.wikiPath);
|
||||
|
||||
// Rename README files to wiki-compatible names
|
||||
await renameReadmeFiles(config.wikiPath);
|
||||
|
||||
// Check for changes
|
||||
const changed = await hasChanges(config.wikiPath);
|
||||
|
||||
if (changed) {
|
||||
console.log('\nChanges detected in wiki');
|
||||
// GitHub Actions output format
|
||||
process.stdout.write('::set-output name=changes::true\n');
|
||||
} else {
|
||||
console.log('\nNo changes detected in wiki');
|
||||
process.stdout.write('::set-output name=changes::false\n');
|
||||
}
|
||||
|
||||
console.log('Sync completed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error during sync:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
syncDocsToWiki();
|
||||
}
|
||||
|
||||
export { syncDocsToWiki };
|
||||
190
.github/workflows/deploy-docs.yml
vendored
190
.github/workflows/deploy-docs.yml
vendored
@@ -1,190 +0,0 @@
|
||||
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
|
||||
# This workflow builds and deploys your MkDocs site when changes are pushed to main
|
||||
name: Deploy MkDocs Documentation
|
||||
|
||||
on:
|
||||
# Trigger on push to main branch
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master # Also support master branch
|
||||
# Only run when docs files change
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'README.md' # README is synced to docs/index.md
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
- 'scripts/fix-mkdocs-structure.ts'
|
||||
- 'validate-docs-links.ts'
|
||||
|
||||
# Allow manual triggering from Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Run on pull requests for preview deployments
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'README.md' # README is synced to docs/index.md
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-docs.txt'
|
||||
- '.github/workflows/deploy-docs.yml'
|
||||
- 'scripts/fix-mkdocs-structure.ts'
|
||||
- 'validate-docs-links.ts'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
name: Build and Deploy MkDocs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
# Required permissions for deployment
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write # For PR preview comments
|
||||
id-token: write # For OIDC authentication (if needed)
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
cache: 'pip'
|
||||
cache-dependency-path: 'requirements-docs.txt'
|
||||
|
||||
- name: Install MkDocs and Dependencies
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements-docs.txt
|
||||
env:
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: 1
|
||||
|
||||
# Setup pnpm before fixing docs structure
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
# Setup Node.js with pnpm
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
# Install Node.js dependencies for the TypeScript script
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Fix Documentation Structure
|
||||
run: |
|
||||
# Fix duplicate navigation entries by moving overview pages to index.md
|
||||
pnpm run chore:fix-mkdocs-structure
|
||||
|
||||
- name: Build MkDocs Site
|
||||
run: |
|
||||
# Build with strict mode but allow expected warnings
|
||||
mkdocs build --verbose || {
|
||||
EXIT_CODE=$?
|
||||
# Check if the only issue is expected warnings
|
||||
if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
|
||||
[ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
|
||||
echo "✅ Build succeeded with expected warnings"
|
||||
mkdocs build --verbose
|
||||
else
|
||||
echo "❌ Build failed with unexpected errors"
|
||||
exit $EXIT_CODE
|
||||
fi
|
||||
}
|
||||
|
||||
- name: Validate Built Site
|
||||
run: |
|
||||
# Basic validation that important files exist
|
||||
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
|
||||
test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
|
||||
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
|
||||
echo "✅ Site validation passed"
|
||||
|
||||
- name: Validate Documentation Links
|
||||
run: |
|
||||
# Run the TypeScript link validation script
|
||||
pnpm tsx validate-docs-links.ts
|
||||
|
||||
# Install wrangler globally to avoid workspace issues
|
||||
- name: Install Wrangler
|
||||
run: |
|
||||
npm install -g wrangler
|
||||
|
||||
# Deploy using Wrangler (use pre-installed wrangler)
|
||||
- name: Deploy to Cloudflare Pages
|
||||
id: deploy
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy site --project-name=trilium-docs --branch=${{ github.ref_name }}
|
||||
wranglerVersion: '' # Use pre-installed version
|
||||
|
||||
# Deploy preview for PRs
|
||||
- name: Deploy Preview to Cloudflare Pages
|
||||
id: preview-deployment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: pages deploy site --project-name=trilium-docs --branch=pr-${{ github.event.pull_request.number }}
|
||||
wranglerVersion: '' # Use pre-installed version
|
||||
|
||||
# Post deployment URL as PR comment
|
||||
- name: Comment PR with Preview URL
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
// Construct preview URL based on Cloudflare Pages pattern
|
||||
const previewUrl = `https://pr-${prNumber}.trilium-docs.pages.dev`;
|
||||
const mainUrl = 'https://docs.triliumnotes.org';
|
||||
|
||||
// Check if we already commented
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const botComment = comments.data.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body.includes('Documentation preview is ready')
|
||||
);
|
||||
|
||||
const commentBody = `📚 Documentation preview is ready!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`;
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
// Create new comment
|
||||
await github.rest.issues.createComment({
|
||||
issue_number: prNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
37
.github/workflows/dev.yml
vendored
37
.github/workflows/dev.yml
vendored
@@ -19,24 +19,45 @@ permissions:
|
||||
pull-requests: write # for PR comments
|
||||
|
||||
jobs:
|
||||
check-affected:
|
||||
name: Check affected jobs (NX)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # needed for https://github.com/marketplace/actions/nx-set-shas
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Check affected
|
||||
run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build
|
||||
|
||||
test_dev:
|
||||
name: Test development
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check-affected
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Run the unit tests
|
||||
run: pnpm run test:all
|
||||
|
||||
@@ -45,6 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test_dev
|
||||
- check-affected
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
@@ -53,7 +75,7 @@ jobs:
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
- name: Trigger client build
|
||||
run: pnpm client:build
|
||||
run: pnpm nx run client:build
|
||||
- name: Send client bundle stats to RelativeCI
|
||||
if: false
|
||||
uses: relative-ci/agent-action@v3
|
||||
@@ -61,7 +83,7 @@ jobs:
|
||||
webpackStatsFile: ./apps/client/dist/webpack-stats.json
|
||||
key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
|
||||
- name: Trigger server build
|
||||
run: pnpm run server:build
|
||||
run: pnpm nx run server:build
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/build-push-action@v6
|
||||
with:
|
||||
@@ -73,6 +95,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_docker
|
||||
- check-affected
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -89,7 +112,7 @@ jobs:
|
||||
- name: Update build info
|
||||
run: pnpm run chore:update-build-info
|
||||
- name: Trigger build
|
||||
run: pnpm server:build
|
||||
run: pnpm nx run server:build
|
||||
|
||||
- name: Set IMAGE_NAME to lowercase
|
||||
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
|
||||
|
||||
6
.github/workflows/main-docker.yml
vendored
6
.github/workflows/main-docker.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
require-healthy: true
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e
|
||||
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
|
||||
|
||||
- name: Upload Playwright trace
|
||||
if: failure()
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
|
||||
7
.github/workflows/nightly.yml
vendored
7
.github/workflows/nightly.yml
vendored
@@ -51,12 +51,13 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Update nightly version
|
||||
run: npm run chore:ci-update-nightly-version
|
||||
- name: Run the build
|
||||
@@ -78,7 +79,7 @@ jobs:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.3.3
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
@@ -119,7 +120,7 @@ jobs:
|
||||
arch: ${{ matrix.arch }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2.3.3
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
with:
|
||||
make_latest: false
|
||||
|
||||
22
.github/workflows/playwright.yml
vendored
22
.github/workflows/playwright.yml
vendored
@@ -19,8 +19,14 @@ jobs:
|
||||
filter: tree:0
|
||||
fetch-depth: 0
|
||||
|
||||
# This enables task distribution via Nx Cloud
|
||||
# Run this command as early as possible, before dependencies are installed
|
||||
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
|
||||
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
|
||||
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
@@ -28,12 +34,10 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- run: pnpm exec playwright install --with-deps
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
|
||||
- run: pnpm --filter server-e2e e2e
|
||||
|
||||
- name: Upload test report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e report
|
||||
path: apps/server-e2e/test-output
|
||||
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
|
||||
# - run: npx nx-cloud record -- echo Hello World
|
||||
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
|
||||
# When you enable task distribution, run the e2e-ci task instead of e2e
|
||||
- run: pnpm exec nx affected -t e2e --exclude desktop-e2e
|
||||
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
@@ -35,12 +35,13 @@ jobs:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- uses: nrwl/nx-set-shas@v4
|
||||
- name: Run the build
|
||||
uses: ./.github/actions/build-electron
|
||||
with:
|
||||
@@ -114,7 +115,7 @@ jobs:
|
||||
path: upload
|
||||
|
||||
- name: Publish stable release
|
||||
uses: softprops/action-gh-release@v2.3.3
|
||||
uses: softprops/action-gh-release@v2.3.2
|
||||
with:
|
||||
draft: false
|
||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||
|
||||
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
67
.github/workflows/sync-docs-to-wiki.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Sync Docs to Wiki
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
|
||||
permissions:
|
||||
contents: read # Read access to repository contents
|
||||
# Note: Writing to wiki requires a PAT or GITHUB_TOKEN with additional permissions
|
||||
# The default GITHUB_TOKEN cannot write to wikis, so we need to:
|
||||
# 1. Create a Personal Access Token (PAT) with 'repo' scope
|
||||
# 2. Add it as a repository secret named WIKI_TOKEN
|
||||
|
||||
jobs:
|
||||
sync-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: main-repo
|
||||
|
||||
- name: Checkout wiki repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: TriliumNext/Trilium.wiki
|
||||
path: wiki
|
||||
token: ${{ secrets.WIKI_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install tsx for TypeScript execution
|
||||
run: npm install -g tsx
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "GitHub Action"
|
||||
|
||||
- name: Sync documentation to wiki
|
||||
id: sync
|
||||
run: |
|
||||
tsx main-repo/.github/scripts/sync-docs-to-wiki.ts
|
||||
env:
|
||||
MAIN_REPO_PATH: main-repo
|
||||
WIKI_PATH: wiki
|
||||
|
||||
- name: Commit and push changes
|
||||
if: contains(steps.sync.outputs.changes, 'true')
|
||||
run: |
|
||||
cd wiki
|
||||
git add .
|
||||
git commit -m "Sync documentation from main repository
|
||||
|
||||
Source commit: ${{ github.sha }}
|
||||
Triggered by: ${{ github.event.head_commit.message }}"
|
||||
git push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
/.cache
|
||||
|
||||
# compiled output
|
||||
dist
|
||||
@@ -33,11 +32,14 @@ testem.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
vite.config.*.timestamp*
|
||||
vitest.config.*.timestamp*
|
||||
test-output
|
||||
|
||||
apps/*/data*
|
||||
apps/*/data
|
||||
apps/*/out
|
||||
upload
|
||||
|
||||
@@ -46,6 +48,3 @@ upload
|
||||
|
||||
/result
|
||||
.svelte-kit
|
||||
|
||||
# docs
|
||||
site/
|
||||
|
||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -5,6 +5,7 @@
|
||||
"lokalise.i18n-ally",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"ms-playwright.playwright",
|
||||
"nrwl.angular-console",
|
||||
"redhat.vscode-yaml",
|
||||
"tobermory.es6-string-html",
|
||||
"vitest.explorer",
|
||||
|
||||
8
.vscode/mcp.json
vendored
Normal file
8
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"servers": {
|
||||
"nx-mcp": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:9461/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -35,5 +35,6 @@
|
||||
"docs/**/*.png": true,
|
||||
"apps/server/src/assets/doc_notes/**": true,
|
||||
"apps/edit-docs/demo/**": true
|
||||
}
|
||||
},
|
||||
"nxConsole.generateAiAgentRules": true
|
||||
}
|
||||
13
CLAUDE.md
13
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using NX, with multiple applications and shared packages.
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -14,9 +14,12 @@ Trilium Notes is a hierarchical note-taking application with advanced features l
|
||||
|
||||
### Running Applications
|
||||
- `pnpm run server:start` - Start development server (http://localhost:8080)
|
||||
- `pnpm nx run server:serve` - Alternative server start command
|
||||
- `pnpm nx run desktop:serve` - Run desktop Electron app
|
||||
- `pnpm run server:start-prod` - Run server in production mode
|
||||
|
||||
### Building
|
||||
- `pnpm nx build <project>` - Build specific project (server, client, desktop, etc.)
|
||||
- `pnpm run client:build` - Build client application
|
||||
- `pnpm run server:build` - Build server application
|
||||
- `pnpm run electron:build` - Build desktop application
|
||||
@@ -25,8 +28,13 @@ Trilium Notes is a hierarchical note-taking application with advanced features l
|
||||
- `pnpm test:all` - Run all tests (parallel + sequential)
|
||||
- `pnpm test:parallel` - Run tests that can run in parallel
|
||||
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
|
||||
- `pnpm nx test <project>` - Run tests for specific project
|
||||
- `pnpm coverage` - Generate coverage reports
|
||||
|
||||
### Linting & Type Checking
|
||||
- `pnpm nx run <project>:lint` - Lint specific project
|
||||
- `pnpm nx run <project>:typecheck` - Type check specific project
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Monorepo Structure
|
||||
@@ -86,6 +94,7 @@ Frontend uses a widget system (`apps/client/src/widgets/`):
|
||||
- `apps/server/src/assets/db/schema.sql` - Core database structure
|
||||
|
||||
4. **Configuration**:
|
||||
- `nx.json` - NX workspace configuration
|
||||
- `package.json` - Project dependencies and scripts
|
||||
|
||||
## Note Types and Features
|
||||
@@ -145,7 +154,7 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Update schema in `apps/server/src/assets/db/schema.sql`
|
||||
|
||||
## Build System Notes
|
||||
- Uses pnpm for monorepo management
|
||||
- Uses NX for monorepo management with build caching
|
||||
- Vite for fast development builds
|
||||
- ESBuild for production optimization
|
||||
- pnpm workspaces for dependency management
|
||||
|
||||
@@ -142,7 +142,7 @@ Download the repository, install dependencies using `pnpm` and then run the envi
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm edit-docs:edit-docs
|
||||
pnpm nx run edit-docs:edit-docs
|
||||
```
|
||||
|
||||
### Building the Executable
|
||||
@@ -151,7 +151,7 @@ Download the repository, install dependencies using `pnpm` and then build the de
|
||||
git clone https://github.com/TriliumNext/Trilium.git
|
||||
cd Trilium
|
||||
pnpm install
|
||||
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
"@playwright/test": "1.55.0",
|
||||
"@stylistic/eslint-plugin": "5.3.1",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.18.1",
|
||||
"@types/node": "22.18.0",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.35.0",
|
||||
"eslint": "9.34.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"esm": "3.2.25",
|
||||
"jsdoc": "4.0.4",
|
||||
|
||||
@@ -9,13 +9,8 @@
|
||||
"email": "contact@eliandoran.me",
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
|
||||
"test": "vitest",
|
||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "9.35.0",
|
||||
"@eslint/js": "9.34.0",
|
||||
"@excalidraw/excalidraw": "0.18.0",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
@@ -24,7 +19,7 @@
|
||||
"@fullcalendar/multimonth": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@mermaid-js/layout-elk": "0.1.9",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
@@ -39,9 +34,9 @@
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.2.0",
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.5.2",
|
||||
"i18next": "25.4.2",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -52,8 +47,8 @@
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "16.2.1",
|
||||
"mermaid": "11.11.0",
|
||||
"mind-elixir": "5.1.1",
|
||||
"mermaid": "11.10.1",
|
||||
"mind-elixir": "5.0.6",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.1",
|
||||
@@ -69,12 +64,25 @@
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.20",
|
||||
"@types/leaflet-gpx": "1.3.8",
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/tabulator-tables": "6.2.10",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "18.0.1",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.2"
|
||||
},
|
||||
"nx": {
|
||||
"name": "client",
|
||||
"targets": {
|
||||
"serve": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"circular-deps": {
|
||||
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { t } from "./services/i18n.js";
|
||||
import options from "./services/options.js";
|
||||
import type ElectronRemote from "@electron/remote";
|
||||
import type Electron from "electron";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "autocomplete.js/index_jquery.js";
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
@@ -132,11 +131,9 @@ export default class MobileLayout {
|
||||
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
|
||||
)
|
||||
.child(
|
||||
new ScreenContainer("detail", "row")
|
||||
new ScreenContainer("detail", "column")
|
||||
.id("detail-container")
|
||||
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
|
||||
.child(
|
||||
new NoteWrapperWidget()
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.contentSized()
|
||||
@@ -160,7 +157,6 @@ export default class MobileLayout {
|
||||
.child(<MobileEditorToolbar />)
|
||||
)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
new FlexContainer("column")
|
||||
.contentSized()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
// @ts-ignore - module = undefined
|
||||
// Required for correct loading of scripts in Electron
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import appContext from "./components/app_context.js";
|
||||
import noteAutocompleteService from "./services/note_autocomplete.js";
|
||||
import glob from "./services/glob.js";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "autocomplete.js/index_jquery.js";
|
||||
|
||||
|
||||
@@ -48,6 +48,6 @@ function getUrl(docNameValue: string, language: string) {
|
||||
// Cannot have spaces in the URL due to how JQuery.load works.
|
||||
docNameValue = docNameValue.replaceAll(" ", "%20");
|
||||
|
||||
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
|
||||
const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath;
|
||||
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ let leftInstance: ReturnType<typeof Split> | null;
|
||||
let rightPaneWidth: number;
|
||||
let rightInstance: ReturnType<typeof Split> | null;
|
||||
|
||||
const noteSplitMap = new Map<string[], ReturnType<typeof Split> | undefined>(); // key: a group of ntxIds, value: the corresponding Split instance
|
||||
const noteSplitRafMap = new Map<string[], number>();
|
||||
let splitNoteContainer: HTMLElement | undefined;
|
||||
|
||||
function setupLeftPaneResizer(leftPaneVisible: boolean) {
|
||||
if (leftInstance) {
|
||||
leftInstance.destroy();
|
||||
@@ -87,86 +83,7 @@ function setupRightPaneResizer() {
|
||||
}
|
||||
}
|
||||
|
||||
function findKeyByNtxId(ntxId: string): string[] | undefined {
|
||||
// Find the corresponding key in noteSplitMap based on ntxId
|
||||
for (const key of noteSplitMap.keys()) {
|
||||
if (key.includes(ntxId)) return key;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setupNoteSplitResizer(ntxIds: string[]) {
|
||||
let targetNtxIds: string[] | undefined;
|
||||
for (const ntxId of ntxIds) {
|
||||
targetNtxIds = findKeyByNtxId(ntxId);
|
||||
if (targetNtxIds) break;
|
||||
}
|
||||
|
||||
if (targetNtxIds) {
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
for (const id of ntxIds) {
|
||||
if (!targetNtxIds.includes(id)) {
|
||||
targetNtxIds.push(id)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
targetNtxIds = [...ntxIds];
|
||||
}
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
|
||||
|
||||
function delNoteSplitResizer(ntxIds: string[]) {
|
||||
let targetNtxIds = findKeyByNtxId(ntxIds[0]);
|
||||
if (!targetNtxIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
noteSplitMap.delete(targetNtxIds);
|
||||
targetNtxIds = targetNtxIds.filter(id => !ntxIds.includes(id));
|
||||
|
||||
if (targetNtxIds.length >= 2) {
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
}
|
||||
|
||||
function moveNoteSplitResizer(ntxId: string) {
|
||||
const targetNtxIds = findKeyByNtxId(ntxId);
|
||||
if (!targetNtxIds) {
|
||||
return;
|
||||
}
|
||||
noteSplitMap.get(targetNtxIds)?.destroy();
|
||||
noteSplitMap.set(targetNtxIds, undefined);
|
||||
createSplitInstance(targetNtxIds);
|
||||
}
|
||||
|
||||
function createSplitInstance(targetNtxIds: string[]) {
|
||||
const prevRafId = noteSplitRafMap.get(targetNtxIds);
|
||||
if (prevRafId) {
|
||||
cancelAnimationFrame(prevRafId);
|
||||
}
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
splitNoteContainer = splitNoteContainer ?? $("#center-pane").find(".split-note-container-widget")[0];
|
||||
const splitPanels = [...splitNoteContainer.querySelectorAll<HTMLElement>(':scope > .note-split')]
|
||||
.filter(el => targetNtxIds.includes(el.getAttribute('data-ntx-id') ?? ""));
|
||||
const splitInstance = Split(splitPanels, {
|
||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||
minSize: 150,
|
||||
});
|
||||
noteSplitMap.set(targetNtxIds, splitInstance);
|
||||
noteSplitRafMap.delete(targetNtxIds);
|
||||
});
|
||||
noteSplitRafMap.set(targetNtxIds, rafId);
|
||||
}
|
||||
|
||||
export default {
|
||||
setupLeftPaneResizer,
|
||||
setupRightPaneResizer,
|
||||
setupNoteSplitResizer,
|
||||
delNoteSplitResizer,
|
||||
moveNoteSplitResizer
|
||||
setupRightPaneResizer
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import shortcuts, { keyMatches, matchesShortcut, isIMEComposing } from "./shortcuts.js";
|
||||
import shortcuts, { keyMatches, matchesShortcut } from "./shortcuts.js";
|
||||
|
||||
// Mock utils module
|
||||
vi.mock("./utils.js", () => ({
|
||||
@@ -320,36 +320,4 @@ describe("shortcuts", () => {
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIMEComposing', () => {
|
||||
it('should return true when event.isComposing is true', () => {
|
||||
const event = { isComposing: true, keyCode: 65 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when keyCode is 229', () => {
|
||||
const event = { isComposing: false, keyCode: 229 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when both isComposing is true and keyCode is 229', () => {
|
||||
const event = { isComposing: true, keyCode: 229 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for normal keys', () => {
|
||||
const event = { isComposing: false, keyCode: 65 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when isComposing is undefined and keyCode is not 229', () => {
|
||||
const event = { keyCode: 13 } as KeyboardEvent;
|
||||
expect(isIMEComposing(event)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null/undefined events gracefully', () => {
|
||||
expect(isIMEComposing(null as any)).toBe(false);
|
||||
expect(isIMEComposing(undefined as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,24 +40,6 @@ for (let i = 1; i <= 19; i++) {
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IME (Input Method Editor) is composing
|
||||
* This is used to prevent keyboard shortcuts from firing during IME composition
|
||||
* @param e - The keyboard event to check
|
||||
* @returns true if IME is currently composing, false otherwise
|
||||
*/
|
||||
export function isIMEComposing(e: KeyboardEvent): boolean {
|
||||
// Handle null/undefined events gracefully
|
||||
if (!e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Standard check for composition state
|
||||
// e.isComposing is true when IME is actively composing
|
||||
// e.keyCode === 229 is a fallback for older browsers where 229 indicates IME processing
|
||||
return e.isComposing || e.keyCode === 229;
|
||||
}
|
||||
|
||||
function removeGlobalShortcut(namespace: string) {
|
||||
bindGlobalShortcut("", null, namespace);
|
||||
}
|
||||
@@ -86,13 +68,6 @@ function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: st
|
||||
}
|
||||
|
||||
const e = evt as KeyboardEvent;
|
||||
|
||||
// Skip processing if IME is composing to prevent shortcuts from
|
||||
// interfering with text input in CJK languages
|
||||
if (isIMEComposing(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesShortcut(e, keyboardShortcut)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -297,54 +297,6 @@ function isHtmlEmpty(html: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatHtml(html: string) {
|
||||
let indent = "\n";
|
||||
const tab = "\t";
|
||||
let i = 0;
|
||||
let pre: { indent: string; tag: string }[] = [];
|
||||
|
||||
html = html
|
||||
.replace(new RegExp("<pre>([\\s\\S]+?)?</pre>"), function (x) {
|
||||
pre.push({ indent: "", tag: x });
|
||||
return "<--TEMPPRE" + i++ + "/-->";
|
||||
})
|
||||
.replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) {
|
||||
let ret;
|
||||
const tagRegEx = /<\/?([^\s/>]+)/.exec(x);
|
||||
let tag = tagRegEx ? tagRegEx[1] : "";
|
||||
let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
|
||||
|
||||
if (p) {
|
||||
const pInd = parseInt(p[1]);
|
||||
pre[pInd].indent = indent;
|
||||
}
|
||||
|
||||
if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) {
|
||||
// self closing tag
|
||||
ret = indent + x;
|
||||
} else {
|
||||
if (x.indexOf("</") < 0) {
|
||||
//open tag
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
!p && (indent += tab);
|
||||
} else {
|
||||
//close tag
|
||||
indent = indent.substr(0, indent.length - 1);
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
for (i = pre.length; i--;) {
|
||||
html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>"));
|
||||
}
|
||||
|
||||
return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
|
||||
}
|
||||
|
||||
export async function clearBrowserCache() {
|
||||
if (isElectron()) {
|
||||
const win = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
@@ -903,7 +855,6 @@ export default {
|
||||
getNoteTypeClass,
|
||||
getMimeTypeClass,
|
||||
isHtmlEmpty,
|
||||
formatHtml,
|
||||
clearBrowserCache,
|
||||
copySelectionToClipboard,
|
||||
dynamicRequire,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "./stylesheets/auth.css";
|
||||
|
||||
// @TriliumNextTODO: is this even needed anymore?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "jquery";
|
||||
import utils from "./services/utils.js";
|
||||
import ko from "knockout";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
||||
// TriliumNextTODO: properly make use of below types
|
||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "normalize.css";
|
||||
import "boxicons/css/boxicons.min.css";
|
||||
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
|
||||
import "@triliumnext/ckeditor5/content.css";
|
||||
import "@triliumnext/share-theme/styles/index.css";
|
||||
import "@triliumnext/share-theme/scripts/index.js";
|
||||
|
||||
|
||||
2
apps/client/src/stylesheets/bootstrap.scss
vendored
Normal file
2
apps/client/src/stylesheets/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Import all of Bootstrap's CSS */
|
||||
@use "bootstrap/scss/bootstrap";
|
||||
@@ -1134,7 +1134,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
|
||||
.toast-body {
|
||||
white-space: preserve-breaks;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ck-mentions .ck-button {
|
||||
@@ -1243,10 +1242,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.hidden-ext.note-split + .gutter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#context-menu-cover.show {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -1776,6 +1771,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
||||
}
|
||||
|
||||
.note-split {
|
||||
flex-basis: 0; /* so that each split has same width */
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -2379,12 +2375,3 @@ footer.webview-footer button {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.revision-diff-added {
|
||||
background: rgba(100, 200, 100, 0.5);
|
||||
}
|
||||
|
||||
.revision-diff-removed {
|
||||
background: rgba(255, 100, 100, 0.5);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
--theme-style: dark;
|
||||
--native-titlebar-background: #00000000;
|
||||
--window-background-color-bgfx: transparent; /* When background effects enabled */
|
||||
|
||||
--main-background-color: #272727;
|
||||
--main-text-color: #ccc;
|
||||
@@ -148,7 +147,6 @@
|
||||
--launcher-pane-vert-button-hover-background: #ffffff1c;
|
||||
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.2);
|
||||
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
--launcher-pane-vert-background-color-bgfx: #00000026; /* When background effects enabled */
|
||||
|
||||
--launcher-pane-horiz-border-color: rgb(22, 22, 22);
|
||||
--launcher-pane-horiz-background-color: #282828;
|
||||
@@ -157,8 +155,6 @@
|
||||
--launcher-pane-horiz-button-hover-background: #ffffff1c;
|
||||
--launcher-pane-horiz-button-hover-shadow: unset;
|
||||
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
--launcher-pane-horiz-background-color-bgfx: #ffffff17; /* When background effects enabled */
|
||||
--launcher-pane-horiz-border-color-bgfx: #00000080; /* When background effects enabled */
|
||||
|
||||
--protected-session-active-icon-color: #8edd8e;
|
||||
--sync-status-error-pulse-color: #f47871;
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
--theme-style: light;
|
||||
--native-titlebar-background: #ffffff00;
|
||||
--window-background-color-bgfx: transparent; /* When background effects enabled */
|
||||
|
||||
--main-background-color: white;
|
||||
--main-text-color: black;
|
||||
@@ -122,11 +121,11 @@
|
||||
--left-pane-collapsed-border-color: #0000000d;
|
||||
--left-pane-background-color: #f2f2f2;
|
||||
--left-pane-text-color: #383838;
|
||||
--left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
|
||||
--left-pane-item-hover-background: #eaeaea;
|
||||
--left-pane-item-selected-background: white;
|
||||
--left-pane-item-selected-color: black;
|
||||
--left-pane-item-selected-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--left-pane-item-action-button-background: rgba(0, 0, 0, 0.11);
|
||||
--left-pane-item-action-button-background: #d7d7d7;
|
||||
--left-pane-item-action-button-color: inherit;
|
||||
--left-pane-item-action-button-hover-background: white;
|
||||
--left-pane-item-action-button-hover-shadow: 2px 2px 3px rgba(0, 0, 0, 0.15);
|
||||
@@ -142,7 +141,6 @@
|
||||
--launcher-pane-vert-button-hover-background: white;
|
||||
--launcher-pane-vert-button-hover-shadow: 4px 4px 4px rgba(0, 0, 0, 0.075);
|
||||
--launcher-pane-vert-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
--launcher-pane-vert-background-color-bgfx: #00000009; /* When background effects enabled */
|
||||
|
||||
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.1);
|
||||
--launcher-pane-horiz-background-color: #fafafa;
|
||||
@@ -150,8 +148,6 @@
|
||||
--launcher-pane-horiz-button-hover-background: var(--icon-button-hover-background);
|
||||
--launcher-pane-horiz-button-hover-shadow: unset;
|
||||
--launcher-pane-horiz-button-focus-outline-color: var(--input-focus-outline-color);
|
||||
--launcher-pane-horiz-background-color-bgfx: #ffffffb3; /* When background effects enabled */
|
||||
--launcher-pane-horiz-border-color-bgfx: #00000026; /* When background effects enabled */
|
||||
|
||||
--protected-session-active-icon-color: #16b516;
|
||||
--sync-status-error-pulse-color: #ff5528;
|
||||
|
||||
@@ -36,23 +36,31 @@ body.mobile {
|
||||
|
||||
/* #region Mica */
|
||||
body.background-effects.platform-win32 {
|
||||
--background-material: tabbed;
|
||||
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
|
||||
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
|
||||
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
|
||||
--tab-background-color: var(--window-background-color-bgfx);
|
||||
--new-tab-button-background: var(--window-background-color-bgfx);
|
||||
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.15);
|
||||
--launcher-pane-horiz-background-color: rgba(255, 255, 255, 0.7);
|
||||
--launcher-pane-vert-background-color: rgba(255, 255, 255, 0.055);
|
||||
--tab-background-color: transparent;
|
||||
--new-tab-button-background: transparent;
|
||||
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
|
||||
--background-material: tabbed;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.background-effects.platform-win32 {
|
||||
--launcher-pane-horiz-border-color: rgba(0, 0, 0, 0.5);
|
||||
--launcher-pane-horiz-background-color: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-vertical {
|
||||
--left-pane-background-color: var(--window-background-color-bgfx);
|
||||
--left-pane-background-color: transparent;
|
||||
--left-pane-item-hover-background: rgba(127, 127, 127, 0.05);
|
||||
--background-material: mica;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32,
|
||||
body.background-effects.platform-win32 #root-widget {
|
||||
background: var(--window-background-color-bgfx) !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
||||
@@ -81,7 +89,7 @@ body.background-effects.zen #root-widget {
|
||||
* Gutter
|
||||
*/
|
||||
|
||||
.gutter {
|
||||
.gutter {
|
||||
background: var(--gutter-color) !important;
|
||||
transition: background 150ms ease-out;
|
||||
}
|
||||
@@ -878,80 +886,6 @@ body.layout-horizontal .tab-row-container {
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container {
|
||||
border-bottom: unset !important;
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .note-tab-wrapper {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .toggle-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .toggle-button:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -10px;
|
||||
right: -10px;
|
||||
top: 29px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left,
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-left:after,
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .tab-scroll-button-right:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-tab[active]:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -32768px;
|
||||
top: var(--tab-height);
|
||||
right: calc(100% - 1px);
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-tab[active]:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 100%;
|
||||
top: var(--tab-height);
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.electron.background-effects.layout-horizontal .tab-row-container .note-new-tab:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -4px;
|
||||
top: calc(var(--tab-height), -1);
|
||||
right: 0;
|
||||
width: 100vw;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--launcher-pane-horiz-border-color);
|
||||
}
|
||||
|
||||
body.layout-vertical.electron.platform-darwin .tab-row-container {
|
||||
border-bottom: 1px solid var(--subtle-border-color);
|
||||
}
|
||||
@@ -1167,11 +1101,6 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
||||
/* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
|
||||
}
|
||||
|
||||
.split-note-container-widget > .gutter {
|
||||
background: var(--root-background) !important;
|
||||
transition: background 150ms ease-out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Ribbon & note header
|
||||
*/
|
||||
@@ -1180,6 +1109,10 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.note-split:not(.hidden-ext) + .note-split:not(.hidden-ext) {
|
||||
border-left: 4px solid var(--root-background);
|
||||
}
|
||||
|
||||
@keyframes note-entrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -263,11 +263,6 @@
|
||||
"confirm_delete_all": "Do you want to delete all revisions of this note?",
|
||||
"no_revisions": "No revisions for this note yet...",
|
||||
"restore_button": "Restore",
|
||||
"diff_on": "Show diff",
|
||||
"diff_off": "Show content",
|
||||
"diff_on_hint": "Click to show note source diff",
|
||||
"diff_off_hint": "Click to show note content",
|
||||
"diff_not_available": "Diff isn't available.",
|
||||
"confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
|
||||
"delete_button": "Delete",
|
||||
"confirm_delete": "Do you want to delete this revision?",
|
||||
@@ -1123,9 +1118,7 @@
|
||||
"title": "Performance",
|
||||
"enable-motion": "Enable transitions and animations",
|
||||
"enable-shadows": "Enable shadows",
|
||||
"enable-backdrop-effects": "Enable background effects for menus, popups and panels",
|
||||
"enable-smooth-scroll": "Enable smooth scrolling",
|
||||
"app-restart-required": "(a restart of the application is required for the change to take effect)"
|
||||
"enable-backdrop-effects": "Enable background effects for menus, popups and panels"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Not started",
|
||||
|
||||
@@ -30,16 +30,13 @@
|
||||
"search_note": "Wyszukaj notatkę po nazwie",
|
||||
"link_title_arbitrary": "Tytuł linku można dowolnie zmieniać",
|
||||
"link_title": "Tytuł linku",
|
||||
"button_add_link": "Dodaj link",
|
||||
"help_on_links": "Pomoc dotycząca linków",
|
||||
"link_title_mirrors": "tytuł linku odzwierciedla tytuł obecnej notatki"
|
||||
"button_add_link": "Dodaj link"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Zapisz",
|
||||
"edit_branch_prefix": "Edytuj prefiks gałęzi",
|
||||
"prefix": "Prefiks: ",
|
||||
"branch_prefix_saved": "Zapisano prefiks gałęzi.",
|
||||
"help_on_tree_prefix": "Pomoc dotycząca prefiksu drzewa"
|
||||
"branch_prefix_saved": "Zapisano prefiks gałęzi."
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etykiety",
|
||||
@@ -101,8 +98,7 @@
|
||||
"prefix_optional": "Prefiks (opcjonalne)",
|
||||
"clone_to_selected_note": "Sklonuj do wybranej notatki",
|
||||
"no_path_to_clone_to": "Brak ścieżki do sklonowania.",
|
||||
"note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\"",
|
||||
"help_on_links": "Pomoc dotycząca linków"
|
||||
"note_cloned": "Notatka \"{{clonedTitle}}\" została sklonowana do \"{{targetTitle}}\""
|
||||
},
|
||||
"help": {
|
||||
"title": "Ściągawka",
|
||||
|
||||
@@ -2013,9 +2013,7 @@
|
||||
"title": "Setări de performanță",
|
||||
"enable-motion": "Activează tranzițiile și animațiile",
|
||||
"enable-shadows": "Activează umbrirea elementelor",
|
||||
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri",
|
||||
"enable-smooth-scroll": "Activează derularea lină",
|
||||
"app-restart-required": "(este necesară repornirea aplicației pentru ca modificarea să aibă efect)"
|
||||
"enable-backdrop-effects": "Activează efectele de fundal pentru meniuri, popup-uri și panouri"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Setări similare"
|
||||
|
||||
@@ -3,7 +3,7 @@ import appContext, { type CommandData, type CommandListenerData, type EventData,
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import type NoteContext from "../../components/note_context.js";
|
||||
import Component from "../../components/component.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
|
||||
interface NoteContextEvent {
|
||||
noteContext: NoteContext;
|
||||
}
|
||||
@@ -52,10 +52,6 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
await widget.handleEvent("setNoteContext", { noteContext });
|
||||
|
||||
this.child(widget);
|
||||
|
||||
if (noteContext.mainNtxId && noteContext.ntxId) {
|
||||
splitService.setupNoteSplitResizer([noteContext.mainNtxId,noteContext.ntxId]);
|
||||
}
|
||||
}
|
||||
|
||||
async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: EventData<"openNewNoteSplit">) {
|
||||
@@ -99,9 +95,9 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
async closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) {
|
||||
closeThisNoteSplitCommand({ ntxId }: CommandListenerData<"closeThisNoteSplit">) {
|
||||
if (ntxId) {
|
||||
await appContext.tabManager.removeNoteContext(ntxId);
|
||||
appContext.tabManager.removeNoteContext(ntxId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +137,6 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
|
||||
// activate context that now contains the original note
|
||||
await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]);
|
||||
|
||||
splitService.moveNoteSplitResizer(ntxIds[leftIndex]);
|
||||
}
|
||||
|
||||
activeContextChangedEvent() {
|
||||
@@ -163,8 +157,6 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
recursiveCleanup(widget);
|
||||
delete this.widgets[ntxId];
|
||||
}
|
||||
|
||||
splitService.delNoteSplitResizer(ntxIds);
|
||||
}
|
||||
|
||||
contextsReopenedEvent({ ntxId, afterNtxId }: EventData<"contextsReopened">) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { t } from "../../services/i18n";
|
||||
import server from "../../services/server";
|
||||
import toast from "../../services/toast";
|
||||
import Button from "../react/Button";
|
||||
import FormToggle from "../react/FormToggle";
|
||||
import Modal from "../react/Modal";
|
||||
import FormList, { FormListItem } from "../react/FormList";
|
||||
import utils from "../../services/utils";
|
||||
@@ -19,15 +18,12 @@ import open from "../../services/open";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { diffWords } from "diff";
|
||||
|
||||
export default function RevisionsDialog() {
|
||||
const [ note, setNote ] = useState<FNote>();
|
||||
const [ noteContent, setNoteContent ] = useState<string>();
|
||||
const [ revisions, setRevisions ] = useState<RevisionItem[]>();
|
||||
const [ currentRevision, setCurrentRevision ] = useState<RevisionItem>();
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const [ showDiff, setShowDiff ] = useState(false);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
|
||||
useTriliumEvent("showRevisions", async ({ noteId }) => {
|
||||
@@ -41,10 +37,8 @@ export default function RevisionsDialog() {
|
||||
useEffect(() => {
|
||||
if (note?.noteId) {
|
||||
server.get<RevisionItem[]>(`notes/${note.noteId}/revisions`).then(setRevisions);
|
||||
note.getContent().then(setNoteContent);
|
||||
} else {
|
||||
setRevisions(undefined);
|
||||
setNoteContent(undefined);
|
||||
}
|
||||
}, [ note?.noteId, refreshCounter ]);
|
||||
|
||||
@@ -60,23 +54,7 @@ export default function RevisionsDialog() {
|
||||
helpPageId="vZWERwf8U3nx"
|
||||
bodyStyle={{ display: "flex", height: "80vh" }}
|
||||
header={
|
||||
!!revisions?.length && (
|
||||
<>
|
||||
{["text", "code", "mermaid"].includes(currentRevision?.type ?? "") && (
|
||||
<FormToggle
|
||||
currentValue={showDiff}
|
||||
onChange={(newValue) => setShowDiff(newValue)}
|
||||
switchOnName={t("revisions.diff_on")}
|
||||
switchOffName={t("revisions.diff_off")}
|
||||
switchOnTooltip={t("revisions.diff_on_hint")}
|
||||
switchOffTooltip={t("revisions.diff_off_hint")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
text={t("revisions.delete_all_revisions")}
|
||||
size="small"
|
||||
style={{ padding: "0 10px" }}
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} size="small" style={{ padding: "0 10px" }}
|
||||
onClick={async () => {
|
||||
const text = t("revisions.confirm_delete_all");
|
||||
|
||||
@@ -86,16 +64,12 @@ export default function RevisionsDialog() {
|
||||
setCurrentRevision(undefined);
|
||||
toast.showMessage(t("revisions.revisions_deleted"));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}}/>)
|
||||
}
|
||||
footer={<RevisionFooter note={note} />}
|
||||
footerStyle={{ paddingTop: 0, paddingBottom: 0 }}
|
||||
onHidden={() => {
|
||||
setShown(false);
|
||||
setShowDiff(false);
|
||||
setNote(undefined);
|
||||
setCurrentRevision(undefined);
|
||||
setRevisions(undefined);
|
||||
@@ -118,13 +92,10 @@ export default function RevisionsDialog() {
|
||||
marginLeft: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxWidth: "calc(100% - 150px)",
|
||||
minWidth: 0
|
||||
}}>
|
||||
<RevisionPreview
|
||||
noteContent={noteContent}
|
||||
revisionItem={currentRevision}
|
||||
showDiff={showDiff}
|
||||
setShown={setShown}
|
||||
onRevisionDeleted={() => {
|
||||
setRefreshCounter(c => c + 1);
|
||||
@@ -150,10 +121,8 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
|
||||
</FormList>);
|
||||
}
|
||||
|
||||
function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevisionDeleted }: {
|
||||
noteContent?: string,
|
||||
function RevisionPreview({ revisionItem, setShown, onRevisionDeleted }: {
|
||||
revisionItem?: RevisionItem,
|
||||
showDiff: boolean,
|
||||
setShown: Dispatch<StateUpdater<boolean>>,
|
||||
onRevisionDeleted?: () => void
|
||||
}) {
|
||||
@@ -210,7 +179,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="revision-content use-tn-links" style={{ overflow: "auto", wordBreak: "break-word" }}>
|
||||
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
|
||||
<RevisionContent revisionItem={revisionItem} fullRevision={fullRevision} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -228,15 +197,12 @@ const CODE_STYLE: CSSProperties = {
|
||||
whiteSpace: "pre-wrap"
|
||||
};
|
||||
|
||||
function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) {
|
||||
function RevisionContent({ revisionItem, fullRevision }: { revisionItem?: RevisionItem, fullRevision?: RevisionPojo }) {
|
||||
const content = fullRevision?.content;
|
||||
if (!revisionItem || !content) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (showDiff) {
|
||||
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>
|
||||
}
|
||||
switch (revisionItem.type) {
|
||||
case "text":
|
||||
return <RevisionContentText content={content} />
|
||||
@@ -301,48 +267,6 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
|
||||
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
|
||||
}
|
||||
|
||||
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
|
||||
noteContent?: string,
|
||||
itemContent: string | Buffer<ArrayBufferLike> | undefined,
|
||||
itemType: string
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!noteContent || typeof itemContent !== "string") {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.textContent = t("revisions.diff_not_available");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let processedNoteContent = noteContent;
|
||||
let processedItemContent = itemContent;
|
||||
|
||||
if (itemType === "text") {
|
||||
processedNoteContent = utils.formatHtml(noteContent);
|
||||
processedItemContent = utils.formatHtml(itemContent);
|
||||
}
|
||||
|
||||
const diff = diffWords(processedNoteContent, processedItemContent);
|
||||
const diffHtml = diff.map(part => {
|
||||
if (part.added) {
|
||||
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else if (part.removed) {
|
||||
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
|
||||
} else {
|
||||
return utils.escapeHtml(part.value);
|
||||
}
|
||||
}).join("");
|
||||
|
||||
if (contentRef.current) {
|
||||
contentRef.current.innerHTML = diffHtml;
|
||||
}
|
||||
}, [noteContent, itemContent, itemType]);
|
||||
|
||||
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
|
||||
}
|
||||
|
||||
function RevisionFooter({ note }: { note?: FNote }) {
|
||||
if (!note) {
|
||||
return <></>;
|
||||
|
||||
@@ -8,7 +8,6 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import FindInText from "./find_in_text.js";
|
||||
import FindInCode from "./find_in_code.js";
|
||||
import { isIMEComposing } from "../services/shortcuts.js";
|
||||
import FindInHtml from "./find_in_html.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
@@ -163,11 +162,6 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
this.$replaceButton.on("click", () => this.replace());
|
||||
|
||||
this.$input.on("keydown", async (e) => {
|
||||
// Skip processing during IME composition
|
||||
if (isIMEComposing(e.originalEvent as KeyboardEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.metaKey || e.ctrlKey) && (e.key === "F" || e.key === "f")) {
|
||||
// If ctrl+f is pressed when the findbox is shown, select the
|
||||
// whole input to find
|
||||
|
||||
@@ -8,7 +8,6 @@ import "./note_title.css";
|
||||
import { isLaunchBarConfig } from "../services/utils";
|
||||
import appContext from "../components/app_context";
|
||||
import branches from "../services/branches";
|
||||
import { isIMEComposing } from "../services/shortcuts";
|
||||
|
||||
export default function NoteTitleWidget() {
|
||||
const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext();
|
||||
@@ -79,12 +78,6 @@ export default function NoteTitleWidget() {
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// Skip processing if IME is composing to prevent interference
|
||||
// with text input in CJK languages
|
||||
if (isIMEComposing(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Focus on the note content when pressing enter.
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -219,22 +219,21 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$tree = this.$widget.find(".tree");
|
||||
this.$treeActions = this.$widget.find(".tree-actions");
|
||||
|
||||
this.$tree.on("mousedown", (e: JQuery.MouseDownEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (e.button !== 0) return;
|
||||
|
||||
if (target.classList.contains("unhoist-button")) {
|
||||
hoistedNoteService.unhoist();
|
||||
} else if (target.classList.contains("refresh-search-button")) {
|
||||
this.refreshSearch(e);
|
||||
} else if (target.classList.contains("add-note-button")) {
|
||||
this.$tree.on("mousedown", ".unhoist-button", () => hoistedNoteService.unhoist());
|
||||
this.$tree.on("mousedown", ".refresh-search-button", (e) => this.refreshSearch(e));
|
||||
this.$tree.on("mousedown", ".add-note-button", (e) => {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
const parentNotePath = treeService.getNotePath(node);
|
||||
noteCreateService.createNote(parentNotePath, { isProtected: node.data.isProtected });
|
||||
} else if (target.classList.contains("enter-workspace-button")) {
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
isProtected: node.data.isProtected
|
||||
});
|
||||
});
|
||||
|
||||
this.$tree.on("mousedown", ".enter-workspace-button", (e) => {
|
||||
const node = $.ui.fancytree.getNode(e as unknown as Event);
|
||||
|
||||
this.triggerCommand("hoistNote", { noteId: node.data.noteId });
|
||||
}
|
||||
});
|
||||
|
||||
// fancytree doesn't support middle click, so this is a way to support it
|
||||
|
||||
@@ -23,21 +23,15 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
noteSwitchedAndActivatedEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||
this.noteContext = noteContext;
|
||||
|
||||
noteSwitchedAndActivatedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
noteSwitchedEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||
this.noteContext = noteContext;
|
||||
|
||||
noteSwitchedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
activeContextChangedEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||
this.noteContext = noteContext;
|
||||
|
||||
activeContextChangedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import linkService from "../services/link.js";
|
||||
import froca from "../services/froca.js";
|
||||
import utils from "../services/utils.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
|
||||
import shortcutService from "../services/shortcuts.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import { Dropdown, Tooltip } from "bootstrap";
|
||||
|
||||
@@ -180,14 +180,6 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
|
||||
if (utils.isMobile()) {
|
||||
this.$searchString.keydown((e) => {
|
||||
// Skip processing if IME is composing to prevent interference
|
||||
// with text input in CJK languages
|
||||
// Note: jQuery wraps the native event, so we access originalEvent
|
||||
const originalEvent = e.originalEvent as KeyboardEvent;
|
||||
if (originalEvent && isIMEComposing(originalEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === 13) {
|
||||
if (this.$dropdownMenu.is(":visible")) {
|
||||
this.search(); // just update already visible dropdown
|
||||
|
||||
@@ -13,7 +13,6 @@ import attribute_parser, { Attribute } from "../../../services/attribute_parser"
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { escapeQuotes, getErrorMessage } from "../../../services/utils";
|
||||
import link from "../../../services/link";
|
||||
import { isIMEComposing } from "../../../services/shortcuts";
|
||||
import froca from "../../../services/froca";
|
||||
import contextMenu from "../../../menus/context_menu";
|
||||
import type { CommandData, FilteredCommandNames } from "../../../components/app_context";
|
||||
@@ -288,11 +287,6 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
ref={wrapperRef}
|
||||
style="position: relative; padding-top: 10px; padding-bottom: 10px"
|
||||
onKeyDown={(e) => {
|
||||
// Skip processing during IME composition
|
||||
if (isIMEComposing(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "Enter") {
|
||||
// allow autocomplete to fill the result textarea
|
||||
setTimeout(() => save(), 100);
|
||||
|
||||
@@ -388,7 +388,7 @@ async function setupFonts() {
|
||||
if (!glob.isDev) {
|
||||
path = `${window.location.pathname}/node_modules/@excalidraw/excalidraw/dist/prod`;
|
||||
} else {
|
||||
path = (await import("../../../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2?url")).default;
|
||||
path = (await import("../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/Excalifont/Excalifont-Regular-a88b72a24fb54c9f94e3b5fdaa7481c9.woff2?url")).default;
|
||||
let pathComponents = path.split("/");
|
||||
path = pathComponents.slice(0, pathComponents.length - 2).join("/");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { buildExtraCommands, type EditorConfig, PREMIUM_PLUGINS } from "@trilium
|
||||
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||
import options from "../../../services/options.js";
|
||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en.json?url";
|
||||
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import getTemplates from "./snippets.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
|
||||
@@ -12,6 +12,7 @@ import { buildSelectedBackgroundColor } from "../../components/touch_bar.js";
|
||||
import { buildConfig, BuildEditorOptions, OPEN_SOURCE_LICENSE_KEY } from "./ckeditor/config.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig, EditorConfig } from "@triliumnext/ckeditor5";
|
||||
import "@triliumnext/ckeditor5/index.css";
|
||||
import { updateTemplateCache } from "./ckeditor/snippets.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
|
||||
@@ -266,20 +266,9 @@ function Performance() {
|
||||
label={t("ui-performance.enable-backdrop-effects")}
|
||||
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
|
||||
/>
|
||||
|
||||
{isElectron() && <SmoothScrollEnabledOption />}
|
||||
|
||||
</OptionsSection>
|
||||
}
|
||||
|
||||
function SmoothScrollEnabledOption() {
|
||||
const [ smoothScrollEnabled, setSmoothScrollEnabled ] = useTriliumOptionBool("smoothScrollEnabled");
|
||||
|
||||
return <FormCheckbox
|
||||
label={`${t("ui-performance.enable-smooth-scroll")} ${t("ui-performance.app-restart-required")}`}
|
||||
currentValue={smoothScrollEnabled} onChange={setSmoothScrollEnabled}
|
||||
/>
|
||||
}
|
||||
|
||||
function MaxContentWidth() {
|
||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import AbstractCodeTypeWidget from "./abstract_code_type_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="note-detail-readonly-code note-detail-printable">
|
||||
@@ -34,7 +33,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
if (!blob) return;
|
||||
|
||||
const isFormattable = note.type === "text" && this.noteContext?.viewScope?.viewMode === "source";
|
||||
const content = isFormattable ? utils.formatHtml(blob.content) : blob.content;
|
||||
const content = isFormattable ? this.format(blob.content) : blob.content;
|
||||
|
||||
this._update(note, content);
|
||||
this.show();
|
||||
@@ -55,4 +54,52 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
|
||||
resolve(this.$editor);
|
||||
}
|
||||
|
||||
format(html: string) {
|
||||
let indent = "\n";
|
||||
const tab = "\t";
|
||||
let i = 0;
|
||||
let pre: { indent: string; tag: string }[] = [];
|
||||
|
||||
html = html
|
||||
.replace(new RegExp("<pre>((.|\\t|\\n|\\r)+)?</pre>"), function (x) {
|
||||
pre.push({ indent: "", tag: x });
|
||||
return "<--TEMPPRE" + i++ + "/-->";
|
||||
})
|
||||
.replace(new RegExp("<[^<>]+>[^<]?", "g"), function (x) {
|
||||
let ret;
|
||||
const tagRegEx = /<\/?([^\s/>]+)/.exec(x);
|
||||
let tag = tagRegEx ? tagRegEx[1] : "";
|
||||
let p = new RegExp("<--TEMPPRE(\\d+)/-->").exec(x);
|
||||
|
||||
if (p) {
|
||||
const pInd = parseInt(p[1]);
|
||||
pre[pInd].indent = indent;
|
||||
}
|
||||
|
||||
if (["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr"].indexOf(tag) >= 0) {
|
||||
// self closing tag
|
||||
ret = indent + x;
|
||||
} else {
|
||||
if (x.indexOf("</") < 0) {
|
||||
//open tag
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + tab + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
!p && (indent += tab);
|
||||
} else {
|
||||
//close tag
|
||||
indent = indent.substr(0, indent.length - 1);
|
||||
if (x.charAt(x.length - 1) !== ">") ret = indent + x.substr(0, x.length - 1) + indent + x.substr(x.length - 1, x.length);
|
||||
else ret = indent + x;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
for (i = pre.length; i--;) {
|
||||
html = html.replace("<--TEMPPRE" + i + "/-->", pre[i].tag.replace("<pre>", "<pre>\n").replace("</pre>", pre[i].indent + "</pre>"));
|
||||
}
|
||||
|
||||
return html.charAt(0) === "\n" ? html.substr(1, html.length - 1) : html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
import { join, resolve } from 'path';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import asset_path from './src/asset_path';
|
||||
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
|
||||
import preact from "@preact/preset-vite";
|
||||
|
||||
const assets = [ "assets", "stylesheets", "fonts", "translations" ];
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
let plugins: any = [
|
||||
preact({
|
||||
babel: {
|
||||
compact: !isDev
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
if (!isDev) {
|
||||
plugins = [
|
||||
...plugins,
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/client',
|
||||
base: process.env.NODE_ENV === "production" ? "" : asset_path,
|
||||
server: {
|
||||
port: 4200,
|
||||
host: 'localhost',
|
||||
},
|
||||
preview: {
|
||||
port: 4300,
|
||||
host: 'localhost',
|
||||
},
|
||||
plugins: [
|
||||
preact(),
|
||||
viteStaticCopy({
|
||||
targets: assets.map((asset) => ({
|
||||
src: `src/${asset}/*`,
|
||||
@@ -29,22 +32,20 @@ if (!isDev) {
|
||||
structured: true,
|
||||
targets: [
|
||||
{
|
||||
src: "../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*",
|
||||
src: "node_modules/@excalidraw/excalidraw/dist/prod/fonts/*",
|
||||
dest: "",
|
||||
}
|
||||
]
|
||||
}),
|
||||
webpackStatsPlugin()
|
||||
]
|
||||
}
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/client',
|
||||
base: "",
|
||||
plugins,
|
||||
] as Plugin[],
|
||||
resolve: {
|
||||
alias: [
|
||||
// Force the use of dist in development mode because upstream ESM is broken (some hybrid between CJS and ESM, will be improved in upcoming versions).
|
||||
{
|
||||
find: "@triliumnext/highlightjs",
|
||||
replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist")
|
||||
},
|
||||
{
|
||||
find: "react",
|
||||
replacement: "preact/compat"
|
||||
@@ -62,6 +63,10 @@ export default defineConfig(() => ({
|
||||
"preact/hooks"
|
||||
]
|
||||
},
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
build: {
|
||||
target: "esnext",
|
||||
outDir: './dist',
|
||||
@@ -100,6 +105,18 @@ export default defineConfig(() => ({
|
||||
"./src/test/setup.ts"
|
||||
]
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
"@triliumnext/highlightjs"
|
||||
]
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
quietDeps: true
|
||||
}
|
||||
}
|
||||
},
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
To build and run manually:
|
||||
|
||||
```sh
|
||||
pnpm build db-compare
|
||||
nx build db-compare
|
||||
node ./apps/db-compare/dist/compare.js
|
||||
```
|
||||
|
||||
To serve development build with arguments:
|
||||
|
||||
```sh
|
||||
pnpm dev --args "apps/server/spec/db/document_v214.db" --args "apps/server/spec/db/document_v214_migrated.db"
|
||||
nx serve db-compare --args "apps/server/spec/db/document_v214.db" --args "apps/server/spec/db/document_v214_migrated.db"
|
||||
```
|
||||
@@ -9,8 +9,63 @@
|
||||
"sqlite": "5.1.1",
|
||||
"sqlite3": "5.1.7"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/compare.ts",
|
||||
"build": "esbuild --platform=node --format=cjs --outdir=dist src/compare.ts"
|
||||
"nx": {
|
||||
"name": "db-compare",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"platform": "node",
|
||||
"outputPath": "apps/db-compare/dist",
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"bundle": false,
|
||||
"main": "apps/db-compare/src/compare.ts",
|
||||
"tsConfig": "apps/db-compare/tsconfig.app.json",
|
||||
"assets": [],
|
||||
"esbuildOptions": {
|
||||
"sourcemap": true,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"development": {},
|
||||
"production": {
|
||||
"esbuildOptions": {
|
||||
"sourcemap": false,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/js:node",
|
||||
"defaultConfiguration": "development",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"buildTarget": "db-compare:build",
|
||||
"runBuildTargetDependencies": false
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "db-compare:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "db-compare:build:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
apps/desktop-e2e/.env
Normal file
3
apps/desktop-e2e/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_INTEGRATION_TEST=memory-no-store
|
||||
TRILIUM_PORT=8082
|
||||
TRILIUM_DATA_DIR=data
|
||||
15
apps/desktop-e2e/eslint.config.mjs
Normal file
15
apps/desktop-e2e/eslint.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import playwright from "eslint-plugin-playwright";
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [
|
||||
playwright.configs["flat/recommended"],
|
||||
...baseConfig,
|
||||
{
|
||||
files: [
|
||||
"**/*.ts",
|
||||
"**/*.js"
|
||||
],
|
||||
// Override or add rules here
|
||||
rules: {}
|
||||
}
|
||||
];
|
||||
24
apps/desktop-e2e/package.json
Normal file
24
apps/desktop-e2e/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@triliumnext/desktop-e2e",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"nx": {
|
||||
"name": "desktop-e2e",
|
||||
"implicitDependencies": [
|
||||
"client",
|
||||
"desktop"
|
||||
],
|
||||
"targets": {
|
||||
"e2e": {
|
||||
"dependsOn": [
|
||||
"desktop:build",
|
||||
"desktop:rebuild-deps"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "17.2.2",
|
||||
"electron": "37.4.0"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
require('dotenv').config({
|
||||
path: __dirname + "/" + ".env"
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
@@ -9,8 +14,6 @@ export default defineConfig({
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
testDir: "e2e",
|
||||
outputDir: "test-output",
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
@@ -60,7 +60,7 @@ test('First setup', async () => {
|
||||
// Verify the shared link is valid
|
||||
const requestContext = await request.newContext();
|
||||
const response = await requestContext.get(linkUrl!);
|
||||
await expect(response).toBeOK();
|
||||
expect(response).toBeOK();
|
||||
|
||||
await mainWindow.waitForTimeout(5000);
|
||||
});
|
||||
25
apps/desktop-e2e/tsconfig.json
Normal file
25
apps/desktop-e2e/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "out-tsc/playwright",
|
||||
"sourceMap": false
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js",
|
||||
"playwright.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"out-tsc",
|
||||
"test-output",
|
||||
"eslint.config.js",
|
||||
"eslint.config.mjs",
|
||||
"eslint.config.cjs"
|
||||
]
|
||||
}
|
||||
1
apps/desktop/.npmrc
Normal file
1
apps/desktop/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
node-linker = hoisted
|
||||
1
apps/desktop/.serve-nodir.env
Normal file
1
apps/desktop/.serve-nodir.env
Normal file
@@ -0,0 +1 @@
|
||||
TRILIUM_PORT=37743
|
||||
3
apps/desktop/.serve.env
Normal file
3
apps/desktop/.serve.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_PORT=37741
|
||||
TRILIUM_DATA_DIR=../data
|
||||
NODE_OPTIONS=--enable-source-maps
|
||||
@@ -3,23 +3,7 @@
|
||||
"version": "0.98.1",
|
||||
"description": "Build your personal knowledge base with Trilium Notes",
|
||||
"private": true,
|
||||
"main": "src/main.ts",
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": {
|
||||
"name": "Trilium Notes Team",
|
||||
"email": "contact@eliandoran.me",
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data tsx ../../scripts/electron-start.mts src/main.ts",
|
||||
"start-no-dir": "cross-env TRILIUM_PORT=37743 tsx ../../scripts/electron-start.mts src/main.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
|
||||
"electron-forge:make": "pnpm build && cross-env electron-forge make dist",
|
||||
"electron-forge:package": "pnpm build && electron-forge package dist",
|
||||
"electron-forge:start": "pnpm build && electron-forge start dist",
|
||||
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
|
||||
},
|
||||
"main": "main.cjs",
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
@@ -31,7 +15,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/electron-squirrel-startup": "1.0.2",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "37.4.0",
|
||||
@@ -44,5 +27,174 @@
|
||||
"@electron-forge/maker-zip": "7.8.3",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.8.3",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"config": {
|
||||
"forge": "./electron-forge/forge.config.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"
|
||||
},
|
||||
"license": "AGPL-3.0-only",
|
||||
"author": {
|
||||
"name": "Trilium Notes Team",
|
||||
"email": "contact@eliandoran.me",
|
||||
"url": "https://github.com/TriliumNext/Notes"
|
||||
},
|
||||
"nx": {
|
||||
"name": "desktop",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"minify": true,
|
||||
"sourcemap": false
|
||||
},
|
||||
"development": {
|
||||
"minify": false,
|
||||
"sourcemap": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"main": "apps/desktop/src/electron-main.ts",
|
||||
"outputPath": "apps/desktop/dist",
|
||||
"outputFileName": "main.js",
|
||||
"tsConfig": "apps/desktop/tsconfig.app.json",
|
||||
"platform": "node",
|
||||
"external": [
|
||||
"electron",
|
||||
"@electron/remote",
|
||||
"better-sqlite3",
|
||||
"./xhr-sync-worker.js"
|
||||
],
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"thirdParty": true,
|
||||
"declaration": false,
|
||||
"esbuildOptions": {
|
||||
"splitting": false,
|
||||
"loader": {
|
||||
".css": "text"
|
||||
}
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/node_modules",
|
||||
"output": "node_modules"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/desktop/node_modules/@electron/remote",
|
||||
"output": "node_modules/@electron/remote"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "packages/share-theme/src/templates",
|
||||
"output": "share-theme/templates"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/desktop/src/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/public",
|
||||
"output": "public"
|
||||
},
|
||||
{
|
||||
"glob": "xhr-sync-worker.js",
|
||||
"input": "apps/server/node_modules/jsdom/lib/jsdom/living/xhr",
|
||||
"output": ""
|
||||
}
|
||||
],
|
||||
"declarationRootDir": "apps/desktop/src"
|
||||
}
|
||||
},
|
||||
"rebuild-deps": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"cache": false,
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_35 --run \"electron --version\")"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"rebuild-deps"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "electron main.cjs",
|
||||
"cwd": "{projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/main.cjs\"",
|
||||
"cwd": ".",
|
||||
"forwardAllArgs": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-nodir": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"rebuild-deps"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "electron main.cjs",
|
||||
"cwd": "{projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/main.cjs\"",
|
||||
"cwd": ".",
|
||||
"forwardAllArgs": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"electron-forge:make": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
"rebuild-deps"
|
||||
],
|
||||
"command": "pnpm -C apps/desktop exec cross-env NODE_INSTALLER=npm electron-forge make dist"
|
||||
},
|
||||
"electron-forge:package": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
"rebuild-deps"
|
||||
],
|
||||
"command": "pnpm -C apps/desktop exec cross-env NODE_INSTALLER=npm electron-forge package dist"
|
||||
},
|
||||
"electron-forge:start": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
"rebuild-deps"
|
||||
],
|
||||
"command": "pnpm -C apps/desktop exec cross-env NODE_INSTALLER=npm TRILIUM_DATA_DIR=./data electron-forge start dist"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { join } from "path";
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
import originalPackageJson from "../package.json" with { type: "json" };
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
const build = new BuildHelper("apps/desktop");
|
||||
|
||||
async function main() {
|
||||
await build.buildBackend([ "src/main.ts"]);
|
||||
|
||||
// Copy assets.
|
||||
build.copy("src/assets", "assets/");
|
||||
build.copy("/apps/server/src/assets", "assets/");
|
||||
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
||||
|
||||
// Copy node modules dependencies
|
||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote" ]);
|
||||
build.copy("/node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js", "xhr-sync-worker.js");
|
||||
|
||||
// Integrate the client.
|
||||
build.triggerBuildAndCopyTo("apps/client", "public/");
|
||||
build.deleteFromOutput("public/webpack-stats.json");
|
||||
|
||||
generatePackageJson();
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const { version, author, license, description, dependencies, devDependencies } = originalPackageJson;
|
||||
const packageJson = {
|
||||
name: "trilium",
|
||||
main: "main.cjs",
|
||||
version, author, license, description,
|
||||
dependencies: {
|
||||
"better-sqlite3": dependencies["better-sqlite3"],
|
||||
},
|
||||
devDependencies: {
|
||||
electron: devDependencies.electron
|
||||
},
|
||||
config: {
|
||||
forge: "../electron-forge/forge.config.ts"
|
||||
}
|
||||
};
|
||||
writeFileSync(join(build.outDir, "package.json"), JSON.stringify(packageJson, null, "\t"), "utf-8");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -26,12 +26,6 @@ async function main() {
|
||||
electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
|
||||
electron.app.commandLine.appendSwitch("lang", options.getOptionOrNull("formattingLocale") ?? "en");
|
||||
|
||||
// Disable smooth scroll if the option is set
|
||||
const smoothScrollEnabled = options.getOptionOrNull("smoothScrollEnabled");
|
||||
if (smoothScrollEnabled === "false") {
|
||||
electron.app.commandLine.appendSwitch("disable-smooth-scrolling");
|
||||
}
|
||||
|
||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||
if (process.platform === "linux") {
|
||||
@@ -15,8 +15,65 @@
|
||||
"@types/mime-types": "^3.0.0",
|
||||
"@types/yargs": "^17.0.33"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/main.ts",
|
||||
"build": "esbuild --platform=node --format=cjs --outdir=dist src/main.ts"
|
||||
"nx": {
|
||||
"name": "dump-db",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"platform": "node",
|
||||
"outputPath": "apps/dump-db/dist",
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"bundle": false,
|
||||
"main": "apps/dump-db/src/main.ts",
|
||||
"tsConfig": "apps/dump-db/tsconfig.app.json",
|
||||
"assets": [
|
||||
"apps/dump-db/src/assets"
|
||||
],
|
||||
"esbuildOptions": {
|
||||
"sourcemap": true,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"development": {},
|
||||
"production": {
|
||||
"esbuildOptions": {
|
||||
"sourcemap": false,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/js:node",
|
||||
"defaultConfiguration": "development",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"buildTarget": "dump-db:build",
|
||||
"runBuildTargetDependencies": false
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "dump-db:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "dump-db:build:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
apps/edit-docs/.env
Normal file
7
apps/edit-docs/.env
Normal file
@@ -0,0 +1,7 @@
|
||||
TRILIUM_DATA_DIR=../data
|
||||
TRILIUM_INTEGRATION_TEST=memory-no-store
|
||||
TRILIUM_PORT=37741
|
||||
|
||||
# Paths are relative to dist root
|
||||
DOCS_ROOT=../../../docs
|
||||
USER_GUIDE_ROOT=../../../apps/server/src/assets/doc_notes/en/User Guide
|
||||
@@ -15,8 +15,120 @@
|
||||
"electron": "37.4.0",
|
||||
"fs-extra": "11.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||
"edit-demo": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
|
||||
"nx": {
|
||||
"name": "edit-docs",
|
||||
"implicitDependencies": [
|
||||
"server"
|
||||
],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"main": "apps/edit-docs/src/edit-docs.ts",
|
||||
"outputPath": "apps/edit-docs/dist",
|
||||
"tsConfig": "apps/edit-docs/tsconfig.app.json",
|
||||
"platform": "node",
|
||||
"additionalEntryPoints": [
|
||||
"apps/edit-docs/src/edit-demo.ts"
|
||||
],
|
||||
"external": [
|
||||
"electron",
|
||||
"@electron/remote",
|
||||
"better-sqlite3",
|
||||
"./xhr-sync-worker.js"
|
||||
],
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"minify": false,
|
||||
"thirdParty": true,
|
||||
"declaration": false,
|
||||
"esbuildOptions": {
|
||||
"splitting": false,
|
||||
"loader": {
|
||||
".css": "text"
|
||||
}
|
||||
},
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/node_modules",
|
||||
"output": "node_modules"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/dist/public",
|
||||
"output": "public"
|
||||
},
|
||||
{
|
||||
"glob": "xhr-sync-worker.js",
|
||||
"input": "apps/server/node_modules/jsdom/lib/jsdom/living/xhr",
|
||||
"output": ""
|
||||
}
|
||||
],
|
||||
"declarationRootDir": "apps/edit-docs/src"
|
||||
}
|
||||
},
|
||||
"rebuild-deps": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"cache": true,
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_35 --run \"electron --version\")"
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-docs": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"rebuild-deps"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "electron edit-docs.cjs",
|
||||
"cwd": "{projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/edit-docs.cjs\"",
|
||||
"cwd": ".",
|
||||
"forwardAllArgs": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"edit-demo": {
|
||||
"executor": "nx:run-commands",
|
||||
"dependsOn": [
|
||||
"rebuild-deps"
|
||||
],
|
||||
"defaultConfiguration": "default",
|
||||
"configurations": {
|
||||
"default": {
|
||||
"command": "electron edit-demo.cjs",
|
||||
"cwd": "{projectRoot}/dist"
|
||||
},
|
||||
"nixos": {
|
||||
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/edit-demo.cjs\"",
|
||||
"cwd": ".",
|
||||
"forwardAllArgs": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
apps/server-e2e/.env
Normal file
3
apps/server-e2e/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_INTEGRATION_TEST=memory
|
||||
TRILIUM_PORT=8082
|
||||
TRILIUM_DATA_DIR=apps/server/spec/db
|
||||
@@ -2,8 +2,19 @@
|
||||
"name": "@triliumnext/server-e2e",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"e2e": "playwright test"
|
||||
"nx": {
|
||||
"name": "server-e2e",
|
||||
"implicitDependencies": [
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"targets": {
|
||||
"e2e": {
|
||||
"dependsOn": [
|
||||
"server:build"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "17.2.2"
|
||||
|
||||
@@ -1,44 +1,68 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { join } from 'path';
|
||||
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
require('dotenv').config({
|
||||
path: __dirname + "/" + ".env"
|
||||
});
|
||||
|
||||
// For CI, you may want to set BASE_URL to the deployed application.
|
||||
const port = process.env['TRILIUM_PORT'] ?? "8082";
|
||||
const port = process.env['TRILIUM_PORT'];
|
||||
const baseURL = process.env['BASE_URL'] || `http://127.0.0.1:${port}`;
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "src",
|
||||
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
|
||||
outputDir: "test-output",
|
||||
retries: 3,
|
||||
|
||||
...nxE2EPreset(__filename, { testDir: './src' }),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: !process.env.TRILIUM_DOCKER ? {
|
||||
command: 'pnpm start-prod-no-dir',
|
||||
command: 'pnpm server:start-prod',
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
cwd: join(__dirname, "../server"),
|
||||
env: {
|
||||
TRILIUM_DATA_DIR: "spec/db",
|
||||
TRILIUM_PORT: port,
|
||||
TRILIUM_INTEGRATION_TEST: "memory"
|
||||
},
|
||||
cwd: workspaceRoot,
|
||||
timeout: 5 * 60 * 1000
|
||||
} : undefined,
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// name: "firefox",
|
||||
// use: { ...devices["Desktop Firefox"] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
// Uncomment for mobile browsers support
|
||||
/* {
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
}, */
|
||||
|
||||
// Uncomment for branded browsers
|
||||
/* {
|
||||
name: 'Microsoft Edge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ test("Tabs are restored in right order", async ({ page, context }) => {
|
||||
|
||||
// Refresh the page and check the order.
|
||||
await app.goto( { preserveTabs: true });
|
||||
await expect(app.getTab(0)).toContainText("Code notes", { timeout: 15_000 });
|
||||
await expect(app.getTab(0)).toContainText("Code notes");
|
||||
await expect(app.getTab(1)).toContainText("Text notes");
|
||||
await expect(app.getTab(2)).toContainText("Mermaid");
|
||||
|
||||
|
||||
6
apps/server/.edit-integration-db.env
Normal file
6
apps/server/.edit-integration-db.env
Normal file
@@ -0,0 +1,6 @@
|
||||
TRILIUM_ENV=dev
|
||||
TRILIUM_DATA_DIR=./apps/server/spec/db
|
||||
TRILIUM_RESOURCE_DIR=./apps/server/dist
|
||||
TRILIUM_PUBLIC_SERVER=http://localhost:4200
|
||||
TRILIUM_PORT=8086
|
||||
TRILIUM_INTEGRATION_TEST=edit
|
||||
3
apps/server/.serve-nodir.env
Normal file
3
apps/server/.serve-nodir.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_ENV=dev
|
||||
TRILIUM_RESOURCE_DIR=./apps/server/dist
|
||||
TRILIUM_PUBLIC_SERVER=http://localhost:4200
|
||||
4
apps/server/.serve.env
Normal file
4
apps/server/.serve.env
Normal file
@@ -0,0 +1,4 @@
|
||||
TRILIUM_ENV=dev
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_RESOURCE_DIR=./apps/server/dist
|
||||
TRILIUM_PUBLIC_SERVER=http://localhost:4200
|
||||
3
apps/server/.start-prod.env
Normal file
3
apps/server/.start-prod.env
Normal file
@@ -0,0 +1,3 @@
|
||||
TRILIUM_ENV=production
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_PORT=8082
|
||||
4
apps/server/.test.env
Normal file
4
apps/server/.test.env
Normal file
@@ -0,0 +1,4 @@
|
||||
TRILIUM_ENV=dev
|
||||
TRILIUM_DATA_DIR=./spec/db
|
||||
TRILIUM_PUBLIC_SERVER=http://localhost:4200
|
||||
TRILIUM_INTEGRATION_TEST=memory
|
||||
@@ -3,38 +3,11 @@
|
||||
"version": "0.98.1",
|
||||
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
|
||||
"private": true,
|
||||
"main": "./src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"start-no-dir": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"edit-integration-db": "cross-env NODE_ENV=development TRILIUM_PORT=8086 TRILIUM_ENV=dev TRILIUM_DATA_DIR=spec/db TRILIUM_INTEGRATION_TEST=edit TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"package": "pnpm build && bash scripts/build-server.sh",
|
||||
"test": "vitest",
|
||||
"test-build": "vitest --config vitest.build.config.mts",
|
||||
"start-prod": "cross-env TRILIUM_DATA_DIR=data pnpm start-prod-no-dir",
|
||||
"start-prod-no-dir": "pnpm build && cross-env TRILIUM_ENV=production TRILIUM_PORT=8082 node dist/main.cjs",
|
||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular",
|
||||
"docker-build-debian": "pnpm build && docker build . -t triliumnext-debian -f Dockerfile",
|
||||
"docker-build-alpine": "pnpm build && docker build . -t triliumnext-alpine -f Dockerfile.alpine",
|
||||
"docker-build-rootless-debian": "pnpm build && docker build . -t triliumnext-rootless-debian -f Dockerfile.rootless",
|
||||
"docker-build-rootless-alpine": "pnpm build && docker build . -t triliumnext-rootless-alpine -f Dockerfile.alpine.rootless",
|
||||
"docker-start-debian": "pnpm docker-build-debian && docker run -p 8081:8080 triliumnext-debian",
|
||||
"docker-start-alpine": "pnpm docker-build-alpine && docker run -p 8081:8080 triliumnext-alpine",
|
||||
"docker-start-rootless-debian": "pnpm docker-build-rootless-debian && docker run -p 8081:8080 triliumnext-rootless-debian",
|
||||
"docker-start-rootless-alpine": "pnpm docker-build-rootless-alpine && docker run -p 8081:8080 triliumnext-rootless-alpine"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.61.0",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
"@triliumnext/turndown-plugin-gfm": "workspace:*",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cls-hooked": "4.3.9",
|
||||
@@ -65,11 +38,16 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/xml2js": "0.4.14",
|
||||
"express-http-proxy": "2.1.1",
|
||||
"@anthropic-ai/sdk": "0.61.0",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
"@triliumnext/turndown-plugin-gfm": "workspace:*",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "1.11.0",
|
||||
"bindings": "1.5.0",
|
||||
"bootstrap": "5.3.8",
|
||||
"chardet": "2.1.0",
|
||||
"cheerio": "1.1.2",
|
||||
"chokidar": "4.0.3",
|
||||
@@ -86,9 +64,8 @@
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"express": "5.1.0",
|
||||
"express-http-proxy": "2.1.1",
|
||||
"express-openid-connect": "^2.17.1",
|
||||
"express-rate-limit": "8.1.0",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"express-session": "1.18.2",
|
||||
"file-uri-to-path": "2.0.0",
|
||||
"fs-extra": "11.3.1",
|
||||
@@ -97,7 +74,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.5.2",
|
||||
"i18next": "25.4.2",
|
||||
"i18next-fs-backend": "2.6.0",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "5.0.0",
|
||||
@@ -128,9 +105,272 @@
|
||||
"tmp": "0.2.5",
|
||||
"turndown": "7.2.1",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "^7.1.3",
|
||||
"ws": "8.18.3",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0"
|
||||
},
|
||||
"nx": {
|
||||
"name": "server",
|
||||
"implicitDependencies": [
|
||||
"share-theme"
|
||||
],
|
||||
"targets": {
|
||||
"serve": {
|
||||
"executor": "@nx/js:node",
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"client"
|
||||
],
|
||||
"target": "serve"
|
||||
},
|
||||
"build-without-client"
|
||||
],
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"buildTarget": "server:build-without-client:development",
|
||||
"runBuildTargetDependencies": false
|
||||
}
|
||||
},
|
||||
"serve-nodir": {
|
||||
"executor": "@nx/js:node",
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"client"
|
||||
],
|
||||
"target": "serve"
|
||||
},
|
||||
"build-without-client"
|
||||
],
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"buildTarget": "server:build-without-client:development",
|
||||
"runBuildTargetDependencies": false
|
||||
}
|
||||
},
|
||||
"edit-integration-db": {
|
||||
"executor": "@nx/js:node",
|
||||
"dependsOn": [
|
||||
{
|
||||
"projects": [
|
||||
"client"
|
||||
],
|
||||
"target": "serve"
|
||||
},
|
||||
"build-without-client"
|
||||
],
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"buildTarget": "server:build-without-client:development",
|
||||
"runBuildTargetDependencies": false
|
||||
}
|
||||
},
|
||||
"package": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"command": "bash apps/server/scripts/build-server.sh"
|
||||
},
|
||||
"start-prod": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"command": "node apps/server/dist/main.cjs"
|
||||
},
|
||||
"docker-build": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}"
|
||||
},
|
||||
"executor": "nx:run-commands",
|
||||
"defaultConfiguration": "alpine",
|
||||
"configurations": {
|
||||
"debian": {
|
||||
"command": "docker build . -t triliumnext-debian -f Dockerfile"
|
||||
},
|
||||
"alpine": {
|
||||
"command": "docker build . -t triliumnext-alpine -f Dockerfile.alpine"
|
||||
},
|
||||
"rootless-debian": {
|
||||
"command": "docker build . -t triliumnext-rootless-debian -f Dockerfile.rootless"
|
||||
},
|
||||
"rootless-alpine": {
|
||||
"command": "docker build . -t triliumnext-rootless-alpine -f Dockerfile.alpine.rootless"
|
||||
}
|
||||
}
|
||||
},
|
||||
"docker-start": {
|
||||
"dependsOn": [
|
||||
"docker-build"
|
||||
],
|
||||
"executor": "nx:run-commands",
|
||||
"defaultConfiguration": "alpine",
|
||||
"configurations": {
|
||||
"debian": {
|
||||
"command": "docker run -p 8081:8080 triliumnext-debian"
|
||||
},
|
||||
"alpine": {
|
||||
"command": "docker run -p 8081:8080 triliumnext-alpine"
|
||||
},
|
||||
"rootless-debian": {
|
||||
"command": "docker run -p 8081:8080 triliumnext-rootless-debian"
|
||||
},
|
||||
"rootless-alpine": {
|
||||
"command": "docker run -p 8081:8080 triliumnext-rootless-alpine"
|
||||
}
|
||||
}
|
||||
},
|
||||
"build-without-client": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"main": "apps/server/src/main.ts",
|
||||
"outputPath": "apps/server/dist",
|
||||
"outputFileName": "main.js",
|
||||
"tsConfig": "apps/server/tsconfig.app.json",
|
||||
"platform": "node",
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"esbuildOptions": {
|
||||
"loader": {
|
||||
".css": "text",
|
||||
".ejs": "text"
|
||||
}
|
||||
},
|
||||
"declarationRootDir": "apps/server/src",
|
||||
"minify": false,
|
||||
"sourcemap": true,
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/src/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "packages/share-theme/src/templates",
|
||||
"output": "share-theme/templates"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"dependsOn": [
|
||||
"^build",
|
||||
"client:build"
|
||||
],
|
||||
"defaultConfiguration": "production",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"minify": true,
|
||||
"sourcemap": false
|
||||
},
|
||||
"development": {
|
||||
"minify": false,
|
||||
"sourcemap": true
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"main": "apps/server/src/main.ts",
|
||||
"outputPath": "apps/server/dist",
|
||||
"tsConfig": "apps/server/tsconfig.app.json",
|
||||
"platform": "node",
|
||||
"external": [
|
||||
"electron",
|
||||
"@electron/remote",
|
||||
"better-sqlite3",
|
||||
"./xhr-sync-worker.js"
|
||||
],
|
||||
"format": [
|
||||
"cjs"
|
||||
],
|
||||
"declarationRootDir": "apps/server/src",
|
||||
"thirdParty": true,
|
||||
"declaration": false,
|
||||
"esbuildOptions": {
|
||||
"splitting": false,
|
||||
"loader": {
|
||||
".css": "text",
|
||||
".ejs": "text"
|
||||
}
|
||||
},
|
||||
"additionalEntryPoints": [
|
||||
"apps/server/src/docker_healthcheck.ts"
|
||||
],
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/src/assets",
|
||||
"output": "assets"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "packages/share-theme/src/templates",
|
||||
"output": "share-theme/templates"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/client/dist",
|
||||
"output": "public",
|
||||
"ignore": [
|
||||
"webpack-stats.json"
|
||||
]
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/node_modules/better-sqlite3",
|
||||
"output": "node_modules/better-sqlite3"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/node_modules/bindings",
|
||||
"output": "node_modules/bindings"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/server/node_modules/file-uri-to-path",
|
||||
"output": "node_modules/file-uri-to-path"
|
||||
},
|
||||
{
|
||||
"glob": "xhr-sync-worker.js",
|
||||
"input": "apps/server/node_modules/jsdom/lib/jsdom/living/xhr",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"test-build": {
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"command": "vitest --config {projectRoot}/vitest.build.config.mts"
|
||||
},
|
||||
"circular-deps": {
|
||||
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
"./src/*": "./src/*",
|
||||
".": {
|
||||
"development": "./src/main.ts",
|
||||
"types": "./dist/main.d.ts",
|
||||
"import": "./dist/main.js",
|
||||
"default": "./dist/main.js"
|
||||
}
|
||||
},
|
||||
"types": "./dist/main.d.ts",
|
||||
"module": "./dist/main.js",
|
||||
"main": "./dist/main.js"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
|
||||
const build = new BuildHelper("apps/server");
|
||||
|
||||
async function main() {
|
||||
await build.buildBackend([ "src/main.ts", "src/docker_healthcheck.ts" ])
|
||||
|
||||
// Copy assets
|
||||
build.copy("src/assets", "assets/");
|
||||
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
||||
|
||||
// Copy node modules dependencies
|
||||
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
|
||||
build.copy("/node_modules/jsdom/lib/jsdom/living/xhr/xhr-sync-worker.js", "xhr-sync-worker.js");
|
||||
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
|
||||
|
||||
// Integrate the client.
|
||||
build.triggerBuildAndCopyTo("apps/client", "public/");
|
||||
build.deleteFromOutput("public/webpack-stats.json");
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -30,5 +30,5 @@ describe("etapi/import", () => {
|
||||
.expect(201);
|
||||
expect(response.body.note.title).toStrictEqual("Journal");
|
||||
expect(response.body.branch.parentNoteId).toStrictEqual("root");
|
||||
}, 10_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,10 +74,7 @@
|
||||
"zoom-out": "Pomniejsz",
|
||||
"zoom-in": "Powiększ",
|
||||
"print-active-note": "Drukuj aktywną notatkę",
|
||||
"toggle-full-screen": "Przełącz pełny ekran",
|
||||
"cut-into-note": "Wycina zaznaczony tekst i tworzy podrzędną notatkę z tym tekstem",
|
||||
"edit-readonly-note": "Edytuj notatkę tylko do odczytu",
|
||||
"other": "Inne"
|
||||
"toggle-full-screen": "Przełącz pełny ekran"
|
||||
},
|
||||
"keyboard_action_names": {
|
||||
"zoom-in": "Powiększ",
|
||||
|
||||
@@ -65,7 +65,6 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"monthlyBackupEnabled",
|
||||
"motionEnabled",
|
||||
"shadowsEnabled",
|
||||
"smoothScrollEnabled",
|
||||
"backdropEffectsEnabled",
|
||||
"maxContentWidth",
|
||||
"compressImages",
|
||||
|
||||
@@ -3,6 +3,7 @@ import path from "path";
|
||||
import express from "express";
|
||||
import { getResourceDir, isDev } from "../services/utils.js";
|
||||
import type serveStatic from "serve-static";
|
||||
import proxy from "express-http-proxy";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
|
||||
@@ -16,22 +17,17 @@ const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOp
|
||||
};
|
||||
|
||||
async function register(app: express.Application) {
|
||||
const srcRoot = path.join(__dirname, "..", "..");
|
||||
const srcRoot = path.join(__dirname, "..");
|
||||
const resourceDir = getResourceDir();
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const { createServer: createViteServer } = await import("vite");
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: "custom",
|
||||
cacheDir: path.join(srcRoot, "../../.cache/vite"),
|
||||
base: `/${assetUrlFragment}/`,
|
||||
root: path.join(srcRoot, "../client")
|
||||
});
|
||||
app.use(`/${assetUrlFragment}/`, (req, res, next) => {
|
||||
req.url = `/${assetUrlFragment}` + req.url;
|
||||
vite.middlewares(req, res, next);
|
||||
});
|
||||
if (isDev) {
|
||||
const publicUrl = process.env.TRILIUM_PUBLIC_SERVER;
|
||||
if (!publicUrl) {
|
||||
throw new Error("Missing TRILIUM_PUBLIC_SERVER");
|
||||
}
|
||||
app.use("/" + assetUrlFragment + `/@fs`, proxy(publicUrl, {
|
||||
proxyReqPathResolver: (req) => "/" + assetUrlFragment + `/@fs` + req.url
|
||||
}));
|
||||
} else {
|
||||
const publicDir = path.join(resourceDir, "public");
|
||||
if (!existsSync(publicDir)) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import attributeService from "../services/attributes.js";
|
||||
import config from "../services/config.js";
|
||||
import optionService from "../services/options.js";
|
||||
import log from "../services/log.js";
|
||||
import { isDev, isElectron, isWindows11 } from "../services/utils.js";
|
||||
import { isDev, isElectron } from "../services/utils.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import assetPath from "../services/asset_path.js";
|
||||
@@ -42,7 +42,7 @@ function index(req: Request, res: Response) {
|
||||
platform: process.platform,
|
||||
isElectron,
|
||||
hasNativeTitleBar: isElectron && options.nativeTitleBarVisible === "true",
|
||||
hasBackgroundEffects: isElectron && isWindows11 && options.backgroundEffects === "true",
|
||||
hasBackgroundEffects: isElectron && options.backgroundEffects === "true",
|
||||
mainFontSize: parseInt(options.mainFontSize),
|
||||
treeFontSize: parseInt(options.treeFontSize),
|
||||
detailFontSize: parseInt(options.detailFontSize),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import assetPath from "./asset_path.js";
|
||||
import { isDev } from "./utils.js";
|
||||
|
||||
export default isDev ? assetPath : assetPath + "/src";
|
||||
export default assetPath + "/src";
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import packageJson from "../../package.json" with { type: "json" };
|
||||
import { isDev } from "./utils";
|
||||
|
||||
/**
|
||||
* The URL prefix for assets (e.g. `assets/v1.2.3`).
|
||||
*/
|
||||
export const assetUrlFragment = `assets/v${packageJson.version}`;
|
||||
|
||||
/**
|
||||
* Similar to the {@link assetUrlFragment}, but on dev mode it also contains the `/src` suffix.
|
||||
*/
|
||||
const assetPath = isDev ? assetUrlFragment + "/src" : assetUrlFragment;
|
||||
const assetPath = isDev ? `http://localhost:4200/${assetUrlFragment}` : assetUrlFragment;
|
||||
|
||||
export default assetPath;
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "path";
|
||||
import mimeTypes from "mime-types";
|
||||
import mdService from "./markdown.js";
|
||||
import packageInfo from "../../../package.json" with { type: "json" };
|
||||
import { getContentDisposition, escapeHtml, getResourceDir, isDev } from "../utils.js";
|
||||
import { getContentDisposition, escapeHtml, getResourceDir } from "../utils.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import sanitize from "sanitize-filename";
|
||||
import fs from "fs";
|
||||
@@ -21,6 +21,7 @@ import type AttributeMeta from "../meta/attribute_meta.js";
|
||||
import type BBranch from "../../becca/entities/bbranch.js";
|
||||
import type { Response } from "express";
|
||||
import type { NoteMetaFile } from "../meta/note_meta.js";
|
||||
import cssContent from "@triliumnext/ckeditor5/content.css";
|
||||
|
||||
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
|
||||
|
||||
@@ -514,11 +515,7 @@ ${markdownContent}`;
|
||||
return;
|
||||
}
|
||||
|
||||
const cssFile = isDev
|
||||
? path.join(__dirname, "../../../../../node_modules/ckeditor5/dist/ckeditor5-content.css")
|
||||
: path.join(getResourceDir(), "ckeditor5-content.css");
|
||||
|
||||
archive.append(fs.readFileSync(cssFile, "utf-8"), { name: cssMeta.dataFileName });
|
||||
archive.append(cssContent, { name: cssMeta.dataFileName });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -155,7 +155,7 @@ const defaultOptions: DefaultOption[] = [
|
||||
{ name: "motionEnabled", value: "true", isSynced: false },
|
||||
{ name: "shadowsEnabled", value: "true", isSynced: false },
|
||||
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
|
||||
{ name: "smoothScrollEnabled", value: "true", isSynced: false },
|
||||
|
||||
|
||||
// Internationalization
|
||||
{ name: "locale", value: "en", isSynced: true },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import log from "./log.js";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
@@ -9,7 +10,7 @@ const ELECTRON_APP_ROOT_DIR = path.resolve(RESOURCE_DIR, "../..");
|
||||
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
|
||||
|
||||
if (!fs.existsSync(DB_INIT_DIR)) {
|
||||
console.error(`Could not find DB initialization directory: ${DB_INIT_DIR}`);
|
||||
log.error(`Could not find DB initialization directory: ${DB_INIT_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ import becca_loader from "../becca/becca_loader.js";
|
||||
import entity_changes from "./entity_changes.js";
|
||||
import config from "./config.js";
|
||||
|
||||
const dbOpts: Database.Options = {
|
||||
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
||||
};
|
||||
|
||||
let dbConnection: DatabaseType = buildDatabase();
|
||||
let statementCache: Record<string, Statement> = {};
|
||||
|
||||
@@ -27,18 +23,15 @@ function buildDatabase() {
|
||||
if (process.env.TRILIUM_INTEGRATION_TEST === "memory") {
|
||||
return buildIntegrationTestDatabase();
|
||||
} else if (process.env.TRILIUM_INTEGRATION_TEST === "memory-no-store") {
|
||||
return new Database(":memory:", dbOpts);
|
||||
return new Database(":memory:");
|
||||
}
|
||||
|
||||
return new Database(dataDir.DOCUMENT_PATH, {
|
||||
...dbOpts,
|
||||
readonly: config.General.readOnly
|
||||
});
|
||||
return new Database(dataDir.DOCUMENT_PATH, { readonly: config.General.readOnly });
|
||||
}
|
||||
|
||||
function buildIntegrationTestDatabase(dbPath?: string) {
|
||||
const dbBuffer = fs.readFileSync(dbPath ?? dataDir.DOCUMENT_PATH);
|
||||
return new Database(dbBuffer, dbOpts);
|
||||
return new Database(dbBuffer);
|
||||
}
|
||||
|
||||
function rebuildIntegrationTestDatabase(dbPath?: string) {
|
||||
|
||||
@@ -29,21 +29,12 @@ function getTrayIconPath() {
|
||||
name = "icon-color";
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return path.join(__dirname, "../../../desktop/src/assets/images/tray", `${name}.png`);
|
||||
} else {
|
||||
return path.resolve(path.join(getResourceDir(), "assets", "images", "tray", `${name}.png`));
|
||||
}
|
||||
}
|
||||
|
||||
function getIconPath(name: string) {
|
||||
const suffix = !isMac && electron.nativeTheme.shouldUseDarkColors ? "-inverted" : "";
|
||||
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return path.join(__dirname, "../../../desktop/src/assets/images/tray", `${name}Template${suffix}.png`);
|
||||
} else {
|
||||
return path.resolve(path.join(getResourceDir(), "assets", "images", "tray", `${name}Template${suffix}.png`));
|
||||
}
|
||||
}
|
||||
|
||||
function registerVisibilityListener(window: BrowserWindow) {
|
||||
|
||||
@@ -12,9 +12,6 @@ import path from "path";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
import log from "./log.js";
|
||||
import { t } from "i18next";
|
||||
import { release as osRelease } from "os";
|
||||
|
||||
const osVersion = osRelease().split('.').map(Number);
|
||||
|
||||
const randtoken = generator({ source: "crypto" });
|
||||
|
||||
@@ -22,8 +19,6 @@ export const isMac = process.platform === "darwin";
|
||||
|
||||
export const isWindows = process.platform === "win32";
|
||||
|
||||
export const isWindows11 = isWindows && osVersion[0] === 10 && osVersion[2] >= 22000;
|
||||
|
||||
export const isElectron = !!process.versions["electron"];
|
||||
|
||||
export const isDev = !!(process.env.TRILIUM_ENV && process.env.TRILIUM_ENV === "dev");
|
||||
|
||||
@@ -224,7 +224,7 @@ function getWindowExtraOpts() {
|
||||
}
|
||||
|
||||
async function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
|
||||
const remoteMain = (await import("@electron/remote/main/index.js"));
|
||||
const remoteMain = (await import("@electron/remote/main/index.js")).default;
|
||||
remoteMain.enable(webContents);
|
||||
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
@@ -257,11 +257,7 @@ async function configureWebContents(webContents: WebContents, spellcheckEnabled:
|
||||
}
|
||||
|
||||
function getIcon() {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return path.join(__dirname, "../../../desktop/electron-forge/app-icon/png/256x256-dev.png");
|
||||
} else {
|
||||
return path.join(RESOURCE_DIR, "../public/assets/icon.png");
|
||||
}
|
||||
}
|
||||
|
||||
async function createSetupWindow() {
|
||||
|
||||
@@ -18,7 +18,6 @@ import utils, { isDev, safeExtractMessageAndStackFromError } from "../services/u
|
||||
import options from "../services/options.js";
|
||||
import { t } from "i18next";
|
||||
import ejs from "ejs";
|
||||
import { join } from "path";
|
||||
|
||||
function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
|
||||
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
@@ -402,9 +401,7 @@ function register(router: Router) {
|
||||
|
||||
function renderDefault(res: Response<any, Record<string, any>>, template: "page" | "404", opts: any = {}) {
|
||||
// Path is relative to apps/server/dist/assets/views
|
||||
const shareThemePath = process.env.NODE_ENV === "development"
|
||||
? join(__dirname, `../../../../packages/share-theme/src/templates/${template}.ejs`)
|
||||
: `../../share-theme/templates/${template}.ejs`;
|
||||
const shareThemePath = `../../share-theme/templates/${template}.ejs`;
|
||||
res.render(shareThemePath, opts);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@ import sql_init from "../services/sql_init.js";
|
||||
let dbConnection!: Database.Database;
|
||||
|
||||
sql_init.dbReady.then(() => {
|
||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, {
|
||||
readonly: true,
|
||||
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
||||
});
|
||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user