diff --git a/packages/router-generator/src/generator.ts b/packages/router-generator/src/generator.ts index d91e561a1..f282381db 100644 --- a/packages/router-generator/src/generator.ts +++ b/packages/router-generator/src/generator.ts @@ -5,6 +5,7 @@ import * as prettier from 'prettier' import { determineInitialRoutePath, logging, + multiSortBy, removeExt, removeTrailingSlash, removeUnderscores, @@ -671,36 +672,6 @@ function spaces(d: number): string { .join('') } -export function multiSortBy( - arr: Array, - accessors: Array<(item: T) => any> = [(d) => d], -): Array { - return arr - .map((d, i) => [d, i] as const) - .sort(([a, ai], [b, bi]) => { - for (const accessor of accessors) { - const ao = accessor(a) - const bo = accessor(b) - - if (typeof ao === 'undefined') { - if (typeof bo === 'undefined') { - continue - } - return 1 - } - - if (ao === bo) { - continue - } - - return ao > bo ? 1 : -1 - } - - return ai - bi - }) - .map(([d]) => d) -} - function removeTrailingUnderscores(s?: string) { return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/') } diff --git a/packages/router-generator/src/utils.ts b/packages/router-generator/src/utils.ts index ffcac863f..201341f77 100644 --- a/packages/router-generator/src/utils.ts +++ b/packages/router-generator/src/utils.ts @@ -1,3 +1,33 @@ +export function multiSortBy( + arr: Array, + accessors: Array<(item: T) => any> = [(d) => d], +): Array { + return arr + .map((d, i) => [d, i] as const) + .sort(([a, ai], [b, bi]) => { + for (const accessor of accessors) { + const ao = accessor(a) + const bo = accessor(b) + + if (typeof ao === 'undefined') { + if (typeof bo === 'undefined') { + continue + } + return 1 + } + + if (ao === bo) { + continue + } + + return ao > bo ? 1 : -1 + } + + return ai - bi + }) + .map(([d]) => d) +} + export function cleanPath(path: string) { // remove double slashes return path.replace(/\/{2,}/g, '/') diff --git a/packages/router-generator/tests/utils.test.ts b/packages/router-generator/tests/utils.test.ts new file mode 100644 index 000000000..22cb05f20 --- /dev/null +++ b/packages/router-generator/tests/utils.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, it } from 'vitest' +import { + cleanPath, + determineInitialRoutePath, + multiSortBy, + removeExt, + removeUnderscores, + routePathToVariable, +} from '../src/utils' + +describe('cleanPath', () => { + it('keeps path with leading slash and trailing slash', () => { + expect(cleanPath('/test/')).toBe('/test/') + }) +}) + +describe('determineInitialRoutePath', () => { + it('removes dots and adds slashes', () => { + expect(determineInitialRoutePath('test.test')).toBe('/test/test') + }) + + it('keeps leading slash', () => { + expect(determineInitialRoutePath('/test.test')).toBe('/test/test') + }) + + it('keeps trailing slash', () => { + expect(determineInitialRoutePath('test.test/')).toBe('/test/test/') + }) + + it('removes dots and adds slashes with leading and trailing slashes', () => { + expect(determineInitialRoutePath('/test.test/')).toBe('/test/test/') + }) + + it("returns '/' if path is empty", () => { + expect(determineInitialRoutePath('')).toBe('/') + }) + + it("returns '/' if path is '.'", () => { + expect(determineInitialRoutePath('.')).toBe('/') + }) + + it("returns '/' if path is './'", () => { + expect(determineInitialRoutePath('./')).toBe('/') + }) +}) + +describe('multiSortBy', () => { + it('sorts by multiple criteria', () => { + const data = [ + { routePath: '/test/1/2/index', f: 'b' }, + { routePath: '/test/1', f: 'b' }, + { routePath: '/test/1/2/3/4/index', f: 'b' }, + { routePath: '/test/1/2/3', f: 'b' }, + { routePath: '/test/1/2/3/index', f: 'b' }, + { routePath: '/test/1/2', f: 'b' }, + { routePath: '/test/1/2/3/4', f: 'b' }, + ] + + const sorted = multiSortBy(data, [ + (d) => (d.routePath.includes('1') ? -1 : 1), + (d) => d.routePath.split('/').length, + (d) => (d.routePath.endsWith('index') ? -1 : 1), + (d) => d, + ]) + + expect(sorted).toEqual([ + { routePath: '/test/1', f: 'b' }, + { routePath: '/test/1/2', f: 'b' }, + { routePath: '/test/1/2/index', f: 'b' }, + { routePath: '/test/1/2/3', f: 'b' }, + { routePath: '/test/1/2/3/index', f: 'b' }, + { routePath: '/test/1/2/3/4', f: 'b' }, + { routePath: '/test/1/2/3/4/index', f: 'b' }, + ]) + }) +}) + +describe('removeExt', () => { + it('removes extension', () => { + expect(removeExt('test.ts')).toBe('test') + }) + + it('does not remove extension if no extension', () => { + expect(removeExt('test')).toBe('test') + }) + + it('removes extension with multiple dots', () => { + expect(removeExt('test.test.ts')).toBe('test.test') + }) + + it('removes extension with leading dot', () => { + expect(removeExt('.test.ts')).toBe('.test') + }) + + it('removes extension when in a route path', () => { + expect(removeExt('/test/test.ts')).toBe('/test/test') + }) +}) + +describe('removeUnderscores', () => { + it('removes leading underscore', () => { + expect(removeUnderscores('_test')).toBe('test') + }) + + it('removes trailing underscore', () => { + expect(removeUnderscores('test_')).toBe('test') + }) + + it('removes leading and trailing underscores', () => { + expect(removeUnderscores('_test_')).toBe('test') + }) +}) + +describe('routePathToVariable', () => { + it.each([ + ['/test/$/index', 'TestSplatIndex'], + ['/test/$', 'TestSplat'], + ['/test/$/', 'TestSplat'], + ['/test/index', 'TestIndex'], + ['/test', 'Test'], + ['/test/', 'Test'], + ])(`converts "%s" to "%s"`, (routePath, expected) => { + expect(routePathToVariable(routePath)).toBe(expected) + }) +})