From 1666c2f38130654bac78349ef81f62531eb01422 Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:45:11 -0400 Subject: [PATCH 1/5] add setWasmModuleOptions() to override where the WASM backend fetches cld.web.wasm from: - wasm/browser-entry.js: export setWasmModuleOptions({ locateFile }), stored module-level and passed to createCldModule() -- Emscripten's own glue already checks Module["locateFile"] before falling back to its bundled-relative-URL default, so this is purely additive - index.d.ts: types for the new export - README.md: document the override for bundlers that serve cld.web.wasm from a non-default path - fix browser-entry.js's metadata.json import to use the `with { type: 'json' }` attribute Node's ESM loader requires -- without it the real browser entry point couldn't be loaded via plain Node import() at all, which is what made it impossible to test - test/runner-wasm-browser.js: confirms the override is actually honored, not just accepted -- spawns the real browser-entry.js in a child process with no override (fails, since the default WASM URL resolves to a file:// URL that Node's fetch() can't handle) and again with locateFile pointed at a local HTTP server serving the real wasm binary (succeeds with a correct detection), so success in the second case can only come from the override taking effect - package.json: add the new test to npm test --- README.md | 11 ++++++ index.d.ts | 5 +++ package.json | 2 +- test/runner-wasm-browser.js | 76 +++++++++++++++++++++++++++++++++++++ wasm/browser-entry.js | 14 ++++++- 5 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 test/runner-wasm-browser.js diff --git a/README.md b/README.md index eba3070..0eb6e73 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,17 @@ const result = await detect('This is a language recognition example'); console.log(result); ``` +If your bundler serves the WASM binary from a non-default path (a CDN, a renamed/hashed asset, etc.), call `setWasmModuleOptions({ locateFile })` before the first `detect()` call to override where `cld.web.wasm` is fetched from: +```js +import { detect, setWasmModuleOptions } from 'cld-universal'; + +setWasmModuleOptions({ + locateFile: (path) => `/assets/${path}`, +}); + +const result = await detect('This is a language recognition example'); +``` + ## Result shape `detect()` resolves to (or, in callback form, passes as the second argument) an object of the following shape: diff --git a/index.d.ts b/index.d.ts index 01dc991..6ba6160 100644 --- a/index.d.ts +++ b/index.d.ts @@ -29,6 +29,11 @@ export const LANGUAGES: {[name: string]: string}; export const DETECTED_LANGUAGES: string[]; export const ENCODINGS: string[]; +// Browser entry point only (./wasm/browser-entry.js). Overrides where the +// WASM backend fetches cld.web.wasm from -- has no effect on the native +// backend. Must be called before the first detect() call. +export declare function setWasmModuleOptions(options: { locateFile?: (path: string, scriptDirectory: string) => string }): void; + export declare function detect(text: string, options: Options, callback: (err: string, result: DetectLanguage) => void): void; export declare function detect(text: string, callback: (err: string, result: DetectLanguage) => void): void; export declare function detect(text: string, options: Options): Promise; diff --git a/package.json b/package.json index 974de01..2d33358 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "install": "node bin/build-native.js", "build": "node-gyp rebuild", "build:wasm": "scripts/build-wasm.sh", - "test": "node test/runner.js && node test/runner-wasm.js", + "test": "node test/runner.js && node test/runner-wasm.js && node test/runner-wasm-browser.js", "postinstall": "node bin/postinstall.js" }, "author": { diff --git a/test/runner-wasm-browser.js b/test/runner-wasm-browser.js new file mode 100644 index 0000000..168a932 --- /dev/null +++ b/test/runner-wasm-browser.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node +// Proves wasm/browser-entry.js's setWasmModuleOptions({ locateFile }) hook +// actually controls where the WASM backend fetches cld.web.wasm from -- +// not just that the option is accepted, but that redirecting it to a +// custom URL is what makes detection succeed. +// +// Each case runs in its own child process: browser-entry.js caches +// moduleOptions/modulePromise at module scope on first use, so "no +// override" and "with override" can't share one process/import. +// +// With no override, browser-entry.js's default WASM URL resolves against +// import.meta.url, which is a file:// URL here -- and Node's fetch() +// doesn't support file://, so detect() is expected to reject. That's not a +// Node quirk being worked around, it's exactly the gap the override exists +// for, and it doubles as proof that the override (not some other +// already-working path) is what makes the second case below succeed. + +const assert = require('assert'); +const http = require('http'); +const path = require('path'); +const fs = require('fs'); +const { execFile } = require('child_process'); +const { promisify } = require('util'); + +const execFileAsync = promisify(execFile); + +const SAMPLE = require('./data').basic[0]; +assert.equal(SAMPLE.name, 'ENGLISH'); + +const wasmBinary = fs.readFileSync(path.join(__dirname, '..', 'wasm', 'dist', 'cld.web.wasm')); + +// Runs the child asynchronously (not execFileSync): the second case below +// needs an HTTP server alive *in this same process* to answer the child's +// request, which a synchronous, event-loop-blocking child_process call +// would deadlock against. +function runChild(script) { + return execFileAsync(process.execPath, ['-e', script], { encoding: 'utf8', cwd: __dirname }); +} + +(async () => { + const withoutOverride = ` + import('../wasm/browser-entry.js') + .then(m => m.detect(${JSON.stringify(SAMPLE.sample)})) + .then(() => { console.log('UNEXPECTED_SUCCESS'); process.exit(0); }) + .catch(() => { console.log('EXPECTED_FAILURE'); process.exit(0); }); + `; + let out1; + try { + out1 = (await runChild(withoutOverride)).stdout; + } catch (e) { + throw new Error(`child process crashed running detect() without an override: ${e.message}`); + } + assert.match(out1, /EXPECTED_FAILURE/, 'detect() should fail without a locateFile override (file:// is not fetchable in Node)'); + + const server = http.createServer((req, res) => res.end(wasmBinary)); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const port = server.address().port; + + try { + const withOverride = ` + import('../wasm/browser-entry.js').then(async m => { + m.setWasmModuleOptions({ locateFile: () => 'http://127.0.0.1:${port}/custom-path/cld.web.wasm' }); + const result = await m.detect(${JSON.stringify(SAMPLE.sample)}); + console.log(JSON.stringify(result.languages[0])); + process.exit(0); + }).catch(e => { console.error(e.stack); process.exit(1); }); + `; + const out2 = (await runChild(withOverride)).stdout; + const topLanguage = JSON.parse(out2.trim().split('\n').pop()); + assert.equal(topLanguage.name, 'ENGLISH', 'detect() with a locateFile override should still detect correctly'); + } finally { + server.close(); + } + + console.log('Browser entry setWasmModuleOptions()/locateFile override verified'); +})(); diff --git a/wasm/browser-entry.js b/wasm/browser-entry.js index f7dc312..31fbc90 100644 --- a/wasm/browser-entry.js +++ b/wasm/browser-entry.js @@ -6,18 +6,28 @@ // the native path at all -- it goes straight to the WASM backend. import createCldModule from './dist/cld.web.mjs'; -import meta from '../lib/metadata.json'; +import meta from '../lib/metadata.json' with { type: 'json' }; import { createDetect } from '../lib/detect-shape.js'; import { wrapWasmModule } from '../lib/wasm-wrap.js'; +let moduleOptions = null; let modulePromise = null; function loadBackend() { if (!modulePromise) { - modulePromise = createCldModule().then(wrapWasmModule); + modulePromise = createCldModule(moduleOptions ?? {}).then(wrapWasmModule); } return modulePromise; } +// Lets consumers override where cld.web.wasm is fetched from (e.g. when a +// bundler moves wasm assets to a different path/CDN than the JS glue +// expects by default). Must be called before the first detect() call -- +// loadBackend() only reads moduleOptions the first time it instantiates +// the module. +export function setWasmModuleOptions(options) { + moduleOptions = options; +} + export const LANGUAGES = meta.LANGUAGES; export const DETECTED_LANGUAGES = meta.DETECTED_LANGUAGES; export const ENCODINGS = meta.ENCODINGS; From 731a4dfbf9addee96ec7deee57aa20cabe4cdf63 Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:57:10 -0400 Subject: [PATCH 2/5] chore: add changelog - add a fork changelog with entries for 2.11.0 and unreleased changes --- CHANGELOG.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..75b90db --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +This fork ([`cld-universal`](https://github.com/figulusproject/cld-universal)) diverged from +[`dachev/node-cld`](https://github.com/dachev/node-cld) at `2.10.1`. Entries below start from that +point. + +## [Unreleased] + +### Added + +- `setWasmModuleOptions({ locateFile })` in the browser entry point, letting bundlers/apps override + where the WASM backend fetches `cld.web.wasm` from when it isn't served at the default relative + path. + +### Fixed + +- Browser entry point's `metadata.json` import now uses the `with { type: 'json' }` attribute + required by Node's ESM loader. + +## [2.11.0] - 2026-08-02 + +### Added + +- WASM fallback backend: the native addon is tried first, and the package now falls back to a + bundled WASM build (built via Emscripten from the same CLD2 sources) when the native addon is + unavailable, including in the browser and in environments without a C++ toolchain. +- GitHub Actions CI covering both the native and WASM backends across Node 20/22, replacing the + old Travis/AppVeyor setup. +- npm publish workflow. + +### Changed + +- Package renamed from `cld` to `cld-universal`; metadata, description, keywords, and + repository/bugs/homepage links updated to point at this fork. +- README rewritten to document the fork's scope, maintenance expectations, and the WASM fallback. +- Dropped support for Node 8/10/12; `engines` now requires `20 || >=22`. +- `postinstall.js` updated to use `globSync` (required by the `glob` upgrade below). +- Install step (`bin/build-native.js`) now always exits `0` after attempting `node-gyp rebuild`, so + a missing or broken C++ toolchain no longer fails `npm install` before the WASM fallback is + reachable. +- `package.json` now declares an explicit `files` field (previously unset, so publishing silently + fell back to `.gitignore` rules). + +### Fixed + +- Missing emsdk setup step in the publish workflow. +- AppVeyor/Windows (MSVC) build failures: pinned `node-addon-api` to `^2.0.0`, moved + `NODE_API_MODULE` out of the `NodeCld` namespace, and bumped the CI image to Visual Studio 2022. + +### Security + +- Updated `glob` from `7.x` to `^12` (fixes 2 high severity advisories, including the + brace-expansion DoS in [GHSA-mh99-v99m-4gvg](https://github.com/advisories/GHSA-mh99-v99m-4gvg)) + while retaining support for Node < 20. +- Updated `underscore` from `^1.12.1` to `^1.13.7` (fixes a high severity advisory). +- Updated `minimatch` from `3.0.4` to `3.1.2`. +- Migrated the lockfile from v1 to v3. From 9ebc1fe04944a600f61690a1e66425d6b4ae55a3 Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:07:18 -0400 Subject: [PATCH 3/5] workflow fixes: - add read-only permissions to ci and security workflows - rename branches from `master` to `main` in security workflow --- .github/workflows/ci.yml | 3 +++ .github/workflows/security.yml | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d54bfad..f450bdf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: push: pull_request: +permissions: + contents: read + jobs: test: strategy: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4f31384..abe8b35 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -2,14 +2,17 @@ name: Security on: push: - branches: [master] + branches: [main] pull_request: - branches: [master] + branches: [main] schedule: # Weekly, to catch newly disclosed CVEs even when nothing in the repo # has changed. CodeQL has its own schedule in codeql.yml. - cron: '0 6 * * 1' +permissions: + contents: read + jobs: npm-audit: name: npm audit From adf270f0f5fa9f37094fed3c3bae8441f980a91f Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:10:46 -0400 Subject: [PATCH 4/5] chore: add workflow changes to changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b90db..06d362b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ point. - Browser entry point's `metadata.json` import now uses the `with { type: 'json' }` attribute required by Node's ESM loader. +### Security + +- Added a CodeQL Advanced workflow (`codeql.yml`) analyzing the `actions`, `c-cpp`, and + `javascript-typescript` languages on push/PR to `main` and weekly on a schedule. +- Added a `security.yml` workflow running `npm audit --audit-level=high` against the package's + npm dependencies on push/PR to `main` and weekly on a schedule, separate from the CodeQL + workflow above. +- Restricted the default `GITHUB_TOKEN` permissions to `contents: read` on the `ci.yml` and + `security.yml` workflows. + ## [2.11.0] - 2026-08-02 ### Added From 0fb8941bedd45df8eb73a74110e446ecaa608f29 Mon Sep 17 00:00:00 2001 From: figulusproject <269854178+figulusproject@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:16:18 -0400 Subject: [PATCH 5/5] version++ --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d362b..a037e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This fork ([`cld-universal`](https://github.com/figulusproject/cld-universal)) d [`dachev/node-cld`](https://github.com/dachev/node-cld) at `2.10.1`. Entries below start from that point. -## [Unreleased] +## [2.11.1] - 2026-08-03 ### Added diff --git a/package-lock.json b/package-lock.json index 48cbff2..05ddf14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cld-universal", - "version": "2.11.0", + "version": "2.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cld-universal", - "version": "2.11.0", + "version": "2.11.1", "hasInstallScript": true, "dependencies": { "glob": "^12", diff --git a/package.json b/package.json index 2d33358..41f0a58 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "wasm", "webassembly" ], - "version": "2.11.0", + "version": "2.11.1", "main": "./index.js", "types": "./index.d.ts", "exports": {