Skip to content

Commit 2f56960

Browse files
authored
Fix opencc-jieba npm config resolution (#1197)
* Fix opencc-jieba npm config resolution * Add Windows prebuild for opencc-jieba npm package * Declare as manual * Set version number for npm opencc-jieba to 1.3.1
1 parent 274fe69 commit 2f56960

12 files changed

Lines changed: 280 additions & 36 deletions

File tree

node/README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,18 +189,24 @@ Some OpenCC configurations use jieba segmentation. Install the optional
189189
npm install opencc opencc-jieba
190190
```
191191

192-
When `opencc-jieba` is installed, the JavaScript API can load its configs
193-
automatically:
192+
When `opencc-jieba` is installed, the JavaScript API and npm `opencc` CLI can
193+
load its configs automatically:
194194

195195
```js
196196
import OpenCC from 'opencc';
197197

198-
const converter = new OpenCC('s2twp_jieba.json');
198+
const converter = new OpenCC('s2twp_jieba');
199199
console.log(converter.convertSync('软件鼠标'));
200200
```
201201

202-
The npm `opencc` CLI does not support plugin-backed segmentation. Use the
203-
JavaScript API or the native OpenCC CLI for those workflows.
202+
The npm CLI also accepts the same config names for text conversion:
203+
204+
```bash
205+
opencc -c s2twp_jieba
206+
```
207+
208+
Use the native OpenCC CLI for diagnostic modes such as `--inspect` and
209+
`--segmentation`.
204210

205211
## Related npm Packages
206212

node/README.zh.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,17 +185,23 @@ CLI 的相對配置路徑會從目前工作目錄解析。在 JavaScript API 中
185185
npm install opencc opencc-jieba
186186
```
187187

188-
安裝 `opencc-jieba` 後,JavaScript API 可以自動載入其中的配置:
188+
安裝 `opencc-jieba` 後,JavaScript API 與 npm `opencc` CLI 可以自動
189+
載入其中的配置:
189190

190191
```js
191192
import OpenCC from 'opencc';
192193

193-
const converter = new OpenCC('s2twp_jieba.json');
194+
const converter = new OpenCC('s2twp_jieba');
194195
console.log(converter.convertSync('软件鼠标'));
195196
```
196197

197-
npm `opencc` CLI 不支援插件分詞。這類工作流程請使用 JavaScript API
198-
或原生 OpenCC CLI。
198+
npm CLI 也可使用相同配置名稱進行文字轉換:
199+
200+
```bash
201+
opencc -c s2twp_jieba
202+
```
203+
204+
`--inspect``--segmentation` 等診斷模式仍請使用原生 OpenCC CLI。
199205

200206
## 相關 npm 套件
201207

@@ -231,4 +237,3 @@ npm test
231237

232238
若要為發布建置預編譯原生 addon,請參考
233239
[`node/PUBLISHING.md`](./PUBLISHING.md)
234-

node/cli.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ const BUILT_IN_CONFIG_NAMES = new Set(BUILT_IN_CONFIGS.map(([name]) => name));
2424
const BUILT_IN_CONFIG_STEMS = new Set(
2525
BUILT_IN_CONFIGS.map(([name]) => name.replace(/\.json$/, ''))
2626
);
27+
const OPTIONAL_JIEBA_CONFIGS = new Set([
28+
's2hk_jieba.json',
29+
's2t_jieba.json',
30+
's2tw_jieba.json',
31+
's2twp_jieba.json',
32+
'tw2sp_jieba.json',
33+
]);
34+
const OPTIONAL_JIEBA_CONFIG_STEMS = new Set(
35+
Array.from(OPTIONAL_JIEBA_CONFIGS).map((name) => name.replace(/\.json$/, ''))
36+
);
2737

2838
function printHelp() {
2939
console.log(`Open Chinese Convert (OpenCC) npm Command Line Tool
@@ -41,7 +51,6 @@ Options:
4151
Unsupported in the npm CLI:
4252
--inspect Use the native OpenCC CLI for inspection output.
4353
--segmentation Use the native OpenCC CLI for segmentation output.
44-
plugins Plugin-backed segmentation is not supported by this npm CLI.
4554
4655
Built-in Configurations:
4756
${BUILT_IN_CONFIGS.map(([name, description]) => ` ${name.padEnd(11)} ${description}`).join('\n')}
@@ -129,14 +138,18 @@ function writeOutput(outputFileName, text) {
129138
}
130139

131140
function resolveConfigPath(config) {
132-
if (BUILT_IN_CONFIG_NAMES.has(config) || path.isAbsolute(config)) {
141+
if (BUILT_IN_CONFIG_NAMES.has(config) || OPTIONAL_JIEBA_CONFIGS.has(config) || path.isAbsolute(config)) {
133142
return config;
134143
}
135144

136145
if (!config.endsWith('.json') && BUILT_IN_CONFIG_STEMS.has(config)) {
137146
return config + '.json';
138147
}
139148

149+
if (!config.endsWith('.json') && OPTIONAL_JIEBA_CONFIG_STEMS.has(config)) {
150+
return config + '.json';
151+
}
152+
140153
return path.resolve(process.cwd(), config);
141154
}
142155

node/opencc.js

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,34 @@ const getAssetsPath = function (bindingPath) {
4444
return bindingDir;
4545
};
4646

47+
function requireOptionalPeer(packageName) {
48+
const searchPaths = [
49+
process.cwd(),
50+
path.join(__dirname, '..'),
51+
__dirname,
52+
];
53+
for (const searchPath of searchPaths) {
54+
try {
55+
return require(require.resolve(packageName, { paths: [searchPath] }));
56+
} catch (e) {
57+
// Try the next search path.
58+
}
59+
}
60+
return null;
61+
}
62+
4763
// Detect optional opencc-jieba plugin package.
4864
// When installed, jieba-based configs (e.g. s2twp_jieba.json) become available.
49-
let jiebaInfo = null;
50-
try {
51-
jiebaInfo = require('opencc-jieba');
52-
} catch (e) {
53-
// opencc-jieba not installed — jieba configs won't be available.
54-
}
65+
const jiebaInfo = requireOptionalPeer('opencc-jieba');
5566

5667
const assetsPath = getAssetsPath(bindingPath);
68+
const isAbsolutePath = function (filePath) {
69+
return path.isAbsolute(filePath) || /^[A-Za-z]:[\\/]/.test(filePath);
70+
};
71+
5772
const getConfigPath = function (config) {
5873
let configPath = config;
59-
if (config[0] !== '/' && config[1] !== ':') {
74+
if (!isAbsolutePath(config)) {
6075
// Resolve relative path
6176
configPath = path.join(assetsPath, config);
6277
}
@@ -112,6 +127,29 @@ function patchConfigPaths(config, jieba, mainAssetsDir) {
112127
}
113128
}
114129

130+
function resolveJiebaConfigPath(config, jieba) {
131+
if (!jieba || isAbsolutePath(config)) {
132+
return null;
133+
}
134+
if (typeof jieba.resolveConfigPath === 'function') {
135+
return jieba.resolveConfigPath(config);
136+
}
137+
const candidates = [config];
138+
if (!config.endsWith('.json')) {
139+
candidates.push(config + '.json');
140+
}
141+
for (const candidate of candidates) {
142+
if (candidate.includes('/') || candidate.includes('\\')) {
143+
continue;
144+
}
145+
const configPath = path.join(jieba.dataDir, candidate);
146+
if (fs.existsSync(configPath)) {
147+
return configPath;
148+
}
149+
}
150+
return null;
151+
}
152+
115153
/**
116154
* OpenCC Node.js API
117155
*
@@ -127,17 +165,15 @@ const OpenCC = module.exports = function (config) {
127165
// When opencc-jieba is installed, check if the requested config is a jieba
128166
// config. If so, load its JSON, patch all paths to absolute, and pass the
129167
// patched JSON string directly to the C++ layer via NewFromString.
130-
if (jiebaInfo && config[0] !== '/' && config[1] !== ':') {
131-
const jiebaConfigPath = path.join(jiebaInfo.dataDir, config);
132-
if (fs.existsSync(jiebaConfigPath)) {
133-
const raw = JSON.parse(fs.readFileSync(jiebaConfigPath, 'utf-8'));
134-
patchConfigPaths(raw, jiebaInfo, assetsPath);
135-
this.handler = new binding.Opencc(
136-
JSON.stringify(raw),
137-
jiebaInfo.dataDir + '/'
138-
);
139-
return;
140-
}
168+
const jiebaConfigPath = resolveJiebaConfigPath(config, jiebaInfo);
169+
if (jiebaConfigPath) {
170+
const raw = JSON.parse(fs.readFileSync(jiebaConfigPath, 'utf-8'));
171+
patchConfigPaths(raw, jiebaInfo, assetsPath);
172+
this.handler = new binding.Opencc(
173+
JSON.stringify(raw),
174+
jiebaInfo.dataDir + '/'
175+
);
176+
return;
141177
}
142178

143179
config = getConfigPath(config);

node/test.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,29 @@ const { prepareArtifacts } = require('../scripts/prepare-node-prebuild-artifacts
1111

1212
const cases = JSON.parse(fs.readFileSync('test/testcases/testcases.json', 'utf-8')).cases || [];
1313

14+
function createLocalInstalledShape() {
15+
const rootDir = path.resolve(__dirname, '..');
16+
const jiebaPackageDir = path.join(rootDir, 'plugins', 'jieba', 'node');
17+
const libName = os.platform() === 'win32' ? 'opencc-jieba.dll'
18+
: os.platform() === 'darwin' ? 'libopencc-jieba.dylib'
19+
: 'libopencc-jieba.so';
20+
const requiredFiles = [
21+
path.join(jiebaPackageDir, 'index.js'),
22+
path.join(jiebaPackageDir, 'data', 's2twp_jieba.json'),
23+
path.join(jiebaPackageDir, 'data', 'jieba_dict', 'jieba_merged.ocd2'),
24+
path.join(jiebaPackageDir, 'prebuilds', `${os.platform()}-${os.arch()}`, libName),
25+
];
26+
if (!requiredFiles.every((file) => fs.existsSync(file))) {
27+
return null;
28+
}
29+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-node-jieba-'));
30+
const nodeModulesDir = path.join(root, 'node_modules');
31+
fs.mkdirSync(nodeModulesDir, { recursive: true });
32+
fs.symlinkSync(rootDir, path.join(nodeModulesDir, 'opencc'), 'dir');
33+
fs.symlinkSync(jiebaPackageDir, path.join(nodeModulesDir, 'opencc-jieba'), 'dir');
34+
return root;
35+
}
36+
1437
const testSync = function (tc, cfg, expected, done) {
1538
const opencc = new OpenCC(cfg + '.json');
1639
const converted = opencc.convertSync(tc.input);
@@ -157,7 +180,6 @@ describe('npm CLI', function () {
157180
assert.match(result.stdout, /Unsupported in the npm CLI:/);
158181
assert.match(result.stdout, /--inspect/);
159182
assert.match(result.stdout, /--segmentation/);
160-
assert.match(result.stdout, /plugins/);
161183
});
162184

163185
it('prints version', function () {
@@ -194,6 +216,44 @@ describe('npm CLI', function () {
194216
});
195217
});
196218

219+
describe('Optional opencc-jieba package integration', function () {
220+
it('loads jieba configs by mode name in the JavaScript API', function () {
221+
const installRoot = createLocalInstalledShape();
222+
if (!installRoot) this.skip();
223+
224+
const script = [
225+
"const OpenCC = require('opencc');",
226+
"const converter = new OpenCC('s2twp_jieba');",
227+
"process.stdout.write(converter.convertSync('云计算'));",
228+
].join('');
229+
const result = childProcess.spawnSync(process.execPath, ['-e', script], {
230+
cwd: installRoot,
231+
env: { ...process.env },
232+
encoding: 'utf8',
233+
});
234+
assert.equal(result.status, 0, result.stderr);
235+
assert.equal(result.stdout, '雲端計算');
236+
});
237+
238+
it('loads jieba configs by mode name in the npm CLI', function () {
239+
const installRoot = createLocalInstalledShape();
240+
if (!installRoot) this.skip();
241+
242+
const result = childProcess.spawnSync(process.execPath, [
243+
path.join(installRoot, 'node_modules', 'opencc', 'node', 'cli.js'),
244+
'-c',
245+
's2twp_jieba',
246+
], {
247+
cwd: installRoot,
248+
env: { ...process.env },
249+
input: '云计算',
250+
encoding: 'utf8',
251+
});
252+
assert.equal(result.status, 0, result.stderr);
253+
assert.equal(result.stdout, '雲端計算');
254+
});
255+
});
256+
197257
describe('Node prebuild assets', function () {
198258
it('collects only runtime json and ocd2 assets', function () {
199259
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'opencc-prebuild-assets-'));

plugins/jieba/BUILD.bazel

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ cc_binary(
5151
],
5252
)
5353

54+
genrule(
55+
name = "opencc_jieba_windows_zig",
56+
srcs = [
57+
"//deps/marisa-0.3.1:all_files",
58+
"//plugins/jieba/deps/cppjieba:all_files",
59+
"//src:all_files",
60+
"include/JiebaSegmentation.hpp",
61+
"src/JiebaSegmentation.cpp",
62+
"src/JiebaSegmentationPlugin.cpp",
63+
],
64+
outs = ["windows-x64/opencc-jieba.dll"],
65+
cmd = "bash $(location //scripts:build_jieba_windows_zig) $@",
66+
tags = ["manual"],
67+
tools = ["//scripts:build_jieba_windows_zig"],
68+
)
69+
5470
genrule(
5571
name = "jieba_merged_dict",
5672
srcs = [

plugins/jieba/deps/cppjieba/BUILD.bazel

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ load("@rules_cc//cc:defs.bzl", "cc_library")
22

33
package(default_visibility = ["//visibility:public"])
44

5+
filegroup(
6+
name = "all_files",
7+
srcs = glob(["**/*"]),
8+
)
9+
510
exports_files([
611
"dict/jieba.dict.utf8",
712
"dict/user.dict.utf8",

plugins/jieba/node/build.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ process.chdir(rootDir);
1010
let platform = os.platform();
1111
let arch = os.arch();
1212
let bazelFlags = '-c opt';
13+
let libTarget = '//plugins/jieba:opencc-jieba';
1314

1415
// E.g., node build.js linux-arm64
1516
if (process.argv[2]) {
@@ -20,6 +21,8 @@ if (process.argv[2]) {
2021
if (platform === 'linux') {
2122
const configArch = arch === 'x64' ? 'x86_64' : arch;
2223
bazelFlags += ` --config=linux-${configArch}-remote`;
24+
} else if (platform === 'win32' && arch === 'x64') {
25+
libTarget = '//plugins/jieba:opencc_jieba_windows_zig';
2326
}
2427
}
2528
}
@@ -29,14 +32,14 @@ bazelFlags += ' --remote_download_toplevel';
2932

3033
console.log(`Building native addon and dictionary via Bazel for ${platform}-${arch}...`);
3134
try {
32-
execSync(`bazel build ${bazelFlags} //plugins/jieba:opencc-jieba //plugins/jieba:jieba_merged_dict`, { stdio: 'inherit' });
35+
execSync(`bazel build ${bazelFlags} ${libTarget} //plugins/jieba:jieba_merged_dict`, { stdio: 'inherit' });
3336
} catch (err) {
3437
console.error('Bazel build failed.', err);
3538
process.exit(1);
3639
}
3740

3841
// Dynamically resolve exact output paths using cquery
39-
const libPathCmd = `bazel cquery ${bazelFlags} --output=files //plugins/jieba:opencc-jieba`;
42+
const libPathCmd = `bazel cquery ${bazelFlags} --output=files ${libTarget}`;
4043
const libSrcRaw = execSync(libPathCmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim().split('\n').pop();
4144
const libSrc = path.join(rootDir, libSrcRaw);
4245

plugins/jieba/node/index.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,38 @@ const libName = platform === 'win32' ? 'opencc-jieba.dll'
1010
: platform === 'darwin' ? 'libopencc-jieba.dylib'
1111
: 'libopencc-jieba.so';
1212

13+
const dataDir = path.join(__dirname, 'data');
14+
const configDir = dataDir;
15+
16+
function resolveConfigPath(config) {
17+
if (typeof config !== 'string' || path.isAbsolute(config)) {
18+
return null;
19+
}
20+
const candidates = [config];
21+
if (!config.endsWith('.json')) {
22+
candidates.push(`${config}.json`);
23+
}
24+
for (const candidate of candidates) {
25+
if (candidate.includes('/') || candidate.includes('\\')) {
26+
continue;
27+
}
28+
const configPath = path.join(configDir, candidate);
29+
if (require('fs').existsSync(configPath)) {
30+
return configPath;
31+
}
32+
}
33+
return null;
34+
}
35+
1336
module.exports = {
1437
/** Directory containing the prebuilt libopencc-jieba shared library. */
1538
pluginDir: prebuildDir,
1639
/** Absolute path to the platform-specific plugin shared library. */
1740
pluginLibrary: path.join(prebuildDir, libName),
1841
/** Root data directory (config files and jieba_dict/ live here). */
19-
dataDir: path.join(__dirname, 'data'),
42+
dataDir,
43+
/** Directory containing jieba-backed OpenCC configuration JSON files. */
44+
configDir,
45+
/** Resolve a jieba-backed config name, with or without the .json suffix. */
46+
resolveConfigPath,
2047
};

0 commit comments

Comments
 (0)