cyreader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server/dist/app.js +23 -0
- package/bundle/server/dist/app.test.js +9 -0
- package/bundle/server/dist/config/defaults.js +7 -0
- package/bundle/server/dist/config/paths.js +18 -0
- package/bundle/server/dist/config/schema.js +84 -0
- package/bundle/server/dist/config/schema.test.js +76 -0
- package/bundle/server/dist/config/service.js +38 -0
- package/bundle/server/dist/config/storage.js +54 -0
- package/bundle/server/dist/config/storage.test.js +79 -0
- package/bundle/server/dist/index.js +28 -0
- package/bundle/server/dist/library/comic-url.js +4 -0
- package/bundle/server/dist/library/cover.js +44 -0
- package/bundle/server/dist/library/errors.js +12 -0
- package/bundle/server/dist/library/files.js +35 -0
- package/bundle/server/dist/library/files.test.js +29 -0
- package/bundle/server/dist/library/id.js +6 -0
- package/bundle/server/dist/library/novel-url.js +4 -0
- package/bundle/server/dist/library/scan.js +132 -0
- package/bundle/server/dist/library/scan.test.js +252 -0
- package/bundle/server/dist/library/service.js +87 -0
- package/bundle/server/dist/library/service.test.js +92 -0
- package/bundle/server/dist/library/test-images.js +23 -0
- package/bundle/server/dist/library/text-encoding.js +33 -0
- package/bundle/server/dist/library/text-encoding.test.js +42 -0
- package/bundle/server/dist/library/types.js +1 -0
- package/bundle/server/dist/library-static.js +75 -0
- package/bundle/server/dist/library-static.test.js +81 -0
- package/bundle/server/dist/listen-config.js +23 -0
- package/bundle/server/dist/listen-config.test.js +50 -0
- package/bundle/server/dist/reading/progress/merge.js +32 -0
- package/bundle/server/dist/reading/progress/schema.js +66 -0
- package/bundle/server/dist/reading/progress/schema.test.js +39 -0
- package/bundle/server/dist/reading/progress/service.js +81 -0
- package/bundle/server/dist/reading/progress/service.test.js +104 -0
- package/bundle/server/dist/reading/progress/storage.js +44 -0
- package/bundle/server/dist/reading/recent/schema.js +16 -0
- package/bundle/server/dist/reading/recent/service.js +54 -0
- package/bundle/server/dist/reading/recent/service.test.js +84 -0
- package/bundle/server/dist/reading/recent/storage.js +42 -0
- package/bundle/server/dist/reading/recent/storage.test.js +46 -0
- package/bundle/server/dist/routes/config.js +52 -0
- package/bundle/server/dist/routes/config.test.js +147 -0
- package/bundle/server/dist/routes/library.js +31 -0
- package/bundle/server/dist/routes/library.test.js +125 -0
- package/bundle/server/dist/routes/reading.js +66 -0
- package/bundle/server/dist/routes/reading.test.js +135 -0
- package/bundle/server/dist/web-static.js +31 -0
- package/bundle/server/dist/web-static.test.js +92 -0
- package/bundle/web/dist/assets/_shell-DPQLbvwD.js +5 -0
- package/bundle/web/dist/assets/_shell-gkEQU-gF.js +1 -0
- package/bundle/web/dist/assets/comic._id-BjwrciUv.js +1 -0
- package/bundle/web/dist/assets/dist-Da_WaNYN.js +1 -0
- package/bundle/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/bundle/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/bundle/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/bundle/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/bundle/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/bundle/web/dist/assets/image-BZKAkGUy.js +1 -0
- package/bundle/web/dist/assets/index-BpBtuR9k.css +2 -0
- package/bundle/web/dist/assets/index-CaXGEWDb.js +10 -0
- package/bundle/web/dist/assets/input-DcKYfXao.js +41 -0
- package/bundle/web/dist/assets/library-Dw1qyl0v.js +1 -0
- package/bundle/web/dist/assets/mutation-CwzbY9ri.js +1 -0
- package/bundle/web/dist/assets/novel._id-DsL8say-.js +4 -0
- package/bundle/web/dist/assets/query-keys-CDsPIOEO.js +1 -0
- package/bundle/web/dist/assets/reading-BseNE7P3.js +1 -0
- package/bundle/web/dist/assets/reading-progress-BcmbUd_d.js +1 -0
- package/bundle/web/dist/assets/save-reading-progress-AuKXiup6.js +1 -0
- package/bundle/web/dist/assets/scroll-area-Bzi-DiTy.js +1 -0
- package/bundle/web/dist/assets/settings-2-CCv0KiQI.js +1 -0
- package/bundle/web/dist/assets/settings-2wiLHSOF.js +1 -0
- package/bundle/web/dist/assets/shell-context-BT0BT8PF.js +1 -0
- package/bundle/web/dist/assets/toggle-group-DDKezi4R.js +1 -0
- package/bundle/web/dist/assets/useNavigate-BMyovDLu.js +1 -0
- package/bundle/web/dist/assets/utils-BmM72imW.js +1 -0
- package/bundle/web/dist/favicon.svg +1 -0
- package/bundle/web/dist/index.html +17 -0
- package/bundle/web/dist/novel-default-cover.png +0 -0
- package/dist/cli.js +40 -0
- package/dist/parse-options.js +30 -0
- package/dist/parse-options.test.js +26 -0
- package/dist/paths.js +11 -0
- package/dist/paths.test.js +13 -0
- package/dist/spawn-server.js +40 -0
- package/dist/spawn-server.test.js +46 -0
- package/package.json +32 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN" class="dark h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>CYReader - 个人阅读库</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CaXGEWDb.js"></script>
|
|
9
|
+
<link rel="modulepreload" crossorigin href="/assets/utils-BmM72imW.js">
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/mutation-CwzbY9ri.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/useNavigate-BMyovDLu.js">
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BpBtuR9k.css">
|
|
13
|
+
</head>
|
|
14
|
+
<body class="h-screen overflow-hidden">
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
Binary file
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getPackageRoot } from './paths.js';
|
|
5
|
+
import { parseCliArgs } from './parse-options.js';
|
|
6
|
+
import { spawnServer } from './spawn-server.js';
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`Usage: cyreader [options]
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
--host <host> Host to listen on
|
|
12
|
+
--port <port> Port to listen on (default: 4613)
|
|
13
|
+
-h, --help Show help
|
|
14
|
+
-V, --version Show version
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
function printVersion() {
|
|
18
|
+
const packageJson = JSON.parse(readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf8'));
|
|
19
|
+
console.log(packageJson.version);
|
|
20
|
+
}
|
|
21
|
+
async function main() {
|
|
22
|
+
const options = parseCliArgs(process.argv.slice(2));
|
|
23
|
+
if (options.kind === 'help') {
|
|
24
|
+
printHelp();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (options.kind === 'version') {
|
|
28
|
+
printVersion();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const exitCode = await spawnServer({
|
|
32
|
+
host: options.host,
|
|
33
|
+
port: options.port,
|
|
34
|
+
});
|
|
35
|
+
process.exit(exitCode);
|
|
36
|
+
}
|
|
37
|
+
main().catch((error) => {
|
|
38
|
+
console.error(error);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
export const DEFAULT_PORT = 4613;
|
|
3
|
+
export function parseCliArgs(argv) {
|
|
4
|
+
const { values } = parseArgs({
|
|
5
|
+
args: argv,
|
|
6
|
+
options: {
|
|
7
|
+
host: { type: 'string' },
|
|
8
|
+
port: { type: 'string', default: String(DEFAULT_PORT) },
|
|
9
|
+
help: { type: 'boolean', short: 'h' },
|
|
10
|
+
version: { type: 'boolean', short: 'V' },
|
|
11
|
+
},
|
|
12
|
+
allowPositionals: false,
|
|
13
|
+
strict: true,
|
|
14
|
+
});
|
|
15
|
+
if (values.help) {
|
|
16
|
+
return { kind: 'help' };
|
|
17
|
+
}
|
|
18
|
+
if (values.version) {
|
|
19
|
+
return { kind: 'version' };
|
|
20
|
+
}
|
|
21
|
+
const port = Number(values.port);
|
|
22
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
23
|
+
throw new Error(`Invalid --port: ${values.port}`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
kind: 'run',
|
|
27
|
+
host: values.host,
|
|
28
|
+
port,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_PORT, parseCliArgs } from './parse-options.js';
|
|
3
|
+
describe('parseCliArgs', () => {
|
|
4
|
+
it('returns help when --help is passed', () => {
|
|
5
|
+
expect(parseCliArgs(['--help'])).toEqual({ kind: 'help' });
|
|
6
|
+
expect(parseCliArgs(['-h'])).toEqual({ kind: 'help' });
|
|
7
|
+
});
|
|
8
|
+
it('returns version when --version is passed', () => {
|
|
9
|
+
expect(parseCliArgs(['--version'])).toEqual({ kind: 'version' });
|
|
10
|
+
expect(parseCliArgs(['-V'])).toEqual({ kind: 'version' });
|
|
11
|
+
});
|
|
12
|
+
it('uses the default port when --port is omitted', () => {
|
|
13
|
+
expect(parseCliArgs([])).toEqual({ kind: 'run', port: DEFAULT_PORT });
|
|
14
|
+
});
|
|
15
|
+
it('parses host and port options', () => {
|
|
16
|
+
expect(parseCliArgs(['--host', '0.0.0.0', '--port', '8080'])).toEqual({
|
|
17
|
+
kind: 'run',
|
|
18
|
+
host: '0.0.0.0',
|
|
19
|
+
port: 8080,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
it('rejects invalid port values', () => {
|
|
23
|
+
expect(() => parseCliArgs(['--port', '0'])).toThrow(/Invalid --port/);
|
|
24
|
+
expect(() => parseCliArgs(['--port', 'abc'])).toThrow(/Invalid --port/);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
export function getPackageRoot() {
|
|
4
|
+
return path.resolve(fileURLToPath(import.meta.url), '../..');
|
|
5
|
+
}
|
|
6
|
+
export function getServerEntryPath() {
|
|
7
|
+
return path.join(getPackageRoot(), 'bundle/server/dist/index.js');
|
|
8
|
+
}
|
|
9
|
+
export function getWebRootPath() {
|
|
10
|
+
return path.join(getPackageRoot(), 'bundle/web');
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { getPackageRoot, getServerEntryPath, getWebRootPath } from './paths.js';
|
|
5
|
+
describe('paths', () => {
|
|
6
|
+
it('resolves bundle paths relative to the cli package root', () => {
|
|
7
|
+
const packageRoot = getPackageRoot();
|
|
8
|
+
const expectedRoot = path.resolve(fileURLToPath(import.meta.url), '../..');
|
|
9
|
+
expect(packageRoot).toBe(expectedRoot);
|
|
10
|
+
expect(getServerEntryPath()).toBe(path.join(packageRoot, 'bundle/server/dist/index.js'));
|
|
11
|
+
expect(getWebRootPath()).toBe(path.join(packageRoot, 'bundle/web'));
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { getServerEntryPath, getWebRootPath } from './paths.js';
|
|
3
|
+
export function buildServerEnv(options) {
|
|
4
|
+
const env = {
|
|
5
|
+
...process.env,
|
|
6
|
+
NODE_ENV: 'production',
|
|
7
|
+
CYREADER_WEB_ROOT: getWebRootPath(),
|
|
8
|
+
CYREADER_PORT: String(options.port),
|
|
9
|
+
};
|
|
10
|
+
if (options.host) {
|
|
11
|
+
env.CYREADER_HOST = options.host;
|
|
12
|
+
}
|
|
13
|
+
return env;
|
|
14
|
+
}
|
|
15
|
+
export function spawnServer(options) {
|
|
16
|
+
const serverEntryPath = getServerEntryPath();
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const child = spawn(process.execPath, [serverEntryPath], {
|
|
19
|
+
env: buildServerEnv(options),
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
});
|
|
22
|
+
const forwardSignal = (signal) => {
|
|
23
|
+
if (child.pid !== undefined) {
|
|
24
|
+
child.kill(signal);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
process.on('SIGINT', forwardSignal);
|
|
28
|
+
process.on('SIGTERM', forwardSignal);
|
|
29
|
+
child.on('error', reject);
|
|
30
|
+
child.on('exit', (code, signal) => {
|
|
31
|
+
process.off('SIGINT', forwardSignal);
|
|
32
|
+
process.off('SIGTERM', forwardSignal);
|
|
33
|
+
if (signal) {
|
|
34
|
+
resolve(128 + (signal === 'SIGINT' ? 2 : 15));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
resolve(code ?? 0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { buildServerEnv, spawnServer } from './spawn-server.js';
|
|
4
|
+
const { spawnMock } = vi.hoisted(() => ({
|
|
5
|
+
spawnMock: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('node:child_process', () => ({
|
|
8
|
+
spawn: spawnMock,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('./paths.js', () => ({
|
|
11
|
+
getServerEntryPath: () => '/tmp/cyreader/bundle/server/dist/index.js',
|
|
12
|
+
getWebRootPath: () => '/tmp/cyreader/bundle/web',
|
|
13
|
+
}));
|
|
14
|
+
describe('spawn server', () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.clearAllMocks();
|
|
17
|
+
});
|
|
18
|
+
it('builds production env for the server process', () => {
|
|
19
|
+
const env = buildServerEnv({ port: 8080, host: '127.0.0.1' });
|
|
20
|
+
expect(env.NODE_ENV).toBe('production');
|
|
21
|
+
expect(env.CYREADER_WEB_ROOT).toBe('/tmp/cyreader/bundle/web');
|
|
22
|
+
expect(env.CYREADER_PORT).toBe('8080');
|
|
23
|
+
expect(env.CYREADER_HOST).toBe('127.0.0.1');
|
|
24
|
+
});
|
|
25
|
+
it('omits CYREADER_HOST when host is not provided', () => {
|
|
26
|
+
const env = buildServerEnv({ port: 4613 });
|
|
27
|
+
expect(env.CYREADER_HOST).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
it('spawns the bundled server entrypoint', async () => {
|
|
30
|
+
const child = new EventEmitter();
|
|
31
|
+
child.pid = 1234;
|
|
32
|
+
spawnMock.mockReturnValue(child);
|
|
33
|
+
const exitPromise = spawnServer({ port: 4613, host: '127.0.0.1' });
|
|
34
|
+
child.emit('exit', 0, null);
|
|
35
|
+
await expect(exitPromise).resolves.toBe(0);
|
|
36
|
+
expect(spawnMock).toHaveBeenCalledWith(process.execPath, ['/tmp/cyreader/bundle/server/dist/index.js'], expect.objectContaining({
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
env: expect.objectContaining({
|
|
39
|
+
NODE_ENV: 'production',
|
|
40
|
+
CYREADER_WEB_ROOT: '/tmp/cyreader/bundle/web',
|
|
41
|
+
CYREADER_PORT: '4613',
|
|
42
|
+
CYREADER_HOST: '127.0.0.1',
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cyreader",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A local comic and novel reader",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cyreader": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"bundle"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@hono/node-server": "^1.19.14",
|
|
15
|
+
"hono": "^4.12.25",
|
|
16
|
+
"nanoid": "^5.1.11",
|
|
17
|
+
"sharp": "^0.34.5",
|
|
18
|
+
"zod": "^4.4.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.11.17",
|
|
22
|
+
"typescript": "^5.8.3",
|
|
23
|
+
"vitest": "^4.1.8"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"assemble": "node scripts/assemble.mjs",
|
|
28
|
+
"start": "node dist/cli.js",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
}
|
|
32
|
+
}
|