Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ WORKDIR $APP_HOME
# Source code
COPY --chown=node:node src src/
COPY --chown=node:node package.json ./
COPY --chown=node:node next.config.js ./
COPY --chown=node:node next.config.ts ./
COPY --chown=node:node tsconfig.json ./

# From the clones stage
Expand Down Expand Up @@ -125,7 +125,7 @@ WORKDIR $APP_HOME
# Source code
COPY --chown=node:node src src/
COPY --chown=node:node package.json ./
COPY --chown=node:node next.config.js ./
COPY --chown=node:node next.config.ts ./
COPY --chown=node:node tsconfig.json ./

# From clones stage
Expand Down
36 changes: 9 additions & 27 deletions next.config.js → next.config.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,22 @@
import fs from 'fs'
import path from 'path'
import type { NextConfig } from 'next'

import frontmatter from '@gr2m/gray-matter'
// Hardcoded log level function since next.config.js cannot import from TypeScript files
// Matches ./src/observability/logger/lib/log-levels
function getLogLevelNumber() {
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
}
import { getLogLevelNumber } from '@/observability/logger/lib/log-levels'

let defaultLogLevel = 'info'
if (
!process.env.LOG_LEVEL &&
(process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test')
) {
defaultLogLevel = 'debug'
}

const envLogLevel = process.env.LOG_LEVEL?.toLowerCase() || defaultLogLevel
const logLevel = LOG_LEVELS[envLogLevel] !== undefined ? envLogLevel : defaultLogLevel

return LOG_LEVELS[logLevel]
}

// Replace imports with hardcoded values
const ROOT = process.env.ROOT || '.'

// Hard-coded language keys to avoid TypeScript import in config file
// Language keys are defined here because Next.js config compilation doesn't resolve the @/ path alias
// Importing from src/languages/lib/languages.ts would fail when it tries to import @/frame/lib/constants
// This must match the languages defined in src/languages/lib/languages.ts
const languageKeys = ['en', 'es', 'ja', 'pt', 'zh', 'ru', 'fr', 'ko', 'de']

const homepage = path.posix.join(ROOT, 'content/index.md')
const { data } = frontmatter(fs.readFileSync(homepage, 'utf8'))
const productIds = data.children
const productIds = data.children as string[]

