From 7c6dd1857ed5a669002d9f9908a2ad5b21f193a2 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 25 Feb 2025 02:07:57 +0900 Subject: [PATCH] Reorganize packages --- .github/workflows/main.yaml | 45 ++++---- .gitignore | 1 + .vscode/settings.json | 5 +- CHANGES.md | 7 ++ README.md | 1 + deno.json | 62 +++++++++++ docs/.jsr-cache.file.json | 1 + docs/.vitepress/config.mts | 13 ++- docs/bun.lockb | Bin 153489 -> 153865 bytes docs/manual/sinks.md | 63 +++++++++++- docs/package.json | 7 +- file/README.md | 43 ++++++++ file/deno.json | 20 ++++ file/dnt.ts | 64 ++++++++++++ file/filesink.base.ts | 158 +++++++++++++++++++++++++++++ {logtape => file}/filesink.deno.ts | 15 +-- {logtape => file}/filesink.jsr.ts | 7 +- {logtape => file}/filesink.node.ts | 44 +++----- {logtape => file}/filesink.test.ts | 55 +++++++++- file/mod.ts | 7 ++ logtape/deno.json | 28 ++--- logtape/dnt.ts | 16 +-- logtape/filesink.web.ts | 17 ---- logtape/fs.cjs | 20 ---- logtape/fs.js | 1 - logtape/fs.ts | 22 ---- logtape/mod.ts | 3 - logtape/sink.test.ts | 59 +---------- logtape/sink.ts | 152 --------------------------- scripts/check_versions.ts | 24 +++++ scripts/update_versions.ts | 19 ++++ 31 files changed, 597 insertions(+), 382 deletions(-) create mode 120000 README.md create mode 100644 deno.json create mode 100644 docs/.jsr-cache.file.json create mode 100644 file/README.md create mode 100644 file/deno.json create mode 100644 file/dnt.ts create mode 100644 file/filesink.base.ts rename {logtape => file}/filesink.deno.ts (83%) rename {logtape => file}/filesink.jsr.ts (90%) rename {logtape => file}/filesink.node.ts (63%) rename {logtape => file}/filesink.test.ts (66%) create mode 100644 file/mod.ts delete mode 100644 logtape/filesink.web.ts delete mode 100644 logtape/fs.cjs delete mode 100644 logtape/fs.js delete mode 100644 logtape/fs.ts create mode 100644 scripts/check_versions.ts create mode 100644 scripts/update_versions.ts diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index df09ae1..99fdb94 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -40,15 +40,18 @@ jobs: with: check_name: "Test Results (Windows)" files: .test-report.xml + - if: '!cancelled()' + uses: codecov/test-results-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: .test-report.xml - run: deno coverage --lcov .cov > .cov.lcov - uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} slug: dahlia/logtape file: .cov.lcov - - run: deno task dnt - - run: bun run ./test_runner.js - working-directory: ${{ github.workspace }}/npm/ + - run: deno task test-all:bun - run: deno task check publish: @@ -64,33 +67,39 @@ jobs: deno-version: v2.x - if: github.ref_type == 'branch' run: | - jq \ + v="$(jq \ + --raw-output \ --arg build "$GITHUB_RUN_NUMBER" \ --arg commit "${GITHUB_SHA::8}" \ - '.version = .version + "-dev." + $build + "+" + $commit' \ - deno.json > deno.json.tmp - mv deno.json.tmp deno.json + '.version + "-dev." + $build + "+" + $commit' \ + logtape/deno.json)" + deno run --allow-read --allow-write scripts/update_versions.ts "$v" + deno task check:versions - if: github.ref_type == 'tag' run: | set -ex - [[ "$(jq -r .version deno.json)" = "$GITHUB_REF_NAME" ]] - - run: 'deno task dnt "$(jq -r .version deno.json)"' + [[ "$(jq -r .version logtape/deno.json)" = "$GITHUB_REF_NAME" ]] + deno task check:versions + - run: deno task dnt-all - if: github.event_name == 'push' run: | set -ex npm config set //registry.npmjs.org/:_authToken "$NPM_AUTH_TOKEN" - if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then - npm publish --provenance --access public - else - npm publish --provenance --access public --tag dev - fi + for npm in */npm/; do + pushd "$npm" + if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then + npm publish --provenance --access public + else + npm publish --provenance --access public --tag dev + fi + popd + done env: NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - working-directory: ${{ github.workspace }}/npm/ - if: github.event_name == 'pull_request' - run: deno publish --dry-run --allow-dirty + run: deno task publish --dry-run --allow-dirty - if: github.event_name == 'push' - run: deno publish --allow-dirty + run: deno task publish --allow-dirty publish-docs: if: github.event_name == 'push' @@ -113,12 +122,14 @@ jobs: bun install if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then bun add -D "@logtape/logtape@$GITHUB_REF_NAME" + bun add -D "@logtape/file@$GITHUB_REF_NAME" bun add -D @logtape/otel@latest EXTRA_NAV_TEXT=Unstable \ EXTRA_NAV_LINK="$UNSTABLE_DOCS_URL" \ bun run build else bun add -D @logtape/logtape@dev + bun add -D @logtape/file@dev bun add -D @logtape/otel@dev EXTRA_NAV_TEXT=Stable \ EXTRA_NAV_LINK="$STABLE_DOCS_URL" \ diff --git a/.gitignore b/.gitignore index 5a27765..e4b141a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.dnt-import-map.json coverage/ npm/ diff --git a/.vscode/settings.json b/.vscode/settings.json index a5b0e22..968eac1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,7 +13,7 @@ "editor.defaultFormatter": "denoland.vscode-deno", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.sortImports": "always" } }, "[json]": { @@ -28,13 +28,14 @@ "editor.defaultFormatter": "denoland.vscode-deno", "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": "explicit" + "source.sortImports": "always" } }, "cSpell.words": [ "Codecov", "consolemock", "deno", + "filesink", "hongminhee", "logtape", "runtimes", diff --git a/CHANGES.md b/CHANGES.md index 19bd313..5d1e82e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,13 @@ Version 0.9.0 To be released. + - Moved file sinks and rotating file sinks to separate packages. + + - Moved `getFileSink()` function to `@logtape/file` package. + - Moved `FileSinkOptions` interface to `@logtape/file` package. + - Moved `getRotatingFileSink()` function to `@logtape/file` package. + - Moved `RotatingFileSinkOptions` interface to `@logtape/file` package. + - Added synchronous versions of configuration functions. [[#12], [#29] by Murph Murphy] diff --git a/README.md b/README.md new file mode 120000 index 0000000..c88801f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +logtape/README.md \ No newline at end of file diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..a86da08 --- /dev/null +++ b/deno.json @@ -0,0 +1,62 @@ +{ + "workspace": [ + "./logtape", + "./file" + ], + "imports": { + "@david/which-runtime": "jsr:@david/which-runtime@^0.2.1", + "@deno/dnt": "jsr:@deno/dnt@^0.41.1", + "@std/assert": "jsr:@std/assert@^0.222.1", + "@std/async": "jsr:@std/async@^0.222.1", + "@std/fs": "jsr:@std/fs@^0.223.0", + "@std/path": "jsr:@std/path@^1.0.2", + "@std/testing": "jsr:@std/testing@^0.222.1", + "consolemock": "npm:consolemock@^1.1.0" + }, + "unstable": [ + "fs" + ], + "lock": false, + "exclude": [ + ".github/", + "docs/" + ], + "tasks": { + "check": { + "command": "deno check **/*.ts && deno lint && deno fmt --check", + "dependencies": [ + "check:versions" + ] + }, + "check:versions": "deno run --allow-read scripts/check_versions.ts", + "test": "deno test --allow-read --allow-write", + "coverage": "rm -rf coverage && deno task test --coverage && deno coverage --html coverage", + "dnt-all": "deno task --recursive dnt", + "test-all:bun": "deno task --recursive test:bun", + "test-all": { + "dependencies": [ + "test", + "test-all:bun" + ] + }, + "publish": { + "command": "deno publish", + "dependencies": [ + "check", + "test" + ] + }, + "hooks:install": "deno run --allow-read=deno.json,.git/hooks/ --allow-write=.git/hooks/ jsr:@hongminhee/deno-task-hooks", + "hooks:pre-commit": { + "dependencies": [ + "check" + ] + }, + "hooks:pre-push": { + "dependencies": [ + "check", + "test" + ] + } + } +} diff --git a/docs/.jsr-cache.file.json b/docs/.jsr-cache.file.json new file mode 100644 index 0000000..1e4ccaa --- /dev/null +++ b/docs/.jsr-cache.file.json @@ -0,0 +1 @@ +{"package":"@logtape/file","version":"0.9.0-dev.1","index":{"getFileSink()":{"kind":[{"char":"f","kind":"Function","title":"Function"}],"name":"getFileSink","file":".","doc":"Get a file sink.\n\nNote that this function is unavailable in the browser.\n","url":"https://jsr.io/@logtape/file@0.9.0-dev.1/doc/~/getFileSink","deprecated":false,"label":"getFileSink()"},"getRotatingFileSink()":{"kind":[{"char":"f","kind":"Function","title":"Function"}],"name":"getRotatingFileSink","file":".","doc":"Get a rotating file sink.\n\nThis sink writes log records to a file, and rotates the file when it reaches\nthe `maxSize`. The rotated files are named with the original file name\nfollowed by a dot and a number, starting from 1. The number is incremented\nfor each rotation, and the maximum number of files to keep is `maxFiles`.\n\nNote that this function is unavailable in the browser.\n","url":"https://jsr.io/@logtape/file@0.9.0-dev.1/doc/~/getRotatingFileSink","deprecated":false,"label":"getRotatingFileSink()"}}} \ No newline at end of file diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 5bdb201..0700c7f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,18 +1,24 @@ +import { transformerTwoslash } from "@shikijs/vitepress-twoslash"; import { jsrRef } from "markdown-it-jsr-ref"; import { defineConfig } from "vitepress"; -import { transformerTwoslash } from "@shikijs/vitepress-twoslash"; const jsrRefVersion = process.env.CI === "true" && process.env.GITHUB_REF_TYPE === "tag" ? "stable" : "unstable"; -const jsrRefPlugin = await jsrRef({ +const jsrRef_logtape = await jsrRef({ package: "@logtape/logtape", version: jsrRefVersion, cachePath: ".jsr-cache.json", }); +const jsrRef_file = await jsrRef({ + package: "@logtape/file", + version: jsrRefVersion, + cachePath: ".jsr-cache.file.json", +}); + let extraNav: { text: string; link: string }[] = []; if (process.env.EXTRA_NAV_TEXT && process.env.EXTRA_NAV_LINK) { extraNav = [ @@ -116,7 +122,8 @@ export default defineConfig({ }), ], config(md) { - md.use(jsrRefPlugin); + md.use(jsrRef_logtape); + md.use(jsrRef_file); }, }, diff --git a/docs/bun.lockb b/docs/bun.lockb index f86d53349c52a55479a57e46933e5dbf918ceefd..99dc4eece4361ea70520c597635243719e70b1cc 100755 GIT binary patch delta 27976 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 rOO(Z7k=b6a9 [!NOTE] > File sink is unavailable in the browser environment. -LogTape provides a file sink as well. Here's an example of a file sink that -writes log messages to a file: +LogTape provides a file sink through a separate package *@logtape/file*: + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@logtape/file +~~~~ + +~~~~ sh [npm] +npm add @logtape/file +~~~~ + +~~~~ sh [pnpm] +pnpm add @logtape/file +~~~~ + +~~~~ sh [Yarn] +yarn add @logtape/file +~~~~ + +~~~~ sh [Bun] +bun add @logtape/file +~~~~ + +::: + +Here's an example of a file sink that writes log messages to a file: ~~~~ typescript twoslash // @noErrors: 2345 -import { configure, getFileSink } from "@logtape/logtape"; +import { getFileSink } from "@logtape/file"; +import { configure } from "@logtape/logtape"; await configure({ sinks: { @@ -181,12 +207,39 @@ the ability to *rotate* the log file when it reaches a certain size. This means: This rotation process helps prevent any single log file from growing too large, which can cause issues with file handling, log analysis, and storage management. -To use the rotating file sink, you can use the `getRotatingFileSink()` function. +To use the rotating file sink, you can use the `getRotatingFileSink()` function, +which is provided by the *@logtape/file* package: + +::: code-group + +~~~~ sh [Deno] +deno add jsr:@logtape/file +~~~~ + +~~~~ sh [npm] +npm add @logtape/file +~~~~ + +~~~~ sh [pnpm] +pnpm add @logtape/file +~~~~ + +~~~~ sh [Yarn] +yarn add @logtape/file +~~~~ + +~~~~ sh [Bun] +bun add @logtape/file +~~~~ + +::: + Here's an example of a rotating file sink that writes log messages to a file: ~~~~ typescript twoslash // @noErrors: 2345 -import { configure, getRotatingFileSink } from "@logtape/logtape"; +import { getRotatingFileSink } from "@logtape/file"; +import { configure } from "@logtape/logtape"; await configure({ sinks: { diff --git a/docs/package.json b/docs/package.json index 857d2d9..764034b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,13 +1,12 @@ { - "dependencies": { - "@logtape/sentry": "^0.1.0", - "@sentry/node": "^8.40.0" - }, "devDependencies": { "@biomejs/biome": "^1.8.3", "@cloudflare/workers-types": "^4.20240909.0", + "@logtape/file": "^0.9.0-dev.1", "@logtape/logtape": "0.9.0-dev.127", + "@logtape/sentry": "^0.1.0", "@logtape/otel": "^0.2.0", + "@sentry/node": "^8.40.0", "@shikijs/vitepress-twoslash": "^1.17.6", "@teidesu/deno-types": "^1.46.3", "@types/bun": "^1.1.9", diff --git a/file/README.md b/file/README.md new file mode 100644 index 0000000..58b1104 --- /dev/null +++ b/file/README.md @@ -0,0 +1,43 @@ + + +File sinks for LogTape +====================== + +[![JSR][JSR badge]][JSR] +[![npm][npm badge]][npm] + +This package provides file sinks for [LogTape]. You can use the file sinks to +write log records to files. For details, read the docs: + + - [File sink] + - [Rotating file sink] + +[JSR]: https://jsr.io/@logtape/file +[JSR badge]: https://jsr.io/badges/@logtape/file +[npm]: https://www.npmjs.com/package/@logtape/file +[npm badge]: https://img.shields.io/npm/v/@logtape/file?logo=npm +[LogTape]: https://logtape.org/ +[File sink]: https://logtape.org/manual/sinks#file-sink +[Rotating file sink]: https://logtape.org/manual/sinks#rotating-file-sink + + +Installation +------------ + +This package is available on [JSR] and [npm]. You can install it for various +JavaScript runtimes and package managers: + +~~~~ sh +deno add @logtape/file # for Deno +npm add @logtape/file # for npm +pnpm add @logtape/file # for pnpm +yarn add @logtape/file # for Yarn +bun add @logtape/file # for Bun +~~~~ + + +Docs +---- + +The docs of LogTape is available at . +For the API references, see . diff --git a/file/deno.json b/file/deno.json new file mode 100644 index 0000000..7fd18fe --- /dev/null +++ b/file/deno.json @@ -0,0 +1,20 @@ +{ + "name": "@logtape/file", + "version": "0.9.0", + "license": "MIT", + "exports": "./mod.ts", + "exclude": [ + "coverage/", + "npm/", + ".dnt-import-map.json" + ], + "tasks": { + "dnt": "deno run -A dnt.ts", + "test:bun": { + "command": "cd npm/ && bun run ./test_runner.js && cd ../", + "dependencies": [ + "dnt" + ] + } + } +} diff --git a/file/dnt.ts b/file/dnt.ts new file mode 100644 index 0000000..45e492f --- /dev/null +++ b/file/dnt.ts @@ -0,0 +1,64 @@ +import { build, emptyDir } from "@deno/dnt"; +import workspace from "../deno.json" with { type: "json" }; +import metadata from "./deno.json" with { type: "json" }; + +await emptyDir("./npm"); + +const imports = { + "@logtape/logtape": "../logtape/mod.ts", + ...workspace.imports, +}; + +await Deno.writeTextFile( + ".dnt-import-map.json", + JSON.stringify({ imports }, undefined, 2), +); + +await build({ + package: { + name: metadata.name, + version: Deno.args[0] ?? metadata.version, + description: "File sink and rotating file sink for LogTape", + keywords: ["logging", "log", "logger", "file", "sink", "rotating"], + license: "MIT", + author: { + name: "Hong Minhee", + email: "hong@minhee.org", + url: "https://hongminhee.org/", + }, + homepage: "https://logtape.org/", + repository: { + type: "git", + url: "git+https://github.com/dahlia/logtape.git", + directory: "file/", + }, + bugs: { + url: "https://github.com/dahlia/logtape/issues", + }, + funding: [ + "https://github.com/sponsors/dahlia", + ], + }, + outDir: "./npm", + entryPoints: ["./mod.ts"], + importMap: "./.dnt-import-map.json", + mappings: { + "./filesink.jsr.ts": "./filesink.node.ts", + "./filesink.deno.ts": "./filesink.node.ts", + }, + shims: { + deno: "dev", + }, + typeCheck: "both", + declaration: "separate", + declarationMap: true, + compilerOptions: { + lib: ["ES2021", "DOM"], + }, + async postBuild() { + await Deno.copyFile("../LICENSE", "npm/LICENSE"); + await Deno.copyFile("README.md", "npm/README.md"); + }, +}); + +// cSpell: ignore Minhee filesink diff --git a/file/filesink.base.ts b/file/filesink.base.ts new file mode 100644 index 0000000..e61b933 --- /dev/null +++ b/file/filesink.base.ts @@ -0,0 +1,158 @@ +import { + defaultTextFormatter, + type LogRecord, + type Sink, + type StreamSinkOptions, +} from "@logtape/logtape"; + +/** + * Options for the {@link getBaseFileSink} function. + */ +export type FileSinkOptions = StreamSinkOptions; + +/** + * A platform-specific file sink driver. + * @typeParam TFile The type of the file descriptor. + */ +export interface FileSinkDriver { + /** + * Open a file for appending and return a file descriptor. + * @param path A path to the file to open. + */ + openSync(path: string): TFile; + + /** + * Write a chunk of data to the file. + * @param fd The file descriptor. + * @param chunk The data to write. + */ + writeSync(fd: TFile, chunk: Uint8Array): void; + + /** + * Flush the file to ensure that all data is written to the disk. + * @param fd The file descriptor. + */ + flushSync(fd: TFile): void; + + /** + * Close the file. + * @param fd The file descriptor. + */ + closeSync(fd: TFile): void; +} + +/** + * Get a platform-independent file sink. + * + * @typeParam TFile The type of the file descriptor. + * @param path A path to the file to write to. + * @param options The options for the sink and the file driver. + * @returns A sink that writes to the file. The sink is also a disposable + * object that closes the file when disposed. + */ +export function getBaseFileSink( + path: string, + options: FileSinkOptions & FileSinkDriver, +): Sink & Disposable { + const formatter = options.formatter ?? defaultTextFormatter; + const encoder = options.encoder ?? new TextEncoder(); + const fd = options.openSync(path); + const sink: Sink & Disposable = (record: LogRecord) => { + options.writeSync(fd, encoder.encode(formatter(record))); + options.flushSync(fd); + }; + sink[Symbol.dispose] = () => options.closeSync(fd); + return sink; +} + +/** + * Options for the {@link getBaseRotatingFileSink} function. + */ +export interface RotatingFileSinkOptions extends FileSinkOptions { + /** + * The maximum bytes of the file before it is rotated. 1 MiB by default. + */ + maxSize?: number; + + /** + * The maximum number of files to keep. 5 by default. + */ + maxFiles?: number; +} + +/** + * A platform-specific rotating file sink driver. + */ +export interface RotatingFileSinkDriver extends FileSinkDriver { + /** + * Get the size of the file. + * @param path A path to the file. + * @returns The `size` of the file in bytes, in an object. + */ + statSync(path: string): { size: number }; + + /** + * Rename a file. + * @param oldPath A path to the file to rename. + * @param newPath A path to be renamed to. + */ + renameSync(oldPath: string, newPath: string): void; +} + +/** + * Get a platform-independent rotating file sink. + * + * This sink writes log records to a file, and rotates the file when it reaches + * the `maxSize`. The rotated files are named with the original file name + * followed by a dot and a number, starting from 1. The number is incremented + * for each rotation, and the maximum number of files to keep is `maxFiles`. + * + * @param path A path to the file to write to. + * @param options The options for the sink and the file driver. + * @returns A sink that writes to the file. The sink is also a disposable + * object that closes the file when disposed. + */ +export function getBaseRotatingFileSink( + path: string, + options: RotatingFileSinkOptions & RotatingFileSinkDriver, +): Sink & Disposable { + const formatter = options.formatter ?? defaultTextFormatter; + const encoder = options.encoder ?? new TextEncoder(); + const maxSize = options.maxSize ?? 1024 * 1024; + const maxFiles = options.maxFiles ?? 5; + let offset: number = 0; + try { + const stat = options.statSync(path); + offset = stat.size; + } catch { + // Continue as the offset is already 0. + } + let fd = options.openSync(path); + function shouldRollover(bytes: Uint8Array): boolean { + return offset + bytes.length > maxSize; + } + function performRollover(): void { + options.closeSync(fd); + for (let i = maxFiles - 1; i > 0; i--) { + const oldPath = `${path}.${i}`; + const newPath = `${path}.${i + 1}`; + try { + options.renameSync(oldPath, newPath); + } catch (_) { + // Continue if the file does not exist. + } + } + options.renameSync(path, `${path}.1`); + offset = 0; + fd = options.openSync(path); + } + const sink: Sink & Disposable = (record: LogRecord) => { + const bytes = encoder.encode(formatter(record)); + if (shouldRollover(bytes)) performRollover(); + options.writeSync(fd, bytes); + options.flushSync(fd); + offset += bytes.length; + }; + sink[Symbol.dispose] = () => options.closeSync(fd); + return sink; +} diff --git a/logtape/filesink.deno.ts b/file/filesink.deno.ts similarity index 83% rename from logtape/filesink.deno.ts rename to file/filesink.deno.ts index 17f459e..0af357e 100644 --- a/logtape/filesink.deno.ts +++ b/file/filesink.deno.ts @@ -1,12 +1,11 @@ -import { webDriver } from "./filesink.web.ts"; +import type { Sink } from "@logtape/logtape"; import { type FileSinkOptions, - getFileSink as getBaseFileSink, - getRotatingFileSink as getBaseRotatingFileSink, + getBaseFileSink, + getBaseRotatingFileSink, type RotatingFileSinkDriver, type RotatingFileSinkOptions, - type Sink, -} from "./sink.ts"; +} from "./filesink.base.ts"; /** * A Deno-specific file sink driver. @@ -42,9 +41,6 @@ export function getFileSink( path: string, options: FileSinkOptions = {}, ): Sink & Disposable { - if ("document" in globalThis) { - return getBaseFileSink(path, { ...options, ...webDriver }); - } return getBaseFileSink(path, { ...options, ...denoDriver }); } @@ -67,9 +63,6 @@ export function getRotatingFileSink( path: string, options: RotatingFileSinkOptions = {}, ): Sink & Disposable { - if ("document" in globalThis) { - return getBaseRotatingFileSink(path, { ...options, ...webDriver }); - } return getBaseRotatingFileSink(path, { ...options, ...denoDriver }); } diff --git a/logtape/filesink.jsr.ts b/file/filesink.jsr.ts similarity index 90% rename from logtape/filesink.jsr.ts rename to file/filesink.jsr.ts index 4284e26..7cb1e9c 100644 --- a/logtape/filesink.jsr.ts +++ b/file/filesink.jsr.ts @@ -1,6 +1,11 @@ -import type { FileSinkOptions, RotatingFileSinkOptions, Sink } from "./sink.ts"; +import type { Sink } from "@logtape/logtape"; +import type { + FileSinkOptions, + RotatingFileSinkOptions, +} from "./filesink.base.ts"; const filesink: Omit = + // dnt-shim-ignore await ("Deno" in globalThis ? import("./filesink.deno.ts") : import("./filesink.node.ts")); diff --git a/logtape/filesink.node.ts b/file/filesink.node.ts similarity index 63% rename from logtape/filesink.node.ts rename to file/filesink.node.ts index 8cf65ae..e84ce51 100644 --- a/logtape/filesink.node.ts +++ b/file/filesink.node.ts @@ -1,34 +1,26 @@ -// @ts-ignore: a trick to avoid module resolution error on non-Node.js environ -import fsMod from "./fs.ts"; -import type fsType from "node:fs"; -import { webDriver } from "./filesink.web.ts"; +import type { Sink } from "@logtape/logtape"; +import fs from "node:fs"; import { type FileSinkOptions, - getFileSink as getBaseFileSink, - getRotatingFileSink as getBaseRotatingFileSink, + getBaseFileSink, + getBaseRotatingFileSink, type RotatingFileSinkDriver, type RotatingFileSinkOptions, - type Sink, -} from "./sink.ts"; - -// @ts-ignore: a trick to avoid module resolution error on non-Node.js environ -const fs = fsMod as (typeof fsType | null); +} from "./filesink.base.ts"; /** * A Node.js-specific file sink driver. */ -export const nodeDriver: RotatingFileSinkDriver = fs == null - ? webDriver - : { - openSync(path: string) { - return fs.openSync(path, "a"); - }, - writeSync: fs.writeSync, - flushSync: fs.fsyncSync, - closeSync: fs.closeSync, - statSync: fs.statSync, - renameSync: fs.renameSync, - }; +export const nodeDriver: RotatingFileSinkDriver = { + openSync(path: string) { + return fs.openSync(path, "a"); + }, + writeSync: fs.writeSync, + flushSync: fs.fsyncSync, + closeSync: fs.closeSync, + statSync: fs.statSync, + renameSync: fs.renameSync, +}; /** * Get a file sink. @@ -44,9 +36,6 @@ export function getFileSink( path: string, options: FileSinkOptions = {}, ): Sink & Disposable { - if ("document" in globalThis) { - return getBaseFileSink(path, { ...options, ...webDriver }); - } return getBaseFileSink(path, { ...options, ...nodeDriver }); } @@ -69,9 +58,6 @@ export function getRotatingFileSink( path: string, options: RotatingFileSinkOptions = {}, ): Sink & Disposable { - if ("document" in globalThis) { - return getBaseRotatingFileSink(path, { ...options, ...webDriver }); - } return getBaseRotatingFileSink(path, { ...options, ...nodeDriver }); } diff --git a/logtape/filesink.test.ts b/file/filesink.test.ts similarity index 66% rename from logtape/filesink.test.ts rename to file/filesink.test.ts index c1f2042..686fce9 100644 --- a/logtape/filesink.test.ts +++ b/file/filesink.test.ts @@ -1,8 +1,59 @@ +import { isDeno } from "@david/which-runtime"; +import type { Sink } from "@logtape/logtape"; import { assertEquals } from "@std/assert/assert-equals"; import { join } from "@std/path/join"; +import fs from "node:fs"; +import { debug, error, fatal, info, warning } from "../logtape/fixtures.ts"; +import { type FileSinkDriver, getBaseFileSink } from "./filesink.base.ts"; import { getFileSink, getRotatingFileSink } from "./filesink.deno.ts"; -import { debug, error, fatal, info, warning } from "./fixtures.ts"; -import type { Sink } from "./sink.ts"; + +Deno.test("getBaseFileSink()", () => { + const path = Deno.makeTempFileSync(); + let sink: Sink & Disposable; + if (isDeno) { + const driver: FileSinkDriver = { + openSync(path: string) { + return Deno.openSync(path, { create: true, append: true }); + }, + writeSync(fd, chunk) { + fd.writeSync(chunk); + }, + flushSync(fd) { + fd.syncSync(); + }, + closeSync(fd) { + fd.close(); + }, + }; + sink = getBaseFileSink(path, driver); + } else { + const driver: FileSinkDriver = { + openSync(path: string) { + return fs.openSync(path, "a"); + }, + writeSync: fs.writeSync, + flushSync: fs.fsyncSync, + closeSync: fs.closeSync, + }; + sink = getBaseFileSink(path, driver); + } + sink(debug); + sink(info); + sink(warning); + sink(error); + sink(fatal); + sink[Symbol.dispose](); + assertEquals( + Deno.readTextFileSync(path), + `\ +2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456! +2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456! +2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456! +2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456! +2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456! +`, + ); +}); Deno.test("getFileSink()", () => { const path = Deno.makeTempFileSync(); diff --git a/file/mod.ts b/file/mod.ts new file mode 100644 index 0000000..194862a --- /dev/null +++ b/file/mod.ts @@ -0,0 +1,7 @@ +export type { + FileSinkDriver, + FileSinkOptions, + RotatingFileSinkDriver, + RotatingFileSinkOptions, +} from "./filesink.base.ts"; +export { getFileSink, getRotatingFileSink } from "./filesink.jsr.ts"; diff --git a/logtape/deno.json b/logtape/deno.json index dbd6603..ff9641e 100644 --- a/logtape/deno.json +++ b/logtape/deno.json @@ -3,32 +3,16 @@ "version": "0.9.0", "license": "MIT", "exports": "./mod.ts", - "imports": { - "@deno/dnt": "jsr:@deno/dnt@^0.41.1", - "@std/assert": "jsr:@std/assert@^0.222.1", - "@std/async": "jsr:@std/async@^0.222.1", - "@std/fs": "jsr:@std/fs@^0.223.0", - "@std/path": "jsr:@std/path@^1.0.2", - "@std/testing": "jsr:@std/testing@^0.222.1", - "consolemock": "npm:consolemock@^1.1.0", - "which_runtime": "https://deno.land/x/which_runtime@0.2.0/mod.ts" - }, "exclude": [ - "coverage/", "npm/" ], - "unstable": [ - "fs" - ], - "lock": false, "tasks": { - "check": "deno check **/*.ts && deno lint && deno fmt --check", - "test": "deno test --allow-read --allow-write", - "coverage": "rm -rf coverage && deno task test --coverage && deno coverage --html coverage", "dnt": "deno run -A dnt.ts", - "test-all": "deno task test && deno task dnt && cd npm/ && bun run ./test_runner.js && cd ../", - "hooks:install": "deno run --allow-read=deno.json,.git/hooks/ --allow-write=.git/hooks/ jsr:@hongminhee/deno-task-hooks", - "hooks:pre-commit": "deno task check", - "hooks:pre-push": "deno task test" + "test:bun": { + "command": "cd npm/ && bun run ./test_runner.js && cd ../", + "dependencies": [ + "dnt" + ] + } } } diff --git a/logtape/dnt.ts b/logtape/dnt.ts index d1ee7a2..42f8421 100644 --- a/logtape/dnt.ts +++ b/logtape/dnt.ts @@ -5,7 +5,7 @@ await emptyDir("./npm"); await build({ package: { - name: "@logtape/logtape", + name: metadata.name, version: Deno.args[0] ?? metadata.version, description: "Simple logging library with zero dependencies for " + "Deno/Node.js/Bun/browsers", @@ -20,6 +20,7 @@ await build({ repository: { type: "git", url: "git+https://github.com/dahlia/logtape.git", + directory: "logtape/", }, bugs: { url: "https://github.com/dahlia/logtape/issues", @@ -30,12 +31,7 @@ await build({ }, outDir: "./npm", entryPoints: ["./mod.ts"], - importMap: "./deno.json", - mappings: { - "./filesink.jsr.ts": "./filesink.node.ts", - "./filesink.deno.ts": "./filesink.node.ts", - "./fs.ts": "./fs.js", - }, + importMap: "../deno.json", shims: { deno: "dev", }, @@ -46,12 +42,6 @@ await build({ lib: ["ES2021", "DOM"], }, async postBuild() { - await Deno.writeTextFile( - "npm/esm/fs.js", - 'import fs from "./fs.cjs";\nexport default fs;\n', - ); - await Deno.copyFile("fs.cjs", "npm/esm/fs.cjs"); - await Deno.copyFile("fs.cjs", "npm/script/fs.js"); await Deno.writeTextFile( "npm/esm/nodeUtil.js", 'import util from "./nodeUtil.cjs";\nexport default util;\n', diff --git a/logtape/filesink.web.ts b/logtape/filesink.web.ts deleted file mode 100644 index e589f56..0000000 --- a/logtape/filesink.web.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { RotatingFileSinkDriver } from "./sink.ts"; - -function notImplemented(): T { - throw new Error("File sink is not available in the browser."); -} - -/** - * A browser-specific file sink driver. All methods throw an error. - */ -export const webDriver: RotatingFileSinkDriver = { - openSync: notImplemented, - writeSync: notImplemented, - flushSync: notImplemented, - closeSync: notImplemented, - statSync: notImplemented, - renameSync: notImplemented, -}; diff --git a/logtape/fs.cjs b/logtape/fs.cjs deleted file mode 100644 index fe20ac6..0000000 --- a/logtape/fs.cjs +++ /dev/null @@ -1,20 +0,0 @@ -let fs = null; -if ( - typeof window === "undefined" && ( - "process" in globalThis && "versions" in globalThis.process && - "node" in globalThis.process.versions && - typeof globalThis.caches === "undefined" && - typeof globalThis.addEventListener !== "function" || - "Bun" in globalThis - ) -) { - try { - // Intentionally confuse static analysis of bundlers: - const $require = [require]; - fs = $require[0](`${["node", "fs"].join(":")}`); - } catch { - fs = null; - } -} - -module.exports = fs; diff --git a/logtape/fs.js b/logtape/fs.js deleted file mode 100644 index 1c2b978..0000000 --- a/logtape/fs.js +++ /dev/null @@ -1 +0,0 @@ -export * from "node:fs"; diff --git a/logtape/fs.ts b/logtape/fs.ts deleted file mode 100644 index 29de881..0000000 --- a/logtape/fs.ts +++ /dev/null @@ -1,22 +0,0 @@ -let fs = null; -if ( - // @ts-ignore: process is a global variable - "process" in globalThis && "versions" in globalThis.process && - // @ts-ignore: process is a global variable - "node" in globalThis.process.versions && - typeof globalThis.caches === "undefined" && - typeof globalThis.addEventListener !== "function" || - "Bun" in globalThis -) { - try { - fs = await import("node" + ":fs"); - } catch (e) { - if (e instanceof TypeError) { - fs = null; - } else { - throw e; - } - } -} - -export default fs; diff --git a/logtape/mod.ts b/logtape/mod.ts index e40f504..7c9a858 100644 --- a/logtape/mod.ts +++ b/logtape/mod.ts @@ -11,7 +11,6 @@ export { resetSync, } from "./config.ts"; export { type ContextLocalStorage, withContext } from "./context.ts"; -export { getFileSink, getRotatingFileSink } from "./filesink.jsr.ts"; export { type Filter, type FilterLike, @@ -42,10 +41,8 @@ export { getLogger, type Logger } from "./logger.ts"; export type { LogRecord } from "./record.ts"; export { type ConsoleSinkOptions, - type FileSinkOptions, getConsoleSink, getStreamSink, - type RotatingFileSinkOptions, type Sink, type StreamSinkOptions, withFilter, diff --git a/logtape/sink.test.ts b/logtape/sink.test.ts index 1497821..7b240e6 100644 --- a/logtape/sink.test.ts +++ b/logtape/sink.test.ts @@ -1,20 +1,11 @@ import { assertEquals } from "@std/assert/assert-equals"; import { assertThrows } from "@std/assert/assert-throws"; import makeConsoleMock from "consolemock"; -import fs from "node:fs"; -import { isDeno } from "which_runtime"; import { debug, error, fatal, info, warning } from "./fixtures.ts"; import { defaultTextFormatter } from "./formatter.ts"; import type { LogLevel } from "./level.ts"; import type { LogRecord } from "./record.ts"; -import { - type FileSinkDriver, - getConsoleSink, - getFileSink, - getStreamSink, - type Sink, - withFilter, -} from "./sink.ts"; +import { getConsoleSink, getStreamSink, withFilter } from "./sink.ts"; Deno.test("withFilter()", () => { const buffer: LogRecord[] = []; @@ -223,51 +214,3 @@ Deno.test("getConsoleSink()", () => { }, ]); }); - -Deno.test("getFileSink()", () => { - const path = Deno.makeTempFileSync(); - let sink: Sink & Disposable; - if (isDeno) { - const driver: FileSinkDriver = { - openSync(path: string) { - return Deno.openSync(path, { create: true, append: true }); - }, - writeSync(fd, chunk) { - fd.writeSync(chunk); - }, - flushSync(fd) { - fd.syncSync(); - }, - closeSync(fd) { - fd.close(); - }, - }; - sink = getFileSink(path, driver); - } else { - const driver: FileSinkDriver = { - openSync(path: string) { - return fs.openSync(path, "a"); - }, - writeSync: fs.writeSync, - flushSync: fs.fsyncSync, - closeSync: fs.closeSync, - }; - sink = getFileSink(path, driver); - } - sink(debug); - sink(info); - sink(warning); - sink(error); - sink(fatal); - sink[Symbol.dispose](); - assertEquals( - Deno.readTextFileSync(path), - `\ -2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456! -2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456! -2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456! -2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456! -2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456! -`, - ); -}); diff --git a/logtape/sink.ts b/logtape/sink.ts index 4a3b31b..22246c2 100644 --- a/logtape/sink.ts +++ b/logtape/sink.ts @@ -165,155 +165,3 @@ export function getConsoleSink(options: ConsoleSinkOptions = {}): Sink { } }; } - -/** - * Options for the {@link getFileSink} function. - */ -export type FileSinkOptions = StreamSinkOptions; - -/** - * A platform-specific file sink driver. - * @typeParam TFile The type of the file descriptor. - */ -export interface FileSinkDriver { - /** - * Open a file for appending and return a file descriptor. - * @param path A path to the file to open. - */ - openSync(path: string): TFile; - - /** - * Write a chunk of data to the file. - * @param fd The file descriptor. - * @param chunk The data to write. - */ - writeSync(fd: TFile, chunk: Uint8Array): void; - - /** - * Flush the file to ensure that all data is written to the disk. - * @param fd The file descriptor. - */ - flushSync(fd: TFile): void; - - /** - * Close the file. - * @param fd The file descriptor. - */ - closeSync(fd: TFile): void; -} - -/** - * Get a platform-independent file sink. - * - * @typeParam TFile The type of the file descriptor. - * @param path A path to the file to write to. - * @param options The options for the sink and the file driver. - * @returns A sink that writes to the file. The sink is also a disposable - * object that closes the file when disposed. - */ -export function getFileSink( - path: string, - options: FileSinkOptions & FileSinkDriver, -): Sink & Disposable { - const formatter = options.formatter ?? defaultTextFormatter; - const encoder = options.encoder ?? new TextEncoder(); - const fd = options.openSync(path); - const sink: Sink & Disposable = (record: LogRecord) => { - options.writeSync(fd, encoder.encode(formatter(record))); - options.flushSync(fd); - }; - sink[Symbol.dispose] = () => options.closeSync(fd); - return sink; -} - -/** - * Options for the {@link getRotatingFileSink} function. - */ -export interface RotatingFileSinkOptions extends FileSinkOptions { - /** - * The maximum bytes of the file before it is rotated. 1 MiB by default. - */ - maxSize?: number; - - /** - * The maximum number of files to keep. 5 by default. - */ - maxFiles?: number; -} - -/** - * A platform-specific rotating file sink driver. - */ -export interface RotatingFileSinkDriver extends FileSinkDriver { - /** - * Get the size of the file. - * @param path A path to the file. - * @returns The `size` of the file in bytes, in an object. - */ - statSync(path: string): { size: number }; - - /** - * Rename a file. - * @param oldPath A path to the file to rename. - * @param newPath A path to be renamed to. - */ - renameSync(oldPath: string, newPath: string): void; -} - -/** - * Get a platform-independent rotating file sink. - * - * This sink writes log records to a file, and rotates the file when it reaches - * the `maxSize`. The rotated files are named with the original file name - * followed by a dot and a number, starting from 1. The number is incremented - * for each rotation, and the maximum number of files to keep is `maxFiles`. - * - * @param path A path to the file to write to. - * @param options The options for the sink and the file driver. - * @returns A sink that writes to the file. The sink is also a disposable - * object that closes the file when disposed. - */ -export function getRotatingFileSink( - path: string, - options: RotatingFileSinkOptions & RotatingFileSinkDriver, -): Sink & Disposable { - const formatter = options.formatter ?? defaultTextFormatter; - const encoder = options.encoder ?? new TextEncoder(); - const maxSize = options.maxSize ?? 1024 * 1024; - const maxFiles = options.maxFiles ?? 5; - let offset: number = 0; - try { - const stat = options.statSync(path); - offset = stat.size; - } catch { - // Continue as the offset is already 0. - } - let fd = options.openSync(path); - function shouldRollover(bytes: Uint8Array): boolean { - return offset + bytes.length > maxSize; - } - function performRollover(): void { - options.closeSync(fd); - for (let i = maxFiles - 1; i > 0; i--) { - const oldPath = `${path}.${i}`; - const newPath = `${path}.${i + 1}`; - try { - options.renameSync(oldPath, newPath); - } catch (_) { - // Continue if the file does not exist. - } - } - options.renameSync(path, `${path}.1`); - offset = 0; - fd = options.openSync(path); - } - const sink: Sink & Disposable = (record: LogRecord) => { - const bytes = encoder.encode(formatter(record)); - if (shouldRollover(bytes)) performRollover(); - options.writeSync(fd, bytes); - options.flushSync(fd); - offset += bytes.length; - }; - sink[Symbol.dispose] = () => options.closeSync(fd); - return sink; -} diff --git a/scripts/check_versions.ts b/scripts/check_versions.ts new file mode 100644 index 0000000..1c85958 --- /dev/null +++ b/scripts/check_versions.ts @@ -0,0 +1,24 @@ +import { dirname, join } from "@std/path"; +import metadata from "../deno.json" with { type: "json" }; + +const root = dirname(import.meta.dirname!); +const versions: Record = {}; + +for (const member of metadata.workspace) { + const file = join(root, member, "deno.json"); + const json = await Deno.readTextFile(file); + const data = JSON.parse(json); + versions[join(member, "deno.json")] = data.version; +} +let version: string | undefined; + +for (const file in versions) { + if (version != null && versions[file] !== version) { + console.error("Versions are inconsistent:"); + for (const file in versions) { + console.error(` ${file}: ${versions[file]}`); + } + Deno.exit(1); + } + version = versions[file]; +} diff --git a/scripts/update_versions.ts b/scripts/update_versions.ts new file mode 100644 index 0000000..3b51255 --- /dev/null +++ b/scripts/update_versions.ts @@ -0,0 +1,19 @@ +import { dirname, join } from "@std/path"; +import metadata from "../deno.json" with { type: "json" }; + +const root = dirname(import.meta.dirname!); + +if (Deno.args.length < 1) { + console.error("error: no argument"); + Deno.exit(1); +} + +const version = Deno.args[0]; + +for (const member of metadata.workspace) { + const file = join(root, member, "deno.json"); + const json = await Deno.readTextFile(file); + const data = JSON.parse(json); + data.version = version; + await Deno.writeTextFile(file, JSON.stringify(data, undefined, 2)); +}