feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,127 @@
// React Native WebView cannot load external JS files, however it can load
// arbitrary JS via the injectedJavaScript property. So we use this to load external
// files: First here we convert the JS file to a plain string, and that string
// is then loaded by eg. the Mermaid plugin, and finally injected in the WebView.
import { dirname, extname, basename, resolve } from 'path';
import * as esbuild from 'esbuild';
import copyAssets from './copyAssets';
import { writeFile, readFile } from 'fs-extra';
export default class BundledFile {
private readonly bundleOutputPathBase_: string;
private readonly bundleBaseName_: string;
private readonly rootFileDirectory_: string;
public constructor(
public readonly bundleName: string,
private readonly sourceFilePath_: string,
) {
this.rootFileDirectory_ = dirname(sourceFilePath_);
this.bundleBaseName_ = basename(sourceFilePath_, extname(sourceFilePath_));
this.bundleOutputPathBase_ = `${this.rootFileDirectory_}/${this.bundleBaseName_}.bundle`;
}
private makeBuildContext(mode: 'production' | 'development') {
return esbuild.context({
entryPoints: [this.sourceFilePath_],
outfile: `${this.bundleOutputPathBase_}.js`,
minify: mode === 'production',
bundle: true,
sourcemap: true,
format: 'iife',
globalName: this.bundleName,
metafile: false,
// es5: Have Webpack's generated code target ES5. This doesn't apply to code not
// generated by Webpack.
target: ['chrome58', 'safari14'],
plugins: [
{
name: 'joplin--node-polyfills',
setup: build => {
build.onResolve({ filter: /^(path|events)$/ }, args => {
let path = args.path;
if (args.path === 'path') {
path = require.resolve('path-browserify');
} else if (args.path === 'events') {
path = require.resolve('events/');
}
return { path };
});
},
},
{
// Supports require(...)ing SVG images
name: 'joplin--require-svg',
setup: build => {
// A relative path to an SVG:
build.onResolve({ filter: /^\.{1,2}\/.*\.svg$/ }, args => ({
path: resolve(args.resolveDir, args.path),
namespace: 'joplin-require-svg',
}));
build.onLoad({ filter: /^.*$/, namespace: 'joplin-require-svg' }, async args => {
const fileContent = await readFile(args.path, 'utf-8');
return { contents: `
let svg = null;
export default () => {
svg ??= (() => {
const parser = new DOMParser();
const doc = parser.parseFromString(${JSON.stringify(fileContent)}, 'image/svg+xml');
return doc.querySelector('svg');
})();
return svg.cloneNode(true);
};
` };
});
},
},
{
name: 'joplin--copy-final',
setup: build => {
build.onEnd(async (result) => {
if (result.errors.length === 0) {
console.log('copy output');
await this.copyToImportableFile_();
} else {
console.warn('Copying skipped. Build produced errors');
}
});
},
},
],
});
}
// Creates a file that can be imported by React native. This file contains the
// bundled JS as a string.
private async copyToImportableFile_() {
await copyAssets(`${this.bundleName}.bundle`, {
js: `${this.bundleOutputPathBase_}.js`,
css: `${this.bundleOutputPathBase_}.css`,
});
}
// Create a minified JS file in the same directory as `this.sourceFilePath` with
// the same name.
public async build() {
console.info(`Building bundle: ${this.bundleName}...`);
const compiler = await this.makeBuildContext('production');
const result = await compiler.rebuild();
await compiler.dispose();
if (result?.metafile) {
await writeFile(`${this.bundleOutputPathBase_}.meta.json`, JSON.stringify(result.metafile, undefined, '\t'));
}
}
public async startWatching() {
console.info(`Watching bundle: ${this.bundleName}...`);
const compiler = await this.makeBuildContext('development');
await compiler.watch();
}
}

查看文件

@@ -0,0 +1,6 @@
import { dirname } from 'path';
export const mobileDir = dirname(dirname(__dirname));
export const outputDir = `${mobileDir}/lib/rnInjectedJs`;
export const rootDir = dirname(dirname(mobileDir));

查看文件

@@ -0,0 +1,29 @@
import { exists, readFile, writeFile } from 'fs-extra';
import { outputDir } from './constants';
type InputFilePaths = {
js: string;
css?: string;
};
// Stores the contents of the file at [filePath] as an importable string.
// [name] should be the name (excluding the .js extension) of the output file that will contain
// the JSON-ified file content.
const copyAssets = async (name: string, input: InputFilePaths) => {
const outputPath = `${outputDir}/${name}.js`;
console.info(`Creating: ${outputPath}`);
const hasJs = await exists(input.js);
const js = hasJs ? await readFile(input.js, 'utf-8') : null;
const hasCss = !!input.css && await exists(input.css);
const css = hasCss ? await readFile(input.css, 'utf-8') : null;
const json = `module.exports = {
js: ${JSON.stringify(js)},
css: ${JSON.stringify(css)}
};`;
await writeFile(outputPath, json);
};
export default copyAssets;

查看文件

@@ -0,0 +1,59 @@
import BundledFile from './BundledFile';
import { mkdirp } from 'fs-extra';
import { mobileDir, outputDir } from './constants';
import copyAssets from './copyAssets';
import { readdir, existsSync } from 'fs-extra';
import { join } from 'path';
const getBundles = async () => {
// All folders in the contentScripts/ directories are bundles
const contentScriptDir = `${mobileDir}/contentScripts/`;
const bundles = [];
for (const folderOrFileName of await readdir(contentScriptDir)) {
// Only check subfolders
if (folderOrFileName.includes('.')) continue;
// Skip utilities shared between bundles
if (folderOrFileName === 'utils') continue;
const bundlePath = join(contentScriptDir, folderOrFileName);
const indexPath = join(bundlePath, 'contentScript', 'index.ts');
const contentScriptPath = existsSync(indexPath) ? indexPath : join(bundlePath, 'contentScript.ts');
bundles.push(new BundledFile(folderOrFileName, contentScriptPath));
}
// Bundled JS may also exist within the components/ directory:
bundles.push(new BundledFile(
'pluginBackgroundPage',
`${mobileDir}/components/plugins/backgroundPage/pluginRunnerBackgroundPage.ts`,
));
return bundles;
};
const gulpTasks = {
beforeBundle: {
fn: () => mkdirp(outputDir),
},
buildBundledJs: {
fn: async () => {
for (const bundle of await getBundles()) {
await bundle.build();
}
},
},
watchBundledJs: {
fn: async () => {
const watchPromises = [];
for (const bundle of await getBundles()) {
watchPromises.push(bundle.startWatching());
}
await Promise.all(watchPromises);
},
},
copyWebviewLib: {
fn: () => copyAssets('webviewLib', { js: `${mobileDir}/../lib/renderers/webviewLib.js` }),
},
};
export default gulpTasks;