stovesdk.js 0.0.0-stage → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -2
- package/binding.gyp +73 -0
- package/index.d.ts +241 -0
- package/index.js +117 -0
- package/lib/sdk-dir.js +58 -0
- package/package.json +52 -4
- package/prebuilds/win32-x64/SHA256SUMS +1 -0
- package/prebuilds/win32-x64/stovesdk.node +0 -0
- package/src/stovesdk.cc +904 -0
- package/tools/check-tarball.js +60 -0
- package/tools/fetch-sdk.js +193 -0
- package/tools/sdk-dir.js +36 -0
- package/tools/sdk-manifest.json +19 -0
- package/tools/smoke.js +102 -0
- package/tools/syntax-check.sh +40 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Inspects the file list of `npm pack --dry-run --json` for what must and must not ship.
|
|
5
|
+
//
|
|
6
|
+
// node tools/check-tarball.js <pack.json> source tree: no binary at all, nothing from the SDK (ci.yml)
|
|
7
|
+
// node tools/check-tarball.js <pack.json> --with-prebuild publish: exactly prebuilds/win32-x64/stovesdk.node (+ SHA256SUMS), nothing from the SDK (publish.yml)
|
|
8
|
+
//
|
|
9
|
+
// The SDK is bring-your-own and is never redistributed: no sdk/ path, no .dll/.lib/.pdb/.h.
|
|
10
|
+
// The one binary a published tarball may carry is the maintainer-built addon, and only there.
|
|
11
|
+
//
|
|
12
|
+
// `npm pack --json` is an array of one entry up to npm 11 and an object keyed by package name
|
|
13
|
+
// from npm 12; packFiles() takes either.
|
|
14
|
+
|
|
15
|
+
const PREBUILD = 'prebuilds/win32-x64/stovesdk.node'
|
|
16
|
+
const SUMS = 'prebuilds/win32-x64/SHA256SUMS'
|
|
17
|
+
const SDK_FILE = /^sdk\/|\.(dll|lib|pdb|exe|h|hpp)$/i
|
|
18
|
+
const BINARY = /\.node$/i
|
|
19
|
+
|
|
20
|
+
// The file paths of the one package in `npm pack --dry-run --json` output, whichever shape.
|
|
21
|
+
function packFiles(json) {
|
|
22
|
+
const entry = Array.isArray(json) ? json[0] : json && typeof json === 'object' ? Object.values(json)[0] : undefined
|
|
23
|
+
if (!entry || !Array.isArray(entry.files)) throw new TypeError('not the output of `npm pack --json`: no package entry with a files list')
|
|
24
|
+
return { name: entry.name, version: entry.version, files: entry.files.map(f => f.path) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function checkTarball(files, { withPrebuild = false } = {}) {
|
|
28
|
+
const problems = []
|
|
29
|
+
const sdk = files.filter(p => SDK_FILE.test(p))
|
|
30
|
+
if (sdk.length) problems.push(`would ship SDK material or a binary of the kind: ${sdk.join(', ')}`)
|
|
31
|
+
const binaries = files.filter(p => BINARY.test(p))
|
|
32
|
+
if (withPrebuild) {
|
|
33
|
+
for (const must of [PREBUILD, SUMS, 'index.js', 'index.d.ts', 'package.json']) {
|
|
34
|
+
if (!files.includes(must)) problems.push(`missing from the tarball: ${must}`)
|
|
35
|
+
}
|
|
36
|
+
const stray = binaries.filter(p => p !== PREBUILD)
|
|
37
|
+
if (stray.length) problems.push(`a binary other than the prebuild: ${stray.join(', ')}`)
|
|
38
|
+
} else if (binaries.length) {
|
|
39
|
+
problems.push(`a binary in the source tarball: ${binaries.join(', ')}`)
|
|
40
|
+
}
|
|
41
|
+
return problems
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { checkTarball, packFiles, PREBUILD, SUMS }
|
|
45
|
+
|
|
46
|
+
if (require.main === module) {
|
|
47
|
+
const [file, flag] = process.argv.slice(2)
|
|
48
|
+
if (!file) {
|
|
49
|
+
console.error('usage: node tools/check-tarball.js <pack.json> [--with-prebuild]')
|
|
50
|
+
process.exit(2)
|
|
51
|
+
}
|
|
52
|
+
const { name, version, files } = packFiles(require(require('path').resolve(file)))
|
|
53
|
+
const withPrebuild = flag === '--with-prebuild'
|
|
54
|
+
const problems = checkTarball(files, { withPrebuild })
|
|
55
|
+
if (problems.length) {
|
|
56
|
+
for (const p of problems) console.error(`::error::${p}`)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
console.log(`npm pack: ${files.length} files, ${name}@${version}, ${withPrebuild ? 'prebuild in, ' : 'no binary, '}nothing from the SDK`)
|
|
60
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Fetches the pinned STOVE PC SDK drop from the vendor's download CDN into a directory laid out
|
|
5
|
+
// the way lib/sdk-dir.js expects, verifying every archive against tools/sdk-manifest.json first.
|
|
6
|
+
//
|
|
7
|
+
// node tools/fetch-sdk.js <dest> fetch, verify, extract into <dest>, validate the layout
|
|
8
|
+
// node tools/fetch-sdk.js <dest> --check only validate an existing <dest> against the manifest
|
|
9
|
+
//
|
|
10
|
+
// What this encodes (issue #21):
|
|
11
|
+
// - The SDK version, the archive URLs and their SHA-256 are pinned in the manifest and reviewed
|
|
12
|
+
// like code. Nothing here derives a URL or accepts an unlisted module.
|
|
13
|
+
// - Every archive is hashed before anything is extracted. A mismatch fails the whole fetch: no
|
|
14
|
+
// other version is tried, no hash is rewritten, nothing from that archive touches <dest>.
|
|
15
|
+
// - <dest> must be empty or absent, so two drops never mix; archives are downloaded under it and
|
|
16
|
+
// deleted after extraction, so nothing is cached between runs.
|
|
17
|
+
// - The vendor's zips unpack to `Deploy/…` with no module folder, so each one is extracted into
|
|
18
|
+
// <dest>/<Module>/; that is the layout sdk/README.md documents and binding.gyp reads.
|
|
19
|
+
// - The extracted files are the vendor's. This tool puts them in a build directory and nowhere
|
|
20
|
+
// else — never into the repository, a package or a release.
|
|
21
|
+
|
|
22
|
+
const fs = require('fs')
|
|
23
|
+
const path = require('path')
|
|
24
|
+
const crypto = require('crypto')
|
|
25
|
+
const { execFileSync } = require('child_process')
|
|
26
|
+
const { SDK_MODULES, missingSdkFiles } = require('../lib/sdk-dir')
|
|
27
|
+
|
|
28
|
+
const MANIFEST_FILE = path.join(__dirname, 'sdk-manifest.json')
|
|
29
|
+
|
|
30
|
+
// Problems with a manifest object (empty = usable). Checked before any network access.
|
|
31
|
+
function checkManifest(manifest) {
|
|
32
|
+
const problems = []
|
|
33
|
+
if (!manifest || typeof manifest !== 'object') return ['manifest is not an object']
|
|
34
|
+
if (typeof manifest.version !== 'string' || !/^\d+(\.\d+)+$/.test(manifest.version)) problems.push('version must be a dotted version string')
|
|
35
|
+
const modules = manifest.modules && typeof manifest.modules === 'object' ? manifest.modules : null
|
|
36
|
+
if (!modules) return [...problems, 'modules must be an object']
|
|
37
|
+
const names = Object.keys(modules)
|
|
38
|
+
for (const mod of SDK_MODULES) if (!names.includes(mod)) problems.push(`modules.${mod} is missing`)
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (!SDK_MODULES.includes(name)) problems.push(`modules.${name} is not an SDK module this addon links (${SDK_MODULES.join(', ')})`)
|
|
41
|
+
const entry = modules[name]
|
|
42
|
+
if (!entry || typeof entry !== 'object') { problems.push(`modules.${name} must be an object`); continue }
|
|
43
|
+
if (typeof entry.url !== 'string' || !/^https:\/\/[^\s]+\.zip$/.test(entry.url)) problems.push(`modules.${name}.url must be an https URL to a .zip`)
|
|
44
|
+
if (typeof entry.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(entry.sha256)) problems.push(`modules.${name}.sha256 must be 64 lowercase hex characters`)
|
|
45
|
+
}
|
|
46
|
+
return problems
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadManifest(file = MANIFEST_FILE) {
|
|
50
|
+
const manifest = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
51
|
+
const problems = checkManifest(manifest)
|
|
52
|
+
if (problems.length) {
|
|
53
|
+
const err = new Error(`[stovesdk] ${path.basename(file)} is not usable:\n${problems.map(p => ` - ${p}`).join('\n')}`)
|
|
54
|
+
err.code = 'STOVE_PCSDK_SDK_MANIFEST'
|
|
55
|
+
throw err
|
|
56
|
+
}
|
|
57
|
+
return manifest
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sha256Hex(buffer) {
|
|
61
|
+
return crypto.createHash('sha256').update(buffer).digest('hex')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Throws (code STOVE_PCSDK_SDK_CHECKSUM_MISMATCH) unless the archive bytes hash to the pinned value.
|
|
65
|
+
function verifyArchive(buffer, module, expected, url) {
|
|
66
|
+
const actual = sha256Hex(buffer)
|
|
67
|
+
if (actual === expected) return actual
|
|
68
|
+
const err = new Error(
|
|
69
|
+
`[stovesdk] ${module}: SHA-256 mismatch for ${url}\n expected ${expected}\n actual ${actual}\n` +
|
|
70
|
+
'The archive is not the pinned drop. Nothing was extracted. If the vendor re-published this version, ' +
|
|
71
|
+
'review the change and update tools/sdk-manifest.json in a pull request of its own.'
|
|
72
|
+
)
|
|
73
|
+
err.code = 'STOVE_PCSDK_SDK_CHECKSUM_MISMATCH'
|
|
74
|
+
err.module = module
|
|
75
|
+
err.expected = expected
|
|
76
|
+
err.actual = actual
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function downloadBuffer(url) {
|
|
81
|
+
const res = await fetch(url, { redirect: 'follow' })
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const err = new Error(`[stovesdk] download failed: HTTP ${res.status} for ${url}`)
|
|
84
|
+
err.code = 'STOVE_PCSDK_SDK_DOWNLOAD'
|
|
85
|
+
throw err
|
|
86
|
+
}
|
|
87
|
+
return Buffer.from(await res.arrayBuffer())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// bsdtar reads zip archives. On Windows it is System32\tar.exe (Windows 10 1803+), named in full
|
|
91
|
+
// because a Git Bash PATH puts GNU tar — which does not read zip — in front of it. Elsewhere `tar`
|
|
92
|
+
// is bsdtar on macOS; on Linux, where GNU tar is the default, `unzip` is tried next.
|
|
93
|
+
function extractZip(zipFile, dir) {
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
95
|
+
if (process.platform === 'win32') {
|
|
96
|
+
const tar = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
97
|
+
execFileSync(tar, ['-xf', zipFile, '-C', dir], { stdio: 'inherit' })
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
execFileSync('tar', ['-xf', zipFile, '-C', dir], { stdio: ['ignore', 'inherit', 'pipe'] })
|
|
102
|
+
} catch (tarErr) {
|
|
103
|
+
try {
|
|
104
|
+
execFileSync('unzip', ['-q', '-o', zipFile, '-d', dir], { stdio: 'inherit' })
|
|
105
|
+
} catch {
|
|
106
|
+
const err = new Error(`[stovesdk] could not extract ${zipFile}: need bsdtar or unzip (tar said: ${String(tarErr.stderr || tarErr.message).trim()})`)
|
|
107
|
+
err.code = 'STOVE_PCSDK_SDK_EXTRACT'
|
|
108
|
+
throw err
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isEmptyDir(dir) {
|
|
114
|
+
if (!fs.existsSync(dir)) return true
|
|
115
|
+
return fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length === 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function layoutError(dest, missing) {
|
|
119
|
+
const err = new Error(`[stovesdk] SDK layout incomplete at ${dest}. Missing:\n${missing.map(f => ` - ${f}`).join('\n')}`)
|
|
120
|
+
err.code = 'STOVE_PCSDK_SDK_LAYOUT'
|
|
121
|
+
err.missing = missing
|
|
122
|
+
return err
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fetches, verifies and extracts every module of the manifest into `dest`. Resolves with a summary.
|
|
126
|
+
// `download` and `extract` are injectable so the rules above can be tested without the network.
|
|
127
|
+
async function fetchSdk(dest, { manifest = loadManifest(), download = downloadBuffer, extract = extractZip, log = () => {} } = {}) {
|
|
128
|
+
const problems = checkManifest(manifest)
|
|
129
|
+
if (problems.length) {
|
|
130
|
+
const err = new Error(`[stovesdk] manifest is not usable:\n${problems.map(p => ` - ${p}`).join('\n')}`)
|
|
131
|
+
err.code = 'STOVE_PCSDK_SDK_MANIFEST'
|
|
132
|
+
throw err
|
|
133
|
+
}
|
|
134
|
+
dest = path.resolve(dest)
|
|
135
|
+
if (!isEmptyDir(dest)) {
|
|
136
|
+
const err = new Error(`[stovesdk] ${dest} exists and is not empty. The fetch only fills an empty directory, so two drops never mix.`)
|
|
137
|
+
err.code = 'STOVE_PCSDK_SDK_DEST_NOT_EMPTY'
|
|
138
|
+
throw err
|
|
139
|
+
}
|
|
140
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
141
|
+
const downloads = path.join(dest, '.download')
|
|
142
|
+
fs.mkdirSync(downloads)
|
|
143
|
+
const modules = []
|
|
144
|
+
try {
|
|
145
|
+
for (const module of SDK_MODULES) {
|
|
146
|
+
const { url, sha256 } = manifest.modules[module]
|
|
147
|
+
log(`[stovesdk] ${module}: downloading ${url}`)
|
|
148
|
+
const buffer = await download(url)
|
|
149
|
+
verifyArchive(buffer, module, sha256, url)
|
|
150
|
+
log(`[stovesdk] ${module}: ${buffer.length} bytes, SHA-256 ${sha256} OK`)
|
|
151
|
+
const zipFile = path.join(downloads, `${module}.zip`)
|
|
152
|
+
fs.writeFileSync(zipFile, buffer)
|
|
153
|
+
extract(zipFile, path.join(dest, module))
|
|
154
|
+
fs.rmSync(zipFile, { force: true })
|
|
155
|
+
modules.push({ module, url, sha256, bytes: buffer.length })
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
fs.rmSync(downloads, { recursive: true, force: true })
|
|
159
|
+
}
|
|
160
|
+
fs.writeFileSync(path.join(dest, 'VERSION'), `${manifest.version}\n`)
|
|
161
|
+
const missing = missingSdkFiles(dest)
|
|
162
|
+
if (missing.length) throw layoutError(dest, missing)
|
|
163
|
+
return { dest, version: manifest.version, modules }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { MANIFEST_FILE, checkManifest, loadManifest, sha256Hex, verifyArchive, fetchSdk }
|
|
167
|
+
|
|
168
|
+
if (require.main === module) {
|
|
169
|
+
const args = process.argv.slice(2)
|
|
170
|
+
const check = args.includes('--check')
|
|
171
|
+
const dest = args.find(a => !a.startsWith('--'))
|
|
172
|
+
if (!dest) {
|
|
173
|
+
console.error('usage: node tools/fetch-sdk.js <dest> [--check]')
|
|
174
|
+
process.exit(2)
|
|
175
|
+
}
|
|
176
|
+
if (check) {
|
|
177
|
+
const manifest = loadManifest()
|
|
178
|
+
const missing = missingSdkFiles(path.resolve(dest))
|
|
179
|
+
if (missing.length) { console.error(layoutError(path.resolve(dest), missing).message); process.exit(1) }
|
|
180
|
+
const version = fs.existsSync(path.join(dest, 'VERSION')) ? fs.readFileSync(path.join(dest, 'VERSION'), 'utf8').trim() : 'unknown'
|
|
181
|
+
if (version !== manifest.version) { console.error(`[stovesdk] ${dest} holds SDK ${version}, the manifest pins ${manifest.version}`); process.exit(1) }
|
|
182
|
+
console.log(`[stovesdk] SDK ${version} complete at ${path.resolve(dest)}`)
|
|
183
|
+
process.exit(0)
|
|
184
|
+
}
|
|
185
|
+
fetchSdk(dest, { log: console.log })
|
|
186
|
+
.then(summary => {
|
|
187
|
+
console.log(`[stovesdk] SDK ${summary.version} ready at ${summary.dest} (${summary.modules.length} modules, ${summary.modules.reduce((n, m) => n + m.bytes, 0)} bytes downloaded, archives removed)`)
|
|
188
|
+
})
|
|
189
|
+
.catch(err => {
|
|
190
|
+
console.error(err && err.message ? err.message : err)
|
|
191
|
+
process.exit(1)
|
|
192
|
+
})
|
|
193
|
+
}
|
package/tools/sdk-dir.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Called by binding.gyp (`<!(node tools/sdk-dir.js)`) to print the SDK directory, after checking
|
|
5
|
+
// that the layout lib/sdk-dir.js documents is present. Fails loudly otherwise — node-gyp would
|
|
6
|
+
// fail a few seconds later anyway, but with a far less readable error.
|
|
7
|
+
//
|
|
8
|
+
// node tools/sdk-dir.js → prints the resolved SDK directory
|
|
9
|
+
// node tools/sdk-dir.js version → prints the SDK version string ('unknown' if no VERSION file)
|
|
10
|
+
// node tools/sdk-dir.js check → exit 0 if the layout is complete, 1 otherwise (prints what is missing)
|
|
11
|
+
|
|
12
|
+
const { resolveSdkDir, missingSdkFiles, readSdkVersion } = require('../lib/sdk-dir')
|
|
13
|
+
|
|
14
|
+
const sdkDir = resolveSdkDir()
|
|
15
|
+
const mode = process.argv[2] || 'dir'
|
|
16
|
+
|
|
17
|
+
if (mode === 'version') {
|
|
18
|
+
process.stdout.write(readSdkVersion(sdkDir))
|
|
19
|
+
process.exit(0)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const missing = missingSdkFiles(sdkDir)
|
|
23
|
+
if (missing.length) {
|
|
24
|
+
process.stderr.write(`[stovesdk] STOVE PC SDK not found at ${sdkDir}\n`)
|
|
25
|
+
process.stderr.write('[stovesdk] Place the SDK under sdk/ or set STOVE_PCSDK_DIR (see sdk/README.md). Missing:\n')
|
|
26
|
+
for (const file of missing) process.stderr.write(` - ${file}\n`)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (mode === 'check') {
|
|
31
|
+
process.stdout.write(`[stovesdk] SDK layout OK at ${sdkDir} (version ${readSdkVersion(sdkDir)})\n`)
|
|
32
|
+
process.exit(0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// gyp reads this verbatim; forward slashes keep it portable inside the generated project files.
|
|
36
|
+
process.stdout.write(sdkDir.replace(/\\/g, '/'))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "The STOVE PC SDK drop that tools/fetch-sdk.js fetches and verifies (issue #21). Reviewed like code: the version is the one the addon has been built and smoke-tested against, and a change here is a pull request of its own, never combined with an addon change. Each archive is checked against its SHA-256 before extraction; a mismatch fails the fetch and nothing is substituted. The files are the vendor's and are never committed, packed or redistributed.",
|
|
3
|
+
"version": "3.3.2.19",
|
|
4
|
+
"source": "STOVE developer-center download CDN (the links behind the PC SDK download page)",
|
|
5
|
+
"modules": {
|
|
6
|
+
"BaseSDK": {
|
|
7
|
+
"url": "https://d2x8kymwjom7h7.cloudfront.net/live/application_no/STATIC/developer-center/Store/BaseSDK_3.3.2.19.zip",
|
|
8
|
+
"sha256": "92e9a0e0a8522c8ef176769695f79aca89de33ae67c27f7c2f41fd26035f5faa"
|
|
9
|
+
},
|
|
10
|
+
"OwnershipSDK": {
|
|
11
|
+
"url": "https://d2x8kymwjom7h7.cloudfront.net/live/application_no/STATIC/developer-center/Store/OwnershipSDK_3.3.2.19.zip",
|
|
12
|
+
"sha256": "b542032b9a4a53a2e7d3fdda7a871aeef4a9ea9222cd149deea60b0c29810409"
|
|
13
|
+
},
|
|
14
|
+
"GameSupportSDK": {
|
|
15
|
+
"url": "https://d2x8kymwjom7h7.cloudfront.net/live/application_no/STATIC/developer-center/Store/GameSupportSDK_3.3.2.19.zip",
|
|
16
|
+
"sha256": "ad3a339fbe28055a88839428333700efa34f3a59590dc3a67583145f437f25e9"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/tools/smoke.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Load-only smoke for a freshly built addon (Windows). Proves, without a STOVE client:
|
|
5
|
+
// 1. the .node loads (its DLL imports resolve — the SDK DLLs must sit next to it or next to node.exe),
|
|
6
|
+
// 2. the export surface matches index.d.ts,
|
|
7
|
+
// 3. the pure-addon paths behave: refusals before initialize(), argument validation, no-op pump,
|
|
8
|
+
// no-op uninitialize().
|
|
9
|
+
// Nothing initializes the SDK — that needs a STOVE client session on the machine.
|
|
10
|
+
//
|
|
11
|
+
// node tools/smoke.js [path/to/stovesdk.node]
|
|
12
|
+
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const { load } = require('..')
|
|
15
|
+
|
|
16
|
+
const EXPECTED_EXPORTS = [
|
|
17
|
+
'getAddonInfo', 'getVersion', 'initialize', 'runCallbacks', 'isInitialized', 'uninitialize',
|
|
18
|
+
'getOwnershipList', 'getUser', 'getGds', 'getAchievement', 'getAllAchievements', 'getStat', 'setStat',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
console.error(`[smoke] ${message}`)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sdk = load(process.argv[2] ? path.resolve(process.argv[2]) : undefined)
|
|
27
|
+
|
|
28
|
+
const missing = EXPECTED_EXPORTS.filter(name => typeof sdk[name] !== 'function')
|
|
29
|
+
if (missing.length) fail(`missing exports: ${missing.join(', ')}`)
|
|
30
|
+
if (!sdk.constants || !sdk.constants.OwnershipCode || !sdk.constants.OwnershipGameCode || !sdk.constants.ErrorCode) {
|
|
31
|
+
fail('constants are missing')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const info = sdk.getAddonInfo()
|
|
35
|
+
console.log('[smoke] addon info:', JSON.stringify(info))
|
|
36
|
+
console.log('[smoke] constants:', JSON.stringify(sdk.constants))
|
|
37
|
+
|
|
38
|
+
if (sdk.isInitialized() !== false) fail('expected isInitialized() === false on a fresh load')
|
|
39
|
+
|
|
40
|
+
// Informational — exercises a real call into BaseSDK.dll.
|
|
41
|
+
console.log('[smoke] Base_GetVersion:', JSON.stringify(sdk.getVersion()))
|
|
42
|
+
|
|
43
|
+
// Synchronous identity reads must answer, not throw, before initialize().
|
|
44
|
+
for (const name of ['getUser', 'getGds']) {
|
|
45
|
+
const answer = sdk[name]()
|
|
46
|
+
console.log(`[smoke] ${name}() before init:`, JSON.stringify(answer))
|
|
47
|
+
if (!answer || answer.ok !== false || answer.code !== sdk.constants.ErrorCode.NOT_INITIALIZED) {
|
|
48
|
+
fail(`${name}() before init must report { ok: false, code: ErrorCode.NOT_INITIALIZED }`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// runCallbacks() must be a no-op when nothing is in flight.
|
|
53
|
+
sdk.runCallbacks()
|
|
54
|
+
|
|
55
|
+
// uninitialize() on a fresh addon must be a harmless no-op with all three slots null.
|
|
56
|
+
const down = sdk.uninitialize()
|
|
57
|
+
console.log('[smoke] uninitialize() on fresh addon:', JSON.stringify(down))
|
|
58
|
+
if (down.base !== null || down.ownership !== null || down.gameSupport !== null) {
|
|
59
|
+
fail('uninitialize() reported an UnInitialize on a never-initialized SDK')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Argument validation is synchronous (TypeError / RangeError), never a crash and never an SDK call.
|
|
63
|
+
function expectThrow(label, fn, ctor) {
|
|
64
|
+
try {
|
|
65
|
+
fn()
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err instanceof ctor) {
|
|
68
|
+
console.log(`[smoke] ${label} threw ${ctor.name} as expected:`, err.message)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
fail(`${label} threw the wrong error type: ${err}`)
|
|
72
|
+
}
|
|
73
|
+
fail(`${label} did not throw`)
|
|
74
|
+
}
|
|
75
|
+
expectThrow('initialize() without options', () => sdk.initialize(), TypeError)
|
|
76
|
+
expectThrow('setStat("x") without a value', () => sdk.setStat('x'), TypeError)
|
|
77
|
+
expectThrow('setStat("x", 1.5)', () => sdk.setStat('x', 1.5), RangeError)
|
|
78
|
+
expectThrow('getAchievement(42)', () => sdk.getAchievement(42), TypeError)
|
|
79
|
+
expectThrow('getStat(null)', () => sdk.getStat(null), TypeError)
|
|
80
|
+
|
|
81
|
+
// Before initialize() every asynchronous entry point must reject through its Promise —
|
|
82
|
+
// never crash the process, never reach the SDK.
|
|
83
|
+
function expectReject(label, promise, step) {
|
|
84
|
+
return promise.then(
|
|
85
|
+
() => fail(`${label} resolved without the SDK initialized`),
|
|
86
|
+
err => {
|
|
87
|
+
if (!err || err.step !== step || err.code !== sdk.constants.ErrorCode.NOT_INITIALIZED) {
|
|
88
|
+
fail(`${label} rejected with an unexpected shape: step=${err && err.step} code=${err && err.code}`)
|
|
89
|
+
}
|
|
90
|
+
console.log(`[smoke] ${label} rejected as expected:`, err.message)
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expectReject('getOwnershipList()', sdk.getOwnershipList(), 'ownershipList')
|
|
96
|
+
.then(() => expectReject('setStat("x", 1)', sdk.setStat('x', 1), 'modifyStat'))
|
|
97
|
+
.then(() => expectReject('getStat("x")', sdk.getStat('x'), 'stat'))
|
|
98
|
+
.then(() => expectReject('getAchievement("x")', sdk.getAchievement('x'), 'achievement'))
|
|
99
|
+
.then(() => expectReject('getAllAchievements()', sdk.getAllAchievements(), 'allAchievements'))
|
|
100
|
+
.then(() => {
|
|
101
|
+
console.log('[smoke] OK')
|
|
102
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Parses src/stovesdk.cc with clang `-fsyntax-only` on a non-Windows machine, against the real
|
|
3
|
+
# SDK headers (STOVE_PCSDK_DIR or ./sdk) and a stub <windows.h>. It catches typos, wrong SDK
|
|
4
|
+
# accessor names and N-API misuse before the code reaches a Windows toolchain. It is NOT a build:
|
|
5
|
+
# nothing is linked, and MSVC-only behaviour is not exercised.
|
|
6
|
+
#
|
|
7
|
+
# sh tools/syntax-check.sh (needs: clang++, node_modules/node-addon-api, Node headers)
|
|
8
|
+
#
|
|
9
|
+
# The SDK root is passed with -iquote, not -I: the vendor headers include each other with quoted
|
|
10
|
+
# paths relative to that root ("BaseSDK/Deploy/Include/…"), and a plain -I would also let the C++
|
|
11
|
+
# standard library's `#include <version>` resolve to the drop's VERSION file on a case-insensitive
|
|
12
|
+
# file system (macOS, Windows). binding.gyp's include_dirs are unaffected — MSVC builds have passed
|
|
13
|
+
# with VERSION present.
|
|
14
|
+
set -eu
|
|
15
|
+
|
|
16
|
+
ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
|
17
|
+
SDK_DIR=$(node "$ROOT/tools/sdk-dir.js")
|
|
18
|
+
NAPI_INC=$(node -p "require('$ROOT/node_modules/node-addon-api').include.replace(/\"/g, '')")
|
|
19
|
+
NODE_INC="${NODE_INCLUDE_DIR:-$(dirname "$(dirname "$(command -v node)")")/include/node}"
|
|
20
|
+
[ -f "$NODE_INC/node_api.h" ] || { echo "node_api.h not found under $NODE_INC (set NODE_INCLUDE_DIR)" >&2; exit 1; }
|
|
21
|
+
|
|
22
|
+
STUB=$(mktemp -d)
|
|
23
|
+
trap 'rm -rf "$STUB"' EXIT
|
|
24
|
+
cat > "$STUB/windows.h" <<'EOF'
|
|
25
|
+
// Minimal stand-in for <windows.h>: only what src/stovesdk.cc and the SDK headers touch.
|
|
26
|
+
#pragma once
|
|
27
|
+
#include <cstdint>
|
|
28
|
+
#define __cdecl
|
|
29
|
+
#define CP_UTF8 65001
|
|
30
|
+
#define __out
|
|
31
|
+
extern "C" int WideCharToMultiByte(unsigned, unsigned long, const wchar_t*, int, char*, int, const char*, int*);
|
|
32
|
+
EOF
|
|
33
|
+
|
|
34
|
+
clang++ -fsyntax-only -std=c++17 -fshort-wchar -fdeclspec -fms-extensions \
|
|
35
|
+
-Wno-ignored-attributes -Wno-microsoft -Wno-pragma-pack \
|
|
36
|
+
-DSTOVE_PCSDK_SYNTAX_CHECK -DNAPI_VERSION=8 -DNAPI_DISABLE_CPP_EXCEPTIONS \
|
|
37
|
+
-I "$STUB" -I "$NAPI_INC" -I "$NODE_INC" \
|
|
38
|
+
-iquote "$SDK_DIR" -I "$SDK_DIR/BaseSDK/Deploy/Include" -I "$SDK_DIR/OwnershipSDK/Deploy/Include" -I "$SDK_DIR/GameSupportSDK/Deploy/Include" \
|
|
39
|
+
"$ROOT/src/stovesdk.cc"
|
|
40
|
+
echo "[syntax-check] OK: src/stovesdk.cc parses against the SDK headers at $SDK_DIR"
|