Skip to content

Conversation

@danielroe
Copy link
Owner

@danielroe danielroe commented Oct 26, 2024

this PR rewrites beasties in TypeScript

while it doesn't touch the underlying implementation it does fix some bugs (such as not respecting a false keyframes option) and adds some additional checks for greater safety

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced new types for preload strategies and keyframe strategies in the beasties package.
    • Added a new Logger interface to standardize logging functionality.
    • Enhanced DOM manipulation capabilities with a new dom.ts file.
  • Enhancements

    • Improved type safety across various methods in the Beasties class and other utility functions.
    • Updated build process and output file organization for the beasties package.
  • Bug Fixes

    • Removed outdated CSS and DOM manipulation files to streamline the application.
  • Documentation

    • Simplified JSDoc comments for method signatures in the Beasties class.

@coderabbitai
Copy link

coderabbitai bot commented Oct 26, 2024

Warning

Rate limit exceeded

@danielroe has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Files that changed from the base of the PR and between f97df38 and 9fb2d39.

Walkthrough

The pull request introduces several changes across multiple files in the beasties package. Key updates include the addition of a new development dependency in package.json, restructuring of the exports and build process in the beasties package, and enhancements to type safety in various TypeScript files. The dom.js file is removed and replaced with a new dom.ts file that provides similar functionality with improved type definitions. Additional type definitions and interfaces are introduced to support configuration and logging.

Changes

File Path Change Summary
package.json Added development dependency @types/node with version 18.13.0; updated bumpp version from ^9.7.1 to 9.7.1.
packages/beasties/package.json Updated exports, main, and module fields; changed build tool from microbundle to unbuild; added dependency css-what with version ^6.1.0; removed microbundle.
packages/beasties/src/css.ts Added type annotations and interfaces to enhance type safety for various functions.
packages/beasties/src/dom.js Removed file, which contained DOM parsing functionality.
packages/beasties/src/dom.ts Introduced new file with DOM parsing functionality and extended interfaces for better type safety.
packages/beasties/src/index.d.ts Updated JSDoc comments for process method in Beasties class to remove explicit type annotations.
packages/beasties/src/index.ts Enhanced type safety and method signatures in the Beasties class; added new properties and updated existing method signatures.
packages/beasties/src/text.css Removed file containing CSS styles and media queries.
packages/beasties/src/types.ts Introduced new types and an interface for configuration options in the Beasties constructor.
packages/beasties/src/util.ts Added Logger interface, updated method signatures, and improved type safety for logging functions.
vitest.config.mts Added import for fileURLToPath and updated configuration to include a resolve alias for beasties.

Possibly related PRs

  • refactor: migrate to beasties and beasties-webpack-plugin #2: The changes in the main PR regarding the addition of a new development dependency in package.json are related to the updates in the package.json of the retrieved PR, which also involves modifications to dependencies and project structure during the migration to beasties.

🐰 In the garden where the code does bloom,
New types and tools make room for zoom!
With dependencies fresh and structures neat,
Our beasties package is now complete!
So hop along, let's code and play,
For every change brings a brighter day! 🌼


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@socket-security
Copy link

socket-security bot commented Oct 26, 2024

New and removed dependencies detected. Learn more about Socket for GitHub ↗︎

Package New capabilities Transitives Size Publisher
npm/@types/[email protected] None 0 3.59 MB types
npm/[email protected] None 0 91.3 kB sokra

🚮 Removed packages: npm/[email protected], npm/[email protected]

View full report↗︎

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Outside diff range and nitpick comments (9)
package.json (1)

Remove microbundle and update build system consistently

The verification confirms inconsistent build tooling across packages:

  • Main beasties package uses unbuild for building
  • beasties-webpack-plugin still uses microbundle
  • Root package.json has microbundle as a dependency but doesn't use it directly

Suggested changes:

  • Remove microbundle from root package.json since it's not used directly
  • Consider migrating beasties-webpack-plugin to use unbuild for consistency with the main package
🔗 Analysis chain

Line range hint 44-44: Verify build tool transition.

The AI summary mentions replacing microbundle with unbuild, but microbundle is still listed in the dependencies. If the build system has been transitioned to unbuild, this dependency should be removed and unbuild should be added.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if unbuild is used in any package.json files
rg -l "unbuild" -g "package.json"

# Check for any remaining microbundle usage in build scripts
rg "microbundle" -g "package.json"

