From 1784e437b6173a75168eca546647c3e24be49ab9 Mon Sep 17 00:00:00 2001 From: wellwelwel <46850407+wellwelwel@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:53:54 -0300 Subject: [PATCH] feat: support for direct (global) browser import --- .gitignore | 1 + .prettierignore | 1 + CONTRIBUTING.md | 2 + README.md | 14 ++- package-lock.json | 68 +++++++++++++ package.json | 21 ++++- src/index.ts | 162 +++++++++++++++---------------- tools/browserfy.ts | 231 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 413 insertions(+), 87 deletions(-) create mode 100644 tools/browserfy.ts diff --git a/.gitignore b/.gitignore index 9c53102..2a10141 100755 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules .DS_Store deno.lock /lib +/browser diff --git a/.prettierignore b/.prettierignore index 5f9c485..67839f0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ /lib /CHANGELOG.md +/browser diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6f204c2..956339e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,8 @@ bun run test:bun deno task test:deno ``` +Also, run `npm run build` to compile and run _E2E_ tests in the virtual browser. + --- ### Lint diff --git a/README.md b/README.md index dcba901..ba49d44 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub Workflow Status (Bun)](https://img.shields.io/github/actions/workflow/status/wellwelwel/jsonc.min/ci_bun.yml?event=push&label=&branch=main&logo=bun&logoColor=ffffff&color=f368e0)](https://github.com/wellwelwel/jsonc.min/actions/workflows/ci_bun.yml?query=branch%3Amain) [![GitHub Workflow Status (Deno)](https://img.shields.io/github/actions/workflow/status/wellwelwel/jsonc.min/ci_deno.yml?event=push&label=&branch=main&logo=deno&logoColor=ffffff&color=079992)](https://github.com/wellwelwel/jsonc.min/actions/workflows/ci_deno.yml?query=branch%3Amain) -✨ Faster and safer JSON and JSONC minify, parse and stringify for JavaScript (Browser compatible) — **2.6kB**. +✨ Faster and safer JSON and JSONC minify, parse and stringify for JavaScript (Browser compatible) — **2.3kB**. @@ -31,8 +31,6 @@ ## Install -[![Install Size](https://packagephobia.com/badge?p=jsonc.min)](https://pkg-size.dev/jsonc.min) - ```bash # Node.js npm i jsonc.min @@ -54,14 +52,24 @@ deno add npm:jsonc.min ### Import +#### ES Modules + ```js import { JSONC } from 'jsonc.min'; ``` +#### CommonJS + ```js const { JSONC } = require('jsonc.min'); ``` +#### Browser + +```html + +``` + --- ### `toJSON` diff --git a/package-lock.json b/package-lock.json index 0687980..aca1155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,9 @@ "license": "MIT", "devDependencies": { "@biomejs/biome": "^1.8.3", + "@types/node": "^22.5.0", + "esbuild": "^0.23.1", + "happy-dom": "^15.0.0", "packages-update": "^2.0.0", "poku": "^2.5.0", "prettier": "^3.3.3", @@ -598,6 +601,29 @@ "node": ">=18" } }, + "node_modules/@types/node": { + "version": "22.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz", + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/esbuild": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", @@ -666,6 +692,21 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/happy-dom": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.0.0.tgz", + "integrity": "sha512-DsvANUcxxY20iCo3Yllm7dqwzPVPduGfVFxa7mONwMBLczFeQgkN0LpDir1kIY322JMh+hrcPV3aGLyHCESDlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/packages-update": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/packages-update/-/packages-update-2.0.0.tgz", @@ -759,6 +800,33 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } } } } diff --git a/package.json b/package.json index 56f8233..f89ccbe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jsonc.min", "version": "1.0.1", - "description": "✨ Faster and safer JSON and JSONC minify, parse and stringify for JavaScript (Browser compatible) — 2.6kB.", + "description": "✨ Faster and safer JSON and JSONC minify, parse and stringify for JavaScript (Browser compatible) — 2.3kB.", "main": "./lib/index.js", "license": "MIT", "repository": { @@ -17,6 +17,7 @@ "url": "https://github.com/sponsors/wellwelwel" }, "files": [ + "browser", "lib" ], "engines": { @@ -25,17 +26,23 @@ "deno": ">=1.30.0" }, "scripts": { - "build": "tsc", + "prebuild": "rm -rf ./browser ./lib", + "build:browser": "tsx tools/browserfy.ts", + "build": "tsc && npm run build:browser", "test:node": "poku --node -p", "test:bun": "poku --bun -p", "test:deno": "poku --deno -p", "lint": "npx @biomejs/biome lint && prettier --check .", "lint:fix": "npx @biomejs/biome lint --write && prettier --write .github/workflows/*.yml .", "update": "pu minor && npm i && npm audit fix", - "postupdate": "npm run lint:fix" + "postupdate": "npm run lint:fix", + "size": "ls -lh lib/index.js | awk '{print $5}'" }, "devDependencies": { "@biomejs/biome": "^1.8.3", + "@types/node": "^22.5.0", + "esbuild": "^0.23.1", + "happy-dom": "^15.0.0", "packages-update": "^2.0.0", "poku": "^2.5.0", "prettier": "^3.3.3", @@ -50,6 +57,12 @@ "parse", "parser", "stringify", - "jsonc-to-json" + "jsonc-to-json", + "node", + "nodejs", + "bun", + "deno", + "typescript", + "browser" ] } diff --git a/src/index.ts b/src/index.ts index 6e626df..ce372a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,105 +1,107 @@ -class JsoncProcessor { - toJSON(content: string): string { - const length = content.length; - - let inBlockComment = false; - let inString = false; - let skipChar = false; - let result = ''; - - for (let i = 0; i < length; i++) { - const char = content[i]; +export const JSONC = (() => { + class JsoncProcessor { + toJSON(content: string): string { + const length = content.length; + + let inBlockComment = false; + let inString = false; + let skipChar = false; + let result = ''; + + for (let i = 0; i < length; i++) { + const char = content[i]; + + if (skipChar) { + skipChar = false; + continue; + } - if (skipChar) { - skipChar = false; - continue; - } + if (inBlockComment) { + if (char === '*' && content[i + 1] === '/') { + inBlockComment = false; + skipChar = true; + } - if (inBlockComment) { - if (char === '*' && content[i + 1] === '/') { - inBlockComment = false; - skipChar = true; + continue; } - continue; - } + if (inString) { + if (char === '"' && content[i - 1] !== '\\') { + inString = false; + } - if (inString) { - if (char === '"' && content[i - 1] !== '\\') { - inString = false; + result += char; + continue; } - result += char; - continue; - } + if (char === '"') { + inString = true; + result += char; + continue; + } - if (char === '"') { - inString = true; - result += char; - continue; - } + if (char === '/' && content[i + 1] === '*') { + inBlockComment = true; + skipChar = true; + continue; + } - if (char === '/' && content[i + 1] === '*') { - inBlockComment = true; - skipChar = true; - continue; - } + if (char === '/' && content[i + 1] === '/') { + while (i < length && content[i] !== '\n') { + i++; + } - if (char === '/' && content[i + 1] === '/') { - while (i < length && content[i] !== '\n') { - i++; + continue; } - continue; + result += char; } - result += char; + return result; } - return result; - } + parse( + text: string, + reviver?: (this: unknown, key: string, value: unknown) => unknown + ): T { + const cleanContent = this.toJSON(text); - parse( - text: string, - reviver?: (this: unknown, key: string, value: unknown) => unknown - ): T { - const cleanContent = this.toJSON(text); + return JSON.parse(cleanContent, reviver); + } - return JSON.parse(cleanContent, reviver); - } + stringify( + value: unknown, + replacer?: (this: unknown, key: string, value: unknown) => unknown, + space?: string | number + ): string; + stringify( + value: unknown, + replacer?: (number | string)[] | null, + space?: string | number + ): string; + stringify( + value: unknown, + replacer?: + | ((this: unknown, key: string, value: unknown) => unknown) + | (number | string)[] + | null, + space?: string | number + ): string { + const source = typeof value === 'string' ? this.parse(value) : value; + + if (typeof replacer === 'function') { + return JSON.stringify(source, replacer, space); + } - stringify( - value: unknown, - replacer?: (this: unknown, key: string, value: unknown) => unknown, - space?: string | number - ): string; - stringify( - value: unknown, - replacer?: (number | string)[] | null, - space?: string | number - ): string; - stringify( - value: unknown, - replacer?: - | ((this: unknown, key: string, value: unknown) => unknown) - | (number | string)[] - | null, - space?: string | number - ): string { - const source = typeof value === 'string' ? this.parse(value) : value; - - if (typeof replacer === 'function') { return JSON.stringify(source, replacer, space); } - return JSON.stringify(source, replacer, space); - } + minify(content: string): string { + const parsedConfig = this.parse(content); - minify(content: string): string { - const parsedConfig = this.parse(content); - - return JSON.stringify(parsedConfig, null, 0); + return JSON.stringify(parsedConfig, null, 0); + } } -} -export const JSONC = new JsoncProcessor(); + return new JsoncProcessor(); +})(); diff --git a/tools/browserfy.ts b/tools/browserfy.ts new file mode 100644 index 0000000..2a0d456 --- /dev/null +++ b/tools/browserfy.ts @@ -0,0 +1,231 @@ +import { readFile } from 'node:fs/promises'; +import esbuild from 'esbuild'; +import { Window } from 'happy-dom'; +import { assert, test } from 'poku'; + +(async () => { + const contents = (await readFile('src/index.ts', 'utf8')) + .replace(/export/im, '') + .replace(/(let|const) /gim, 'var '); + + await esbuild.build({ + stdin: { + contents, + loader: 'ts', + }, + platform: 'browser', + target: 'es6', + bundle: false, + outfile: 'browser/jsonc.min.js', + minify: true, + }); + + test('Browser (E2E)', async () => { + const content = ` + /** Configuração do Servidor */ + + /** Configuração do Servidor **/ + /** + Configuração do Servidor + **/ + + /* + Configuração do Servidor + */ + /** + Configuração do Servidor + */ + /* + Configuração do Servidor + **/ + /** + * Configuração do Servidor # // + **/ + + { + // Configuração do Servidor + "server": { + "host": "localhost", // Endereço do servidor + "port": 8080, /* Porta do servidor */ + + /* Configurações de segurança */ + "security": { + "https": true, // Habilitar HTTPS + "certificatePath": "/path/to/cert.pem", /* Caminho para o certificado */ + "keyPath": "/path/to/key.pem" // Caminho para a chave privada + } + }, + + // Configurações de banco de dados + "database": { + "type": "mysql", /* Tipo de banco de dados */ + "host": "db.example.com", // Endereço do banco de dados + "port": 3306, /* Porta do banco de dados */ + "username": "db_user", // Nome de usuário do banco de dados + "password": "db_pass", /* Senha do banco de dados */ + "name": "app_db", // Nome do banco de dados + + /* Configurações avançadas */ + "pool": { + "min": 2, // Conexões mínimas no pool + "max": 10, /* Conexões máximas no pool */ + "idleTimeoutMillis": 30000 // Tempo máximo de ociosidade (ms) + } + }, + + // Configurações de log + "logging": { + "level": "debug", /* Nível de log */ + "file": "/var/log/app.log", // Caminho do arquivo de log + "maxSize": "10m", /* Tamanho máximo do arquivo de log */ + "maxFiles": "5", // Número máximo de arquivos de log + "format": "json" /* Formato do log */ + }, + + /* Configurações de API */ + "api": { + "basePath": "/api/v1", // Caminho base da API + "timeout": 5000, /* Timeout da API (ms) */ + "retries": 3 // Número de tentativas em caso de falha + }, + + // Configurações de autenticação + "auth": { + "jwtSecret": "supersecretkey", /* Chave secreta para JWT */ + "tokenExpiration": "1h", // Expiração do token + "refreshTokenExpiration": "7d" /* Expiração do token de atualização */ + }, + + /* Configurações de cache */ + "cache": { + "enabled": true, // Habilitar cache + "type": "redis", /* Tipo de cache */ + "host": "cache.example.com", // Endereço do cache + "port": 6379, /* Porta do cache */ + "ttl": 3600 // Tempo de vida do cache (s) + } + } +`; + + const window = new Window(); + const jsoncMin = await readFile('browser/jsonc.min.js', 'utf8'); + + window.eval(jsoncMin); + + assert.deepEqual( + // @ts-expect-error + window.JSONC.parse(content), + { + server: { + host: 'localhost', + port: 8080, + security: { + https: true, + certificatePath: '/path/to/cert.pem', + keyPath: '/path/to/key.pem', + }, + }, + database: { + type: 'mysql', + host: 'db.example.com', + port: 3306, + username: 'db_user', + password: 'db_pass', + name: 'app_db', + pool: { min: 2, max: 10, idleTimeoutMillis: 30000 }, + }, + logging: { + level: 'debug', + file: '/var/log/app.log', + maxSize: '10m', + maxFiles: '5', + format: 'json', + }, + api: { basePath: '/api/v1', timeout: 5000, retries: 3 }, + auth: { + jwtSecret: 'supersecretkey', + tokenExpiration: '1h', + refreshTokenExpiration: '7d', + }, + cache: { + enabled: true, + type: 'redis', + host: 'cache.example.com', + port: 6379, + ttl: 3600, + }, + }, + 'paser' + ); + + assert.equal( + // @ts-expect-error + window.JSONC.minify(content), + `{"server":{"host":"localhost","port":8080,"security":{"https":true,"certificatePath":"/path/to/cert.pem","keyPath":"/path/to/key.pem"}},"database":{"type":"mysql","host":"db.example.com","port":3306,"username":"db_user","password":"db_pass","name":"app_db","pool":{"min":2,"max":10,"idleTimeoutMillis":30000}},"logging":{"level":"debug","file":"/var/log/app.log","maxSize":"10m","maxFiles":"5","format":"json"},"api":{"basePath":"/api/v1","timeout":5000,"retries":3},"auth":{"jwtSecret":"supersecretkey","tokenExpiration":"1h","refreshTokenExpiration":"7d"},"cache":{"enabled":true,"type":"redis","host":"cache.example.com","port":6379,"ttl":3600}}`, + 'minify' + ); + + assert.equal( + // @ts-expect-error + window.JSONC.stringify(content, null, 2), + `{ + "server": { + "host": "localhost", + "port": 8080, + "security": { + "https": true, + "certificatePath": "/path/to/cert.pem", + "keyPath": "/path/to/key.pem" + } + }, + "database": { + "type": "mysql", + "host": "db.example.com", + "port": 3306, + "username": "db_user", + "password": "db_pass", + "name": "app_db", + "pool": { + "min": 2, + "max": 10, + "idleTimeoutMillis": 30000 + } + }, + "logging": { + "level": "debug", + "file": "/var/log/app.log", + "maxSize": "10m", + "maxFiles": "5", + "format": "json" + }, + "api": { + "basePath": "/api/v1", + "timeout": 5000, + "retries": 3 + }, + "auth": { + "jwtSecret": "supersecretkey", + "tokenExpiration": "1h", + "refreshTokenExpiration": "7d" + }, + "cache": { + "enabled": true, + "type": "redis", + "host": "cache.example.com", + "port": 6379, + "ttl": 3600 + } +}`, + 'stringify (from a string)' + ); + + assert.equal( + // @ts-expect-error + window.JSONC.stringify({ a: 123 }, null, 2), + `{ + "a": 123 +}`, + 'stringify' + ); + }); +})();