feat: single function to generate mod with output format option#82
feat: single function to generate mod with output format option#82SlayerOrnstein merged 8 commits intoWFCD:mainfrom
Conversation
6c1c39c to
9c25c66
Compare
|
Caution Review failedThe pull request is closed. WalkthroughThe changes in this pull request include updates to font specifications in rendering functions, enhancements to image generation functions by introducing an output parameter, and a transition to asynchronous file operations in utility functions. Additionally, new types and interfaces for canvas output have been added, and the test suite has been modified to accommodate these asynchronous changes. Finally, the TypeScript configuration has been updated to change module resolution settings. Changes
Possibly related PRs
Suggested reviewers
Poem
Warning Rate limit exceeded@SlayerOrnstein has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 15 minutes and 57 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 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? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Outside diff range and nitpick comments (8)
tsconfig.json (1)
Line range hint
1-13: Consider adding type checking to the build process.Since this is a strict TypeScript project, it would be beneficial to add type checking during the build process to catch any module resolution issues early.
Add these scripts to your package.json:
{ "scripts": { "type-check": "tsc --noEmit", "build": "npm run type-check && your-existing-build-command" } }tests/generator.spec.ts (3)
2-4: Consider using async filesystem operations consistentlyThe code mixes synchronous (
existsSync,mkdirSync) and asynchronous (readdir,writeFile) filesystem operations. For better consistency and performance, consider using async operations throughout.-import { existsSync, mkdirSync } from 'node:fs'; +import { mkdir } from 'node:fs/promises';
Line range hint
14-27: Critical: Test function must be async to handle promisesThe test function contains async operations but is not marked as async, which will cause the test runner to complete before the async operations finish. This could lead to false positives and race conditions.
- test('run test', () => { + test('run test', async () => {
Line range hint
14-49: Consider restructuring the test suite for better organization and reliabilityThe current test structure could be improved in several ways:
- Split into smaller, focused test cases for each format
- Add cleanup using
beforeEach/afterEachhooks- Use test fixtures for mod data
- Add proper error messages for assertions
Example structure:
describe('Generate a mod', () => { const testPath = join('.', 'assets', 'tests'); beforeEach(async () => { await mkdir(testPath, { recursive: true }); }); afterEach(async () => { // Clean up generated files await rm(testPath, { recursive: true, force: true }); }); for (const format of ['webp', 'jpeg', 'avif', 'png'] as const) { describe(`with ${format} format`, () => { it('should generate mod images correctly', async () => { // Test implementation for single format }); it('should handle invalid mods gracefully', async () => { // Error handling test }); }); } });src/drawers.ts (1)
159-159: Consider extracting font specifications to constants.While the font change to Roboto is good, consider extracting the repeated font specifications into constants to improve maintainability.
Example refactor:
+ const FONT_SPECS = { + PRIMARY: '300 16px "Roboto"', + SECONDARY: '12px "Roboto"' + }; // In drawText function - context.font = '300 16px "Roboto"'; + context.font = FONT_SPECS.PRIMARY; // In drawBackground function - context.font = '300 16px "Roboto"'; + context.font = FONT_SPECS.PRIMARY;src/utils.ts (3)
42-53: Use asynchronousfs.accessinstead of synchronousexistsSyncTo maintain consistency with the asynchronous operations in your codebase and avoid blocking the event loop, consider replacing the synchronous
existsSyncwith the asynchronousfs.accessfrom'fs/promises'.Apply this diff to update the code:
-import { existsSync } from 'fs'; +import { access } from 'fs/promises'; +import { constants } from 'fs'; ... // In fetchModPiece - if (existsSync(filePath)) { + try { + await access(filePath, constants.F_OK); const image = await readFile(filePath); return loadImage(image); + } catch { + // File does not exist, proceed to download + } ... - if (!existsSync(assetPath)) { + try { + await access(assetPath, constants.F_OK); + } catch { await mkdir(assetPath, { recursive: true }); + } await writeFile(filePath, image); ... // In fetchPolarity - if (existsSync(filePath)) { + try { + await access(filePath, constants.F_OK); const image = await readFile(filePath); return loadImage(image); + } catch { + // File does not exist, proceed to download + } ... - if (!existsSync(assetPath)) { + try { + await access(assetPath, constants.F_OK); + } catch { await mkdir(assetPath, { recursive: true }); + } await writeFile(filePath, image);This refactoring ensures non-blocking I/O operations and keeps the codebase consistently asynchronous.
Also applies to: 105-119
42-53: Refactor duplicated code infetchModPieceandfetchPolarityThe functions
fetchModPieceandfetchPolarityshare similar logic for fetching and caching images. Consider extracting the common code into a shared utility function to reduce duplication and improve maintainability.Also applies to: 105-119
189-200: Add a default case to theswitchstatement inexportCanvasWhile all possible formats are covered by the
Formattype, adding adefaultcase to theswitchstatement enhances robustness by handling any unexpected values, especially if theFormattype is extended in the future.Apply this diff:
export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { format: 'png' }) => { switch (output.format) { case 'png': return canvas.encode('png'); case 'webp': return canvas.encode('webp', output.quality); case 'jpeg': return canvas.encode('jpeg', output.quality); case 'avif': return canvas.encode('avif', output.cfg); + default: + throw new Error(`Unsupported format: ${output.format}`); } };This ensures that any unsupported formats are caught with a clear error message.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (6)
assets/fonts/DroidSans-Bold.ttfis excluded by!**/*.ttfassets/fonts/DroidSans.ttfis excluded by!**/*.ttfassets/fonts/OpenSans-Bold.ttfis excluded by!**/*.ttfassets/fonts/OpenSans-Regular.ttfis excluded by!**/*.ttfassets/fonts/Roboto-Bold.ttfis excluded by!**/*.ttfassets/fonts/Roboto-Regular.ttfis excluded by!**/*.ttf
📒 Files selected for processing (5)
src/drawers.ts(3 hunks)src/generator.ts(4 hunks)src/utils.ts(5 hunks)tests/generator.spec.ts(2 hunks)tsconfig.json(1 hunks)
🔇 Additional comments (10)
tsconfig.json (1)
7-8: LGTM! Verify package.json configuration.
The upgrade to "NodeNext" for both module and moduleResolution is appropriate, especially given the transition to async operations. This provides better ESM support and improved package exports handling.
Let's verify the package.json configuration:
✅ Verification successful
Module configuration is correctly aligned
The codebase is properly configured for ESM:
- Package.json has
"type": "module" - All source files consistently use ESM syntax (
import/export) - No CommonJS patterns (
require/module.exports) were found - The TypeScript configuration with
"NodeNext"matches the ESM setup
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Verify package.json configuration for ESM compatibility
# Expected: Should find "type": "module" in package.json
# Check package.json for module type
echo "Checking package.json configuration..."
jq '.type' package.json
# Look for potential ESM syntax in source files
echo "Checking for ESM syntax usage..."
rg -l "import\s+.*\s+from\s+" "src/"
Length of output: 276
Script:
#!/bin/bash
# Let's verify the actual import/export syntax in these files
# and check for any potential CommonJS patterns
# Check for ESM syntax in the identified files
echo "=== ESM Patterns ==="
for file in src/utils.ts src/generator.ts src/drawers.ts; do
echo "=== $file ==="
rg "^(import|export)" "$file" -A 1
done
# Check for potential CommonJS patterns
echo -e "\n=== CommonJS Patterns ==="
rg "require\(|module\.exports|exports\." "src/"
Length of output: 3529
src/generator.ts (4)
5-5: LGTM: Import statement is well-structured
The new imports are properly organized and include all necessary types and utilities for the enhanced functionality.
12-16: LGTM: Well-structured parameter updates and interface usage
The function signature changes are well-implemented:
- Optional rank parameter with proper fallback
- New output parameter with sensible default
- Clean interface usage for frame parameters
Also applies to: 25-31
48-48: LGTM: Consistent implementation of output format support
The changes to generateRivenMod are minimal and maintain consistency with generateBasicMod:
- Same output parameter default
- Consistent use of exportCanvas
Also applies to: 85-86
88-95:
Improve type safety and consistency in generateMod
Several concerns with the current implementation:
- Riven detection using string.includes() is fragile
- Inconsistent rank parameter handling between mod types
- Unsafe type assertion to RivenMod
Consider this safer implementation:
export const generateMod = async (
mod: Mod,
rank?: number,
output: CanvasOutput = { format: 'png' }
): Promise<Buffer> => {
- const isRiven = mod.name?.includes('Riven');
+ // Type guard function
+ const isRivenMod = (mod: Mod): mod is RivenMod => {
+ return 'attributes' in mod && 'veiled' in mod;
+ };
- return isRiven ? generateRivenMod(mod as RivenMod, output) : generateBasicMod(mod, rank ?? mod.fusionLimit, output);
+ if (isRivenMod(mod)) {
+ return generateRivenMod(mod, output);
+ }
+ return generateBasicMod(mod, rank, output);
};This implementation:
- Uses a proper type guard instead of string matching
- Maintains consistent rank parameter handling
- Eliminates unsafe type assertion
Let's verify the Riven mod structure:
src/drawers.ts (4)
103-103: LGTM: Font standardization for mod name.
The font change to Roboto aligns with the PR objective to standardize fonts.
108-108: LGTM: Font standardization for mod description.
The font change maintains readability while aligning with the standardization effort.
122-122: LGTM: Font standardization for compatibility name.
The font change is consistent with other text elements.
Line range hint 103-122: Verify Roboto font registration.
Ensure that the Roboto font files are properly registered in the registerFonts function.
✅ Verification successful
Roboto font registration is properly implemented
The verification confirms that:
- The
registerFontsfunction correctly registers both Roboto Regular and Bold variants - The font files exist at the expected paths:
assets/fonts/Roboto-Regular.ttfassets/fonts/Roboto-Bold.ttf
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if Roboto font files are properly registered
# Check for Roboto font registration in utils.ts
rg -A 5 "registerFonts" src/utils.ts
# Check for Roboto font files in the project
fd -t f "Roboto.*\.(ttf|otf)"
Length of output: 376
src/utils.ts (1)
166-167: Fonts updated to 'Roboto' successfully
The fonts have been updated to 'Roboto', registering both 'Roboto-Bold.ttf' and 'Roboto-Regular.ttf' under the 'Roboto' family. This change standardizes font usage and ensures consistency in text rendering across the application.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Outside diff range and nitpick comments (3)
tests/generator.spec.ts (2)
2-4: Consider using async fs operations consistentlyThe file mixes synchronous (
existsSync,mkdirSync) and asynchronous (readdir,writeFile) fs operations. For better consistency and performance, consider using async operations throughout.-import { existsSync, mkdirSync } from 'node:fs'; +import { mkdir } from 'node:fs/promises';
Line range hint
14-49: Consider restructuring the test suite for better organizationThe current test structure combines both generation and verification in a single test. Consider:
- Separating the tests into smaller, focused test cases for each format
- Using
describe.eachfor format-specific tests- Creating helper functions for common operations
Example structure:
describe('Mod Generation', () => { const formats = ['webp', 'jpeg', 'avif', 'png'] as const; describe.each(formats)('Format: %s', (format) => { test('generates all mod types correctly', async () => { // Test specific format }); test('verifies generated files', async () => { // Verify specific format }); }); });src/utils.ts (1)
162-163: Add error handling for font registration.While the font standardization to Roboto is good, consider adding error handling to gracefully handle missing font files and verify successful registration.
export const registerFonts = () => { const fontPath = join('.', 'assets', 'fonts'); - GlobalFonts.registerFromPath(join(fontPath, 'Roboto-Bold.ttf'), 'Roboto'); - GlobalFonts.registerFromPath(join(fontPath, 'Roboto-Regular.ttf'), 'Roboto'); + try { + const boldPath = join(fontPath, 'Roboto-Bold.ttf'); + const regularPath = join(fontPath, 'Roboto-Regular.ttf'); + + if (!existsSync(boldPath) || !existsSync(regularPath)) { + throw new Error('Required Roboto font files are missing'); + } + + GlobalFonts.registerFromPath(boldPath, 'Roboto'); + GlobalFonts.registerFromPath(regularPath, 'Roboto'); + } catch (error) { + console.error('Failed to register Roboto fonts:', error); + throw error; // Re-throw to let caller handle the error + } };
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (3)
src/generator.ts(4 hunks)src/utils.ts(5 hunks)tests/generator.spec.ts(2 hunks)
🧰 Additional context used
🪛 GitHub Check: Verify
src/utils.ts
[warning] 198-198:
Unexpected console statement
🔇 Additional comments (10)
tests/generator.spec.ts (2)
29-41: Previous review comment about async handling issues is still valid
The current implementation has the same critical issues as identified in the previous review.
44-49: Previous review comment about verification loop issues is still valid
The verification loop has the same async handling and timing issues as identified in the previous review.
src/generator.ts (4)
5-5: LGTM: Import changes align with new functionality
The new imports support the output format options feature.
12-16: LGTM: Function signature improvements
The updated signature properly handles:
- Optional rank parameter
- New output format option
- Nullable return type
42-44: Redundant PNG encoding still present
This issue was previously flagged but hasn't been addressed yet.
47-50: LGTM: Consistent output format handling
The changes align with the new output format feature and maintain consistency with generateBasicMod.
Also applies to: 87-88
src/utils.ts (4)
42-52: LGTM! Proper async file operations implementation.
The file operations are now correctly implemented with proper separation of concerns and error handling. Previous review comments' concerns about file writing logic have been addressed.
103-116: LGTM! Consistent async implementation.
The async file operations are implemented consistently with fetchModPiece, following the same robust pattern.
177-183: LGTM! Well-defined type definitions.
The type definitions for canvas output formats are clear, comprehensive, and properly typed with optional parameters.
162-163: Verify font usage across the codebase.
Let's ensure all components are updated to use the new Roboto font.
✅ Verification successful
Font usage is consistent across the codebase
All font references in the codebase are already using "Roboto" font family. No references to legacy fonts like DroidSans or OpenSans were found. The font registration in utils.ts aligns with the font usage in drawers.ts.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for any remaining references to old fonts
rg -i "DroidSans|OpenSans" --type ts
# Search for font family specifications in drawing code
rg "font\s*=|fontFamily" --type ts
Length of output: 285
09bf30d to
5bd4f2d
Compare
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.
Actionable comments posted: 2
🧹 Outside diff range and nitpick comments (1)
src/utils.ts (1)
177-183: Add JSDoc documentation for new typesThe new types would benefit from documentation explaining their purpose and constraints.
+/** + * Supported output formats for canvas encoding + */ export type Format = 'webp' | 'jpeg' | 'avif' | 'png'; +/** + * Configuration options for canvas export + * @property quality - Quality setting (0-100) for webp and jpeg formats + * @property format - Output format type + * @property cfg - Advanced configuration for avif format + */ export interface CanvasOutput { quality?: number; format: Format; cfg?: AvifConfig; }
🛑 Comments failed to post (2)
src/utils.ts (2)
162-163: 🛠️ Refactor suggestion
Add error handling for font registration
While the font standardization to Roboto is good, the function should handle potential font loading failures.
export const registerFonts = () => { const fontPath = join('.', 'assets', 'fonts'); - GlobalFonts.registerFromPath(join(fontPath, 'Roboto-Bold.ttf'), 'Roboto'); - GlobalFonts.registerFromPath(join(fontPath, 'Roboto-Regular.ttf'), 'Roboto'); + try { + const boldPath = join(fontPath, 'Roboto-Bold.ttf'); + const regularPath = join(fontPath, 'Roboto-Regular.ttf'); + + if (!existsSync(boldPath) || !existsSync(regularPath)) { + throw new Error('Required Roboto font files are missing'); + } + + GlobalFonts.registerFromPath(boldPath, 'Roboto'); + GlobalFonts.registerFromPath(regularPath, 'Roboto'); + } catch (error) { + throw new Error(`Failed to register fonts: ${error.message}`); + } };📝 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.export const registerFonts = () => { const fontPath = join('.', 'assets', 'fonts'); try { const boldPath = join(fontPath, 'Roboto-Bold.ttf'); const regularPath = join(fontPath, 'Roboto-Regular.ttf'); if (!existsSync(boldPath) || !existsSync(regularPath)) { throw new Error('Required Roboto font files are missing'); } GlobalFonts.registerFromPath(boldPath, 'Roboto'); GlobalFonts.registerFromPath(regularPath, 'Roboto'); } catch (error) { throw new Error(`Failed to register fonts: ${error.message}`); } };
185-204:
⚠️ Potential issueFix error handling and quality validation in exportCanvas
Several issues need to be addressed:
- Error handling currently swallows errors and returns undefined
- Quality validation is inconsistent with common image quality scales
- Console.error usage should be replaced with proper error handling
export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { format: 'png' }) => { const quality = output.quality || output.cfg?.quality; - if (quality !== undefined && (quality < 0 || quality > 100)) { - throw new Error('quality cannot be less then 0 or more then 100'); + if (quality !== undefined) { + if (output.format === 'avif') { + if (quality < 0 || quality > 100) { + throw new Error('AVIF quality must be between 0 and 100'); + } + } else { + // WebP and JPEG expect quality between 0 and 1 + if (quality < 0 || quality > 1) { + throw new Error('Quality must be between 0 and 1 for WebP and JPEG formats'); + } + } } try { switch (output.format) { case 'png': return await canvas.encode('png'); case 'webp': return await canvas.encode('webp', output.quality); case 'jpeg': return await canvas.encode('jpeg', output.quality); case 'avif': return await canvas.encode('avif', output.cfg); + default: + throw new Error(`Unsupported format: ${output.format}`); } - } catch { - console.error(`failed to export canvas as ${output.format}`); + } catch (error) { + throw new Error(`Failed to export canvas as ${output.format}: ${error.message}`); } };📝 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.export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { format: 'png' }) => { const quality = output.quality || output.cfg?.quality; if (quality !== undefined) { if (output.format === 'avif') { if (quality < 0 || quality > 100) { throw new Error('AVIF quality must be between 0 and 100'); } } else { // WebP and JPEG expect quality between 0 and 1 if (quality < 0 || quality > 1) { throw new Error('Quality must be between 0 and 1 for WebP and JPEG formats'); } } } try { switch (output.format) { case 'png': return await canvas.encode('png'); case 'webp': return await canvas.encode('webp', output.quality); case 'jpeg': return await canvas.encode('jpeg', output.quality); case 'avif': return await canvas.encode('avif', output.cfg); default: throw new Error(`Unsupported format: ${output.format}`); } } catch (error) { throw new Error(`Failed to export canvas as ${output.format}: ${error.message}`); } };🧰 Tools
🪛 GitHub Check: Verify
[warning] 203-203:
Unexpected console statement
5bd4f2d to
049872b
Compare
chore: use only one font family for simplicity
test: add a riven for test
049872b to
4976b5e
Compare
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's limits when posting large numbers of comments.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (2)
tests/generator.spec.ts (2)
2-4: Replace sync operations with async alternativesFor consistency with the async operations already in use, consider replacing the sync fs operations with their async counterparts.
-import { existsSync, mkdirSync } from 'node:fs'; +import { mkdir } from 'node:fs/promises';
15-23: Enhance test coverage with additional test casesConsider adding:
- Negative test cases (invalid mod paths, unsupported formats)
- Verification of generated image properties (dimensions, format)
- Edge cases (mods with special characters in names)
Example test cases to add:
test('should handle invalid mod paths', async () => { const invalidMod = find.findItem('invalid_path') as Mod; const result = await generateMod(invalidMod, undefined, { format: 'png' as Format }); assert.equal(result, undefined); }); test('should handle unsupported format', async () => { const mod = find.findItem(mods[0]) as Mod; await assert.rejects( async () => generateMod(mod, undefined, { format: 'invalid' as Format }), /Unsupported format/ ); });
🛑 Comments failed to post (1)
src/utils.ts (1)
185-205:
⚠️ Potential issueSeveral improvements needed in exportCanvas function
- Quality validation range seems incorrect:
- The error message suggests a 0-100 range
- Most image encoding libraries expect quality in 0-1 range
- Error handling needs improvement:
- Function silently returns undefined on error
- Console.error should be replaced with proper error handling
- Type safety can be enhanced:
- Switch statement should handle all possible cases
Apply this diff to fix these issues:
export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { format: 'png' }) => { const quality = output.quality || output.cfg?.quality; - if (quality !== undefined && (quality < 0 || quality > 100)) { - throw new Error('quality cannot be less then 0 or more then 100'); + if (quality !== undefined && (quality < 0 || quality > 1)) { + throw new Error('Quality must be between 0 and 1'); } try { switch (output.format) { case 'png': return await canvas.encode('png'); case 'webp': return await canvas.encode('webp', output.quality); case 'jpeg': return await canvas.encode('jpeg', output.quality); case 'avif': return await canvas.encode('avif', output.cfg); + default: + throw new Error(`Unsupported format: ${output.format}`); } - } catch { - console.error(`failed to export canvas as ${output.format}`); + } catch (error) { + throw new Error(`Failed to export canvas as ${output.format}: ${error.message}`); } };📝 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.export const exportCanvas = async (canvas: Canvas, output: CanvasOutput = { format: 'png' }) => { const quality = output.quality || output.cfg?.quality; if (quality !== undefined && (quality < 0 || quality > 1)) { throw new Error('Quality must be between 0 and 1'); } try { switch (output.format) { case 'png': return await canvas.encode('png'); case 'webp': return await canvas.encode('webp', output.quality); case 'jpeg': return await canvas.encode('jpeg', output.quality); case 'avif': return await canvas.encode('avif', output.cfg); default: throw new Error(`Unsupported format: ${output.format}`); } } catch (error) { throw new Error(`Failed to export canvas as ${output.format}: ${error.message}`); } };🧰 Tools
🪛 GitHub Check: Verify
[warning] 203-203:
Unexpected console statement
What did you fix?
Use a single function to generate a mod image for riven and all other mods with optional output format option.
Also removed all the fonts and just decided to use one, Roboto is pretty good
Reproduction steps
Evidence/screenshot/link to line
Considerations
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Tests
Documentation