diff --git a/Dockerfile b/Dockerfile index db8aeb7097e2..b38af4eb26f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 diff --git a/next.config.js b/next.config.ts similarity index 76% rename from next.config.js rename to next.config.ts index 3e0720539bb1..5db11da8cb33 100644 --- a/next.config.js +++ b/next.config.ts @@ -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'], @@ -106,3 +86,5 @@ export default { styledComponents: true, }, } + +export default config diff --git a/src/content-linter/lib/helpers/get-lintable-yml.js b/src/content-linter/lib/helpers/get-lintable-yml.ts similarity index 82% rename from src/content-linter/lib/helpers/get-lintable-yml.js rename to src/content-linter/lib/helpers/get-lintable-yml.ts index 1a53648df11c..f697943349ea 100755 --- a/src/content-linter/lib/helpers/get-lintable-yml.js +++ b/src/content-linter/lib/helpers/get-lintable-yml.ts @@ -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() +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 @@ -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 }, @@ -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 | 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 @@ -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, dataFilePath: string): Map { 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 } diff --git a/src/content-linter/lib/linting-rules/early-access-references.js b/src/content-linter/lib/linting-rules/early-access-references.ts similarity index 80% rename from src/content-linter/lib/linting-rules/early-access-references.js rename to src/content-linter/lib/linting-rules/early-access-references.ts index c9e6f1ca8b54..666ae1e0ae2d 100644 --- a/src/content-linter/lib/linting-rules/early-access-references.js +++ b/src/content-linter/lib/linting-rules/early-access-references.ts @@ -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.' @@ -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 @@ -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 diff --git a/src/content-linter/lib/linting-rules/expired-content.js b/src/content-linter/lib/linting-rules/expired-content.ts similarity index 76% rename from src/content-linter/lib/linting-rules/expired-content.js rename to src/content-linter/lib/linting-rules/expired-content.ts index ba0bf4cc6f37..152de51cbbe0 100644 --- a/src/content-linter/lib/linting-rules/expired-content.js +++ b/src/content-linter/lib/linting-rules/expired-content.ts @@ -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: // @@ -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: // - const match = token.content.match(//) - if (!match) return + const match = token.content?.match(//) + if (!match || !token.content) return const expireDate = new Date(match.splice(1, 3).join(' ')) const today = new Date() @@ -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: // - const match = token.content.match(//) - if (!match) return + const match = token.content?.match(//) + if (!match || !token.content) return const expireDate = new Date(match.splice(1, 3).join(' ')) const today = new Date() diff --git a/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts similarity index 84% rename from src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js rename to src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts index befc1d5ee882..680873b80dd4 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.js +++ b/src/content-linter/lib/linting-rules/frontmatter-versions-whitespace.ts @@ -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 + [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 @@ -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 @@ -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) { diff --git a/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.js b/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts similarity index 83% rename from src/content-linter/lib/linting-rules/frontmatter-video-transcripts.js rename to src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts index ed736a54cdfe..b4ff762c1548 100644 --- a/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.js +++ b/src/content-linter/lib/linting-rules/frontmatter-video-transcripts.ts @@ -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 = @@ -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( diff --git a/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.js b/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts similarity index 80% rename from src/content-linter/lib/linting-rules/multiple-emphasis-patterns.js rename to src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts index 5e02edd74b1e..3075ce51ff8d 100644 --- a/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.js +++ b/src/content-linter/lib/linting-rules/multiple-emphasis-patterns.ts @@ -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 @@ -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'] }, @@ -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 diff --git a/src/content-linter/lib/linting-rules/table-liquid-versioning.js b/src/content-linter/lib/linting-rules/table-liquid-versioning.ts similarity index 86% rename from src/content-linter/lib/linting-rules/table-liquid-versioning.js rename to src/content-linter/lib/linting-rules/table-liquid-versioning.ts index e2dea1f4805f..12bc2dc96645 100644 --- a/src/content-linter/lib/linting-rules/table-liquid-versioning.js +++ b/src/content-linter/lib/linting-rules/table-liquid-versioning.ts @@ -1,4 +1,7 @@ -import { addError, filterTokens } from 'markdownlint-rule-helpers' +// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations +import { addError } from 'markdownlint-rule-helpers' + +import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types' // Detects a Markdown table delimiter row const delimiterRegexPure = /(\s)*(:)?(-+)(:)?(\s)*(\|)/ @@ -9,13 +12,13 @@ const liquidRegex = /^{%-?\s*(ifversion|else|endif).*-?%}/ // Detects a Markdown table row with a Liquid versioning tag const liquidAfterRowRegex = /(\|{1}).*(\|{1}).*{%\s*(ifversion|else|endif).*%}$/ -export const tableLiquidVersioning = { +export const tableLiquidVersioning: Rule = { names: ['GHD040', 'table-liquid-versioning'], description: 'Tables must use the correct liquid versioning format', severity: 'error', tags: ['tables'], - function: function GHD040(params, onError) { + function: function GHD040(params: RuleParams, onError: RuleErrorCallback) { const lines = params.lines let inTable = false for (let i = 0; i < lines.length; i++) { @@ -75,7 +78,7 @@ export const tableLiquidVersioning = { }, } -function isPreviousLineIndented(line, previousLine) { +function isPreviousLineIndented(line: string, previousLine: string): boolean { if (!line || !previousLine) return false const numWhitespaceLine = line.length - line.trimLeft().length const numWhitespacePrevLine = previousLine.length - previousLine.trimLeft().length diff --git a/src/content-linter/lib/linting-rules/third-party-action-pinning.js b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts similarity index 69% rename from src/content-linter/lib/linting-rules/third-party-action-pinning.js rename to src/content-linter/lib/linting-rules/third-party-action-pinning.ts index 78cffe7165ea..ed3ffc88fb0f 100644 --- a/src/content-linter/lib/linting-rules/third-party-action-pinning.js +++ b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts @@ -1,8 +1,10 @@ -import yaml from 'js-yaml' +// @ts-ignore - markdownlint-rule-helpers doesn't provide TypeScript declarations import { addError, filterTokens } from 'markdownlint-rule-helpers' +import yaml from 'js-yaml' import { liquid } from '@/content-render/index' import { allVersions } from '@/versions/lib/all-versions' +import type { RuleParams, RuleErrorCallback, MarkdownToken, Rule } from '@/content-linter/types' // Detects third-party actions in the format `owner/repo@ref` const actionRegex = /[\w-]+\/[\w-]+@[\w-]+/ @@ -11,16 +13,33 @@ const shaRegex = /[\w-]+\/[\w-]+@[0-9a-fA-F]{40}/ // Detects first-party actions const firstPartyPrefixes = ['actions/', './.github/actions/', 'github/', 'octo-org/', 'OWNER/'] -export const thirdPartyActionPinning = { +interface WorkflowStep { + uses?: string + [key: string]: any +} + +interface WorkflowJob { + steps?: WorkflowStep[] + [key: string]: any +} + +interface WorkflowYaml { + jobs?: Record + steps?: WorkflowStep[] + [key: string]: any +} + +export const thirdPartyActionPinning: Rule = { names: ['GHD041', 'third-party-action-pinning'], description: 'Code examples that use third-party actions must always pin to a full length commit SHA', tags: ['feature', 'actions'], parser: 'markdownit', asynchronous: true, - function: (params, onError) => { - filterTokens(params, 'fence', async (token) => { - const lang = token.info.trim().split(/\s+/u).shift().toLowerCase() + function: (params: RuleParams, onError: RuleErrorCallback) => { + filterTokens(params, 'fence', async (token: MarkdownToken) => { + if (!token.info || !token.content) return + const lang = token.info.trim().split(/\s+/u).shift()?.toLowerCase() if (lang !== 'yaml' && lang !== 'yml') return if (!token.content.includes('steps:')) return if (!token.content.includes('uses:')) return @@ -32,7 +51,7 @@ export const thirdPartyActionPinning = { // If we don't parse the Liquid first, yaml loading chokes on {% raw %} tags const renderedYaml = await liquid.parseAndRender(token.content, context) try { - const yamlObj = yaml.load(renderedYaml) + const yamlObj = yaml.load(renderedYaml) as WorkflowYaml const steps = getWorkflowSteps(yamlObj) if (!steps.some((step) => step.uses)) return @@ -40,11 +59,13 @@ export const thirdPartyActionPinning = { if (step.uses) { const actionMatch = step.uses.match(actionRegex) if (actionMatch) { - const isFirstParty = firstPartyPrefixes.some((prefix) => step.uses.startsWith(prefix)) + const isFirstParty = firstPartyPrefixes.some((prefix) => + step.uses!.startsWith(prefix), + ) if (!isFirstParty && !shaRegex.test(step.uses)) { addError( onError, - getLineNumber(token.content, step.uses) + token.lineNumber, + getLineNumber(token.content!, step.uses) + token.lineNumber, 'Code examples that use third-party actions must always pin to a full length commit SHA', step.uses, ) @@ -64,7 +85,7 @@ export const thirdPartyActionPinning = { }, } -function getWorkflowSteps(yamlObj) { +function getWorkflowSteps(yamlObj: WorkflowYaml): WorkflowStep[] { if (yamlObj?.jobs) { const jobs = Object.values(yamlObj.jobs) return jobs.flatMap((job) => job.steps || []) @@ -74,7 +95,7 @@ function getWorkflowSteps(yamlObj) { return [] } -function getLineNumber(tokenContent, step) { +function getLineNumber(tokenContent: string, step: string): number { const contentLines = tokenContent.split('\n') return contentLines.findIndex((line) => line.includes(step)) + 1 } diff --git a/src/content-render/liquid/data.js b/src/content-render/liquid/data.ts similarity index 76% rename from src/content-render/liquid/data.js rename to src/content-render/liquid/data.ts index 732dbc8eaaf8..fce51925c73f 100644 --- a/src/content-render/liquid/data.js +++ b/src/content-render/liquid/data.ts @@ -1,4 +1,5 @@ import { TokenizationError } from 'liquidjs' +import type { TagToken, Liquid, Template } from 'liquidjs' import { THROW_ON_EMPTY, DataReferenceError } from './error-handling' import { getDataByLanguage } from '@/data-directory/lib/get-data' @@ -6,8 +7,22 @@ import { getDataByLanguage } from '@/data-directory/lib/get-data' const Syntax = /([a-z0-9/\\_.\-[\]]+)/i const SyntaxHelp = "Syntax Error in 'data' - Valid syntax: data [path]" +// Using any for scope because it has custom environments property not in Liquid's Scope type +interface CustomScope { + environments: any + [key: string]: any +} + +interface DataTag { + path: string + tagToken: TagToken + liquid: Liquid + parse(tagToken: TagToken): void + render(scope: CustomScope): Promise