Length of output: 326


Script:

#!/bin/bash
# Check the build script in the root package.json to see if it's using microbundle or unbuild
rg '"build":|"dev":|"watch":|"compile":' package.json -A 1

# Check the build script in beasties package to see if it's using unbuild
cat packages/beasties/package.json

Length of output: 1720

packages/beasties/src/util.ts (2)

Line range hint 22-48: Consider adding defensive error handling for console methods.

While the implementation is good, it might be worth adding try-catch blocks to handle environments where console methods might be undefined or restricted.

Here's a safer implementation pattern:

 export const defaultLogger = {
   trace(msg) {
+    try {
       // eslint-disable-next-line no-console
       console.trace(msg)
+    } catch {
+      // Fallback to console.log or silently fail
+      try { console.log('[TRACE]', msg) } catch {}
+    }
   },
   // Apply similar pattern to other methods
 } satisfies Logger

Line range hint 51-63: Consider a more efficient logger creation approach.

The current implementation using reduce works but creates intermediate objects. A more efficient approach would be to use Object.fromEntries.

Here's a more efficient implementation:

 export function createLogger(logLevel: LogLevel) {
   const logLevelIdx = LOG_LEVELS.indexOf(logLevel)
-  return LOG_LEVELS.reduce((logger: Partial<Logger>, type, index) => {
-    if (index >= logLevelIdx) {
-      logger[type] = defaultLogger[type]
-    }
-    else {
-      logger[type] = defaultLogger.silent
-    }
-    return logger
-  }, {})
+  return Object.fromEntries(
+    LOG_LEVELS.map(type => [
+      type,
+      LOG_LEVELS.indexOf(type) >= logLevelIdx
+        ? defaultLogger[type]
+        : defaultLogger.silent
+    ])
+  ) as Partial<Logger>
 }
packages/beasties/src/types.ts (2)

97-97: Add documentation for reduceInlineStyles property.

The reduceInlineStyles property lacks JSDoc documentation explaining its purpose and default value.

Add documentation similar to other properties:

/**
 * Reduce duplicate inline styles by combining identical rules _(default: `false`)_
 */
reduceInlineStyles?: boolean

79-85: Clarify fonts property documentation.

The documentation for the fonts property could be clearer about its relationship with inlineFonts and preloadFonts.

Consider updating the documentation:

/**
 * Shorthand for setting both `inlineFonts` and `preloadFonts` properties
 * @example
 * fonts: true  // equivalent to { inlineFonts: true, preloadFonts: true }
 * fonts: false // equivalent to { inlineFonts: false, preloadFonts: false }
 */
fonts?: boolean
packages/beasties/src/index.ts (4)

386-394: Ensure consistent handling of keyframes options

In the processStyle method, the keyframes option is processed to determine the keyframesMode. There might be scenarios where unexpected values could lead to inconsistent behavior. Consider adding validation or default cases to handle all possible values of options.keyframes.

For example:

       let keyframesMode = options.keyframes ?? 'critical'
       // we also accept a boolean value for options.keyframes
       if (keyframesMode === true)
         keyframesMode = 'all'
       if (keyframesMode === false)
         keyframesMode = 'none'
+      if (!['all', 'none', 'critical'].includes(keyframesMode)) {
+        keyframesMode = 'critical'
+      }

Line range hint 487-522: Handle potential exceptions in selector processing

When filtering selectors, exceptions are caught and added to failedSelectors, but the original selector is modified before the exception occurs. To aid in debugging, consider logging the original selector that caused the exception.

