Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:
include:
- os: macos-14
node-version: 24
- os: macos-15-intel
node-version: 24
- os: windows-2022
node-version: 24
steps:
Expand Down Expand Up @@ -66,3 +68,50 @@ jobs:
node/build/**
prebuilds/**
**/npm-debug.log

# Install the packed npm tarball without prebuilt binaries, so the
# node-gyp source build path is exercised exactly as it runs on platforms
# without a scoped binary package (issue #1409).
pack-source-install:
name: Source build from npm tarball (${{ matrix.os }})
permissions:
contents: read
runs-on: ${{ matrix.os }}

strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, macos-15-intel]
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v5
with:
node-version: 24
- run: npm ci
- name: Build prebuild assets
run: npm run prebuild
- name: Pack npm tarball
shell: bash
run: |
mkdir -p dist/npm-artifacts
npm pack --pack-destination dist/npm-artifacts
- name: Install tarball from source and smoke test
shell: bash
run: |
tarball="$(ls "$PWD"/dist/npm-artifacts/opencc-*.tgz)"
install_dir="$(mktemp -d)"
cd "$install_dir"
npm init -y
npm install --omit=optional --foreground-scripts "$tarball"
test -f node_modules/opencc/build/Release/opencc.node
node -e '
const assert = require("assert");
const OpenCC = require("opencc");
const converter = new OpenCC("s2t.json");
const converted = converter.convertSync("汉字");
assert.strictEqual(converted, "漢字");
console.log("Source-built opencc converts 汉字 ->", converted);
'
5 changes: 5 additions & 0 deletions .github/workflows/release-npm-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
- target: darwin-arm64
runner: macos-15
library: libopencc-jieba.dylib
- target: darwin-x64
runner: macos-15-intel
library: libopencc-jieba.dylib
- target: linux-arm64
runner: ubuntu-24.04-arm
library: libopencc-jieba.so
Expand Down Expand Up @@ -194,6 +197,8 @@ jobs:
include:
- target: darwin-arm64
runner: macos-15
- target: darwin-x64
runner: macos-15-intel
- target: linux-arm64
runner: ubuntu-24.04-arm
- target: linux-x64
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"src/Common.hpp",
"src/Config.cpp",
"src/Config.hpp",
"src/ConfigBasedConverter.hpp",
"src/Conversion.cpp",
"src/Conversion.hpp",
"src/ConversionChain.cpp",
Expand All @@ -98,9 +99,12 @@
"src/MaxMatchSegmentation.cpp",
"src/MaxMatchSegmentation.hpp",
"src/opencc.h",
"src/opencc_config_schema.inc",
"src/Optional.hpp",
"src/PhraseExtract.cpp",
"src/PhraseExtract.hpp",
"src/PipelineConverter.cpp",
"src/PipelineConverter.hpp",
"src/plugin/OpenCCPlugin.h",
"src/PluginSegmentation.cpp",
"src/PluginSegmentation.hpp",
Expand All @@ -117,6 +121,8 @@
"src/SerializedValues.hpp",
"src/SimpleConverter.cpp",
"src/SimpleConverter.hpp",
"src/SingleStageConverter.cpp",
"src/SingleStageConverter.hpp",
"src/TextDict.cpp",
"src/TextDict.hpp",
"src/UTF8StringSlice.cpp",
Expand Down
93 changes: 93 additions & 0 deletions scripts/verify-node-main-package.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,97 @@ function fail(message) {
process.exitCode = 1;
}

// The npm tarball must contain every file the node-gyp source build compiles
// or includes, so that installs without a prebuilt binary (issue #1409) can
// fall back to building from source. Walk the gyp source list and its
// #include closure and check each file against the "files" whitelist.
function parseGypSourcesAndIncludeDirs() {
const gypiPath = path.join(packageRoot, 'node', 'node_opencc.gypi');
const gypi = fs.readFileSync(gypiPath, 'utf8');
const sources = [];
const includeDirs = [];
for (const match of gypi.matchAll(/"\.\.(\/[^"]*)?"/g)) {
const relativePath = match[1] ? match[1].slice(1) : '.';
if (/\.(cpp|cc)$/.test(relativePath)) {
sources.push(relativePath);
} else if (
fs.existsSync(path.join(packageRoot, relativePath)) &&
fs.statSync(path.join(packageRoot, relativePath)).isDirectory()
) {
includeDirs.push(relativePath);
}
}
return { sources, includeDirs };
}

function collectSourceBuildClosure() {
const { sources, includeDirs } = parseGypSourcesAndIncludeDirs();
const closure = new Set();
const queue = [...sources];
while (queue.length > 0) {
const relativePath = queue.pop();
if (closure.has(relativePath)) {
continue;
}
const filePath = path.join(packageRoot, relativePath);
if (!fs.existsSync(filePath)) {
fail(`Source build references a file missing from the repository: ${relativePath}`);
continue;
}
closure.add(relativePath);
const content = fs.readFileSync(filePath, 'utf8');
for (const match of content.matchAll(/^\s*#\s*include\s+"([^"]+)"/gm)) {
const include = match[1];
const candidates = [
path.posix.join(path.posix.dirname(relativePath), include),
...includeDirs.map((dir) => path.posix.normalize(path.posix.join(dir, include))),
];
for (const candidate of candidates) {
if (fs.existsSync(path.join(packageRoot, candidate))) {
queue.push(candidate);
break;
}
}
}
}
return closure;
}

function isPackedByFilesWhitelist(relativePath, filesList) {
for (const pattern of filesList) {
if (pattern === relativePath) {
return true;
}
if (!pattern.includes('*') && relativePath.startsWith(`${pattern.replace(/\/$/, '')}/`)) {
return true;
}
if (pattern.includes('*')) {
const regex = new RegExp(
`^${pattern.split('*').map((part) =>
part.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
).join('[^/]*')}$`
);
if (regex.test(relativePath)) {
return true;
}
}
}
return false;
}

function verifySourceBuildFilesPacked() {
const filesList = packageJson.files || [];
const missing = [...collectSourceBuildClosure()]
.filter((relativePath) => !isPackedByFilesWhitelist(relativePath, filesList))
.sort();
for (const relativePath of missing) {
fail(
`Source build requires ${relativePath} but it is not covered by ` +
'the "files" whitelist in package.json'
);
}
}

for (const relativePath of requiredFiles) {
const filePath = path.join(packageRoot, relativePath);
if (!fs.existsSync(filePath)) {
Expand All @@ -50,6 +141,8 @@ for (const packageName of expectedOptionalPackages) {
}
}

verifySourceBuildFilesPacked();

if (process.exitCode) {
console.error(
'opencc package is not ready. Run npm run prebuild before publishing.'
Expand Down
Loading