@@ -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 - Z a - z 0 - 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 - Z a - 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 + ) a s ( \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 + ) a s ( \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