export default {
const config: NextConfig = {
// Transpile @primer/react so Next's webpack can process its CSS and other assets
// This ensures CSS in node_modules/@primer/react is handled by the app's loaders.
transpilePackages: ['@primer/react'],
Expand Down Expand Up @@ -106,3 +86,5 @@ export default {
styledComponents: true,
},
}

export default config
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import ajv from '@/tests/lib/validate-json-schema'
// mdDict will be populated with:
//
// { '/foo/bar/0': 'item 1', '/foo/bar/1': 'item 2' }
const mdDict = new Map()
const lintableData = Object.keys(dataSchemas)
const mdDict = new Map<string, string>()
const lintableData: string[] = Object.keys(dataSchemas)

// To redefine a custom keyword, you must remove it
// then re-add it with the new definition. The default
Expand All @@ -37,7 +37,8 @@ ajv.addKeyword({
type: 'string',
// For docs on defining validate see
// https://ajv.js.org/keywords.html#define-keyword-with-validate-function
validate: (compiled, data, schema, parentInfo) => {
// Using any for validate function params because AJV's type definitions for custom keywords are complex
validate: (compiled: any, data: any, schema: any, parentInfo: any): boolean => {
mdDict.set(parentInfo.instancePath, data)
return true
},
Expand All @@ -55,13 +56,14 @@ ajv.addKeyword({
// back to the location in the original schema file,
// so we also need the parent path of the `lintable`
// property in the schema.
export async function getLintableYml(dataFilePath) {
export async function getLintableYml(dataFilePath: string): Promise<Record<string, string> | null> {
const matchingDataPath = lintableData.find(
(ref) => dataFilePath === ref || dataFilePath.startsWith(ref),
)
if (!matchingDataPath) return null

const schemaFilePath = dataSchemas[matchingDataPath]
if (!schemaFilePath) return null
const schema = (await import(schemaFilePath)).default
if (!schema) return null

Expand All @@ -78,13 +80,15 @@ export async function getLintableYml(dataFilePath) {
// back to a file in the data directory.
// The resulting key looks like:
// 'data/variables/product.yml /pat_v1_caps'
function addPathToKey(mdDict, dataFilePath) {
function addPathToKey(mdDict: Map<string, string>, dataFilePath: string): Map<string, string> {
const keys = Array.from(mdDict.keys())
keys.forEach((key) => {
const newKey = `${dataFilePath} ${key}`
const value = mdDict.get(key)
mdDict.delete(key)
mdDict.set(newKey, value)
if (value !== undefined) {
mdDict.delete(key)
mdDict.set(newKey, value)
}
})
return mdDict
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import yaml from 'js-yaml'

import { getRange, getFrontmatter } from '../helpers/utils'
import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'

interface Frontmatter {
redirect_from?: string | string[]
children?: string[]
[key: string]: any
}

const ERROR_MESSAGE =
'An early access reference appears to be used in a non-early access doc. Remove early access references or disable this rule.'
Expand All @@ -10,20 +18,20 @@ const ERROR_MESSAGE =
// There are several existing allowed references to `early access`
// as a GitHub feature. This rule focuses on references to early
// access pages.
const isEarlyAccessFilepath = (filepath) => filepath.includes('early-access')
const isEarlyAccessFilepath = (filepath: string): boolean => filepath.includes('early-access')

const EARLY_ACCESS_REGEX = /early-access/gi
// This is a pattern seen in link paths for articles about
// early access. This pattern is ok.
const EARLY_ACCESS_ARTICLE_REGEX = /-early-access-/

export const earlyAccessReferences = {
export const earlyAccessReferences: Rule = {
names: ['GHD008', 'early-access-references'],
description:
'Files that are not early access should not reference early-access or early-access files',
tags: ['feature', 'early-access'],
severity: 'error',
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
if (isEarlyAccessFilepath(params.name)) return

// Find errors in content
Expand All @@ -44,17 +52,17 @@ export const earlyAccessReferences = {
},
}

export const frontmatterEarlyAccessReferences = {
export const frontmatterEarlyAccessReferences: Rule = {
names: ['GHD009', 'frontmatter-early-access-references'],
description:
'Files that are not early access should not have frontmatter that references early-access',
tags: ['frontmatter', 'feature', 'early-access'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const filepath = params.name
if (isEarlyAccessFilepath(filepath)) return

// Find errors in frontmatter
const fm = getFrontmatter(params.lines)
const fm = getFrontmatter(params.lines) as Frontmatter | null
if (!fm) return

// The redirect_from property is allowed to contain early-access paths
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError, newLineRe } from 'markdownlint-rule-helpers'

import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '@/content-linter/types'

// This rule looks for opening and closing HTML comment tags that
// contain an expiration date in the format:
//
Expand All @@ -8,20 +11,20 @@ import { addError, newLineRe } from 'markdownlint-rule-helpers'
//
// The `end expires` closing tag closes the content that is expired
// and must be removed.
export const expiredContent = {
export const expiredContent: Rule = {
names: ['GHD038', 'expired-content'],
description: 'Expired content must be remediated.',
tags: ['expired'],
function: (params, onError) => {
const tokensToCheck = params.tokens.filter(
(token) => token.type === 'inline' || token.type === 'html_block',
function: (params: RuleParams, onError: RuleErrorCallback) => {
const tokensToCheck = (params.tokens || []).filter(
(token: MarkdownToken) => token.type === 'inline' || token.type === 'html_block',
)

tokensToCheck.forEach((token) => {
tokensToCheck.forEach((token: MarkdownToken) => {
// Looking for just opening tag with format:
// <!-- expires yyyy-mm-dd -->
const match = token.content.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match) return
const match = token.content?.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match || !token.content) return

const expireDate = new Date(match.splice(1, 3).join(' '))
const today = new Date()
Expand Down Expand Up @@ -57,20 +60,20 @@ export const DAYS_TO_WARN_BEFORE_EXPIRED = 14
//
// The `end expires` closing tag closes the content that is expired
// and must be removed.
export const expiringSoon = {
export const expiringSoon: Rule = {
names: ['GHD039', 'expiring-soon'],
description: 'Content that expires soon should be proactively addressed.',
tags: ['expired'],
function: (params, onError) => {
const tokensToCheck = params.tokens.filter(
(token) => token.type === 'inline' || token.type === 'html_block',
function: (params: RuleParams, onError: RuleErrorCallback) => {
const tokensToCheck = (params.tokens || []).filter(
(token: MarkdownToken) => token.type === 'inline' || token.type === 'html_block',
)

tokensToCheck.forEach((token) => {
tokensToCheck.forEach((token: MarkdownToken) => {
// Looking for just opening tag with format:
// <!-- expires yyyy-mm-dd -->
const match = token.content.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match) return
const match = token.content?.match(/<!--\s*expires\s(\d\d\d\d)-(\d\d)-(\d\d)\s*-->/)
if (!match || !token.content) return

const expireDate = new Date(match.splice(1, 3).join(' '))
const today = new Date()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import { getFrontmatter } from '@/content-linter/lib/helpers/utils'
import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'

export const frontmatterVersionsWhitespace = {
interface Frontmatter {
versions?: Record<string, string | string[]>
[key: string]: any
}

export const frontmatterVersionsWhitespace: Rule = {
names: ['GHD051', 'frontmatter-versions-whitespace'],
description: 'Versions frontmatter should not contain unnecessary whitespace',
tags: ['frontmatter', 'versions'],
function: (params, onError) => {
const fm = getFrontmatter(params.lines)
function: (params: RuleParams, onError: RuleErrorCallback) => {
const fm = getFrontmatter(params.lines) as Frontmatter | null
if (!fm || !fm.versions) return

const versionsObj = fm.versions
Expand Down Expand Up @@ -58,7 +65,7 @@ export const frontmatterVersionsWhitespace = {
* Allows whitespace in complex expressions like '<3.6 >3.8'
* but disallows leading/trailing whitespace
*/
function checkForUnwantedWhitespace(value) {
function checkForUnwantedWhitespace(value: string): boolean {
// Don't flag if the value is just whitespace or empty
if (!value || value.trim() === '') return false

Expand All @@ -82,7 +89,7 @@ function checkForUnwantedWhitespace(value) {
/**
* Get the cleaned version of a value by removing appropriate whitespace
*/
function getCleanedValue(value) {
function getCleanedValue(value: string): string {
// For values with operators, just trim leading/trailing whitespace
const hasOperators = /[<>=]/.test(value)
if (hasOperators) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import path from 'path'

import { getFrontmatter } from '../helpers/utils'
import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'

export const frontmatterVideoTranscripts = {
interface Frontmatter {
product_video?: string
product_video_transcript?: string
title?: string
layout?: string
[key: string]: any
}

export const frontmatterVideoTranscripts: Rule = {
names: ['GHD011', 'frontmatter-video-transcripts'],
description: 'Video transcript must be configured correctly',
tags: ['frontmatter', 'feature', 'video-transcripts'],
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
const filepath = params.name

const fm = getFrontmatter(params.lines)
const fm = getFrontmatter(params.lines) as Frontmatter | null
if (!fm) return

const isTranscriptContent =
Expand All @@ -29,7 +39,7 @@ export const frontmatterVideoTranscripts = {
null, // No fix possible
)
}
if (!fm.title.startsWith('Transcript - ')) {
if (fm.title && !fm.title.startsWith('Transcript - ')) {
const lineNumber = params.lines.findIndex((line) => line.startsWith('title:')) + 1
const lineContent = params.lines[lineNumber - 1]
addError(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations
import { addError } from 'markdownlint-rule-helpers'
import { getRange } from '../helpers/utils'
import frontmatter from '@/frame/lib/read-frontmatter'
import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'

export const multipleEmphasisPatterns = {
interface Frontmatter {
autogenerated?: boolean
[key: string]: any
}

export const multipleEmphasisPatterns: Rule = {
names: ['GHD050', 'multiple-emphasis-patterns'],
description: 'Do not use more than one emphasis/strong, italics, or uppercase for a string',
tags: ['formatting', 'emphasis', 'style'],
severity: 'warning',
function: (params, onError) => {
function: (params: RuleParams, onError: RuleErrorCallback) => {
// Skip autogenerated files
const frontmatterString = params.frontMatterLines.join('\n')
const fm = frontmatter(frontmatterString).data
const fm = frontmatter(frontmatterString).data as Frontmatter
if (fm && fm.autogenerated) return

const lines = params.lines
Expand Down Expand Up @@ -38,9 +45,9 @@ export const multipleEmphasisPatterns = {
/**
* Check for multiple emphasis types in a single text segment
*/
function checkMultipleEmphasis(line, lineNumber, onError) {
function checkMultipleEmphasis(line: string, lineNumber: number, onError: RuleErrorCallback): void {
// Focus on the clearest violations of the style guide
const multipleEmphasisPatterns = [
const multipleEmphasisPatterns: Array<{ regex: RegExp; types: string[] }> = [
// Bold + italic combinations (***text***)
{ regex: /\*\*\*([^*]+)\*\*\*/g, types: ['bold', 'italic'] },
{ regex: /___([^_]+)___/g, types: ['bold', 'italic'] },
Expand Down Expand Up @@ -76,7 +83,7 @@ function checkMultipleEmphasis(line, lineNumber, onError) {
/**
* Determine if a match should be skipped (likely intentional formatting)
*/
function shouldSkipMatch(fullMatch, content) {
function shouldSkipMatch(fullMatch: string, content: string): boolean {
// Skip common false positives
if (!content) return true

Expand Down
Loading
Loading