Skip to content

Commit bc5e35a

Browse files
authoredFeb 28, 2025··
fix(types)!: fix @netlify/headers-parser types (#6104)
* fix(types): fix @netlify/headers-parser types These types were very weak, containing plentiful `any`s, both explicit and implicit, as well as some incorrect inferred types. * chore(tsconfig): simplify tsconfig strict config We should be opting *into* strict mode and only opting *out* of some flags incrementally while we fix errors. This commit: - flips the global `strict` on - removes values being set to the default via the above - stops disabling flags that obscured no errors (or very few, which I then fixed) - moves a few flag disablings to the specific packages that require it - explictly configures strict flags for already rather strict packages * fix(types): mark netlify.toml [[headers]].values required * style: add empty line after imports * refactor: improve variable name * docs: add inline comment explain funky type * refactor: remove incorrect extraneous property from type * fix(parseAllHeaders)!: mark `minimal` as required All callers already pass in a non-nil `boolean`, so this is only technically breaking. * fix(types)!: increase strictness of optional types * fix(types): add missing PollingStrategy.name type * feat(types): export Category, PollingStrategy
1 parent 2aa50a6 commit bc5e35a

31 files changed

+235
-117
lines changed
 

‎packages/build-info/src/frameworks/framework.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export enum Accuracy {
2020
}
2121

2222
export type PollingStrategy = {
23-
name
23+
// TODO(serhalp) Define an enum
24+
name: string
2425
}
2526

2627
/** Information on how it was detected and how accurate the detection is */

‎packages/build-info/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * from './file-system.js'
22
export * from './logger.js'
3-
export { DetectedFramework, FrameworkInfo } from './frameworks/framework.js'
3+
export type { Category, DetectedFramework, FrameworkInfo, PollingStrategy } from './frameworks/framework.js'
44
export * from './get-framework.js'
55
export * from './project.js'
66
export * from './settings/get-build-settings.js'

‎packages/build-info/src/settings/netlify-toml.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export type RequestHeaders = {
324324
*/
325325
export type Headers = {
326326
for: For
327-
values?: Values
327+
values: Values
328328
}
329329
/**
330330
* Define the actual headers.

‎packages/build/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
4-
"outDir": "lib" /* Specify an output folder for all emitted files. */
4+
"outDir": "lib" /* Specify an output folder for all emitted files. */,
5+
"strictBindCallApply": false /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
56
},
67
"include": ["src/**/*.js", "src/**/*.ts"],
78
"exclude": ["tests/**"]

‎packages/config/src/headers.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,11 @@ export const getHeadersPath = function ({ build: { publish } }) {
1212
const HEADERS_FILENAME = '_headers'
1313

1414
// Add `config.headers`
15-
export const addHeaders = async function ({
16-
config: { headers: configHeaders, ...config },
17-
headersPath,
18-
logs,
19-
featureFlags,
20-
}) {
15+
export const addHeaders = async function ({ config: { headers: configHeaders, ...config }, headersPath, logs }) {
2116
const { headers, errors } = await parseAllHeaders({
2217
headersFiles: [headersPath],
2318
configHeaders,
2419
minimal: true,
25-
featureFlags,
2620
})
2721
warnHeadersParsing(logs, errors)
2822
warnHeadersCaseSensitivity(logs, headers)

‎packages/config/src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ const getFullConfig = async function ({
288288
base: baseA,
289289
} = await resolveFiles({ packagePath, config: configA, repositoryRoot, base, baseRelDir })
290290
const headersPath = getHeadersPath(configB)
291-
const configC = await addHeaders({ config: configB, headersPath, logs, featureFlags })
291+
const configC = await addHeaders({ config: configB, headersPath, logs })
292292
const redirectsPath = getRedirectsPath(configC)
293293
const configD = await addRedirects({ config: configC, redirectsPath, logs, featureFlags })
294294
return { configPath, config: configD, buildDir, base: baseA, redirectsPath, headersPath }

‎packages/config/src/mutations/update.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const updateConfig = async function (
3333
const inlineConfig = applyMutations({}, configMutations)
3434
const normalizedInlineConfig = ensureConfigPriority(inlineConfig, context, branch)
3535
const updatedConfig = await mergeWithConfig(normalizedInlineConfig, configPath)
36-
const configWithHeaders = await addHeaders({ config: updatedConfig, headersPath, logs, featureFlags })
36+
const configWithHeaders = await addHeaders({ config: updatedConfig, headersPath, logs })
3737
const finalConfig = await addRedirects({ config: configWithHeaders, redirectsPath, logs, featureFlags })
3838
const simplifiedConfig = simplifyConfig(finalConfig)
3939

‎packages/headers-parser/src/all.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import { mergeHeaders } from './merge.js'
33
import { parseConfigHeaders } from './netlify_config_parser.js'
44
import { normalizeHeaders } from './normalize.js'
55
import { splitResults, concatResults } from './results.js'
6+
import type { Header, MinimalHeader } from './types.js'
7+
8+
export type { Header, MinimalHeader }
69

710
// Parse all headers from `netlify.toml` and `_headers` file, then normalize
811
// and validate those.
912
export const parseAllHeaders = async function ({
1013
headersFiles = [],
1114
netlifyConfigPath,
1215
configHeaders = [],
13-
minimal = false,
16+
minimal,
17+
}: {
18+
headersFiles: undefined | string[]
19+
netlifyConfigPath?: undefined | string
20+
configHeaders: undefined | MinimalHeader[]
21+
minimal: boolean
1422
}) {
1523
const [
1624
{ headers: fileHeaders, errors: fileParseErrors },
@@ -37,12 +45,12 @@ export const parseAllHeaders = async function ({
3745
return { headers, errors }
3846
}
3947

40-
const getFileHeaders = async function (headersFiles) {
48+
const getFileHeaders = async function (headersFiles: string[]) {
4149
const resultsArrays = await Promise.all(headersFiles.map(parseFileHeaders))
4250
return concatResults(resultsArrays)
4351
}
4452

45-
const getConfigHeaders = async function (netlifyConfigPath) {
53+
const getConfigHeaders = async function (netlifyConfigPath?: string) {
4654
if (netlifyConfigPath === undefined) {
4755
return splitResults([])
4856
}

‎packages/headers-parser/src/for_regexp.js ‎packages/headers-parser/src/for_regexp.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import escapeStringRegExp from 'escape-string-regexp'
22

33
// Retrieve `forRegExp` which is a `RegExp` used to match the `for` path
4-
export const getForRegExp = function (forPath) {
4+
export const getForRegExp = function (forPath: string): RegExp {
55
const pattern = forPath.split('/').map(trimString).filter(Boolean).map(getPartRegExp).join('/')
66
return new RegExp(`^/${pattern}/?$`, 'iu')
77
}
88

9-
const trimString = function (part) {
9+
const trimString = function (part: string): string {
1010
return part.trimEnd()
1111
}
1212

13-
const getPartRegExp = function (part) {
13+
const getPartRegExp = function (part: string): string {
1414
// Placeholder like `/segment/:placeholder/test`
1515
// Matches everything up to a /
1616
if (part.startsWith(':')) {

‎packages/headers-parser/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { parseAllHeaders } from './all.js'
1+
export { parseAllHeaders, type Header, type MinimalHeader } from './all.js'

‎packages/headers-parser/src/line_parser.ts

+32-14
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
import { promises as fs } from 'fs'
1+
import fs from 'fs/promises'
22

33
import { pathExists } from 'path-exists'
44

55
import { splitResults } from './results.js'
6+
import type { MinimalHeader } from './types.js'
7+
8+
type RawHeaderFileLine = { path: string } | { name: string; value: string }
9+
10+
export interface ParseHeadersResult {
11+
headers: MinimalHeader[]
12+
errors: Error[]
13+
}
614

715
// Parse `_headers` file to an array of objects following the same syntax as
816
// the `headers` property in `netlify.toml`
9-
export const parseFileHeaders = async function (headersFile: string) {
17+
export const parseFileHeaders = async function (headersFile: string): Promise<ParseHeadersResult> {
1018
const results = await parseHeaders(headersFile)
1119
const { headers, errors: parseErrors } = splitResults(results)
1220
const { headers: reducedHeaders, errors: reducedErrors } = headers.reduce(reduceLine, { headers: [], errors: [] })
1321
const errors = [...parseErrors, ...reducedErrors]
1422
return { headers: reducedHeaders, errors }
1523
}
1624

17-
const parseHeaders = async function (headersFile: string) {
25+
const parseHeaders = async function (headersFile: string): Promise<Array<Error | RawHeaderFileLine>> {
1826
if (!(await pathExists(headersFile))) {
1927
return []
2028
}
@@ -23,7 +31,12 @@ const parseHeaders = async function (headersFile: string) {
2331
if (typeof text !== 'string') {
2432
return [text]
2533
}
26-
return text.split('\n').map(normalizeLine).filter(hasHeader).map(parseLine).filter(Boolean)
34+
return text
35+
.split('\n')
36+
.map(normalizeLine)
37+
.filter(hasHeader)
38+
.map(parseLine)
39+
.filter((line): line is RawHeaderFileLine => line != null)
2740
}
2841

2942
const readHeadersFile = async function (headersFile: string) {
@@ -38,22 +51,22 @@ const normalizeLine = function (line: string, index: number) {
3851
return { line: line.trim(), index }
3952
}
4053

41-
const hasHeader = function ({ line }) {
54+
const hasHeader = function ({ line }: { line: string }) {
4255
return line !== '' && !line.startsWith('#')
4356
}
4457

45-
const parseLine = function ({ line, index }) {
58+
const parseLine = function ({ line, index }: { line: string; index: number }) {
4659
try {
4760
return parseHeaderLine(line)
4861
} catch (error) {
4962
return new Error(`Could not parse header line ${index + 1}:
5063
${line}
51-
${error.message}`)
64+
${error instanceof Error ? error.message : error?.toString()}`)
5265
}
5366
}
5467

5568
// Parse a single header line
56-
const parseHeaderLine = function (line: string) {
69+
const parseHeaderLine = function (line: string): undefined | RawHeaderFileLine {
5770
if (isPathLine(line)) {
5871
return { path: line }
5972
}
@@ -63,7 +76,7 @@ const parseHeaderLine = function (line: string) {
6376
}
6477

6578
const [rawName, ...rawValue] = line.split(HEADER_SEPARATOR)
66-
const name = rawName.trim()
79+
const name = rawName?.trim() ?? ''
6780

6881
if (name === '') {
6982
throw new Error(`Missing header name`)
@@ -83,18 +96,23 @@ const isPathLine = function (line: string) {
8396

8497
const HEADER_SEPARATOR = ':'
8598

86-
const reduceLine = function ({ headers, errors }, { path, name, value }) {
87-
if (path !== undefined) {
99+
const reduceLine = function (
100+
{ headers, errors }: ParseHeadersResult,
101+
parsedHeader: RawHeaderFileLine,
102+
): ParseHeadersResult {
103+
if ('path' in parsedHeader) {
104+
const { path } = parsedHeader
88105
return { headers: [...headers, { for: path, values: {} }], errors }
89106
}
90107

91-
if (headers.length === 0) {
108+
const { name, value } = parsedHeader
109+
const previousHeaders = headers.slice(0, -1)
110+
const currentHeader = headers[headers.length - 1]
111+
if (headers.length === 0 || currentHeader == null) {
92112
const error = new Error(`Path should come before header "${name}"`)
93113
return { headers, errors: [...errors, error] }
94114
}
95115

96-
const previousHeaders = headers.slice(0, -1)
97-
const currentHeader = headers[headers.length - 1]
98116
const { values } = currentHeader
99117
const newValue = values[name] === undefined ? value : `${values[name]}, ${value}`
100118
const newHeaders = [...previousHeaders, { ...currentHeader, values: { ...values, [name]: newValue } }]

‎packages/headers-parser/src/merge.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import stringify from 'fast-safe-stringify'
22

33
import { splitResults } from './results.js'
4-
import type { Header } from './types.js'
4+
import type { Header, MinimalHeader } from './types.js'
55

66
// Merge headers from `_headers` with the ones from `netlify.toml`.
77
// When:
@@ -21,8 +21,8 @@ export const mergeHeaders = function ({
2121
fileHeaders,
2222
configHeaders,
2323
}: {
24-
fileHeaders: (Error | Header)[]
25-
configHeaders: (Error | Header)[]
24+
fileHeaders: MinimalHeader[] | Header[]
25+
configHeaders: MinimalHeader[] | Header[]
2626
}) {
2727
const results = [...fileHeaders, ...configHeaders]
2828
const { headers, errors } = splitResults(results)
@@ -35,23 +35,22 @@ export const mergeHeaders = function ({
3535
// `netlifyConfig.headers` is modified by plugins.
3636
// The latest duplicate value is the one kept, hence why we need to iterate the
3737
// array backwards and reverse it at the end
38-
const removeDuplicates = function (headers: Header[]) {
38+
const removeDuplicates = function (headers: MinimalHeader[] | Header[]) {
3939
const uniqueHeaders = new Set()
40-
const result: Header[] = []
41-
for (let i = headers.length - 1; i >= 0; i--) {
42-
const h = headers[i]
43-
const key = generateHeaderKey(h)
40+
const result: (MinimalHeader | Header)[] = []
41+
for (const header of [...headers].reverse()) {
42+
const key = generateHeaderKey(header)
4443
if (uniqueHeaders.has(key)) continue
4544
uniqueHeaders.add(key)
46-
result.push(h)
45+
result.push(header)
4746
}
4847
return result.reverse()
4948
}
5049

5150
// We generate a unique header key based on JSON stringify. However, because some
5251
// properties can be regexes, we need to replace those by their toString representation
5352
// given the default will be and empty object
54-
const generateHeaderKey = function (header: Header) {
53+
const generateHeaderKey = function (header: MinimalHeader | Header): string {
5554
return stringify.default.stableStringify(header, (_, value) => {
5655
if (value instanceof RegExp) return value.toString()
5756
return value

‎packages/headers-parser/src/netlify_config_parser.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { parse as loadToml } from '@iarna/toml'
44
import { pathExists } from 'path-exists'
55

66
import { splitResults } from './results.js'
7+
import type { MinimalHeader } from './types.js'
78

89
// Parse `headers` field in "netlify.toml" to an array of objects.
910
// This field is already an array of objects, so it only validates and
@@ -27,7 +28,8 @@ const parseConfig = async function (configPath: string) {
2728
if (!Array.isArray(headers)) {
2829
throw new TypeError(`"headers" must be an array`)
2930
}
30-
return headers
31+
// TODO(serhalp) Validate shape instead of assuming and asserting type
32+
return headers as MinimalHeader[]
3133
} catch (error) {
3234
return [new Error(`Could not parse configuration file: ${error}`)]
3335
}

0 commit comments

Comments
 (0)
Please sign in to comment.