UNPKG

update-file-content

Version:

A simple utility for executing RegEx replacement on files, powered by stream.

421 lines (379 loc) 12.4 kB
import { updateFileContent, updateFiles } from "../src/index.mjs"; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { Readable } from "stream"; import { createWriteStream, existsSync, promises as fsp } from "fs"; import assert from "assert"; const __dirname = dirname(fileURLToPath(import.meta.url)); describe("update files" ,() => { const char_map = "dbdbdbThisIsADumbTestbbbsms".split(""); const dump$ = [ "-dumplings", "-limitations", "-truncate-self", "-empty" ]; const dump_ = [ "-truncate-pipe", "-premature" ]; it("should pipe one Readable to multiple dumps", async () => { let counter = 0; await updateFiles({ readStream: new Readable({ highWaterMark: 100, read (size) { for(let i = 0; i < size; i++) { this.push( Math.random() > .6 ? char_map[i % char_map.length].toUpperCase() : char_map[i % char_map.length].toLowerCase() ) } if(++counter > 90) { this.push(null); } else { this.push(",\n"); } } }), writeStream: dump$.map(id => createWriteStream(join(__dirname, `./dump${id}`)) ), separator: /(?=,\n)/, search: /dum(b)/i, replacement: "pling", encoding: "utf-8" }); dump$.forEach(id => assert.ok(existsSync(join(__dirname, `./dump${id}`)))); }); it("should have replaced /dum(b)/i to dumpling (while preserving dum's case)", async () => { await fsp.readFile(join(__dirname, `./dump${dump$[0]}`), "utf-8") .then(result => { assert.strictEqual( null, /dum(b)/i.exec(result) ); return updateFileContent({ file: join(__dirname, `./dump${dump$[0]}`), separator: /,(?=\n)/i, search: /(.+?)(dumpling)/i, replacement: "$2 " // this is a full replacement }); }) }); it("should have global and partial limitations in replacement amount", async () => { await updateFileContent({ file: join(__dirname, `./dump${dump$[1]}`), search: /((.|\n){15})/, replacement: "^^^^^^^1^^^^^^^", // 15 limit: 1 }); await updateFileContent({ file: join(__dirname, `./dump${dump$[1]}`), replace: [{ search: /(([^^]){13})/, replacement: "%%%%%%2%%%%%%", // 13 limit: 2 }], limit: 1 }); await fsp.readFile(join(__dirname, `./dump${dump$[1]}`), "utf-8") .then(result => { assert.strictEqual( "^^^^^^^1^^^^^^^%%%%%%2%%%%%%", result.slice(0, 15 + 13) ); assert.ok( !result.slice(15 + 13, result.length).includes("%") ); }) }); it("should check arguments", async () => { await assert.rejects( () => updateFileContent({ file: "", search: /(.|\n)*/, replacement: () => "", limit: 88 }), { name: "TypeError", message: "updateFileContent: options.file is invalid." } ); await assert.rejects( () => updateFileContent({ file: "./", search: /(.|\n)*/, replacement: () => "", limit: 88 }), /(^.*?EISDIR)|(filepath \.\/ is invalid\.$)/ ); await assert.rejects( () => updateFileContent({ file: "./", replace: [ { search: /((.|\n)*)/, replacement: "$1" }, { search: /[a-z]{5}.{5}/i, replacement: "-" }, ], join: part => part === "" ? "" : part.concat("\n"), limit: 88 }), { name: "TypeError", message: "update-file-content: received non-function full replacement $1 while limit being specified" } ); await assert.rejects( () => updateFileContent({ file: "./", replace: [ { search: /((.|\n)*)/, replacement: "$1" }, { search: /[a-z]{5}.{5}/i, replacement: "-" }, ], join: null, limit: 88 }), { name: "TypeError", message: "update-file-content: options.join null is invalid." } ) }); describe("truncation & limitation", () => { it("truncating the rest when limitations reached", async () => { await updateFileContent({ // search string with limit file: join(__dirname, `./dump${dump$[1]}`), replace: [{ search: "%%", replacement: "---3---%%", // 7 limit: 2 }], limit: 1, truncate: true }); await fsp.readFile(join(__dirname, `./dump${dump$[1]}`), "utf-8") .then(result => { assert.strictEqual( "^^^^^^^1^^^^^^^---3---%%%%%%2%%%%%%", result.slice(0, 15 + 13 + 7) ); assert.ok( result.lastIndexOf(",") === result.length - 1 ); assert.ok( !result.slice(15 + 13 + 7, result.length).includes("%") ); }) }); it("not: self rw-stream", async () => { await fsp.readFile(join(__dirname, `./dump${dump$[2]}`), "utf-8") .then( result => assert.strictEqual( 90, result.match(/\n/g).length ) ) await updateFileContent({ file: join(__dirname, `./dump${dump$[2]}`), separator: /,/, search: /(.|\n)+/, // must + rather than * otherwise '' will be captured too replacement: () => "", // full replacement limit: 88, // totally there are 91 lines, // 90 of them are prefixed with \n, except the first one truncate: false }); await fsp.readFile(join(__dirname, `./dump${dump$[2]}`), "utf-8") .then( result => { assert.strictEqual( 91 - 88, (result.match(/\n/g) || []).length ); fsp.writeFile( join(__dirname, `./dump${dump$[2]}`), `Checked✅: ${91 - 88} lines prefixed with \\n left\n` .concat(result) ); } ); }); it("not: piping stream", async () => { let counter = 0; await updateFileContent({ from: new Readable({ highWaterMark: 20, read (size) { counter++; if(counter === 10) { return this.push("==SEALED==\n"); } if(counter === 15) { this.push("==END=="); return this.push(null); } for(let i = 0; i < size; i++) { this.push(`${Math.random()} `); } this.push(",\n"); } }), to: createWriteStream(join(__dirname, `./dump${dump_[0]}`)), separator: /,\n/, join: part => part === "" ? "" : part.concat(",\n"), search: /.+/, replacement: () => "", limit: 9, truncate: false }); await fsp.readFile(join(__dirname, `./dump${dump_[0]}`), "utf-8") .then( result => assert.strictEqual( "==SEALED==\n", result.slice(0, 11) ) ); }) }); describe("corner cases", () => { xit("can handle empty content", async () => { //NOTE: Error: EBADF: bad file descriptor, read try { await updateFileContent({ file: join(__dirname, `./dump${dump$[3]}`), separator: /,/, search: /(.|\n)+/, replacement: () => "", // full replacement limit: 88, truncate: true }); await fsp.readFile(join(__dirname, `./dump${dump$[3]}`), "utf-8") .then(result => assert.strictEqual( '', result )); } catch (err) { assert.strictEqual( "Cannot read property 'then' of undefined", err.message ); // } }); it("can handle premature stream close", async () => { // streams by themselves can only propagate errors up but not down. const writeStream = createWriteStream(join(__dirname, `./dump${dump_[1]}`)) // .once("error", () => logs.push("Event: writeStream errored")) // .once("close", () => logs.push("Event: writeStream closed")) // see https://github.com/edfus/update-file-content/runs/1641959273 ; writeStream.destroy = new Proxy(writeStream.destroy, { apply (target, thisArg, argumentsList) { logs.push("Proxy: writeStream.destroy.apply"); return target.apply(thisArg, argumentsList); } }) const logs = []; let counter = 0; try { await updateFileContent({ from: new Readable({ highWaterMark: 5, read (size) { for(let i = 0; i < size; i++) { if(++counter > 10) { logs.push("I will destroy the Readable now"); this.destroy(); process.nextTick(() => writeStream.destroyed && logs.push(`nextTick: writeStream.destroyed: true`)) return ; } else { this.push(`${Math.random()},\n`); } } } }).once("close", () => logs.push("Event: readStream closed")), to: writeStream, separator: /,/, join: "$", search: /.$/, replacement: () => "" }); } catch (err) { logs.push(`catch: Error ${err.message}`); } finally { assert.strictEqual( [ "I will destroy the Readable now", "Event: readStream closed", "Proxy: writeStream.destroy.apply", "nextTick: writeStream.destroyed: true", // "Event: writeStream errored", "catch: Error Premature close" ].join(" -> "), logs.join(" -> ") ) } }) }) describe("try-on", () => { it("can handle files larger than 16KiB", async () => { const processFiles = (await import("../examples/helpers/process-files.mjs")).processFiles ; await resolveNodeDependencies( "/node_modules/three/build/three.module.js", "three" ); async function resolveNodeDependencies (from, to) { const handler = async file => { if(/\.tmp$/.test(file)) return ; const options = { file, search: new RegExp( `${/\s*from\s*['"]/.source}(${from.replace(/\//g, "\/")})${/['"]/.source}`, "g" ), replacement: to } await fsp.readFile(file, "utf-8") .then(result => fsp.writeFile(file.concat(".tmp"), result.replace ( options.search, to ) ) ) await updateFileContent(options); await fsp.readFile(file, "utf-8") .then(async result => { const should_be = await fsp.readFile(file.concat(".tmp"), "utf-8"); assert.strictEqual( should_be, result ); }) }; if(!existsSync(join(__dirname, "./dump/"))) return ; await processFiles(join(__dirname, "./dump/"), handler); } }); }); //TODO: test encoding });