Skip to content

Commit

Permalink
refactor(pkg-manifest): separate comment-handling code
Browse files Browse the repository at this point in the history
  Move comment extraction and reinsertion into their own functions,
  called only when the manifest is Json5.
  • Loading branch information
gwhitney committed Nov 26, 2022
1 parent 843becc commit 7fba4af
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 82 deletions.
41 changes: 31 additions & 10 deletions pkg-manifest/read-project-manifest/src/index.ts
Expand Up @@ -50,6 +50,12 @@ export async function readProjectManifestOnly (projectDir: string): Promise<Proj
return manifest
}

enum ManifestIs {
Json,
Json5,
Yaml
}

export async function tryReadProjectManifest (projectDir: string): Promise<{
fileName: string
manifest: ProjectManifest | null
Expand All @@ -62,7 +68,7 @@ export async function tryReadProjectManifest (projectDir: string): Promise<{
fileName: 'package.json',
manifest: data,
writeProjectManifest: createManifestWriter({
...detectFileFormatting(text),
...detectFileFormatting(text, ManifestIs.Json),
initialManifest: data,
manifestPath,
}),
Expand All @@ -77,7 +83,7 @@ export async function tryReadProjectManifest (projectDir: string): Promise<{
fileName: 'package.json5',
manifest: data,
writeProjectManifest: createManifestWriter({
...detectFileFormatting(text),
...detectFileFormatting(text, ManifestIs.Json5),
initialManifest: data,
manifestPath,
}),
Expand Down Expand Up @@ -118,9 +124,25 @@ export async function tryReadProjectManifest (projectDir: string): Promise<{
}
}

function detectFileFormatting (text: string) {
function detectFileFormatting (text: string, kind: ManifestIs) {
const finalNewline = text.endsWith('\n')
if (!finalNewline) {
let comments
if (kind === ManifestIs.Json5) {
const result = extractJson5Comments(text, finalNewline)
comments = result.comments
// The comment extractor replaces the text because comments should
// not affect indent detection
text = result.text
}
return {
comments,
indent: detectIndent(text).indent,
insertFinalNewline: finalNewline,
}
}

function extractJson5Comments (text: string, hasFinalNewline: boolean) {
if (!hasFinalNewline) {
/* For the sake of the comment parser, which otherwise loses the
* final character of a final comment
*/
Expand All @@ -129,7 +151,7 @@ function detectFileFormatting (text: string) {
const { comments: rawComments } = parseString(text)
const comments: CommentSpecifier[] = []
let stripped = stripComments(text)
if (!finalNewline) {
if (!hasFinalNewline) {
stripped = stripped.slice(0, -1)
}
let offset = 0 // accumulates difference of indices from text to stripped
Expand Down Expand Up @@ -182,9 +204,8 @@ function detectFileFormatting (text: string) {
offset += comment.indexEnd - comment.index
}
return {
comments,
indent: detectIndent(stripped).indent, // Comments shouldn't affect indent
insertFinalNewline: finalNewline,
text: stripped,
comments: comments.length ? comments : undefined,
}
}

Expand All @@ -196,7 +217,7 @@ export async function readExactProjectManifest (manifestPath: string) {
return {
manifest: data,
writeProjectManifest: createManifestWriter({
...detectFileFormatting(text),
...detectFileFormatting(text, ManifestIs.Json),
initialManifest: data,
manifestPath,
}),
Expand All @@ -207,7 +228,7 @@ export async function readExactProjectManifest (manifestPath: string) {
return {
manifest: data,
writeProjectManifest: createManifestWriter({
...detectFileFormatting(text),
...detectFileFormatting(text, ManifestIs.Json5),
initialManifest: data,
manifestPath,
}),
Expand Down
150 changes: 78 additions & 72 deletions pkg-manifest/write-project-manifest/src/index.ts
Expand Up @@ -40,85 +40,91 @@ export async function writeProjectManifest (
let json = (fileType === 'json5' ? JSON5 : JSON)
.stringify(manifest, undefined, opts?.indent ?? '\t')

if (opts?.comments) {
// We need to reintroduce the comments. So create an index of
// the lines of the manifest so we can try to match them up.
// We eliminate whitespace and quotes because pnpm may have changed them.
const jsonLines = json.split('\n')
const index = {}
for (let i = 0; i < jsonLines.length; ++i) {
const key = jsonLines[i].replace(/[\s'"]/g, '')
if (key in index) {
index[key] = -1
} else {
index[key] = i
}
if (fileType === 'json5' && opts?.comments) {
json = insertJson5Comments(json, opts.comments)
}

return writeFileAtomic(filePath, `${json}${trailingNewline}`)
}

function insertJson5Comments (json: string, comments: CommentSpecifier[]) {
// We need to reintroduce the comments. So create an index of
// the lines of the manifest so we can try to match them up.
// We eliminate whitespace and quotes in the index entries,
// because pnpm may have changed them.
const jsonLines = json.split('\n')
const index = {}
const canonicalizer = /[\s'"]/g
for (let i = 0; i < jsonLines.length; ++i) {
const key = jsonLines[i].replace(canonicalizer, '')
if (key in index) {
index[key] = -1 // Mark this line as occurring twice
} else {
index[key] = i
}
}

// A place to put comments that come _before_ the lines they are
// anchored to:
const jsonPrefix: Record<string, string> = {}
for (const comment of opts.comments) {
// First if we can find the line the comment was on, that is
// the most reliable locator:
let key = comment.on.replace(/[\s'"]/g, '')
if (key && index[key] !== undefined && index[key] >= 0) {
jsonLines[index[key]] += ' ' + comment.content
continue
}
// Next, if it's not before anything, it must have been at the very end:
if (comment.before === undefined) {
jsonLines[jsonLines.length - 1] += comment.whitespace + comment.content
continue
// A place to put comments that come _before_ the lines they are
// anchored to:
const jsonPrefix: Record<string, string> = {}
for (const comment of comments) {
// First if we can find the line the comment was on, that is
// the most reliable locator:
let key = comment.on.replace(canonicalizer, '')
if (key && index[key] !== undefined && index[key] >= 0) {
jsonLines[index[key]] += ' ' + comment.content
continue
}
// Next, if it's not before anything, it must have been at the very end:
if (comment.before === undefined) {
jsonLines[jsonLines.length - 1] += comment.whitespace + comment.content
continue
}
// Next, try to put it before something; note the comment extractor
// used the convention that position 0 is before the first line:
let location = (comment.lineNumber === 0) ? 0 : -1
if (location < 0) {
key = comment.before.replace(canonicalizer, '')
if (key && index[key] !== undefined) {
location = index[key]
}
// Next, try to put it before something; note the comment extractor
// used the convention that position 0 is before the first line:
let location = (comment.lineNumber === 0) ? 0 : -1
if (location < 0) {
key = comment.before.replace(/[\s'"]/g, '')
if (key && index[key] !== undefined) {
location = index[key]
}
}
if (location >= 0) {
if (jsonPrefix[location]) {
jsonPrefix[location] += ' ' + comment.content
} else {
const inlineWhitespace = comment.whitespace.startsWith('\n')
? comment.whitespace.slice(1)
: comment.whitespace
jsonPrefix[location] = inlineWhitespace + comment.content
}
if (location >= 0) {
if (jsonPrefix[location]) {
jsonPrefix[location] += ' ' + comment.content
} else {
const inlineWhitespace = comment.whitespace.startsWith('\n')
? comment.whitespace.slice(1)
: comment.whitespace
jsonPrefix[location] = inlineWhitespace + comment.content
}
continue
}
// The last definite indicator we can use is that it is after something:
if (comment.after) {
key = comment.after.replace(canonicalizer, '')
if (key && index[key] !== undefined && index[key] >= 0) {
jsonLines[index[key]] += comment.whitespace + comment.content
continue
}
// The last definite indicator we can use is that it is after something:
if (comment.after) {
key = comment.after.replace(/[\s'"]/g, '')
if (key && index[key] !== undefined && index[key] >= 0) {
jsonLines[index[key]] += comment.whitespace + comment.content
continue
}
}
// Finally, try to get it in the right general location by using the
// line number, but warn the user the comment may have been relocated:
location = comment.lineNumber - 1 // 0 was handled above
let separator = ' '
if (location >= jsonLines.length) {
location = jsonLines.length - 1
separator = '\n'
}
jsonLines[location] += separator + comment.content +
' /* [comment possibly relocated by pnpm] */'
}
// Insert the accumulated prefixes:
for (let i = 0; i < jsonLines.length; ++i) {
if (jsonPrefix[i]) {
jsonLines[i] = jsonPrefix[i] + '\n' + jsonLines[i]
}
// Finally, try to get it in the right general location by using the
// line number, but warn the user the comment may have been relocated:
location = comment.lineNumber - 1 // 0 was handled above
let separator = ' '
if (location >= jsonLines.length) {
location = jsonLines.length - 1
separator = '\n'
}
// And reassemble the manifest:
json = jsonLines.join('\n')
jsonLines[location] += separator + comment.content +
' /* [comment possibly relocated by pnpm] */'
}

return writeFileAtomic(filePath, `${json}${trailingNewline}`)
// Insert the accumulated prefixes:
for (let i = 0; i < jsonLines.length; ++i) {
if (jsonPrefix[i]) {
jsonLines[i] = jsonPrefix[i] + '\n' + jsonLines[i]
}
}
// And reassemble the manifest:
return jsonLines.join('\n')
}

0 comments on commit 7fba4af

Please sign in to comment.