Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
node_modules/
npm-debug.log

coverage/
.nyc_output/
dist/
types/
tmp/
!.vscode/
2 changes: 1 addition & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require('colors');

const minimist = require('minimist');
const glob = require('glob-promise');
const EncodingChecker = require('../src/index');
const EncodingChecker = require('../dist/index');

const argv = minimist(process.argv.slice(2), {
string: ['pattern', 'p', 'd', 'ignore-encoding', 'i'],
Expand Down
4 changes: 3 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
module.exports = require('./src/index');
'use strict';

module.exports = require('./dist/index');
4,834 changes: 1,436 additions & 3,398 deletions package-lock.json

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
"scripts": {
"clear": "rm -rf coverage/",
"clear:all": "npm run clear && rm -rf node_modules/",
"test": "jest --verbose",
"coverage": "jest --coverage"
"check-ts": "tsc --noEmit",
"prebuild": "rm -rf dist/ types/",
"build": "tsc",
"test": "vitest run",
"coverage": "vitest run --coverage"
},
"dependencies": {
"colors": "^1.4.0",
Expand All @@ -22,8 +25,10 @@
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"jest": "^30.2.0"
"@types/node": "^22.10.0",
"@vitest/coverage-v8": "^2.1.5",
"typescript": "^5.6.3",
"vitest": "^2.1.5"
},
"repository": {
"type": "git",
Expand All @@ -34,12 +39,13 @@
},
"files": [
"bin",
"src",
"dist",
"types",
"index.js",
"package.json",
"README.md",
"!**/*.spec.*",
"LICENSE"
"LICENSE",
"!**/*.spec.*"
],
"keywords": [
"cli",
Expand All @@ -53,7 +59,8 @@
"get",
"utf-8"
],
"main": "./index.js",
"main": "index.js",
"types": "types/index.d.ts",
"bin": {
"encoding-checker": "bin/cli.js"
}
Expand Down
9 changes: 9 additions & 0 deletions src/colors.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The `colors` package (loaded for its side effects in `bin/cli.js`) patches
// `String.prototype` with color accessors. Declare the ones used in the source
// so TypeScript recognizes them without a type assertion.
interface String {
red: string;
blue: string;
gray: string;
green: string;
}
51 changes: 0 additions & 51 deletions src/index.js

This file was deleted.

83 changes: 43 additions & 40 deletions src/index.spec.js → src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
"use strict";

const verify = require("./index").verify;
const glob = require("glob-promise");
const fs = require("fs");
const path = require("path");
const jschardet = require("jschardet");
import { describe, it, expect, vi } from "vitest";
import { verify } from "./index";
import glob from "glob-promise";
import * as fs from "fs";
import * as path from "path";
import * as jschardet from "jschardet";

// Partial mock: keep the real `detect` implementation by default so most tests
// exercise actual encoding detection, while still allowing individual tests to
// override the return value. The mocked module is shared with the code under
// test, so overrides take effect there.
vi.mock("jschardet", async (importOriginal) => {
const actual = await importOriginal<typeof import("jschardet")>();
return {
...actual,
detect: vi.fn(actual.detect),
};
});

describe("General", () => {
it("should works with single file", () => {
Expand Down Expand Up @@ -76,9 +87,9 @@ describe("General", () => {
expect(result.length).toEqual(1);

// One should be a valid file with encoding
const validResult = result.find(r => r.file === validFile);
const validResult = result.find((r) => r.file === validFile);
expect(validResult).toBeDefined();
expect(validResult.encoding).toEqual("ascii");
expect(validResult?.encoding).toEqual("ascii");
});
});

Expand All @@ -95,7 +106,7 @@ describe("General", () => {
const tmpFile = path.join(__dirname, "temp-test-file.bin");

// Create a file with binary content that might be hard to detect
const buffer = Buffer.from([0xFF, 0xFE, 0x00, 0x00]);
const buffer = Buffer.from([0xff, 0xfe, 0x00, 0x00]);
fs.writeFileSync(tmpFile, buffer);

try {
Expand All @@ -113,7 +124,7 @@ describe("General", () => {

it("should process multiple files concurrently", async () => {
const files = await glob("*.js");
const validFiles = files.filter(file => {
const validFiles = files.filter((file) => {
try {
return fs.lstatSync(file).isFile();
} catch (e) {
Expand All @@ -132,18 +143,14 @@ describe("General", () => {
});

it("should handle fs.readFile errors gracefully", async () => {
// Mock fs.readFile to simulate an error
const originalReadFile = fs.readFile;
const testFile = __filename;

// We need to actually trigger the error path in fetchCharset
// Create a test file, then make it unreadable
// We need to actually trigger the error path in fetchCharset.
// Create a test file, then make it unreadable.
const tmpFile = path.join(__dirname, "unreadable-test.txt");
fs.writeFileSync(tmpFile, "test content");

try {
// Change permissions to make it unreadable (works on Unix-like systems)
if (process.platform !== 'win32') {
if (process.platform !== "win32") {
fs.chmodSync(tmpFile, 0o000);

const result = await verify("utf-8", [tmpFile]);
Expand All @@ -154,7 +161,7 @@ describe("General", () => {
} finally {
// Clean up - restore permissions and delete
try {
if (process.platform !== 'win32') {
if (process.platform !== "win32") {
fs.chmodSync(tmpFile, 0o644);
}
fs.unlinkSync(tmpFile);
Expand All @@ -165,8 +172,8 @@ describe("General", () => {
});

it("should handle files with undetectable encoding", async () => {
// Create a file with content that might result in no encoding detection
// Empty files or files with very little data might not be detected properly
// Create a file with content that might result in no encoding detection.
// Empty files or files with very little data might not be detected properly.
const tmpFile = path.join(__dirname, "empty-test.txt");
fs.writeFileSync(tmpFile, "");

Expand Down Expand Up @@ -194,48 +201,44 @@ describe("General", () => {
});

it("should handle when jschardet returns null encoding", async () => {
// Mock jschardet to return null encoding
const originalDetect = jschardet.detect;
const tmpFile = path.join(__dirname, "mock-test.txt");
fs.writeFileSync(tmpFile, "test");

try {
// Mock jschardet.detect to return null encoding
jschardet.detect = jest.fn(() => ({ encoding: null }));
// Mock jschardet.detect to return null encoding for this single call.
vi.mocked(jschardet.detect).mockReturnValueOnce({
encoding: null,
confidence: 0,
});

// Use a different ignore encoding so it shows up in results
try {
// Use a different ignore encoding so it shows up in results.
const result = await verify("utf-8", [tmpFile]);
expect(result).toEqual(expect.any(Array));
// When encoding is null, it returns 'unknown', which is filtered out
// if it doesn't match the ignore encoding
// When encoding is null, it returns 'unknown', which is kept
// because it doesn't match the ignored encoding.
expect(result.length).toEqual(1);
} finally {
// Restore original function
jschardet.detect = originalDetect;
// Clean up
if (fs.existsSync(tmpFile)) {
fs.unlinkSync(tmpFile);
}
}
});

it("should handle when jschardet returns undefined encoding", async () => {
// Mock jschardet to return undefined encoding
const originalDetect = jschardet.detect;
const tmpFile = path.join(__dirname, "mock-test2.txt");
fs.writeFileSync(tmpFile, "test");

try {
// Mock jschardet.detect to return undefined encoding
jschardet.detect = jest.fn(() => ({ encoding: undefined }));
// Mock jschardet.detect to return undefined encoding for this call.
vi.mocked(jschardet.detect).mockReturnValueOnce({
encoding: undefined,
confidence: 0,
});

try {
const result = await verify("utf-8", [tmpFile]);
expect(result).toEqual(expect.any(Array));
expect(result.length).toEqual(1);
} finally {
// Restore original function
jschardet.detect = originalDetect;
// Clean up
if (fs.existsSync(tmpFile)) {
fs.unlinkSync(tmpFile);
}
Expand Down
51 changes: 51 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as fs from "fs";
import * as jschardet from "jschardet";

export interface EncodingResult {
file?: string;
encoding?: string;
error?: NodeJS.ErrnoException;
}

function fetchCharset(file: string): Promise<EncodingResult> {
return new Promise<EncodingResult>((resolve) => {
fs.readFile(file, (error, data) => {
if (error) {
return resolve({
error: error,
});
}

const result = jschardet.detect(data);
const encoding = result.encoding
? result.encoding.toLowerCase()
: "unknown".red;

resolve({
file: file,
encoding: encoding,
});
});
});
}

function isFile(path: string): boolean {
try {
return fs.lstatSync(path).isFile();
} catch (error) {
return false;
}
}

export async function verify(
ignoreEncoding: string,
matches: string[]
): Promise<EncodingResult[]> {
const files = matches.filter(isFile);

const charset = await Promise.all(files.map(fetchCharset));

return charset.filter(({ encoding }) => {
return encoding !== ignoreEncoding;
});
}
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2020",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"declarationDir": "types"
},
"include": ["src/**/*.ts"],
"exclude": ["**/*.spec.ts", "node_modules", "dist", "types"]
}
Loading