diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 24416c0..720d443 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -4,7 +4,7 @@ name: Node.js CI on: push: - branches: [ master ] + branches: [ master, next ] pull_request: branches: [ master ] @@ -15,7 +15,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest] # Remove windows as tests fail on `\r\n` style newlines + os: [ubuntu-latest, windows-latest] node-version: [12, 14, 16, 18, 20, 22, 23] steps: @@ -26,5 +26,6 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npm install - - run: npm i -g @75lb/nature + - run: npm i -D @75lb/nature - run: npm run test:ci + diff --git a/dist/index.cjs b/dist/index.cjs index 9b74e26..15564cd 100644 --- a/dist/index.cjs +++ b/dist/index.cjs @@ -100,7 +100,7 @@ class JsdocCommand { } } -const exec = util.promisify(cp.exec); +util.promisify(cp.exec); class Explain extends JsdocCommand { async getOutput () { @@ -120,18 +120,39 @@ class Explain extends JsdocCommand { } async _runJsdoc () { - const cmd = this.options.source.length - ? `node ${this.jsdocPath} ${toSpawnArgs(this.jsdocOptions).join(' ')} -X ${this.tempFileSet.files.join(' ')}` - : `node ${this.jsdocPath} ${toSpawnArgs(this.jsdocOptions).join(' ')} -X ${this.inputFileSet.files.join(' ')}`; - + const jsdocArgs = [ + this.jsdocPath, + ...toSpawnArgs(this.jsdocOptions), + '-X', + ...(this.options.source.length ? this.tempFileSet.files : this.inputFileSet.files) + ]; let jsdocOutput = { stdout: '', stderr: '' }; + + const code = await new Promise((resolve, reject) => { + const handle = cp.spawn('node', jsdocArgs); + handle.stdout.setEncoding('utf8'); + handle.stderr.setEncoding('utf8'); + handle.stdout.on('data', chunk => { + jsdocOutput.stdout += chunk; + }); + handle.stderr.on('data', chunk => { + jsdocOutput.stderr += chunk; + }); + handle.on('exit', (code) => { + resolve(code); + }); + handle.on('error', reject); + }); try { - jsdocOutput = await exec(cmd, { maxBuffer: 1024 * 1024 * 100 }); /* 100MB */ - const explainOutput = JSON.parse(jsdocOutput.stdout); - if (this.options.cache) { - await this.cache.write(this.cacheKey, explainOutput); + if (code > 0) { + throw new Error('jsdoc exited with non-zero code: ' + code) + } else { + const explainOutput = JSON.parse(jsdocOutput.stdout); + if (this.options.cache) { + await this.cache.write(this.cacheKey, explainOutput); + } + return explainOutput } - return explainOutput } catch (err) { const firstLineOfStdout = jsdocOutput.stdout.split(/\r?\n/)[0]; const jsdocErr = new Error(jsdocOutput.stderr.trim() || firstLineOfStdout || 'Jsdoc failed.'); diff --git a/lib/explain.js b/lib/explain.js index d9c4cfe..d1b3ed9 100644 --- a/lib/explain.js +++ b/lib/explain.js @@ -23,18 +23,39 @@ class Explain extends JsdocCommand { } async _runJsdoc () { - const cmd = this.options.source.length - ? `node ${this.jsdocPath} ${toSpawnArgs(this.jsdocOptions).join(' ')} -X ${this.tempFileSet.files.join(' ')}` - : `node ${this.jsdocPath} ${toSpawnArgs(this.jsdocOptions).join(' ')} -X ${this.inputFileSet.files.join(' ')}` - + const jsdocArgs = [ + this.jsdocPath, + ...toSpawnArgs(this.jsdocOptions), + '-X', + ...(this.options.source.length ? this.tempFileSet.files : this.inputFileSet.files) + ] let jsdocOutput = { stdout: '', stderr: '' } + + const code = await new Promise((resolve, reject) => { + const handle = cp.spawn('node', jsdocArgs) + handle.stdout.setEncoding('utf8') + handle.stderr.setEncoding('utf8') + handle.stdout.on('data', chunk => { + jsdocOutput.stdout += chunk + }) + handle.stderr.on('data', chunk => { + jsdocOutput.stderr += chunk + }) + handle.on('exit', (code) => { + resolve(code) + }) + handle.on('error', reject) + }) try { - jsdocOutput = await exec(cmd, { maxBuffer: 1024 * 1024 * 100 }) /* 100MB */ - const explainOutput = JSON.parse(jsdocOutput.stdout) - if (this.options.cache) { - await this.cache.write(this.cacheKey, explainOutput) + if (code > 0) { + throw new Error('jsdoc exited with non-zero code: ' + code) + } else { + const explainOutput = JSON.parse(jsdocOutput.stdout) + if (this.options.cache) { + await this.cache.write(this.cacheKey, explainOutput) + } + return explainOutput } - return explainOutput } catch (err) { const firstLineOfStdout = jsdocOutput.stdout.split(/\r?\n/)[0] const jsdocErr = new Error(jsdocOutput.stderr.trim() || firstLineOfStdout || 'Jsdoc failed.') diff --git a/package-lock.json b/package-lock.json index 3e20a8d..8207aab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,30 +30,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", - "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.8" + "@babel/types": "^7.26.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -63,14 +63,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", - "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -591,15 +590,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index 0c503f2..a6e4d3f 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ }, "scripts": { "test": "npm run dist && npm run test:ci", - "test:ci": "75lb-nature test-runner test/*.js", + "test:ci": "75lb-nature test-runner test/caching.js test/explain.js test/render.js", "dist": "75lb-nature cjs-build index.js", "docs": "75lb-nature jsdoc2md index.js lib/*.js > docs/api.md" }, diff --git a/test/caching.js b/test/caching.js index f92c627..a2750f5 100644 --- a/test/caching.js +++ b/test/caching.js @@ -11,12 +11,13 @@ test.set('.explain({ files, cache: true })', async function () { const f = new Fixture('class-all') jsdoc.cache.dir = 'tmp/test/cache1' await jsdoc.cache.clear() - const output = await jsdoc.explain({ files: f.sourcePath, cache: true }) - const cachedFiles = readdirSync(jsdoc.cache.dir) - .map(file => path.resolve(jsdoc.cache.dir, file)) + let output = await jsdoc.explain({ files: f.sourcePath, cache: true }) + output = Fixture.normaliseNewLines(output) + const cachedFiles = readdirSync(jsdoc.cache.dir).map(file => path.resolve(jsdoc.cache.dir, file)) a.equal(cachedFiles.length, 1) a.deepEqual(output, f.getExpectedOutput(output)) - const cachedData = JSON.parse(readFileSync(cachedFiles[0], 'utf8')) + let cachedData = JSON.parse(readFileSync(cachedFiles[0], 'utf8')) + cachedData = Fixture.normaliseNewLines(cachedData) Fixture.removeFileSpecificData(cachedData) a.deepEqual( cachedData, diff --git a/test/explain.js b/test/explain.js index b62732d..beac409 100644 --- a/test/explain.js +++ b/test/explain.js @@ -7,13 +7,15 @@ const [test, only, skip] = [new Map(), new Map(), new Map()] test.set('.explain({ files })', async function () { const f = new Fixture('class-all') - const output = await jsdoc.explain({ files: f.sourcePath }) + let output = await jsdoc.explain({ files: f.sourcePath }) + output = Fixture.normaliseNewLines(output) a.deepEqual(output, f.getExpectedOutput(output)) }) test.set('.explain({ source })', async function () { const f = new Fixture('class-all') - const output = await jsdoc.explain({ source: f.getSource() }) + let output = await jsdoc.explain({ source: f.getSource() }) + output = Fixture.normaliseNewLines(output) a.deepEqual(output, f.getExpectedOutput(output)) }) @@ -47,4 +49,12 @@ test.set('.explain({ files }): files is empty', async function () { ) }) +test.set('Spaces in jsdoc command path', async function () { + process.env.JSDOC_PATH = 'test/fixture/folder with spaces/fake-jsdoc.js' + const f = new Fixture('class-all') + let output = await jsdoc.explain({ files: f.sourcePath }) + a.equal(output.length, 4) + process.env.JSDOC_PATH = '' +}) + export { test, only, skip } diff --git a/test/fixture/folder with spaces/fake-jsdoc.js b/test/fixture/folder with spaces/fake-jsdoc.js new file mode 100644 index 0000000..29899ac --- /dev/null +++ b/test/fixture/folder with spaces/fake-jsdoc.js @@ -0,0 +1 @@ +console.log(JSON.stringify(process.argv)) diff --git a/test/lib/fixture.js b/test/lib/fixture.js index aa2c0c8..9b52462 100644 --- a/test/lib/fixture.js +++ b/test/lib/fixture.js @@ -44,6 +44,16 @@ class Fixture { } }) } + + static normaliseNewLines (doclets) { + const input = JSON.stringify(doclets, null, ' ') + /* Normalise all newlines to posix style to avoid differences while testing on Windows */ + let result = input.replace(/\\r?\\n/gm, '\\n') + /* Additional check for naked \r characters created by jsdoc */ + /* See: https://github.com/jsdoc2md/dmd/issues/102 */ + result = result.replace(/\\r(?!\\n)/g, '\\n') + return JSON.parse(result) + } } export default Fixture