UNPKG

@nx/js

Version:

The JS plugin for Nx contains executors and generators that provide the best experience for developing JavaScript and TypeScript projects.

120 lines (119 loc) 4.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractNpmPublishJsonData = extractNpmPublishJsonData; const expectedNpmPublishJsonKeys = [ 'id', 'name', 'version', 'size', 'filename', ]; // Pair each '{' with its matching '}' in the publish output. The output is npm's // pretty-printed JSON summary interleaved with arbitrary lifecycle-script text, // so we recognize only genuine JSON structure and treat everything else as // opaque, without interpreting it as JSON (unlike a JSONC parser): // - Braces and quotes inside a JSON string value are ignored, so a files[].path // like "templates/{{name}}/file.txt" doesn't throw off the pairing (#36236). // - '//' and '/*' are not comments; lifecycle output legitimately prints globs // and paths such as dist/*.js, and npm/pnpm emit plain JSON. // - A raw newline ends a string. A valid JSON string never contains one (it is // escaped as \n), so this can't truncate a real value, but it stops a stray // unpaired '"' in log text from swallowing the summary on a later line. // - A quote only opens a string inside an object, so a lone '"' in the preamble // or between objects stays inert. // Stray unmatched braces are left unpaired. function matchBracePositions(str) { const matches = new Map(); const openBraceStack = []; let inString = false; for (let i = 0; i < str.length; i++) { const char = str[i]; if (inString) { if (char === '\\') { i++; // skip the escaped character } else if (char === '"' || char === '\n' || char === '\r') { inString = false; } continue; } if (char === '"') { if (openBraceStack.length > 0) { inString = true; } } else if (char === '{') { openBraceStack.push(i); } else if (char === '}') { const openIndex = openBraceStack.pop(); if (openIndex !== undefined) { matches.set(openIndex, i); } } } return matches; } // Yield every balanced {...} object in the output, left to right by start // position, so an outer wrapper is seen before the objects nested inside it. function* iterateBalancedJsonObjects(str) { const closeByOpen = matchBracePositions(str); for (let i = 0; i < str.length; i++) { const closeIndex = closeByOpen.get(i); if (closeIndex !== undefined) { yield { text: str.slice(i, closeIndex + 1), index: i }; } } } function isNpmPublishSummary(value) { return (typeof value === 'object' && value !== null && expectedNpmPublishJsonKeys.every((key) => value[key] !== undefined)); } function extractNpmPublishJsonData(str) { for (const { text: match, index } of iterateBalancedJsonObjects(str)) { // Cheap check to skip candidates that can't be the summary before parsing if (!expectedNpmPublishJsonKeys.every((key) => match.includes(key))) { continue; } // Full JSON parsing to identify the JSON object let parsedJson; try { parsedJson = JSON.parse(match); } catch { // Ignore parsing errors for unrelated JSON blocks continue; } // npm <= 11.13 emits the summary as a flat object, while npm >= 11.16 (and // pnpm publish run from the workspace root) nest it one level deep under the // package name. Support both by unwrapping a single level when the matched // object isn't itself the summary. let publishData = null; if (isNpmPublishSummary(parsedJson)) { publishData = parsedJson; } else if (typeof parsedJson === 'object' && parsedJson !== null) { for (const value of Object.values(parsedJson)) { if (isNpmPublishSummary(value)) { publishData = value; break; } } } if (!publishData) { continue; } return { beforeJsonData: str.slice(0, index), jsonData: publishData, afterJsonData: str.slice(index + match.length), }; } // No applicable jsonData detected, the whole contents is the beforeJsonData return { beforeJsonData: str, jsonData: null, afterJsonData: '', }; }