Apply this diff to improve error handling:

               let originalSel = sel
               // Strip pseudo-elements and pseudo-classes...
               sel = sel
                 .replace(/(?<!\\)::?[a-z-]+(?![a-z-(])/gi, '')
                 // Additional replacements
               try {
                 return beastiesContainer.exists(sel)
               }
               catch (e) {
-                failedSelectors.push(`${sel} -> ${(e as Error).message || (e as Error).toString()}`)
+                failedSelectors.push(`${originalSel} -> ${(e as Error).message || (e as Error).toString()}`)
                 return false
               }

Line range hint 566-571: Review logging of failed selectors for potential information leakage

The warning message logs all failed selectors, which could potentially expose sensitive information. Consider limiting the amount of logged data or sanitizing the output to prevent any unintended information disclosure.

For example, you could limit the number of selectors logged:

         if (failedSelectors.length !== 0) {
+          const displayedSelectors = failedSelectors.slice(0, 10)
           this.logger.warn?.(
             `${failedSelectors.length
             } rules skipped due to selector errors:\n  ${displayedSelectors.join(
               '\n  ',
             )}${failedSelectors.length > 10 ? '\n  ...' : ''}`,
           )
         }

Line range hint 687-696: Use consistent units in formatSize function

In the formatSize function, the abbreviations array uses 'kB', 'MB', 'GB', which implies base 10 units (kilobytes, megabytes). However, the calculation uses base 1024, which is appropriate for kibibytes (KiB), mebibytes (MiB), etc. Consider updating the abbreviations or calculations for consistency.

Option 1: Use base 1000 for calculations:

   const roundedSize = size / 1000 ** index

Option 2: Update abbreviations to binary units:

-  const abbreviations = ['bytes', 'kB', 'MB', 'GB']
+  const abbreviations = ['bytes', 'KiB', 'MiB', 'GiB']
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 89b3603 and 3601fac.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • package.json (1 hunks)
  • packages/beasties/package.json (2 hunks)
  • packages/beasties/src/css.ts (9 hunks)
  • packages/beasties/src/dom.js (0 hunks)
  • packages/beasties/src/dom.ts (1 hunks)
  • packages/beasties/src/index.d.ts (1 hunks)
  • packages/beasties/src/index.ts (24 hunks)
  • packages/beasties/src/text.css (0 hunks)
  • packages/beasties/src/types.ts (1 hunks)
  • packages/beasties/src/util.ts (3 hunks)
💤 Files with no reviewable changes (2)
  • packages/beasties/src/dom.js
  • packages/beasties/src/text.css
✅ Files skipped from review due to trivial changes (1)
  • packages/beasties/src/index.d.ts
🧰 Additional context used
🪛 Biome
packages/beasties/src/css.ts

[error] 89-89: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

packages/beasties/src/dom.ts

[error] 212-212: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🔇 Additional comments (20)
packages/beasties/package.json (5)

33-35: LGTM! Export configuration follows Node.js best practices.

The changes to use .cjs extension for CommonJS and conventional index.* naming are improvements.


38-39: LGTM! Main/module fields are properly configured.

The fields are correctly aligned with the exports configuration and follow dual package hazard guidelines.


42-42: LGTM! Distribution files configuration is correct for a TypeScript package.

Removing src from the files array is appropriate as TypeScript source files shouldn't be published.


51-51: LGTM! Dependencies are properly configured.

The addition of css-what aligns with the existing css-select dependency, and unbuild is a suitable replacement for microbundle in a TypeScript project.

Also applies to: 62-62

✅ Verification successful

LGTM! The css-what dependency is correctly added and properly used

The codebase directly uses css-what in packages/beasties/src/dom.ts for parsing CSS selectors in the cachedQuerySelector function through selectorParser. This validates the addition of css-what as a direct dependency, not just as a peer dependency of css-select.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify css-what usage in the codebase
rg -l 'css-what'

# Check if css-what is used directly or only through css-select
ast-grep --pattern 'require("css-what")' || ast-grep --pattern 'from "css-what"'

Length of output: 231


Script:

#!/bin/bash
# Let's check the actual usage of css-what in dom.ts
cat packages/beasties/src/dom.ts

# Also check if css-what is used through css-select
ast-grep --pattern 'from "css-select"'

# And verify the imports in dom.ts specifically
ast-grep --pattern 'from "css-what"' packages/beasties/src/dom.ts

Length of output: 8773


45-45: Verify type definitions handling with unbuild.

The manual copying of type definitions (cp src/index.d.ts dist/index.d.ts) suggests unbuild might not be properly configured for TypeScript declaration files. Consider configuring unbuild to handle type definitions automatically.

✅ Verification successful

Manual copy of type definitions is necessary in this case

The current setup is actually correct and necessary because:

  1. The project uses a handwritten index.d.ts file with detailed type definitions and documentation
  2. The tsconfig.json has "noEmit": true, indicating type generation is not handled by TypeScript
  3. Unbuild is being used primarily for bundling, while the types are maintained separately

The manual copy step is the right approach here since we're using a pre-written type definition file rather than relying on automatic type generation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if unbuild configuration exists and verify its TypeScript settings
fd -e config.ts -e config.js -e json -g '*unbuild*'

# Verify if there are other type definition files that need handling
fd -e d.ts -t f

Length of output: 231


Script:

#!/bin/bash
# Check unbuild configuration in build.config.ts (common location)
fd build.config.ts

# Look for any unbuild configuration in package.json
rg -l "unbuild" packages/beasties/package.json

# Check the content of src/index.d.ts to understand what types need handling
cat packages/beasties/src/index.d.ts

# Check if there's a tsconfig.json that might affect type generation
fd tsconfig.json

Length of output: 2510


Script:

#!/bin/bash
# Check tsconfig.json content for declaration settings
cat tsconfig.json

# Check if unbuild is configured in package.json
rg -A 5 -B 5 "unbuild" packages/beasties/package.json

# Look for any build output to see if types are generated
fd -t f . packages/beasties/dist/

Length of output: 1214

packages/beasties/src/util.ts (1)

5-20: Well-structured type definitions with good documentation!

The type definitions are well-designed with:

  • Immutable log levels using as const
  • Clear interface documentation
  • Consistent method signatures
packages/beasties/src/types.ts (1)

1-2: LGTM! Clean type imports and exports.

The type imports and re-exports are well-structured, following TypeScript best practices for creating a public API.

Also applies to: 104-104

packages/beasties/src/css.ts (13)

17-20: Imports are well-defined and enhance type safety

The addition of type imports from 'postcss' and 'postcss-media-query-parser' improves the code's readability and type safety.


28-28: Explicit parameter typing in parseStylesheet function

Specifying the stylesheet parameter type as string in the parseStylesheet function enhances type safety and makes the function interface clearer.


32-39: Well-defined SerializeStylesheetOptions interface

Defining the SerializeStylesheetOptions interface provides clarity on the expected options for the serializeStylesheet function, improving maintainability.


Line range hint 45-70: Enhanced type annotations in serializeStylesheet function

The serializeStylesheet function now includes explicit types for its parameters, which enhances code clarity and reduces potential type-related errors.


70-70: Nullish coalescing and optional chaining used appropriately

The use of optional chaining (node?.type) and checks for properties like node.selectors are correctly implemented to prevent runtime errors when properties may be undefined.


97-106: Type annotations in markOnly function enhance clarity

The markOnly function's updated type annotations for its parameters and return type improve code readability and ensure correct usage.


133-137: Correct use of type guards in walkStyleRules function

The function appropriately checks if node has nodes property before proceeding, preventing potential runtime errors.


155-166: Proper handling of optional parameters in walkStyleRulesWithReverseMirror

The function correctly handles cases where node2 might be undefined or null, ensuring robustness in recursive calls.


175-179: Effective use of type guard in hasNestedRules function

The hasNestedRules function correctly identifies nodes with nested rules, improving the reliability of rule processing.


187-199: Generic splitFilter function improves reusability

Making splitFilter a generic function enhances its versatility across different types, promoting code reuse.


Line range hint 235-254: Secure media query validation in validateMediaType and validateMediaQuery

The functions perform necessary checks to validate media types and queries, enhancing security by preventing injection issues.


258-262: Compatibility handling for mediaParser import

The conditional handling of the mediaParser import ensures compatibility across different module systems, which is crucial for broader environment support.


279-286: Declaration merging correctly extends postcss Node interface

Extending the Node interface with additional properties via declaration merging is appropriate and allows for custom properties needed in the codebase.

package.json Outdated
"devDependencies": {
"@antfu/eslint-config": "3.8.0",
"@codspeed/vitest-plugin": "3.1.1",
"@types/node": "^22.8.1",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update @types/node version to match supported Node.js version.

The added @types/node@^22.8.1 references Node.js 22 which isn't released yet. Since the engine requirement is "node": ">=18.13.0", you should use a compatible version of @types/node:

-    "@types/node": "^22.8.1",
+    "@types/node": "^18.13.0",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@types/node": "^22.8.1",
"@types/node": "^18.13.0",

Comment on lines +65 to 67
export function isSubpath(basePath: string, currentPath: string) {
return !path.relative(basePath, currentPath).startsWith('..')
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for path parameters.

The function should validate inputs to ensure they are valid path strings.

Here's a more robust implementation:

 export function isSubpath(basePath: string, currentPath: string) {
+  if (typeof basePath !== 'string' || typeof currentPath !== 'string') {
+    throw new TypeError('Both basePath and currentPath must be strings')
+  }
+  if (!basePath || !currentPath) {
+    throw new Error('Paths cannot be empty')
+  }
   return !path.relative(basePath, currentPath).startsWith('..')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function isSubpath(basePath: string, currentPath: string) {
return !path.relative(basePath, currentPath).startsWith('..')
}
export function isSubpath(basePath: string, currentPath: string) {
if (typeof basePath !== 'string' || typeof currentPath !== 'string') {
throw new TypeError('Both basePath and currentPath must be strings')
}
if (!basePath || !currentPath) {
throw new Error('Paths cannot be empty')
}
return !path.relative(basePath, currentPath).startsWith('..')
}

Comment on lines +19 to +26
/**
* Controls which keyframes rules are inlined.
*
* - **"critical":** _(default)_ inline keyframes rules that are used by the critical CSS.
* - **"all":** Inline all keyframes rules.
* - **"none":** Remove all keyframes rules.
*/
export type KeyframeStrategy = 'critical' | 'all' | 'none'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety for keyframes option.

The keyframes option in the Options interface accepts KeyframeStrategy | boolean, but the documentation for KeyframeStrategy doesn't explain how boolean values are interpreted. This could lead to confusion.

Consider either:

  1. Document the boolean behavior in the KeyframeStrategy type documentation
  2. Or create a separate type that includes boolean values:
export type KeyframeStrategyOption = KeyframeStrategy | boolean

Also applies to: 88-88

Comment on lines +41 to +46
external?: boolean
/**
* Inline external stylesheets smaller than a given size _(default: `0`)_
*/
inlineThreshold?: number
/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using more specific types for size thresholds.

The inlineThreshold and minimumExternalSize properties accept any number, but they likely represent sizes in bytes.

Consider adding a type to make this explicit:

type Bytes = number
/**
 * Inline external stylesheets smaller than a given size in bytes _(default: `0`)_
 */
inlineThreshold?: Bytes

Comment on lines +3 to +17
/**
* The mechanism to use for lazy-loading stylesheets.
*
* Note: <kbd>JS</kbd> indicates a strategy requiring JavaScript (falls back to `<noscript>` unless disabled).
*
* - **default:** Move stylesheet links to the end of the document and insert preload meta tags in their place.
* - **"body":** Move all external stylesheet links to the end of the document.
* - **"media":** Load stylesheets asynchronously by adding `media="not x"` and removing once loaded. <kbd>JS</kbd>
* - **"swap":** Convert stylesheet links to preloads that swap to `rel="stylesheet"` once loaded ([details](https://www.filamentgroup.com/lab/load-css-simpler/#the-code)). <kbd>JS</kbd>
* - **"swap-high":** Use `<link rel="alternate stylesheet preload">` and swap to `rel="stylesheet"` once loaded ([details](http://filamentgroup.github.io/loadCSS/test/new-high.html)). <kbd>JS</kbd>
* - **"js":** Inject an asynchronous CSS loader similar to [LoadCSS](https://github.com/filamentgroup/loadCSS) and use it to load stylesheets. <kbd>JS</kbd>
* - **"js-lazy":** Like `"js"`, but the stylesheet is disabled until fully loaded.
* - **false:** Disables adding preload tags.
*/
export type PreloadStrategy = 'body' | 'media' | 'swap' | 'swap-high' | 'js' | 'js-lazy'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Documentation and type definition mismatch.

The documentation mentions a "default" strategy that moves stylesheet links and inserts preload meta tags, but this option is not included in the PreloadStrategy type. Additionally, the type allows false as per line 65, but this isn't documented in the main type description.

Consider either:

  1. Adding the default strategy to the type:
-export type PreloadStrategy = 'body' | 'media' | 'swap' | 'swap-high' | 'js' | 'js-lazy'
+export type PreloadStrategy = 'default' | 'body' | 'media' | 'swap' | 'swap-high' | 'js' | 'js-lazy'
  1. Or clarifying in the documentation that 'body' is the default strategy if that's the case.

Also, update the type documentation to mention that false is a valid value.

Committable suggestion was skipped due to low confidence.

Comment on lines +59 to 60
readFile(filename: string): string | Promise<string> {
const fs = this.fs
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Adjust readFile method return type to Promise<string>

The readFile method is declared to return string | Promise<string>, but it always returns a Promise<string>. To ensure consistency and clarity, consider changing the return type to Promise<string>.

Apply this diff to update the return type:

-  readFile(filename: string): string | Promise<string> {
+  readFile(filename: string): Promise<string> {
     const fs = this.fs
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
readFile(filename: string): string | Promise<string> {
const fs = this.fs
readFile(filename: string): Promise<string> {
const fs = this.fs

Comment on lines +34 to +35
urlFilter: (url: string) => boolean

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove deprecated urlFilter property

The urlFilter property is maintained for backward compatibility, but there's a TODO comment indicating it should be removed. Since it's undocumented and slated for removal, consider removing it to clean up the codebase.

Apply this diff to remove the deprecated property:

-  // TODO: remove (undocumented) support
-  urlFilter: (url: string) => boolean

Additionally, remove the associated assignment in the constructor:

     this.logger = this.options.logger || createLogger(this.options.logLevel)
   }

+  // Remove deprecated `urlFilter` code
-  // @ts-expect-error TODO: remove support
-  this.urlFilter = this.options.filter
-  if (this.urlFilter instanceof RegExp) {
-    this.urlFilter = this.urlFilter.test.bind(this.urlFilter)
-  }

Committable suggestion was skipped due to low confidence.

options || {},
)

options: Options & Required<Pick<Options, 'logLevel' | 'path' | 'publicPath' | 'reduceInlineStyles' | 'pruneSource' | 'additionalStylesheets'>> & { allowRules: Array<string | RegExp> }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify the options type declaration for better readability

The options property has a complex type that can be hard to read and maintain. Consider defining a separate interface or type alias to simplify the type declaration and improve readability.

Apply this diff to extract the type into a separate interface:

+interface BeastiesOptions extends Options, Required<Pick<Options, 'logLevel' | 'path' | 'publicPath' | 'reduceInlineStyles' | 'pruneSource' | 'additionalStylesheets'>> {
+  allowRules: Array<string | RegExp>
+}

 export default class Beasties {
-  options: Options & Required<Pick<Options, 'logLevel' | 'path' | 'publicPath' | 'reduceInlineStyles' | 'pruneSource' | 'additionalStylesheets'>> & { allowRules: Array<string | RegExp> }
+  options: BeastiesOptions

Committable suggestion was skipped due to low confidence.

Comment on lines +605 to +606
// eslint-disable-next-line regexp/no-super-linear-backtracking,regexp/no-misleading-capturing-group
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Refactor regular expression to avoid disabling ESLint rules

The regular expression used in line 606 requires disabling multiple ESLint rules. Consider refactoring the regex to improve performance and maintainability, and to avoid the need to disable ESLint rules.

Apply this diff to update the regular expression:

               // TODO: parse this properly and generate multiple preloads with type="font/woff2" etc
-              // eslint-disable-next-line regexp/no-super-linear-backtracking,regexp/no-misleading-capturing-group
-              src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2]
+              src = (decl.value.match(/url\(\s*(['"]?)([^'")]+)\1\s*\)/) || [])[2]

This refactored regex prevents super-linear backtracking and avoids misleading capturing groups.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// eslint-disable-next-line regexp/no-super-linear-backtracking,regexp/no-misleading-capturing-group
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2]
// TODO: parse this properly and generate multiple preloads with type="font/woff2" etc
src = (decl.value.match(/url\(\s*(['"]?)([^'")]+)\1\s*\)/) || [])[2]

Comment on lines +61 to 69
return new Promise<string>((resolve, reject) => {
// eslint-disable-next-line node/prefer-global/buffer
const callback = (err: NodeJS.ErrnoException | null, data: string | Buffer) => {
if (err)
reject(err)
else resolve(data)
else resolve(data.toString())
}
if (fs && fs.readFile) {
fs.readFile(filename, callback)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use fs.promises for cleaner asynchronous file reading

The readFile method uses callbacks wrapped in a Promise, which can be simplified. Utilize fs.promises.readFile for a cleaner and more modern asynchronous implementation.

Apply this diff to refactor the method:

   readFile(filename: string): Promise<string> {
     const fs = this.fs
-    return new Promise<string>((resolve, reject) => {
-      // eslint-disable-next-line node/prefer-global/buffer
-      const callback = (err: NodeJS.ErrnoException | null, data: string | Buffer) => {
-        if (err)
-          reject(err)
-        else resolve(data.toString())
-      }
-      if (fs && fs.readFile) {
-        fs.readFile(filename, callback)
-      }
-      else {
-        readFile(filename, 'utf-8', callback)
-      }
-    })
+    if (fs && fs.promises?.readFile) {
+      return fs.promises.readFile(filename, 'utf-8')
+    } else {
+      return readFile(filename, 'utf-8')
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return new Promise<string>((resolve, reject) => {
// eslint-disable-next-line node/prefer-global/buffer
const callback = (err: NodeJS.ErrnoException | null, data: string | Buffer) => {
if (err)
reject(err)
else resolve(data)
else resolve(data.toString())
}
if (fs && fs.readFile) {
fs.readFile(filename, callback)
return new Promise<string>((resolve, reject) => {
// eslint-disable-next-line node/prefer-global/buffer
const callback = (err: NodeJS.ErrnoException | null, data: string | Buffer) => {
if (err)
reject(err)
else resolve(data.toString())
}
if (fs && fs.promises?.readFile) {
return fs.promises.readFile(filename, 'utf-8')
} else {
return readFile(filename, 'utf-8')
}
})
🧰 Tools
🪛 Biome

[error] 68-68: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (2)
packages/beasties/src/css.ts (2)

Line range hint 45-70: Security: Improve CSS injection protection

The current check for </style> in declaration values might not catch all potential CSS injection vectors. Consider using a more robust sanitization approach.

Suggested improvement:

- if (node?.type === 'decl' && node.value.includes('</style>')) {
+ if (node?.type === 'decl' && (
+   node.value.includes('</style>') ||
+   node.value.includes('</STYLE>') ||
+   node.value.includes('<script>') ||
+   node.value.includes('<SCRIPT>')
+ )) {
   return
 }

Consider using a dedicated CSS sanitization library for more comprehensive protection against CSS-based attacks.


Line range hint 235-277: Consider improving media query validation maintainability

While the validation is thorough, consider these improvements:

  1. The media features list might need updates for newer CSS specs
  2. The validation logic could be more maintainable

Consider extracting the validation logic into a separate class:

class MediaQueryValidator {
  private static readonly MEDIA_TYPES = new Set(['all', 'print', 'screen', 'speech']);
  private static readonly MEDIA_KEYWORDS = new Set(['and', 'not', ',']);
  // Allow easier updates for new CSS specs
  private static readonly BASE_FEATURES = [
    'width', 'aspect-ratio', 'color', 'color-index',
    'grid', 'height', 'monochrome', 'orientation',
    'resolution', 'scan'
  ] as const;
  
  private static readonly MEDIA_FEATURES = new Set(
    MediaQueryValidator.BASE_FEATURES.flatMap(
      feature => [feature, `min-${feature}`, `max-${feature}`]
    )
  );

  static validateNode(node: Child | Root): boolean {
    return this.validateMediaType(node);
  }

  static validateQuery(query: string): boolean {
    // Current validation logic
  }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 3601fac and f97df38.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • package.json (1 hunks)
  • packages/beasties/package.json (2 hunks)
  • packages/beasties/src/css.ts (9 hunks)
  • packages/beasties/src/index.ts (25 hunks)
  • vitest.config.mts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • packages/beasties/package.json
🧰 Additional context used
🪛 Biome
packages/beasties/src/css.ts

[error] 89-89: void is confusing inside a union type.

Unsafe fix: Use undefined instead.

(lint/suspicious/noConfusingVoidType)

🔇 Additional comments (6)
vitest.config.mts (2)

1-1: LGTM! Clean import of Node.js URL utilities.

The addition of fileURLToPath is appropriate for handling file paths in an ESM context.


8-12: LGTM! Verify test execution with TypeScript files.

The alias configuration correctly points to the source files, which is ideal for testing. This aligns well with the TypeScript rewrite objective.

Let's verify the TypeScript setup:

✅ Verification successful

TypeScript setup verified successfully - source files and entry point are properly configured

The verification confirms:

  • Source directory exists at packages/beasties/src/
  • Entry point is correctly set as TypeScript file at packages/beasties/src/index.ts
  • No leftover JavaScript files in the source directory
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the existence of TypeScript source files and their extensions

# Test 1: Check if the source directory exists
echo "Checking source directory..."
fd --type d "src$" packages/beasties

# Test 2: Check the entry point file extension (.ts or .tsx expected)
echo "Checking entry point file..."
fd "index\.(ts|tsx)$" packages/beasties/src

# Test 3: Verify no leftover JavaScript files in source
echo "Checking for leftover JS files..."
fd "\.js$" packages/beasties/src

Length of output: 412

packages/beasties/src/css.ts (4)

17-38: LGTM! Well-structured type imports and interface definition.

The type imports are properly organized and the SerializeStylesheetOptions interface is well-documented.


175-182: LGTM! Well-implemented type guard.

The hasNestedRules type guard effectively narrows the type while handling edge cases like @Keyframes rules.


186-199: LGTM! Well-structured generic implementation.

The splitFilter function makes good use of TypeScript generics and provides clear type safety.


279-286: LGTM! Well-structured type declarations.

The module augmentation for postcss Node interface is properly implemented.

Comment on lines 217 to +223
if (styleSheetsIncluded.includes(cssFile)) {
return undefined
return []
}
styleSheetsIncluded.push(cssFile)
const style = document.createElement('style')
style.$$external = true
return this.getCssAsset(cssFile, style).then(sheet => [sheet, style])
return this.getCssAsset(cssFile, style).then(sheet => [sheet, style] as const)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure consistent return types in 'additionalStylesheets' mapping function

The mapping function in this.options.additionalStylesheets.map returns inconsistent types: it returns an empty array in one branch and a Promise resolving to a tuple in the other. This inconsistency can cause runtime errors when iterating over sources.

Apply this diff to fix the issue by filtering duplicates before mapping:

+    const uniqueStylesheets = this.options.additionalStylesheets.filter((cssFile, index, self) => self.indexOf(cssFile) === index);

-    const sources = await Promise.all(
-      this.options.additionalStylesheets.map((cssFile) => {
-        if (styleSheetsIncluded.includes(cssFile)) {
-          return []
-        }
-        styleSheetsIncluded.push(cssFile)
+    const sources = await Promise.all(
+      uniqueStylesheets.map((cssFile) => {
         const style = document.createElement('style')
         style.$$external = true
         return this.getCssAsset(cssFile, style).then(sheet => [sheet, style] as const)
       }),
     )

This ensures that duplicates are handled before mapping, and the mapping function consistently returns Promises resolving to a tuple.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (styleSheetsIncluded.includes(cssFile)) {
return undefined
return []
}
styleSheetsIncluded.push(cssFile)
const style = document.createElement('style')
style.$$external = true
return this.getCssAsset(cssFile, style).then(sheet => [sheet, style])
return this.getCssAsset(cssFile, style).then(sheet => [sheet, style] as const)
const uniqueStylesheets = this.options.additionalStylesheets.filter((cssFile, index, self) => self.indexOf(cssFile) === index);
const sources = await Promise.all(
uniqueStylesheets.map((cssFile) => {
const style = document.createElement('style')
style.$$external = true
return this.getCssAsset(cssFile, style).then(sheet => [sheet, style] as const)
}),
)

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 26, 2024

CodSpeed Performance Report

Merging #16 will improve performances by ×2.3

Comparing refactor/ts (9fb2d39) with main (89b3603)

Summary

⚡ 2 improvements
✅ 6 untouched benchmarks

Benchmarks breakdown

Benchmark main refactor/ts Change
does not decode entities in HTML document 1.3 ms 1.1 ms +14.97%
run on HTML file 17.3 ms 7.4 ms ×2.3

@codecov-commenter
Copy link

Codecov Report

Attention: Patch coverage is 82.41758% with 64 lines in your changes missing coverage. Please review.

Project coverage is 83.46%. Comparing base (89b3603) to head (9fb2d39).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
packages/beasties/src/dom.ts 83.91% 37 Missing ⚠️
packages/beasties/src/index.ts 71.91% 25 Missing ⚠️
packages/beasties/src/css.ts 94.87% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #16      +/-   ##
==========================================
+ Coverage   82.74%   83.46%   +0.71%     
==========================================
  Files           6        7       +1     
  Lines        1420     1173     -247     
  Branches      242      268      +26     
==========================================
- Hits         1175      979     -196     
+ Misses        245      194      -51     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@danielroe danielroe merged commit 7a43836 into main Oct 26, 2024
@danielroe danielroe deleted the refactor/ts branch October 26, 2024 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants