-
Notifications
You must be signed in to change notification settings - Fork 9
feat: add feature related configuration #261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,27 @@ | ||
| import type { ModuleOptions } from './module' | ||
| import type { FeaturesName } from './runtime/core/types' | ||
| import type { FeatureOptionsMap } from './runtime/feature-options' | ||
|
|
||
| export function isFeatureDevtoolsEnabled(options: ModuleOptions, feature: FeaturesName): boolean { | ||
| return typeof options.features[feature] === 'object' ? options.features[feature].devtools : !!options.features[feature] | ||
| const val = options.features[feature] | ||
| return typeof val === 'object' ? val.devtools !== false : !!val | ||
| } | ||
|
|
||
| export function isFeatureEnabled(options: ModuleOptions, feature: FeaturesName): boolean { | ||
| return !!options.features[feature] | ||
| } | ||
|
|
||
| /** | ||
| * Extract the per-feature options from the module config. | ||
| * Returns an empty object if the feature is set to a simple boolean. | ||
| */ | ||
| export function getFeatureOptions<K extends keyof FeatureOptionsMap>( | ||
| options: ModuleOptions, | ||
| feature: K, | ||
| ): FeatureOptionsMap[K] { | ||
| const val = options.features[feature] | ||
| if (typeof val === 'object' && val.options) { | ||
| return val.options as FeatureOptionsMap[K] | ||
| } | ||
| return {} as FeatureOptionsMap[K] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,24 @@ | ||
| // @ts-expect-error virtual file | ||
| import { features } from '#hints-config' | ||
| import type { FeaturesName } from './types' | ||
| import type { FeatureOptionsMap } from '../feature-options' | ||
|
|
||
| export function isFeatureDevtoolsEnabled(feature: FeaturesName): boolean { | ||
| return typeof features[feature] === 'object' ? features[feature].devtools : !!features[feature] | ||
| return typeof features[feature] === 'object' ? features[feature].devtools !== false : !!features[feature] | ||
| } | ||
|
|
||
| export function isFeatureLogsEnabled(feature: FeaturesName): boolean { | ||
| return typeof features[feature] === 'object' ? features[feature].logs : !!features[feature] | ||
| return typeof features[feature] === 'object' ? features[feature].logs !== false : !!features[feature] | ||
| } | ||
|
|
||
| export function isFeatureEnabled(feature: FeaturesName): boolean { | ||
| return !!features[feature] | ||
| } | ||
|
|
||
| export function getFeatureOptions<K extends keyof FeatureOptionsMap>(feature: K): FeatureOptionsMap[K] | undefined { | ||
| const val = features[feature] | ||
| if (typeof val === 'object' && val.options) { | ||
| return val.options as FeatureOptionsMap[K] | ||
| } | ||
| return undefined | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,11 +1,20 @@ | ||||||
| import type { FeatureOptionsMap } from '../feature-options' | ||||||
|
|
||||||
| export type FeaturesName = 'hydration' | 'lazyLoad' | 'webVitals' | 'thirdPartyScripts' | 'htmlValidate' | ||||||
|
|
||||||
| export type FeatureFlags<T extends Record<string, any> = Record<string, never>> = { | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace explicit Line 5 violates Suggested fix-export type FeatureFlags<T extends Record<string, any> = Record<string, never>> = {
+export type FeatureFlags<T extends Record<string, unknown> = Record<string, never>> = {📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: ci[error] 5-5: eslint error: Unexpected any. Specify a different type ( 🪛 GitHub Check: ci[failure] 5-5: 🤖 Prompt for AI Agents |
||||||
| logs?: boolean | ||||||
| devtools?: boolean | ||||||
| /** | ||||||
| * Any feature specific options | ||||||
| */ | ||||||
| options?: T | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * FF used by modules options and to expose in the payload for devtools | ||||||
| * Fully-resolved features configuration type, where each feature | ||||||
| * can be a simple boolean or a FeatureFlags with its own options. | ||||||
| */ | ||||||
| export type FeatureFlags = { | ||||||
| logs: boolean | ||||||
| devtools: boolean | ||||||
| export type Features = { | ||||||
| [K in FeaturesName]: boolean | FeatureFlags<K extends keyof FeatureOptionsMap ? FeatureOptionsMap[K] : Record<string, never>> | ||||||
| } | ||||||
|
|
||||||
| export type Features = Record<FeaturesName, FeatureFlags> | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import type { HtmlValidateFeatureOptions } from './html-validate/types' | ||
| import type { ThirdPartyScriptsFeatureOptions } from './third-party-scripts/types' | ||
| import type { WebVitalsFeatureOptions } from './web-vitals/types' | ||
|
|
||
| export interface FeatureOptionsMap { | ||
| thirdPartyScripts: ThirdPartyScriptsFeatureOptions | ||
| htmlValidate: HtmlValidateFeatureOptions | ||
| webVitals: WebVitalsFeatureOptions | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,33 +1,41 @@ | ||||||||||||||||||
| import type { NitroAppPlugin } from 'nitropack/types' | ||||||||||||||||||
| import { HtmlValidate } from 'html-validate' | ||||||||||||||||||
| import { HtmlValidate, type ConfigData, type RuleConfig } from 'html-validate' | ||||||||||||||||||
| import { addBeforeBodyEndTag } from './utils' | ||||||||||||||||||
| import { storage } from './storage' | ||||||||||||||||||
| import { randomUUID } from 'crypto' | ||||||||||||||||||
| import { stringify } from 'devalue' | ||||||||||||||||||
| import type { HtmlValidateReport } from './types' | ||||||||||||||||||
| import { format } from 'prettier/standalone' | ||||||||||||||||||
| import html from 'prettier/parser-html' | ||||||||||||||||||
| import { getFeatureOptions } from '../core/features' | ||||||||||||||||||
| import { defu } from 'defu' | ||||||||||||||||||
|
|
||||||||||||||||||
| const DEFAULT_EXTENDS = [ | ||||||||||||||||||
| 'html-validate:standard', | ||||||||||||||||||
| 'html-validate:document', | ||||||||||||||||||
| 'html-validate:browser', | ||||||||||||||||||
| ] | ||||||||||||||||||
|
|
||||||||||||||||||
| const DEFAULT_RULES: RuleConfig = { | ||||||||||||||||||
| 'svg-focusable': 'off', | ||||||||||||||||||
| 'no-unknown-elements': 'error', | ||||||||||||||||||
| 'void-style': 'off', | ||||||||||||||||||
| 'no-trailing-whitespace': 'off', | ||||||||||||||||||
| // Conflict with Nuxt defaults | ||||||||||||||||||
| 'require-sri': 'off', | ||||||||||||||||||
| 'attribute-boolean-style': 'off', | ||||||||||||||||||
| 'doctype-style': 'off', | ||||||||||||||||||
| // Unreasonable rule | ||||||||||||||||||
| 'no-inline-style': 'off', | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| export default <NitroAppPlugin> function (nitro) { | ||||||||||||||||||
| const validator = new HtmlValidate({ | ||||||||||||||||||
| extends: [ | ||||||||||||||||||
| 'html-validate:standard', | ||||||||||||||||||
| 'html-validate:document', | ||||||||||||||||||
| 'html-validate:browser', | ||||||||||||||||||
| ], | ||||||||||||||||||
| rules: { | ||||||||||||||||||
| 'svg-focusable': 'off', | ||||||||||||||||||
| 'no-unknown-elements': 'error', | ||||||||||||||||||
| 'void-style': 'off', | ||||||||||||||||||
| 'no-trailing-whitespace': 'off', | ||||||||||||||||||
| // Conflict with Nuxt defaults | ||||||||||||||||||
| 'require-sri': 'off', | ||||||||||||||||||
| 'attribute-boolean-style': 'off', | ||||||||||||||||||
| 'doctype-style': 'off', | ||||||||||||||||||
| // Unreasonable rule | ||||||||||||||||||
| 'no-inline-style': 'off', | ||||||||||||||||||
| }, | ||||||||||||||||||
| }) | ||||||||||||||||||
| const opts: ConfigData = defu({ | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove trailing whitespace on Line 33 to clear CI style checks. The current CI output flags trailing spaces here. 🧰 Tools🪛 GitHub Check: ci[failure] 33-33: 🤖 Prompt for AI Agents |
||||||||||||||||||
| extends: DEFAULT_EXTENDS, | ||||||||||||||||||
| rules: DEFAULT_RULES, | ||||||||||||||||||
| }, getFeatureOptions('htmlValidate') ?? {}) | ||||||||||||||||||
|
Comment on lines
+33
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In Priority order (highest → lowest):
So if the same key exists in multiple arguments, the earliest (leftmost) value is kept, and later arguments only fill in missing properties. [1] Example: Source: UnJS defu docs. [1] References: Citations: 🏁 Script executed: find . -name "nitro.plugin.ts" -type fRepository: nuxt/hints Length of output: 227 🏁 Script executed: cat -n ./src/runtime/html-validate/nitro.plugin.ts | head -50Repository: nuxt/hints Length of output: 2154 User In Suggested fix- const opts: ConfigData = defu({
- extends: DEFAULT_EXTENDS,
- rules: DEFAULT_RULES,
- }, getFeatureOptions('htmlValidate') ?? {})
+ const opts: ConfigData = defu(getFeatureOptions('htmlValidate') ?? {}, {
+ extends: DEFAULT_EXTENDS,
+ rules: DEFAULT_RULES,
+ })📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: ci[failure] 33-33: 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| const validator = new HtmlValidate(opts) | ||||||||||||||||||
|
|
||||||||||||||||||
| nitro.hooks.hook('render:response', async (response, { event }) => { | ||||||||||||||||||
| if (typeof response.body === 'string' && (response.headers?.['Content-Type'] || response.headers?.['content-type'])?.includes('html')) { | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,13 +1,19 @@ | ||||
| import { defineNuxtPlugin, ref, useNuxtApp } from '#imports' | ||||
| import { defu } from 'defu' | ||||
| import { logger } from './utils' | ||||
| import { getFeatureOptions } from '../core/features' | ||||
| import type { ThirdPartyScriptsFeatureOptions } from './types' | ||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused Line 5 is currently unused and reported by the lint job. Suggested fix-import type { ThirdPartyScriptsFeatureOptions } from './types'📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: ci[failure] 5-5: 🤖 Prompt for AI Agents |
||||
|
|
||||
| const EXTENSIONS_SCHEMES_RE = /^(chrome-extension|moz-extension|safari-extension|ms-browser-extension):/ | ||||
| const DEFAULT_EXTENSION_SCHEMES = ['chrome-extension', 'moz-extension', 'safari-extension', 'ms-browser-extension'] | ||||
|
|
||||
| function isExtensionScript(src: string) { | ||||
| function buildExtensionSchemesRegex(schemes: string[]) { | ||||
| return new RegExp(`^(${schemes.join('|')}):`) | ||||
|
Comment on lines
+9
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape configured schemes before interpolating into Lines 9–10 directly join config values into regex source. A scheme containing regex metacharacters can produce invalid patterns or unintended matches. Suggested fix+function escapeRegex(source: string): string {
+ return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
function buildExtensionSchemesRegex(schemes: string[]) {
- return new RegExp(`^(${schemes.join('|')}):`)
+ return new RegExp(`^(${schemes.map(escapeRegex).join('|')}):`)
}🧰 Tools🪛 ast-grep (0.41.0)[warning] 9-9: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns. (regexp-from-variable) 🤖 Prompt for AI Agents |
||||
| } | ||||
|
|
||||
| function isExtensionScript(src: string, schemesRegex: RegExp) { | ||||
| try { | ||||
| const url = new URL(src, window.location.origin) | ||||
| return EXTENSIONS_SCHEMES_RE.test(url.protocol) | ||||
| return schemesRegex.test(url.protocol) | ||||
| } | ||||
| catch { | ||||
| return false | ||||
|
|
@@ -24,14 +30,30 @@ | |||
| } | ||||
| } | ||||
|
|
||||
| function isIgnoredScript(src: string) { | ||||
| return isSameOriginScript(src) || isExtensionScript(src) | ||||
| function isIgnoredDomain(src: string, ignoredDomains: string[]) { | ||||
| if (ignoredDomains.length === 0) return false | ||||
| try { | ||||
| const url = new URL(src, window.location.origin) | ||||
| return ignoredDomains.some(domain => url.hostname === domain || url.hostname.endsWith(`.${domain}`)) | ||||
| } | ||||
| catch { | ||||
| return false | ||||
| } | ||||
| } | ||||
|
|
||||
| export default defineNuxtPlugin({ | ||||
| name: 'nuxt-hints:third-party-scripts', | ||||
| setup() { | ||||
| const nuxtApp = useNuxtApp() | ||||
| const opts = getFeatureOptions('thirdPartyScripts') ?? {} | ||||
|
|
||||
| const extensionSchemes = [...DEFAULT_EXTENSION_SCHEMES, ...(opts.ignoredSchemes ?? [])] | ||||
| const schemesRegex = buildExtensionSchemesRegex(extensionSchemes) | ||||
| const ignoredDomains = opts.ignoredDomains ?? [] | ||||
|
|
||||
| function isIgnoredScript(src: string) { | ||||
| return isSameOriginScript(src) || isExtensionScript(src, schemesRegex) || isIgnoredDomain(src, ignoredDomains) | ||||
| } | ||||
|
|
||||
| nuxtApp.payload.__hints = defu(nuxtApp.payload.__hints, { | ||||
| thirdPartyScripts: ref<{ element: HTMLScriptElement, loaded: boolean }[]>([]), | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| export interface ThirdPartyScriptsFeatureOptions { | ||
| /** Additional domains to ignore when detecting third-party scripts */ | ||
| ignoredDomains?: string[] | ||
| /** Additional URL scheme patterns to ignore (default includes browser extension schemes) */ | ||
| ignoredSchemes?: string[] | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ | |||||
| import type { LCPMetricWithAttribution, INPMetricWithAttribution, CLSMetricWithAttribution } from 'web-vitals/attribution' | ||||||
| import type { HydrationMismatchPayload, LocalHydrationMismatch } from './hydration/types' | ||||||
| import type { DirectImportInfo, LazyHydrationState } from './lazy-load/composables' | ||||||
| import type { FeaturesName, FeatureFlags } from './core/types' | ||||||
| import type { FeaturesName, Features } from './core/types' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused
Suggested fix-import type { FeaturesName, Features } from './core/types'
+import type { Features } from './core/types'📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Check: ci[failure] 5-5: 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| declare global { | ||||||
| interface Window { | ||||||
|
|
@@ -40,7 +40,7 @@ | |||||
| __tracerRecord: typeof import('vite-plugin-vue-tracer/client/record') | ||||||
| hints: { | ||||||
| config: { | ||||||
| features: Record<FeaturesName, FeatureFlags | boolean> | ||||||
| features: Features | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard object checks against
nullbefore property access.Lines 7, 11, and 20 use
typeof value === 'object'and then access fields. If a feature isnull, this will throw at runtime.Suggested fix
export function isFeatureDevtoolsEnabled(feature: FeaturesName): boolean { - return typeof features[feature] === 'object' ? features[feature].devtools !== false : !!features[feature] + const value = features[feature] + return value && typeof value === 'object' ? value.devtools !== false : !!value } export function isFeatureLogsEnabled(feature: FeaturesName): boolean { - return typeof features[feature] === 'object' ? features[feature].logs !== false : !!features[feature] + const value = features[feature] + return value && typeof value === 'object' ? value.logs !== false : !!value } @@ export function getFeatureOptions<K extends keyof FeatureOptionsMap>(feature: K): FeatureOptionsMap[K] | undefined { const val = features[feature] - if (typeof val === 'object' && val.options) { + if (val && typeof val === 'object' && val.options) { return val.options as FeatureOptionsMap[K] } return undefined }Also applies to: 18-23
🤖 Prompt for AI Agents