Skip to content

Commit 2d14dd9

Browse files
committed
fix: preserve dockerfile instructions (in #27, #28)
1 parent 75b1773 commit 2d14dd9

File tree

1 file changed

+86
-58
lines changed

1 file changed

+86
-58
lines changed

src/lib/parseDockerfile.js

Lines changed: 86 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,75 +10,103 @@ module.exports = function parseDockerfile(content, options = {}) {
1010
...parseOptions,
1111
})
1212
const lines = content.split(/\r?\n/)
13+
14+
// Get 0-based start line index from instruction metadata
15+
// (prefer header fields over lineno)
16+
const getStartLineIndex = (instruction) => {
17+
for (const key of ["lineno", "startLine", "startline", "line"]) {
18+
if (instruction[key]) return instruction[key] - 1
19+
}
20+
return 0
21+
}
22+
23+
// Get raw text from lines[start..end]
24+
const getRaw = (start, end) => lines.slice(start, end + 1).join("\n")
25+
26+
// Check if line starts with instruction
27+
const startsWithInstruction = (line, name) => {
28+
if (!name) return false
29+
const token = (line || "").replace(/^\s+/, "").split(/\s+/, 1)[0]
30+
return token.toUpperCase() === String(name).toUpperCase()
31+
}
32+
33+
// Extract here-doc delimiter token from args
34+
const getHereDocDelimiter = (args) => {
35+
const text = String(args ?? "")
36+
const m = text.match(/<<-?\s*(['"]?)([A-Za-z0-9_]+)\1/)
37+
return m ? m[2] : null
38+
}
39+
40+
// Find the closing line of a here-doc block
41+
const findHereDocClose = (startIndex, delimiter) => {
42+
for (let j = startIndex + 1; j < instructions.length; j++) {
43+
const seg = instructions[j]
44+
const segRaw = seg.raw != null ? String(seg.raw).trim() : ""
45+
if (seg.name === delimiter || segRaw === delimiter) {
46+
return { closeIndex: j, closeLineIdx: getStartLineIndex(seg) }
47+
}
48+
}
49+
return null
50+
}
51+
52+
const results = []
1353
for (let i = 0; i < instructions.length; i++) {
1454
const instruction = instructions[i]
55+
const name = instruction.name.toUpperCase()
1556

16-
// Determine start line (0-based) from available fields; fallback to 0
17-
const startIdx =
18-
(instruction.lineno && instruction.lineno - 1) ||
19-
(instruction.startLine && instruction.startLine - 1) ||
20-
(instruction.startline && instruction.startline - 1) ||
21-
(instruction.line && instruction.line - 1) ||
22-
0
23-
24-
// Determine next instruction start (for fallback end bound)
25-
const next = instructions[i + 1]
26-
const nextStartIdx = next
27-
? (next.lineno && next.lineno - 1) ||
28-
(next.startLine && next.startLine - 1) ||
29-
(next.startline && next.startline - 1) ||
30-
(next.line && next.line - 1) ||
31-
lines.length
32-
: lines.length
33-
34-
// End index: prefer docker-file-parser's lineno (last line of instruction), else before next instruction
35-
const endIdx =
36-
instruction.lineno && Number.isInteger(instruction.lineno)
37-
? instruction.lineno - 1
38-
: Math.max(startIdx, nextStartIdx - 1)
39-
40-
// Recover the true start by scanning upward from endIdx until the header line (instruction keyword) is found
41-
const headerToken = (line) => {
42-
if (!line) return null
43-
const m = line.match(/^\s*([A-Za-z]+)/)
44-
return m ? m[1] : null
45-
}
46-
const startsWithName = (line, name) => {
47-
if (!name) return false
48-
const tok = headerToken(line)
49-
return tok ? tok.toUpperCase() === String(name).toUpperCase() : false
57+
if (
58+
name === "COMMENT" &&
59+
removeSyntaxComments &&
60+
syntaxCommentRegex.test(instruction.args)
61+
) {
62+
// Remove syntax comment
63+
continue
5064
}
5165

52-
let startIdxScan = endIdx
53-
if (instruction.name !== "COMMENT") {
54-
while (
55-
startIdxScan >= 0 &&
56-
!startsWithName(lines[startIdxScan] || "", instruction.name)
57-
) {
58-
startIdxScan--
66+
if (name === "FROM") {
67+
// Normalize "AS" casing but keep args semantics
68+
if (typeof instruction.args === "string") {
69+
instruction.args = instruction.args.replace(/(\s+)as(\s+)/g, "$1AS$2")
5970
}
60-
if (startIdxScan < 0) {
61-
startIdxScan = startIdx
71+
instruction.raw = generateRawInstruction(instruction)
72+
}
73+
74+
// Determine start line (0-based), end line from available fields; fallback to 0
75+
let startIdx = getStartLineIndex(instruction)
76+
const endIdx = startIdx // Default to the same line as the start line
77+
78+
// Find the corresponding start line that really starts with the instruction
79+
if (name !== "COMMENT") {
80+
while (startIdx > 0 && !startsWithInstruction(lines[startIdx], name)) {
81+
startIdx--
6282
}
6383
}
6484

65-
const defaultRaw = lines.slice(startIdxScan, endIdx + 1).join("\n")
85+
// Merge multi-line instructions (RUN, ARG, ENV, etc.)
86+
let closeIndex = i
87+
let closeLineIdx = endIdx
6688

67-
if (instruction.name === "COMMENT") {
68-
if (removeSyntaxComments && syntaxCommentRegex.test(instruction.args)) {
69-
instruction.raw = ""
70-
} else {
71-
// Preserve exact original comment formatting (including indentation)
72-
instruction.raw = defaultRaw
89+
// Check for here-doc in RUN instructions
90+
if (name === "RUN") {
91+
const delimiter = getHereDocDelimiter(instruction.args)
92+
if (delimiter) {
93+
const close = findHereDocClose(i, delimiter)
94+
if (close) {
95+
closeIndex = close.closeIndex
96+
closeLineIdx = close.closeLineIdx
97+
}
7398
}
74-
} else if (instruction.name === "FROM") {
75-
// Normalize "AS" casing but keep args semantics
76-
instruction.args = instruction.args.replace(/(\s+)as(\s+)/gi, "$1AS$2")
77-
instruction.raw = generateRawInstruction(instruction)
78-
} else {
79-
// Preserve exact original formatting for all other instructions (e.g., RUN with continuations/indentation)
80-
instruction.raw = defaultRaw
8199
}
100+
101+
// Update instruction metadata
102+
instruction.lineno = startIdx + 1
103+
instruction.raw = getRaw(startIdx, closeLineIdx)
104+
105+
// Skip merged child instructions (i+1 ... closeIndex)
106+
if (closeIndex > i) i = closeIndex
107+
108+
results.push(instruction)
82109
}
83-
return instructions
110+
111+
return results
84112
}

0 commit comments

Comments
 (0)