diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cf459d9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cSpell.words": ["jsonminify", "minijson"] +} diff --git a/Readme.md b/Readme.md index e5fba89..0085008 100644 --- a/Readme.md +++ b/Readme.md @@ -24,8 +24,7 @@ dub build --config=executable --build=release-nobounds --compiler=ldc2 ``` - Download Native Binaries from -https://github.com/aminya/minijson/releases/latest - + https://github.com/aminya/minijson/releases/latest ### CLI Usage diff --git a/package.json b/package.json index 0c3b531..e5a2173 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "format.d": "dub run --build=release --quiet dfmt -- --soft_max_line_length 110 --indent_size 2 --inplace ./src ./benchmark", "lint": "eslint . --fix", "prepublishOnly": "shx rm -rf ./dist/tsconfig.tsbuildinfo ./dist/*.zip ./dist/build.* && chmod +x ./dist/*/minijson", + "start.benchmark.js": "node ./benchmark/js-benchmark.mjs", "start.benchmark.node": "node ./benchmark/native-benchmark.mjs", "start.browser": "servor ./dist/ --browse --reload", "start.node": "node ./dist/node/cli.js", diff --git a/src/native/cli.d b/src/native/cli.d index 54ff9a3..0faffde 100644 --- a/src/native/cli.d +++ b/src/native/cli.d @@ -3,27 +3,24 @@ module minijson.cli; import minijson.lib : minifyFiles, minifyStrings; import argparse; -@(Command("minijson") - .Description(`minijson: minify json files with support for comments +@(Command("minijson").Description(`minijson: minify json files with support for comments # Minify the specified files minijson ./dist/**/*.json ./build/a.json + minijson file1_with_comment.json file2_with_comment.json - # Minify the specified files (supports comments) - minijson --comment file1_with_comment.json file2_with_comment.json + # Minify the specified files and disable comment support for faster minification + minijson --comment=false file1_no_comment.json file2_no_comment.json # Minify the specified json string minijson --str '{"some_json": "string_here"}' - - # Minify the specified json string (supports comments) - minijson --comment --str '{"some_json": "string_here"} //comment' + minijson --str '{"some_json": "string_here"} //comment' More information at https://github.com/aminya/minijson - `) -) + `)) struct Options { - bool comment = false; + bool comment = true; string[] str; // (Deprecated) A list of files to minify (for backwards compatiblitity with getopt) string[] file; diff --git a/src/native/lib.d b/src/native/lib.d index 249aa67..bb512df 100644 --- a/src/native/lib.d +++ b/src/native/lib.d @@ -12,12 +12,66 @@ const tokenizerNoComment = ctRegex!(`[\n\r"[]]`, "g"); Params: jsonString = the json string you want to minify - hasComment = a boolean to support comments in json. Default: `false`. + hasComment = a boolean to support comments in json. Default: `true`. Return: the minified json string */ -string minifyString(in string jsonString, in bool hasComment = false) @trusted +string minifyString(in string jsonString, in bool hasComment = true) @trusted +{ + return hasComment ? minifyStringWithComments(jsonString) : minifyStringNoComments(jsonString); +} + +string minifyStringNoComments(in string jsonString) @trusted +{ + auto in_string = false; + string result; + size_t from = 0; + auto rightContext = ""; + + auto match = jsonString.matchAll(tokenizerNoComment); + + while (!match.empty()) + { + const matchFrontHit = match.front().hit(); + + rightContext = match.post(); + + // update from for the next iteration + const prevFrom = from; + from = jsonString.length - rightContext.length; // lastIndex + + auto leftContextSubstr = match.pre()[prevFrom .. $]; + const noLeftContext = leftContextSubstr.length == 0; + if (!noLeftContext) + { + if (!in_string) + { + leftContextSubstr = remove_spaces(leftContextSubstr); + } + result ~= leftContextSubstr; + } + if (matchFrontHit == "\"") + { + if (!in_string || noLeftContext || hasNoSlashOrEvenNumberOfSlashes(leftContextSubstr)) + { + // start of string with ", or unescaped " character found to end string + in_string = !in_string; + } + --from; // include " character in next catch + rightContext = jsonString[from .. $]; + } + else if (notSlashAndNoSpaceOrBreak(matchFrontHit)) + { + result ~= matchFrontHit; + } + match.popFront(); + } + result ~= rightContext; + return result; +} + +string minifyStringWithComments(in string jsonString) @trusted { auto in_string = false; auto in_multiline_comment = false; @@ -26,9 +80,7 @@ string minifyString(in string jsonString, in bool hasComment = false) @trusted size_t from = 0; auto rightContext = ""; - const tokenizer = !hasComment ? tokenizerNoComment : tokenizerWithComment; - - auto match = jsonString.matchAll(tokenizer); + auto match = jsonString.matchAll(tokenizerWithComment); while (!match.empty()) { @@ -40,10 +92,9 @@ string minifyString(in string jsonString, in bool hasComment = false) @trusted const prevFrom = from; from = jsonString.length - rightContext.length; // lastIndex - const notInComment = (!in_multiline_comment && !in_singleline_comment); - const noCommentOrNotInComment = !hasComment || notInComment; + const notInComment = !in_multiline_comment && !in_singleline_comment; - if (noCommentOrNotInComment) + if (notInComment) { auto leftContextSubstr = match.pre()[prevFrom .. $]; const noLeftContext = leftContextSubstr.length == 0; @@ -67,7 +118,7 @@ string minifyString(in string jsonString, in bool hasComment = false) @trusted } } // comments - if (hasComment && !in_string) + if (!in_string) { if (notInComment) { @@ -93,7 +144,7 @@ string minifyString(in string jsonString, in bool hasComment = false) @trusted in_singleline_comment = false; } } - else if (!hasComment && notSlashAndNoSpaceOrBreak(matchFrontHit)) + else if (notSlashAndNoSpaceOrBreak(matchFrontHit)) { result ~= matchFrontHit; } @@ -176,7 +227,7 @@ private bool hasNoSpace(const ref string str) @trusted Return: the minified json strings */ -string[] minifyStrings(in string[] jsonStrings, in bool hasComment = false) @trusted +string[] minifyStrings(in string[] jsonStrings, in bool hasComment = true) @trusted { import std.algorithm : map; import std.array : array; @@ -191,7 +242,7 @@ string[] minifyStrings(in string[] jsonStrings, in bool hasComment = false) @tru paths = the paths to the files. It could be glob patterns. hasComment = a boolean to support comments in json. Default: `false`. */ -void minifyFiles(in string[] paths, in bool hasComment = false) @trusted +void minifyFiles(in string[] paths, in bool hasComment = true) @trusted { import std.parallelism : parallel; import std.algorithm; @@ -202,33 +253,70 @@ void minifyFiles(in string[] paths, in bool hasComment = false) @trusted import std.stdio : writeln; // get the files from the given paths (resolve glob patterns) - auto files = paths - .map!((path) { - if (path.exists) + auto files = paths.map!((path) { + if (path.exists) + { + if (path.isFile) { - if (path.isFile) - { - return [path]; - } - else if (path.isDir) - { - return glob(path ~ "/**/*.json"); - } - else - { - throw new Exception("The given path is not a file or a directory: " ~ path); - } + return [path]; + } + else if (path.isDir) + { + return glob(path ~ "/**/*.json"); } else { - return glob(path); + throw new Exception("The given path is not a file or a directory: " ~ path); } - }) - .joiner() - .array(); + } + else + { + return glob(path); + } + }).joiner().array(); + + if (files.empty) + { + writeln("No files found."); + return; + } + + if (!confirmExtension(files)) + { + return; + } foreach (file; files.parallel()) { write(file, minifyString(readText(file), hasComment)); } } + +bool confirmExtension(string[] files) @trusted +{ + auto confirmExtension = false; + import std.path : extension; + + foreach (file; files) + { + // if the file extension is not json, jsonc, or json5, confirm before minifying + auto fileExtension = file.extension(); + if (fileExtension != ".json" && fileExtension != ".jsonc" && fileExtension != ".json5") + { + if (!confirmExtension) + { + import std.stdio : readln, writeln; + + writeln("The file ", file, " doesn't have a json extension. Do you want to minify it? (y/n)"); + auto input = readln(); + confirmExtension = input == "y"; + if (!confirmExtension) + { + return false; + } + } + } + } + + return true; +} diff --git a/src/native/libc.d b/src/native/libc.d index c8b19eb..78e6dcb 100644 --- a/src/native/libc.d +++ b/src/native/libc.d @@ -7,12 +7,12 @@ import minijson.lib : minifyString; Params: jsonString = the json string you want to minify - hasComment = a boolean to support comments in json. Default: `false`. + hasComment = a boolean to support comments in json. Default: `true`. Return: the minified json string */ -extern (C) auto c_minifyString(char* jsonCString, bool hasComment = false) +extern (C) auto c_minifyString(char* jsonCString, bool hasComment = true) { import std : fromStringz, toStringz; diff --git a/src/node/lib.ts b/src/node/lib.ts index c8553d6..2ad32c3 100644 --- a/src/node/lib.ts +++ b/src/node/lib.ts @@ -7,51 +7,66 @@ import { join } from "path" * Minify all the given JSON files in place. It minifies the files in parallel. * * @param files An array of paths to the files - * @param hasComment A boolean to support comments in json. Default `false`. + * @param hasComment A boolean to support comments in json. Default `true`. * @returns {Promise} Returns a void promise that resolves when all the files are minified * @throws {Promise} The promise is rejected with the reason for failure */ -export async function minifyFiles(files: string[], hasComment = false): Promise { - if (process.platform === "darwin" && process.arch === "arm64") { - // fallback to jasonminify due to missing ARM64 binaries - // eslint-disable-next-line @typescript-eslint/no-var-requires - const jsonminify = require("jsonminify") - await Promise.all(files.map(async (file) => { - const jsonString = await readFile(file, "utf8") - const minifiedJsonString = jsonminify(jsonString) as string - await writeFile(file, minifiedJsonString) - })) - return - } - - const filesNum = files.length - if (filesNum === 0) { - return Promise.resolve() - } - - const args = [...files] - const spliceUpper = 2 * filesNum - 2 +export async function minifyFiles(files: readonly string[], hasComment = true): Promise { + try { + const filesNum = files.length + if (filesNum === 0) { + return Promise.resolve() + } - for (let iSplice = 0; iSplice <= spliceUpper; iSplice += 2) { - args.splice(iSplice, 0, "--file") + await spawnMinijson([...files, hasComment ? "--comment=true" : "--comment=false"]) + } catch (e) { + console.error(e, "Falling back to jsonminify") + await minifyFilesFallback(files) } +} - if (hasComment) { - args.push("--comment") - } +/** + * Spawn minijson with the given arguments + * + * @param args An array of arguments + * @returns {Promise} Returns a promise that resolves to stdout output string when the operation finishes + * @throws {Promise} The promise is rejected with the reason for failure + */ +export function spawnMinijson(args: string[]): Promise { + return new Promise((resolve, reject) => { + execFile(minijsonBin, args, (err, stdout, stderr) => { + if (err) { + reject(err) + } + if (stderr !== "") { + reject(stderr) + } + resolve(stdout) + }) + }) +} - await spawnMinijson(args) +async function minifyFilesFallback(files: readonly string[]) { + const jsonminify = require("jsonminify") + await Promise.all( + files.map(async (file) => { + const jsonString = await readFile(file, "utf8") + const minifiedJsonString = jsonminify(jsonString) as string + await writeFile(file, minifiedJsonString) + }), + ) + return } /** * Minify the given JSON string * * @param jsonString The json string you want to minify - * @param hasComment A boolean to support comments in json. Default `false`. + * @param hasComment A boolean to support comments in json. Default `true`. * @returns {Promise} The minified json string * @throws {Promise} The promise is rejected with the reason for failure */ -export async function minifyString(jsonString: string, hasComment = false): Promise { +export async function minifyString(jsonString: string, hasComment = true): Promise { const args = ["--str", jsonString] if (hasComment) { args.push("--comment") @@ -69,24 +84,3 @@ const minijsonBin = join(__dirname, `${process.platform}-${process.arch}`, binNa if (process.platform !== "win32") { chmodSync(minijsonBin, 0o755) } - -/** - * Spawn minijson with the given arguments - * - * @param args An array of arguments - * @returns {Promise} Returns a promise that resolves to stdout output string when the operation finishes - * @throws {Promise} The promise is rejected with the reason for failure - */ -export function spawnMinijson(args: string[]): Promise { - return new Promise((resolve, reject) => { - execFile(minijsonBin, args, (err, stdout, stderr) => { - if (err) { - reject(err) - } - if (stderr !== "") { - reject(stderr) - } - resolve(stdout) - }) - }) -}