diff --git a/.changeset/quiet-regions-wink.md b/.changeset/quiet-regions-wink.md new file mode 100644 index 000000000..8b93730fd --- /dev/null +++ b/.changeset/quiet-regions-wink.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/core-generators': patch +--- + +Add test:affected command to monorepo diff --git a/.changeset/stable-string-comparison.md b/.changeset/stable-string-comparison.md new file mode 100644 index 000000000..fd7047efb --- /dev/null +++ b/.changeset/stable-string-comparison.md @@ -0,0 +1,5 @@ +--- +'@baseplate-dev/utils': patch +--- + +Add stable compareStrings utility to replace localeCompare for deterministic sorting. The new compareStrings function provides consistent, locale-independent string comparison across all operating systems and environments, ensuring stable code generation and preventing merge conflicts caused by locale-dependent sorting. diff --git a/AGENTS.md b/AGENTS.md index 476a7ef4d..d049b1957 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -255,6 +255,20 @@ Baseplate consists of two main tiers: - Organize complex generation with Task Phases - Use Dynamic Tasks for data-driven generation +## String Comparison + +**IMPORTANT**: Always use `compareStrings` from `@baseplate-dev/utils` instead of `String.prototype.localeCompare()`. + +### When to Use localeCompare + +Only use `localeCompare()` when: + +1. Building user-facing features that require locale-aware sorting +2. Displaying sorted lists in the UI +3. Explicitly requested by product requirements + +For all code generation, file sorting, and internal data structures, use `compareStrings`. + ## Key Reminders for Claude Code - Run `pnpm lint:affected` and `pnpm typecheck` before committing changes diff --git a/examples/blog-with-auth/baseplate/generated/package.json b/examples/blog-with-auth/baseplate/generated/package.json index 8e88c49df..8e7d4136c 100644 --- a/examples/blog-with-auth/baseplate/generated/package.json +++ b/examples/blog-with-auth/baseplate/generated/package.json @@ -20,6 +20,7 @@ "prettier:write": "turbo run prettier:write && pnpm run prettier:write:root", "prettier:write:root": "prettier --write . \"!apps/**\"", "test": "turbo run test", + "test:affected": "turbo run test --affected", "typecheck": "turbo run typecheck", "watch": "turbo run watch:gql" }, diff --git a/examples/blog-with-auth/package.json b/examples/blog-with-auth/package.json index 8e88c49df..8e7d4136c 100644 --- a/examples/blog-with-auth/package.json +++ b/examples/blog-with-auth/package.json @@ -20,6 +20,7 @@ "prettier:write": "turbo run prettier:write && pnpm run prettier:write:root", "prettier:write:root": "prettier --write . \"!apps/**\"", "test": "turbo run test", + "test:affected": "turbo run test --affected", "typecheck": "turbo run typecheck", "watch": "turbo run watch:gql" }, diff --git a/examples/todo-with-auth0/baseplate/generated/package.json b/examples/todo-with-auth0/baseplate/generated/package.json index acd62fca6..c9271bf77 100644 --- a/examples/todo-with-auth0/baseplate/generated/package.json +++ b/examples/todo-with-auth0/baseplate/generated/package.json @@ -20,6 +20,7 @@ "prettier:write": "turbo run prettier:write && pnpm run prettier:write:root", "prettier:write:root": "prettier --write . \"!apps/**\"", "test": "turbo run test", + "test:affected": "turbo run test --affected", "typecheck": "turbo run typecheck", "watch": "turbo run watch:gql" }, diff --git a/examples/todo-with-auth0/package.json b/examples/todo-with-auth0/package.json index acd62fca6..c9271bf77 100644 --- a/examples/todo-with-auth0/package.json +++ b/examples/todo-with-auth0/package.json @@ -20,6 +20,7 @@ "prettier:write": "turbo run prettier:write && pnpm run prettier:write:root", "prettier:write:root": "prettier --write . \"!apps/**\"", "test": "turbo run test", + "test:affected": "turbo run test --affected", "typecheck": "turbo run typecheck", "watch": "turbo run watch:gql" }, diff --git a/packages/code-morph/package.json b/packages/code-morph/package.json index 16e1afb3a..02b7da65f 100644 --- a/packages/code-morph/package.json +++ b/packages/code-morph/package.json @@ -42,6 +42,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@baseplate-dev/utils": "workspace:*", "@inquirer/prompts": "7.8.3", "change-case": "5.4.4", "commander": "^12.1.0", diff --git a/packages/code-morph/src/morphers/utils/imports.ts b/packages/code-morph/src/morphers/utils/imports.ts index 06b809fe1..e16ba2440 100644 --- a/packages/code-morph/src/morphers/utils/imports.ts +++ b/packages/code-morph/src/morphers/utils/imports.ts @@ -4,6 +4,7 @@ import type { SourceFile, } from 'ts-morph'; +import { compareStrings } from '@baseplate-dev/utils'; import { Node } from 'ts-morph'; /** @@ -66,7 +67,7 @@ export function addOrUpdateImport( if (newImports.length > 0) { // Collect all import names (existing + new) const allImportNames = [...existingNames, ...newImports].sort((a, b) => - a.localeCompare(b), + compareStrings(a, b), ); // Replace with sorted named imports diff --git a/packages/core-generators/src/generators/node/node-git-ignore/node-git-ignore.generator.ts b/packages/core-generators/src/generators/node/node-git-ignore/node-git-ignore.generator.ts index ea45a15eb..18ae1d146 100644 --- a/packages/core-generators/src/generators/node/node-git-ignore/node-git-ignore.generator.ts +++ b/packages/core-generators/src/generators/node/node-git-ignore/node-git-ignore.generator.ts @@ -3,6 +3,7 @@ import { createGenerator, createGeneratorTask, } from '@baseplate-dev/sync'; +import { compareStrings } from '@baseplate-dev/utils'; import { z } from 'zod'; import { packageScope } from '#src/providers/scopes.js'; @@ -67,7 +68,7 @@ export const nodeGitIgnoreGenerator = createGenerator({ ]; if (exclusions.size > 0) { const sortedExclusions = [...exclusions.entries()].sort((a, b) => - a[0].localeCompare(b[0]), + compareStrings(a[0], b[0]), ); exclusionLines.push( ...sortedExclusions.flatMap(([, value]) => ['', ...value]), diff --git a/packages/core-generators/src/renderers/extractor/plugins/template-paths/paths-file.ts b/packages/core-generators/src/renderers/extractor/plugins/template-paths/paths-file.ts index 27f4f373d..409391b35 100644 --- a/packages/core-generators/src/renderers/extractor/plugins/template-paths/paths-file.ts +++ b/packages/core-generators/src/renderers/extractor/plugins/template-paths/paths-file.ts @@ -1,7 +1,7 @@ import type { TemplateExtractorContext } from '@baseplate-dev/sync'; import { TEMPLATE_EXTRACTOR_GENERATED_DIRECTORY } from '@baseplate-dev/sync'; -import { mapValuesOfMap } from '@baseplate-dev/utils'; +import { compareStrings, mapValuesOfMap } from '@baseplate-dev/utils'; import { posixJoin } from '@baseplate-dev/utils/node'; import { camelCase } from 'change-case'; import { z } from 'zod'; @@ -127,7 +127,7 @@ function createPathsTask( methodName: root.method, })), })) - .toSorted((a, b) => a.providerName.localeCompare(b.providerName)); + .toSorted((a, b) => compareStrings(a.providerName, b.providerName)); const dependencies = TsCodeUtils.mergeFragmentsAsObject( Object.fromEntries( diff --git a/packages/core-generators/src/renderers/extractor/plugins/typed-templates-file.ts b/packages/core-generators/src/renderers/extractor/plugins/typed-templates-file.ts index 37b716c01..7430f39ca 100644 --- a/packages/core-generators/src/renderers/extractor/plugins/typed-templates-file.ts +++ b/packages/core-generators/src/renderers/extractor/plugins/typed-templates-file.ts @@ -2,6 +2,7 @@ import { createTemplateExtractorPlugin, TEMPLATE_EXTRACTOR_GENERATED_DIRECTORY, } from '@baseplate-dev/sync'; +import { compareStrings } from '@baseplate-dev/utils'; import { posixJoin } from '@baseplate-dev/utils/node'; import { normalizeTsPathToJsPath } from '#src/utils/ts-paths.js'; @@ -76,7 +77,7 @@ export const typedTemplatesFilePlugin = createTemplateExtractorPlugin({ templateContents: TS_TEMPLATE, variables: { TPL_TEMPLATE_FRAGMENTS: templatesFragment, - TPL_TEMPLATE_EXPORTS: `{ ${templateExports.toSorted((a, b) => a.localeCompare(b)).join(', ')} }`, + TPL_TEMPLATE_EXPORTS: `{ ${templateExports.toSorted((a, b) => compareStrings(a, b)).join(', ')} }`, TPL_EXPORT_NAME: exportName, }, options: { diff --git a/packages/core-generators/src/renderers/text/render-text-typed-templates.ts b/packages/core-generators/src/renderers/text/render-text-typed-templates.ts index c59924fdc..684a9a19e 100644 --- a/packages/core-generators/src/renderers/text/render-text-typed-templates.ts +++ b/packages/core-generators/src/renderers/text/render-text-typed-templates.ts @@ -1,6 +1,6 @@ import type { TemplateExtractorTemplateEntry } from '@baseplate-dev/sync'; -import { quot, sortObjectKeys } from '@baseplate-dev/utils'; +import { compareStrings, quot, sortObjectKeys } from '@baseplate-dev/utils'; import { camelCase } from 'change-case'; import { groupBy } from 'es-toolkit'; @@ -56,7 +56,7 @@ function renderTextTypedTemplateGroup( ): TemplateExtractorTypedTemplate { const renderedTemplates = templates .map(({ name, config }) => renderTextTypedTemplate(name, config, context)) - .toSorted((a, b) => a.exportName.localeCompare(b.exportName)); + .toSorted((a, b) => compareStrings(a.exportName, b.exportName)); const exportName = `${camelCase(groupName)}Group`; return { diff --git a/packages/core-generators/src/renderers/typescript/extractor/parse-simple-replacements.ts b/packages/core-generators/src/renderers/typescript/extractor/parse-simple-replacements.ts index f2b32fff2..bd6f957e2 100644 --- a/packages/core-generators/src/renderers/typescript/extractor/parse-simple-replacements.ts +++ b/packages/core-generators/src/renderers/typescript/extractor/parse-simple-replacements.ts @@ -5,6 +5,8 @@ * and returns a mapping for simple replacements. */ +import { compareStrings } from '@baseplate-dev/utils'; + const SIMPLE_REPLACEMENT_REGEX = /\/\* TPL_([A-Z0-9_]+)=([^*]*?) \*\/\n*/g; const ALLOWED_VALUE_PATTERN = /^[a-zA-Z0-9_$/.\-?]+$/; @@ -85,7 +87,7 @@ export function generateSimpleReplacementComments( replacements: Record, ): string[] { return Object.entries(replacements) - .sort(([, a], [, b]) => a.localeCompare(b)) // Sort by variable name + .sort(([, a], [, b]) => compareStrings(a, b)) // Sort by variable name .map(([value, variable]) => { // Extract just the part after TPL_ const varName = variable.replace(/^TPL_/, ''); diff --git a/packages/core-generators/src/renderers/typescript/extractor/render-ts-typed-templates.ts b/packages/core-generators/src/renderers/typescript/extractor/render-ts-typed-templates.ts index 71bb2f7e3..753801ca3 100644 --- a/packages/core-generators/src/renderers/typescript/extractor/render-ts-typed-templates.ts +++ b/packages/core-generators/src/renderers/typescript/extractor/render-ts-typed-templates.ts @@ -1,6 +1,6 @@ import type { TemplateExtractorTemplateEntry } from '@baseplate-dev/sync'; -import { quot, sortObjectKeys } from '@baseplate-dev/utils'; +import { compareStrings, quot, sortObjectKeys } from '@baseplate-dev/utils'; import { camelCase } from 'change-case'; import { groupBy } from 'es-toolkit'; @@ -89,7 +89,7 @@ function renderTsTypedTemplateGroup( ): TemplateExtractorTypedTemplate { const renderedTemplates = templates .map(({ name, config }) => renderTsTypedTemplate(name, config, context)) - .toSorted((a, b) => a.exportName.localeCompare(b.exportName)); + .toSorted((a, b) => compareStrings(a.exportName, b.exportName)); const exportName = `${camelCase(groupName)}Group`; return { diff --git a/packages/core-generators/src/renderers/typescript/fragments/utils.ts b/packages/core-generators/src/renderers/typescript/fragments/utils.ts index 998bb346e..bf3d0b0a9 100644 --- a/packages/core-generators/src/renderers/typescript/fragments/utils.ts +++ b/packages/core-generators/src/renderers/typescript/fragments/utils.ts @@ -1,4 +1,4 @@ -import { toposortLocal } from '@baseplate-dev/utils'; +import { compareStrings, toposortLocal } from '@baseplate-dev/utils'; import { isEqual, keyBy, uniqWith } from 'es-toolkit'; import type { TsImportDeclaration } from '../imports/types.js'; @@ -191,7 +191,7 @@ export function mergeFragmentsWithHoistedFragments( // to the root fragment that uses them if (isARoot && !isBRoot) return -1; if (!isARoot && isBRoot) return 1; - return a.localeCompare(b); + return compareStrings(a, b); }, ); diff --git a/packages/core-generators/src/renderers/typescript/imports/merge-ts-import-declarations.ts b/packages/core-generators/src/renderers/typescript/imports/merge-ts-import-declarations.ts index 0643b1136..975dec5d4 100644 --- a/packages/core-generators/src/renderers/typescript/imports/merge-ts-import-declarations.ts +++ b/packages/core-generators/src/renderers/typescript/imports/merge-ts-import-declarations.ts @@ -1,4 +1,4 @@ -import { mapGroupBy } from '@baseplate-dev/utils'; +import { compareStrings, mapGroupBy } from '@baseplate-dev/utils'; import { uniqBy } from 'es-toolkit'; import type { TsImportDeclaration } from './types.js'; @@ -81,7 +81,7 @@ function convertToImportDeclarations( name: e.name as string, alias: e.alias === e.name ? undefined : e.alias, })) - .toSorted((a, b) => (a.alias ?? a.name).localeCompare(b.alias ?? b.name)); + .toSorted((a, b) => compareStrings(a.alias ?? a.name, b.alias ?? b.name)); const importDeclarations: TsImportDeclaration[] = []; const addDeclaration = (declaration: Partial): void => { diff --git a/packages/core-generators/src/renderers/typescript/renderers/file.ts b/packages/core-generators/src/renderers/typescript/renderers/file.ts index c08eaf740..3e5c2a9e2 100644 --- a/packages/core-generators/src/renderers/typescript/renderers/file.ts +++ b/packages/core-generators/src/renderers/typescript/renderers/file.ts @@ -1,5 +1,6 @@ import type { CodeBlockWriter, SourceFile } from 'ts-morph'; +import { compareStrings } from '@baseplate-dev/utils'; import { Project } from 'ts-morph'; import type { @@ -83,7 +84,7 @@ function mergeImportsAndHoistedFragments( // This can be improved in the future but since the use-case is very limited, // we'll just throw an error if this happens. const sortedPositionedHoistedFragments = positionedHoistedFragments.sort( - (a, b) => a.key.localeCompare(b.key), + (a, b) => compareStrings(a.key, b.key), ); if ( new Set(sortedPositionedHoistedFragments.map((f) => f.key)).size !== diff --git a/packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts b/packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts index f580b5838..2819dcc29 100644 --- a/packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts +++ b/packages/core-generators/src/renderers/typescript/utils/ts-code-utils.ts @@ -1,4 +1,4 @@ -import { quot } from '@baseplate-dev/utils'; +import { compareStrings, quot } from '@baseplate-dev/utils'; import { sortBy } from 'es-toolkit'; import type { TsCodeFragmentOptions } from '../fragments/creators.js'; @@ -270,7 +270,9 @@ export const TsCodeUtils = { const sortedKeys = disableSort ? keys - : keys.toSorted(caseSensitive ? undefined : (a, b) => a.localeCompare(b)); + : keys.toSorted( + caseSensitive ? undefined : (a, b) => compareStrings(a, b), + ); if (!disableSort && keys.some((k) => k.startsWith('...'))) { throw new Error('Cannot have spread keys when sorting is enabled'); @@ -350,7 +352,9 @@ export const TsCodeUtils = { const sortedKeys = disableSort ? keys - : keys.toSorted(caseSensitive ? undefined : (a, b) => a.localeCompare(b)); + : keys.toSorted( + caseSensitive ? undefined : (a, b) => compareStrings(a, b), + ); const mergedContent = sortedKeys .filter((key) => map.get(key)) diff --git a/packages/core-generators/src/test-helpers/utils.ts b/packages/core-generators/src/test-helpers/utils.ts index 848ca958a..739223b37 100644 --- a/packages/core-generators/src/test-helpers/utils.ts +++ b/packages/core-generators/src/test-helpers/utils.ts @@ -1,3 +1,4 @@ +import { compareStrings } from '@baseplate-dev/utils'; import { isDeepStrictEqual } from 'node:util'; import type { @@ -31,15 +32,18 @@ export function normalizeImports( ...imp, // Sort named imports alphabetically namedImports: imp.namedImports?.slice().sort((a, b) => { - const nameCompare = a.name.localeCompare(b.name); + const nameCompare = compareStrings(a.name, b.name); if (nameCompare !== 0) return nameCompare; // If names are equal, sort by alias - return (a.alias ?? '').localeCompare(b.alias ?? ''); + return compareStrings(a.alias ?? '', b.alias ?? ''); }), })) .sort((a, b) => { // Primary sort: module specifier - const moduleCompare = a.moduleSpecifier.localeCompare(b.moduleSpecifier); + const moduleCompare = compareStrings( + a.moduleSpecifier, + b.moduleSpecifier, + ); if (moduleCompare !== 0) return moduleCompare; // Secondary sort: import type (namespace > default > named) @@ -82,7 +86,7 @@ export function normalizeHoistedFragments( key: frag.key, } as TsHoistedFragment; }) - .sort((a, b) => a.key.localeCompare(b.key)); + .sort((a, b) => compareStrings(a.key, b.key)); } /** diff --git a/packages/fastify-generators/src/generators/core/service-file/service-file.generator.ts b/packages/fastify-generators/src/generators/core/service-file/service-file.generator.ts index 912c90b8c..c38cfe044 100644 --- a/packages/fastify-generators/src/generators/core/service-file/service-file.generator.ts +++ b/packages/fastify-generators/src/generators/core/service-file/service-file.generator.ts @@ -15,7 +15,7 @@ import { createProviderType, createReadOnlyProviderType, } from '@baseplate-dev/sync'; -import { NamedArrayFieldContainer } from '@baseplate-dev/utils'; +import { compareStrings, NamedArrayFieldContainer } from '@baseplate-dev/utils'; import { posixJoin } from '@baseplate-dev/utils/node'; import { kebabCase } from 'change-case'; import path from 'node:path'; @@ -140,7 +140,7 @@ export const serviceFileGenerator = createGenerator({ build: async (builder) => { const orderedHeaders = headersContainer .getValue() - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => compareStrings(a.name, b.name)); const orderedMethods = methodsContainer .getValue() .sort((a, b) => a.order - b.order); diff --git a/packages/fastify-generators/src/generators/prisma/prisma-data-service/prisma-data-service.generator.ts b/packages/fastify-generators/src/generators/prisma/prisma-data-service/prisma-data-service.generator.ts index 7286f5101..3113aca86 100644 --- a/packages/fastify-generators/src/generators/prisma/prisma-data-service/prisma-data-service.generator.ts +++ b/packages/fastify-generators/src/generators/prisma/prisma-data-service/prisma-data-service.generator.ts @@ -12,6 +12,7 @@ import { createProviderType, } from '@baseplate-dev/sync'; import { + compareStrings, lowercaseFirstChar, NamedArrayFieldContainer, } from '@baseplate-dev/utils'; @@ -161,7 +162,7 @@ export const prismaDataServiceGenerator = createGenerator({ }), ), ...virtualInputFields.toSorted((a, b) => - a.name.localeCompare(b.name), + compareStrings(a.name, b.name), ), ]; diff --git a/packages/fastify-generators/src/writers/prisma-schema/model-writer.ts b/packages/fastify-generators/src/writers/prisma-schema/model-writer.ts index 4e7e7f7b8..4ea419810 100644 --- a/packages/fastify-generators/src/writers/prisma-schema/model-writer.ts +++ b/packages/fastify-generators/src/writers/prisma-schema/model-writer.ts @@ -1,3 +1,5 @@ +import { compareStrings } from '@baseplate-dev/utils'; + import type { ScalarFieldType } from '#src/types/field-types.js'; import type { PrismaOutputModel } from '#src/types/prisma-output.js'; @@ -186,7 +188,7 @@ export class PrismaModelBlockWriter { .sort((a, b) => a.order - b.order); const relationFields = this.fields .filter((field) => field.fieldType === 'relation') - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => compareStrings(a.name, b.name)); // Look for duplicated orders in scalar fields const orderSet = new Set(scalarFields.map((field) => field.order)); diff --git a/packages/project-builder-server/src/compiler/root/root-package-compiler.ts b/packages/project-builder-server/src/compiler/root/root-package-compiler.ts index a012871db..34982ef0d 100644 --- a/packages/project-builder-server/src/compiler/root/root-package-compiler.ts +++ b/packages/project-builder-server/src/compiler/root/root-package-compiler.ts @@ -117,6 +117,7 @@ export class RootPackageCompiler extends PackageCompiler { lint: `turbo run lint`, 'lint:affected': `turbo run lint --affected`, test: `turbo run test`, + 'test:affected': `turbo run test --affected`, 'prettier:check': `turbo run prettier:check && pnpm run prettier:check:root`, 'prettier:check:affected': `turbo run prettier:check --affected`, 'prettier:write': `turbo run prettier:write && pnpm run prettier:write:root`, diff --git a/packages/project-builder-server/src/diff/snapshot/snapshot-manifest.ts b/packages/project-builder-server/src/diff/snapshot/snapshot-manifest.ts index e308c1256..53c34579a 100644 --- a/packages/project-builder-server/src/diff/snapshot/snapshot-manifest.ts +++ b/packages/project-builder-server/src/diff/snapshot/snapshot-manifest.ts @@ -1,4 +1,4 @@ -import { stringifyPrettyStable } from '@baseplate-dev/utils'; +import { compareStrings, stringifyPrettyStable } from '@baseplate-dev/utils'; import { handleFileNotFoundError, readJsonWithSchema, @@ -37,7 +37,7 @@ export async function saveSnapshotManifest( added: manifest.files.added.toSorted(), deleted: manifest.files.deleted.toSorted(), modified: manifest.files.modified.toSorted((a, b) => - a.path.localeCompare(b.path), + compareStrings(a.path, b.path), ), }, }; diff --git a/packages/project-builder-server/src/sync/file-id-map.ts b/packages/project-builder-server/src/sync/file-id-map.ts index 921ffe918..a3aadd9b4 100644 --- a/packages/project-builder-server/src/sync/file-id-map.ts +++ b/packages/project-builder-server/src/sync/file-id-map.ts @@ -1,4 +1,4 @@ -import { enhanceErrorWithContext } from '@baseplate-dev/utils'; +import { compareStrings, enhanceErrorWithContext } from '@baseplate-dev/utils'; import { handleFileNotFoundError, readJsonWithSchema, @@ -51,7 +51,7 @@ export async function writeGeneratedFileIdMap( const fileIdMapPath = path.join(projectDirectory, FILE_ID_MAP_PATH); const fileIdMap = Object.fromEntries( [...fileIdToRelativePathMap.entries()].sort(([a], [b]) => - a.localeCompare(b), + compareStrings(a, b), ), ); await writeJson(fileIdMapPath, fileIdMap); diff --git a/packages/project-builder-server/src/template-extractor/discover-generators.ts b/packages/project-builder-server/src/template-extractor/discover-generators.ts index 8671e0f5e..638edfa1d 100644 --- a/packages/project-builder-server/src/template-extractor/discover-generators.ts +++ b/packages/project-builder-server/src/template-extractor/discover-generators.ts @@ -2,6 +2,7 @@ import type { PluginMetadataWithPaths } from '@baseplate-dev/project-builder-lib import type { Logger, TemplateConfig } from '@baseplate-dev/sync'; import { indexTemplateConfigs } from '@baseplate-dev/sync'; +import { compareStrings } from '@baseplate-dev/utils'; import { findNearestPackageJson } from '@baseplate-dev/utils/node'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -89,7 +90,7 @@ export async function discoverGenerators( })); // Sort generators by name for consistent output - generators.sort((a, b) => a.name.localeCompare(b.name)); + generators.sort((a, b) => compareStrings(a.name, b.name)); return generators; } diff --git a/packages/project-builder-server/src/templates/list/list-templates.ts b/packages/project-builder-server/src/templates/list/list-templates.ts index 08b18be68..f1c9f8d64 100644 --- a/packages/project-builder-server/src/templates/list/list-templates.ts +++ b/packages/project-builder-server/src/templates/list/list-templates.ts @@ -1,5 +1,7 @@ import type { TemplateConfig } from '@baseplate-dev/sync'; +import { compareStrings } from '@baseplate-dev/utils'; + import { readExtractorConfig } from '../utils/extractor-config.js'; export interface ListTemplatesInput { @@ -51,7 +53,7 @@ export async function listTemplates({ } // Sort templates by name for consistent output - templates.sort((a, b) => a.name.localeCompare(b.name)); + templates.sort((a, b) => compareStrings(a.name, b.name)); return { generatorName: config.name, diff --git a/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/new-admin-section-dialog.tsx b/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/new-admin-section-dialog.tsx index dc93a9cac..e293bdcc6 100644 --- a/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/new-admin-section-dialog.tsx +++ b/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/new-admin-section-dialog.tsx @@ -22,6 +22,7 @@ import { SelectFieldController, useControlledState, } from '@baseplate-dev/ui-components'; +import { compareStrings } from '@baseplate-dev/utils'; import { zodResolver } from '@hookform/resolvers/zod'; import { useNavigate } from '@tanstack/react-router'; import { useMemo } from 'react'; @@ -109,7 +110,7 @@ function NewAdminSectionDialog({ webApp.adminApp.sections = [ ...(webApp.adminApp.sections ?? []), { ...data, id: newId }, - ].sort((a, b) => a.name.localeCompare(b.name)); + ].sort((a, b) => compareStrings(a.name, b.name)); }, { successMessage: `Successfully created section "${data.name}"!`, diff --git a/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/react-icon-combobox.tsx b/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/react-icon-combobox.tsx index 1b6196458..c7f7f9c7c 100644 --- a/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/react-icon-combobox.tsx +++ b/packages/project-builder-web/src/routes/admin-sections.$appKey/-components/react-icon-combobox.tsx @@ -7,6 +7,7 @@ import { AsyncComboboxField, useControllerMerged, } from '@baseplate-dev/ui-components'; +import { compareStrings } from '@baseplate-dev/utils'; interface IconOption { label: string; @@ -53,7 +54,7 @@ function ReactIconCombobox({ !query || option.label.toLowerCase().includes(query.toLowerCase()), ) - .sort((a, b) => a.label.localeCompare(b.label)) + .sort((a, b) => compareStrings(a.label, b.label)) .slice(0, 20), ) } diff --git a/packages/react-generators/src/generators/core/react-router/react-router.generator.ts b/packages/react-generators/src/generators/core/react-router/react-router.generator.ts index 16fd4ba73..ca2b68ffd 100644 --- a/packages/react-generators/src/generators/core/react-router/react-router.generator.ts +++ b/packages/react-generators/src/generators/core/react-router/react-router.generator.ts @@ -20,6 +20,7 @@ import { createProviderTask, createProviderType, } from '@baseplate-dev/sync'; +import { compareStrings } from '@baseplate-dev/utils'; import { z } from 'zod'; import { REACT_PACKAGES } from '#src/constants/react-packages.js'; @@ -192,7 +193,7 @@ export const reactRouterGenerator = createGenerator({ } const sortedRootContextFields = rootContextFields.toSorted((a, b) => - a.name.localeCompare(b.name), + compareStrings(a.name, b.name), ); return { diff --git a/packages/sync/src/templates/metadata/write-template-info-files.ts b/packages/sync/src/templates/metadata/write-template-info-files.ts index c7dd5c666..2cc3805d8 100644 --- a/packages/sync/src/templates/metadata/write-template-info-files.ts +++ b/packages/sync/src/templates/metadata/write-template-info-files.ts @@ -1,4 +1,4 @@ -import { stringifyPrettyStable } from '@baseplate-dev/utils'; +import { compareStrings, stringifyPrettyStable } from '@baseplate-dev/utils'; import { promises as fs } from 'node:fs'; import path from 'node:path'; @@ -59,7 +59,7 @@ export async function writeTemplateInfoFiles( const infoPath = path.join(fullDirPath, TEMPLATES_INFO_FILENAME); const sortedInfoEntries = Object.fromEntries( - Object.entries(info).sort(([a], [b]) => a.localeCompare(b)), + Object.entries(info).sort(([a], [b]) => compareStrings(a, b)), ); // Ensure directory exists diff --git a/packages/utils/src/objects/sort-keys-recursive.ts b/packages/utils/src/objects/sort-keys-recursive.ts index 828e079cf..ac6ebfb6d 100644 --- a/packages/utils/src/objects/sort-keys-recursive.ts +++ b/packages/utils/src/objects/sort-keys-recursive.ts @@ -1,3 +1,5 @@ +import { compareStrings } from '../string/compare-strings.js'; + /** * Recursively sorts all keys in an object, including nested objects and arrays. * @@ -15,7 +17,7 @@ export function sortKeysRecursive(obj: T): T { return Object.fromEntries( Object.entries(obj) - .sort(([a], [b]) => a.localeCompare(b)) + .sort(([a], [b]) => compareStrings(a, b)) .map(([key, value]) => [key, sortKeysRecursive(value)]), ) as T; } diff --git a/packages/utils/src/objects/sort-object-keys.ts b/packages/utils/src/objects/sort-object-keys.ts index 1b6284306..5a9c2811c 100644 --- a/packages/utils/src/objects/sort-object-keys.ts +++ b/packages/utils/src/objects/sort-object-keys.ts @@ -1,3 +1,5 @@ +import { compareStrings } from '../string/compare-strings.js'; + /** * Sorts the keys of an object. * @@ -6,6 +8,6 @@ */ export function sortObjectKeys>(obj: T): T { return Object.fromEntries( - Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)), + Object.entries(obj).sort(([a], [b]) => compareStrings(a, b)), ) as T; } diff --git a/packages/utils/src/string/compare-strings.ts b/packages/utils/src/string/compare-strings.ts new file mode 100644 index 000000000..f27f60d98 --- /dev/null +++ b/packages/utils/src/string/compare-strings.ts @@ -0,0 +1,28 @@ +/** + * Stable string comparison function that uses case-insensitive lexicographic ordering + * with a case-sensitive tiebreaker for determinism. + * + * This matches the default behavior of `localeCompare` (case-insensitive) while + * providing consistent results across different operating systems and locales. + * Use this for deterministic sorting where locale-aware comparison is not required. + * + * @param a - First string to compare + * @param b - Second string to compare + * @returns Negative if a < b, positive if a > b, zero if equal + * + * @example + * ```ts + * const items = ['Banana', 'apple', 'Cherry', 'cherry']; + * items.sort(compareStrings); // ['apple', 'Banana', 'Cherry', 'cherry'] + * ``` + */ +export function compareStrings(a: string, b: string): number { + const aLower = a.toLowerCase(); + const bLower = b.toLowerCase(); + if (aLower < bLower) return -1; + if (aLower > bLower) return 1; + // Tiebreaker: case-sensitive comparison for determinism + if (a < b) return -1; + if (a > b) return 1; + return 0; +} diff --git a/packages/utils/src/string/compare-strings.unit.test.ts b/packages/utils/src/string/compare-strings.unit.test.ts new file mode 100644 index 000000000..df44961ad --- /dev/null +++ b/packages/utils/src/string/compare-strings.unit.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from 'vitest'; + +import { compareStrings } from './compare-strings.js'; + +describe('compareStrings', () => { + it('should return negative when first string is less than second', () => { + expect(compareStrings('apple', 'banana')).toBeLessThan(0); + }); + + it('should return positive when first string is greater than second', () => { + expect(compareStrings('banana', 'apple')).toBeGreaterThan(0); + }); + + it('should return zero when strings are equal', () => { + expect(compareStrings('apple', 'apple')).toBe(0); + }); + + it('should sort strings correctly', () => { + const items = ['cherry', 'apple', 'banana']; + items.sort(compareStrings); + expect(items).toEqual(['apple', 'banana', 'cherry']); + }); + + it('should sort case-insensitively with case-sensitive tiebreaker', () => { + const items = ['apple', 'Banana', 'cherry']; + items.sort(compareStrings); + // Case-insensitive primary sort, case-sensitive tiebreaker + expect(items).toEqual(['apple', 'Banana', 'cherry']); + }); + + it('should use case-sensitive tiebreaker for same-letter-different-case strings', () => { + const items = ['Cherry', 'cherry', 'CHERRY']; + items.sort(compareStrings); + // Uppercase comes before lowercase in ASCII tiebreaker + expect(items).toEqual(['CHERRY', 'Cherry', 'cherry']); + }); + + it('should handle empty strings', () => { + expect(compareStrings('', 'a')).toBeLessThan(0); + expect(compareStrings('a', '')).toBeGreaterThan(0); + expect(compareStrings('', '')).toBe(0); + }); + + it('should provide stable sort results', () => { + const items1 = ['zebra', 'apple', 'mango', 'banana']; + const items2 = ['zebra', 'apple', 'mango', 'banana']; + + items1.sort(compareStrings); + items2.sort(compareStrings); + + expect(items1).toEqual(items2); + expect(items1).toEqual(['apple', 'banana', 'mango', 'zebra']); + }); + + it('should sort mixed case strings case-insensitively', () => { + const items = ['Banana', 'apple', 'Cherry', 'cherry']; + items.sort(compareStrings); + expect(items).toEqual(['apple', 'Banana', 'Cherry', 'cherry']); + }); + + it('should compare case-insensitively for different strings', () => { + expect(compareStrings('Apple', 'banana')).toBeLessThan(0); + expect(compareStrings('apple', 'BANANA')).toBeLessThan(0); + expect(compareStrings('Banana', 'apple')).toBeGreaterThan(0); + expect(compareStrings('BANANA', 'apple')).toBeGreaterThan(0); + }); +}); diff --git a/packages/utils/src/string/index.ts b/packages/utils/src/string/index.ts index c2fcaff56..55262f12a 100644 --- a/packages/utils/src/string/index.ts +++ b/packages/utils/src/string/index.ts @@ -1,4 +1,5 @@ export * from './case.js'; +export * from './compare-strings.js'; export * from './convert-case-with-prefix.js'; export * from './find-closest-match.js'; export * from './quot.js'; diff --git a/packages/utils/src/toposort/toposort-local.ts b/packages/utils/src/toposort/toposort-local.ts index b8895ded3..0374a00fd 100644 --- a/packages/utils/src/toposort/toposort-local.ts +++ b/packages/utils/src/toposort/toposort-local.ts @@ -1,3 +1,4 @@ +import { compareStrings } from '../string/compare-strings.js'; import { ToposortCyclicalDependencyError, ToposortUnknownNodeError, @@ -119,7 +120,7 @@ function detectCycle( */ function defaultCompareFunc(a: T, b: T): number { if (typeof a === 'string' && typeof b === 'string') { - return a.localeCompare(b); + return compareStrings(a, b); } if (a === b) return 0; return a < b ? -1 : 1; diff --git a/packages/utils/src/toposort/toposort.ts b/packages/utils/src/toposort/toposort.ts index 7f064190d..f0f75436f 100644 --- a/packages/utils/src/toposort/toposort.ts +++ b/packages/utils/src/toposort/toposort.ts @@ -1,5 +1,6 @@ import TinyQueue from 'tinyqueue'; +import { compareStrings } from '../string/compare-strings.js'; import { ToposortCyclicalDependencyError, ToposortUnknownNodeError, @@ -113,7 +114,7 @@ function detectCycle( */ function defaultCompareFunc(a: T, b: T): number { if (typeof a === 'string' && typeof b === 'string') { - return a.localeCompare(b); + return compareStrings(a, b); } if (a === b) return 0; return a < b ? -1 : 1; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26c0f1847..a58c1cb82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -114,6 +114,9 @@ importers: packages/code-morph: dependencies: + '@baseplate-dev/utils': + specifier: workspace:* + version: link:../utils '@inquirer/prompts': specifier: 7.8.3 version: 7.8.3(@types/node@22.17.2) @@ -4314,8 +4317,8 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} engines: {node: '>=18'} brace-expansion@1.1.12: @@ -4679,8 +4682,8 @@ packages: supports-color: optional: true - debug@4.4.1: - resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -5465,8 +5468,8 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} http-proxy-agent@7.0.2: @@ -5498,6 +5501,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6670,9 +6677,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@3.0.0: - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} - engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -7124,8 +7131,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} std-env@3.9.0: @@ -7879,7 +7886,7 @@ snapshots: '@babel/types': 7.28.4 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -8049,7 +8056,7 @@ snapshots: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -8377,7 +8384,7 @@ snapshots: '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.1 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8391,7 +8398,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -8450,7 +8457,7 @@ snapshots: '@lukeed/ms': 2.0.2 escape-html: 1.0.3 fast-decode-uri-component: 1.0.1 - http-errors: 2.0.0 + http-errors: 2.0.1 mime: 3.0.0 '@fastify/static@8.0.3': @@ -8750,7 +8757,7 @@ snapshots: express: 5.1.0 express-rate-limit: 7.5.1(express@5.1.0) pkce-challenge: 5.0.0 - raw-body: 3.0.0 + raw-body: 3.0.2 zod: 3.25.76 zod-to-json-schema: 3.24.5(zod@3.25.76) transitivePeerDependencies: @@ -10317,7 +10324,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.32.0(jiti@2.6.1) typescript: 5.8.3 transitivePeerDependencies: @@ -10327,7 +10334,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -10346,7 +10353,7 @@ snapshots: '@typescript-eslint/types': 8.38.0 '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/utils': 8.38.0(eslint@9.32.0(jiti@2.6.1))(typescript@5.8.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.32.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 @@ -10361,7 +10368,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/types': 8.38.0 '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -10770,16 +10777,16 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@2.2.0: + body-parser@2.2.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.1 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 on-finished: 2.4.1 qs: 6.14.0 - raw-body: 3.0.0 + raw-body: 3.0.2 type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -11077,7 +11084,7 @@ snapshots: cpx2@8.0.0: dependencies: debounce: 2.0.0 - debug: 4.4.1 + debug: 4.4.3 duplexer: 0.1.2 fs-extra: 11.3.0 glob: 11.1.0 @@ -11156,7 +11163,7 @@ snapshots: ms: 2.1.3 optional: true - debug@4.4.1: + debug@4.4.3: dependencies: ms: 2.1.3 @@ -11223,7 +11230,7 @@ snapshots: docker-modem@5.0.6: dependencies: - debug: 4.4.1 + debug: 4.4.3 readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.16.0 @@ -11438,7 +11445,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.8): dependencies: - debug: 4.4.1 + debug: 4.4.3 esbuild: 0.25.8 transitivePeerDependencies: - supports-color @@ -11502,7 +11509,7 @@ snapshots: eslint-import-resolver-typescript@4.4.4(eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.38.0(eslint@9.32.0(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint@9.32.0(jiti@2.6.1)))(eslint@9.32.0(jiti@2.6.1)): dependencies: - debug: 4.4.1 + debug: 4.4.3 eslint: 9.32.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) get-tsconfig: 4.10.1 @@ -11519,7 +11526,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.38.0 comment-parser: 1.4.1 - debug: 4.4.1 + debug: 4.4.3 eslint: 9.32.0(jiti@2.6.1) eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 @@ -11652,7 +11659,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -11740,18 +11747,18 @@ snapshots: express@5.1.0: dependencies: accepts: 2.0.0 - body-parser: 2.2.0 + body-parser: 2.2.1 content-disposition: 1.0.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 finalhandler: 2.1.0 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 merge-descriptors: 2.0.0 mime-types: 3.0.1 on-finished: 2.4.1 @@ -11763,7 +11770,7 @@ snapshots: router: 2.2.0 send: 1.2.0 serve-static: 2.2.0 - statuses: 2.0.1 + statuses: 2.0.2 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -11875,12 +11882,12 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -12148,25 +12155,25 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 - http-errors@2.0.0: + http-errors@2.0.1: dependencies: depd: 2.0.0 inherits: 2.0.4 setprototypeof: 1.2.0 - statuses: 2.0.1 + statuses: 2.0.2 toidentifier: 1.0.1 http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -12182,6 +12189,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -12600,7 +12611,7 @@ snapshots: dependencies: chalk: 5.4.1 commander: 14.0.0 - debug: 4.4.1 + debug: 4.4.3 lilconfig: 3.1.3 listr2: 8.3.3 micromatch: 4.0.8 @@ -13305,11 +13316,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@3.0.0: + raw-body@3.0.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.6.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 unpipe: 1.0.0 rc9@2.1.2: @@ -13558,7 +13569,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -13625,17 +13636,17 @@ snapshots: send@1.2.0: dependencies: - debug: 4.4.1 + debug: 4.4.3 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 fresh: 2.0.0 - http-errors: 2.0.0 + http-errors: 2.0.1 mime-types: 3.0.1 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -13828,7 +13839,7 @@ snapshots: stackback@0.0.2: {} - statuses@2.0.1: {} + statuses@2.0.2: {} std-env@3.9.0: {} @@ -14055,7 +14066,7 @@ snapshots: archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.4.1 + debug: 4.4.3 docker-compose: 0.24.8 dockerode: 4.0.5 get-port: 7.1.0 @@ -14351,7 +14362,7 @@ snapshots: vite-node@3.2.4(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1 + debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.1.12(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.1)(tsx@4.20.6)(yaml@2.8.1) @@ -14407,7 +14418,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1 + debug: 4.4.3 expect-type: 1.2.2 magic-string: 0.30.19 pathe: 2.0.3