@tanstack/router-generator 1.51.6 → 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/cjs/config.cjs +38 -14
  2. package/dist/cjs/config.cjs.map +1 -1
  3. package/dist/cjs/config.d.cts +9 -0
  4. package/dist/cjs/filesystem/physical/getRouteNodes.cjs +125 -0
  5. package/dist/cjs/filesystem/physical/getRouteNodes.cjs.map +1 -0
  6. package/dist/cjs/filesystem/physical/getRouteNodes.d.cts +3 -0
  7. package/dist/cjs/filesystem/physical/rootPathId.cjs +5 -0
  8. package/dist/cjs/filesystem/physical/rootPathId.cjs.map +1 -0
  9. package/dist/cjs/filesystem/physical/rootPathId.d.cts +1 -0
  10. package/dist/cjs/filesystem/virtual/config.cjs +37 -0
  11. package/dist/cjs/filesystem/virtual/config.cjs.map +1 -0
  12. package/dist/cjs/filesystem/virtual/config.d.cts +3 -0
  13. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs +119 -0
  14. package/dist/cjs/filesystem/virtual/getRouteNodes.cjs.map +1 -0
  15. package/dist/cjs/filesystem/virtual/getRouteNodes.d.cts +5 -0
  16. package/dist/cjs/generator.cjs +48 -171
  17. package/dist/cjs/generator.cjs.map +1 -1
  18. package/dist/cjs/generator.d.cts +2 -27
  19. package/dist/cjs/types.d.cts +27 -0
  20. package/dist/cjs/utils.cjs +34 -0
  21. package/dist/cjs/utils.cjs.map +1 -1
  22. package/dist/cjs/utils.d.cts +8 -0
  23. package/dist/esm/config.d.ts +9 -0
  24. package/dist/esm/config.js +24 -0
  25. package/dist/esm/config.js.map +1 -1
  26. package/dist/esm/filesystem/physical/getRouteNodes.d.ts +3 -0
  27. package/dist/esm/filesystem/physical/getRouteNodes.js +108 -0
  28. package/dist/esm/filesystem/physical/getRouteNodes.js.map +1 -0
  29. package/dist/esm/filesystem/physical/rootPathId.d.ts +1 -0
  30. package/dist/esm/filesystem/physical/rootPathId.js +5 -0
  31. package/dist/esm/filesystem/physical/rootPathId.js.map +1 -0
  32. package/dist/esm/filesystem/virtual/config.d.ts +3 -0
  33. package/dist/esm/filesystem/virtual/config.js +37 -0
  34. package/dist/esm/filesystem/virtual/config.js.map +1 -0
  35. package/dist/esm/filesystem/virtual/getRouteNodes.d.ts +5 -0
  36. package/dist/esm/filesystem/virtual/getRouteNodes.js +119 -0
  37. package/dist/esm/filesystem/virtual/getRouteNodes.js.map +1 -0
  38. package/dist/esm/generator.d.ts +2 -27
  39. package/dist/esm/generator.js +34 -157
  40. package/dist/esm/generator.js.map +1 -1
  41. package/dist/esm/types.d.ts +27 -0
  42. package/dist/esm/utils.d.ts +8 -0
  43. package/dist/esm/utils.js +34 -0
  44. package/dist/esm/utils.js.map +1 -1
  45. package/package.json +3 -2
  46. package/src/config.ts +25 -0
  47. package/src/filesystem/physical/getRouteNodes.ts +151 -0
  48. package/src/filesystem/physical/rootPathId.ts +1 -0
  49. package/src/filesystem/virtual/config.ts +45 -0
  50. package/src/filesystem/virtual/getRouteNodes.ts +141 -0
  51. package/src/generator.ts +52 -236
  52. package/src/types.ts +28 -0
  53. package/src/utils.ts +43 -0
package/src/generator.ts CHANGED
@@ -2,168 +2,25 @@ import path from 'node:path'
2
2
  import * as fs from 'node:fs'
3
3
  import * as fsp from 'node:fs/promises'
4
4
  import * as prettier from 'prettier'
5
- import { cleanPath, logging, trimPathLeft } from './utils'
5
+ import {
6
+ determineInitialRoutePath,
7
+ logging,
8
+ removeExt,
9
+ removeTrailingSlash,
10
+ removeUnderscores,
11
+ replaceBackslash,
12
+ routePathToVariable,
13
+ trimPathLeft,
14
+ } from './utils'
15
+ import { getRouteNodes as physicalGetRouteNodes } from './filesystem/physical/getRouteNodes'
16
+ import { getRouteNodes as virtualGetRouteNodes } from './filesystem/virtual/getRouteNodes'
17
+ import { rootPathId } from './filesystem/physical/rootPathId'
18
+ import type { GetRouteNodesResult, RouteNode } from './types'
6
19
  import type { Config } from './config'
7
20
 
8
21
  let latestTask = 0
9
- export const rootPathId = '__root'
10
22
  const routeGroupPatternRegex = /\(.+\)/g
11
23
  const possiblyNestedRouteGroupPatternRegex = /\([^/]+\)\/?/g
12
- const disallowedRouteGroupConfiguration = /\(([^)]+)\).(ts|js|tsx|jsx)/
13
-
14
- export type RouteNode = {
15
- filePath: string
16
- fullPath: string
17
- variableName: string
18
- routePath?: string
19
- cleanedPath?: string
20
- path?: string
21
- isNonPath?: boolean
22
- isNonLayout?: boolean
23
- isLayout?: boolean
24
- isVirtualParentRequired?: boolean
25
- isVirtualParentRoute?: boolean
26
- isRoute?: boolean
27
- isAPIRoute?: boolean
28
- isLoader?: boolean
29
- isComponent?: boolean
30
- isErrorComponent?: boolean
31
- isPendingComponent?: boolean
32
- isVirtual?: boolean
33
- isLazy?: boolean
34
- isRoot?: boolean
35
- children?: Array<RouteNode>
36
- parent?: RouteNode
37
- }
38
-
39
- async function getRouteNodes(config: Config) {
40
- const { routeFilePrefix, routeFileIgnorePrefix, routeFileIgnorePattern } =
41
- config
42
- const logger = logging({ disabled: config.disableLogging })
43
- const routeFileIgnoreRegExp = new RegExp(routeFileIgnorePattern ?? '', 'g')
44
-
45
- const routeNodes: Array<RouteNode> = []
46
-
47
- async function recurse(dir: string) {
48
- const fullDir = path.resolve(config.routesDirectory, dir)
49
- let dirList = await fsp.readdir(fullDir, { withFileTypes: true })
50
-
51
- dirList = dirList.filter((d) => {
52
- if (
53
- d.name.startsWith('.') ||
54
- (routeFileIgnorePrefix && d.name.startsWith(routeFileIgnorePrefix))
55
- ) {
56
- return false
57
- }
58
-
59
- if (routeFilePrefix) {
60
- return d.name.startsWith(routeFilePrefix)
61
- }
62
-
63
- if (routeFileIgnorePattern) {
64
- return !d.name.match(routeFileIgnoreRegExp)
65
- }
66
-
67
- return true
68
- })
69
-
70
- await Promise.all(
71
- dirList.map(async (dirent) => {
72
- const fullPath = path.join(fullDir, dirent.name)
73
- const relativePath = path.join(dir, dirent.name)
74
-
75
- if (dirent.isDirectory()) {
76
- await recurse(relativePath)
77
- } else if (fullPath.match(/\.(tsx|ts|jsx|js)$/)) {
78
- const filePath = replaceBackslash(path.join(dir, dirent.name))
79
- const filePathNoExt = removeExt(filePath)
80
- let routePath = determineInitialRoutePath(filePathNoExt)
81
-
82
- if (routeFilePrefix) {
83
- routePath = routePath.replaceAll(routeFilePrefix, '')
84
- }
85
-
86
- if (disallowedRouteGroupConfiguration.test(dirent.name)) {
87
- const errorMessage = `A route configuration for a route group was found at \`${filePath}\`. This is not supported. Did you mean to use a layout/pathless route instead?`
88
- logger.error(`ERROR: ${errorMessage}`)
89
- throw new Error(errorMessage)
90
- }
91
-
92
- const variableName = routePathToVariable(routePath)
93
-
94
- // Remove the index from the route path and
95
- // if the route path is empty, use `/'
96
-
97
- const isLazy = routePath.endsWith('/lazy')
98
-
99
- if (isLazy) {
100
- routePath = routePath.replace(/\/lazy$/, '')
101
- }
102
-
103
- const isRoute = routePath.endsWith('/route')
104
- const isComponent = routePath.endsWith('/component')
105
- const isErrorComponent = routePath.endsWith('/errorComponent')
106
- const isPendingComponent = routePath.endsWith('/pendingComponent')
107
- const isLoader = routePath.endsWith('/loader')
108
- const isAPIRoute = routePath.startsWith(
109
- `${removeTrailingSlash(config.apiBase)}/`,
110
- )
111
-
112
- const segments = routePath.split('/')
113
- const isLayout =
114
- segments[segments.length - 1]?.startsWith('_') || false
115
-
116
- ;(
117
- [
118
- [isComponent, 'component'],
119
- [isErrorComponent, 'errorComponent'],
120
- [isPendingComponent, 'pendingComponent'],
121
- [isLoader, 'loader'],
122
- ] as const
123
- ).forEach(([isType, type]) => {
124
- if (isType) {
125
- logger.warn(
126
- `WARNING: The \`.${type}.tsx\` suffix used for the ${filePath} file is deprecated. Use the new \`.lazy.tsx\` suffix instead.`,
127
- )
128
- }
129
- })
130
-
131
- routePath = routePath.replace(
132
- /\/(component|errorComponent|pendingComponent|loader|route|lazy)$/,
133
- '',
134
- )
135
-
136
- if (routePath === 'index') {
137
- routePath = '/'
138
- }
139
-
140
- routePath = routePath.replace(/\/index$/, '/') || '/'
141
-
142
- routeNodes.push({
143
- filePath,
144
- fullPath,
145
- routePath,
146
- variableName,
147
- isRoute,
148
- isComponent,
149
- isErrorComponent,
150
- isPendingComponent,
151
- isLoader,
152
- isLazy,
153
- isLayout,
154
- isAPIRoute,
155
- })
156
- }
157
- }),
158
- )
159
-
160
- return routeNodes
161
- }
162
-
163
- await recurse('./')
164
-
165
- return routeNodes
166
- }
167
24
 
168
25
  let isFirst = false
169
26
  let skipMessage = false
@@ -209,23 +66,31 @@ export async function generator(config: Config) {
209
66
  parser: 'typescript',
210
67
  }
211
68
 
212
- const routePathIdPrefix = config.routeFilePrefix ?? ''
213
- const beforeRouteNodes = await getRouteNodes(config)
214
- const rootRouteNode = beforeRouteNodes.find(
215
- (d) => d.routePath === `/${rootPathId}`,
216
- )
69
+ let getRouteNodesResult: GetRouteNodesResult
217
70
 
71
+ if (config.virtualRouteConfig) {
72
+ getRouteNodesResult = await virtualGetRouteNodes(config)
73
+ } else {
74
+ getRouteNodesResult = await physicalGetRouteNodes(config)
75
+ }
76
+
77
+ const { rootRouteNode, routeNodes: beforeRouteNodes } = getRouteNodesResult
78
+ if (rootRouteNode === undefined) {
79
+ throw new Error(`rootRouteNode must not be undefined`)
80
+ }
218
81
  const preRouteNodes = multiSortBy(beforeRouteNodes, [
219
82
  (d) => (d.routePath === '/' ? -1 : 1),
220
83
  (d) => d.routePath?.split('/').length,
221
- (d) => (d.filePath.match(/[./]index[.]/) ? 1 : -1),
84
+ (d) =>
85
+ d.filePath.match(new RegExp(`[./]${config.indexToken}[.]`)) ? 1 : -1,
222
86
  (d) =>
223
87
  d.filePath.match(
224
88
  /[./](component|errorComponent|pendingComponent|loader|lazy)[.]/,
225
89
  )
226
90
  ? 1
227
91
  : -1,
228
- (d) => (d.filePath.match(/[./]route[.]/) ? -1 : 1),
92
+ (d) =>
93
+ d.filePath.match(new RegExp(`[./]${config.routeToken}[.]`)) ? -1 : 1,
229
94
  (d) => (d.routePath?.endsWith('/') ? -1 : 1),
230
95
  (d) => d.routePath,
231
96
  ]).filter((d) => ![`/${rootPathId}`].includes(d.routePath || ''))
@@ -298,13 +163,11 @@ export const Route = createRootRoute({
298
163
  const trimmedPath = trimPathLeft(node.path ?? '')
299
164
 
300
165
  const split = trimmedPath.split('/')
301
- const first = split[0] ?? trimmedPath
302
166
  const lastRouteSegment = split[split.length - 1] ?? trimmedPath
303
167
 
304
168
  node.isNonPath =
305
169
  lastRouteSegment.startsWith('_') ||
306
170
  routeGroupPatternRegex.test(lastRouteSegment)
307
- node.isNonLayout = first.endsWith('_')
308
171
 
309
172
  node.cleanedPath = removeGroups(
310
173
  removeUnderscores(removeLayoutSegments(node.path)) ?? '',
@@ -542,7 +405,7 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
542
405
  const sortedRouteNodes = multiSortBy(routeNodes, [
543
406
  (d) => (d.routePath?.includes(`/${rootPathId}`) ? -1 : 1),
544
407
  (d) => d.routePath?.split('/').length,
545
- (d) => (d.routePath?.endsWith("index'") ? -1 : 1),
408
+ (d) => (d.routePath?.endsWith(config.indexToken) ? -1 : 1),
546
409
  (d) => d,
547
410
  ])
548
411
 
@@ -562,11 +425,18 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
562
425
  .map((d) => d[0])
563
426
 
564
427
  const virtualRouteNodes = sortedRouteNodes.filter((d) => d.isVirtual)
565
- const rootPathIdExtension =
566
- config.addExtensions && rootRouteNode
567
- ? path.extname(rootRouteNode.filePath)
568
- : ''
569
428
 
429
+ function getImportPath(node: RouteNode) {
430
+ return replaceBackslash(
431
+ removeExt(
432
+ path.relative(
433
+ path.dirname(config.generatedRouteTree),
434
+ path.resolve(config.routesDirectory, node.filePath),
435
+ ),
436
+ config.addExtensions,
437
+ ),
438
+ )
439
+ }
570
440
  const routeImports = [
571
441
  ...config.routeTreeFileHeader,
572
442
  '// This file is auto-generated by TanStack Router',
@@ -575,29 +445,13 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
575
445
  : '',
576
446
  '// Import Routes',
577
447
  [
578
- `import { Route as rootRoute } from './${replaceBackslash(
579
- path.relative(
580
- path.dirname(config.generatedRouteTree),
581
- path.resolve(
582
- config.routesDirectory,
583
- `${routePathIdPrefix}${rootPathId}${rootPathIdExtension}`,
584
- ),
585
- ),
586
- )}'`,
448
+ `import { Route as rootRoute } from './${getImportPath(rootRouteNode)}'`,
587
449
  ...sortedRouteNodes
588
450
  .filter((d) => !d.isVirtual)
589
451
  .map((node) => {
590
452
  return `import { Route as ${
591
453
  node.variableName
592
- }Import } from './${replaceBackslash(
593
- removeExt(
594
- path.relative(
595
- path.dirname(config.generatedRouteTree),
596
- path.resolve(config.routesDirectory, node.filePath),
597
- ),
598
- config.addExtensions,
599
- ),
600
- )}'`
454
+ }Import } from './${getImportPath(node)}'`
601
455
  }),
602
456
  ].join('\n'),
603
457
  virtualRouteNodes.length ? '// Create Virtual Routes' : '',
@@ -695,7 +549,7 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
695
549
  ${routeNodes
696
550
  .map((routeNode) => {
697
551
  const [filePathId, routeId] = getFilePathIdAndRouteIdFromPath(
698
- routeNode.routePath!,
552
+ routeNode.routePath,
699
553
  )
700
554
 
701
555
  return `'${filePathId}': {
@@ -726,14 +580,14 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
726
580
  const createRouteManifest = () => {
727
581
  const routesManifest = {
728
582
  __root__: {
729
- filePath: rootRouteNode?.filePath,
583
+ filePath: rootRouteNode.filePath,
730
584
  children: routeTree.map(
731
- (d) => getFilePathIdAndRouteIdFromPath(d.routePath!)[1],
585
+ (d) => getFilePathIdAndRouteIdFromPath(d.routePath)[1],
732
586
  ),
733
587
  },
734
588
  ...Object.fromEntries(
735
589
  routeNodes.map((d) => {
736
- const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath!)
590
+ const [_, routeId] = getFilePathIdAndRouteIdFromPath(d.routePath)
737
591
 
738
592
  return [
739
593
  routeId,
@@ -744,7 +598,7 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
744
598
  : undefined,
745
599
  children: d.children?.map(
746
600
  (childRoute) =>
747
- getFilePathIdAndRouteIdFromPath(childRoute.routePath!)[1],
601
+ getFilePathIdAndRouteIdFromPath(childRoute.routePath)[1],
748
602
  ),
749
603
  },
750
604
  ]
@@ -811,24 +665,6 @@ export const Route = createAPIFileRoute('${escapedRoutePath}')({
811
665
  )
812
666
  }
813
667
 
814
- function routePathToVariable(routePath: string): string {
815
- return (
816
- removeUnderscores(routePath)
817
- ?.replace(/\/\$\//g, '/splat/')
818
- .replace(/\$$/g, 'splat')
819
- .replace(/\$/g, '')
820
- .split(/[/-]/g)
821
- .map((d, i) => (i > 0 ? capitalize(d) : d))
822
- .join('')
823
- .replace(/([^a-zA-Z0-9]|[.])/gm, '')
824
- .replace(/^(\d)/g, 'R$1') ?? ''
825
- )
826
- }
827
-
828
- export function removeExt(d: string, keepExtension: boolean = false) {
829
- return keepExtension ? d : d.substring(0, d.lastIndexOf('.')) || d
830
- }
831
-
832
668
  function spaces(d: number): string {
833
669
  return Array.from({ length: d })
834
670
  .map(() => ' ')
@@ -865,35 +701,14 @@ export function multiSortBy<T>(
865
701
  .map(([d]) => d)
866
702
  }
867
703
 
868
- function capitalize(s: string) {
869
- if (typeof s !== 'string') return ''
870
- return s.charAt(0).toUpperCase() + s.slice(1)
871
- }
872
-
873
- function removeUnderscores(s?: string) {
874
- return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
875
- }
876
-
877
704
  function removeTrailingUnderscores(s?: string) {
878
705
  return s?.replaceAll(/(_$)/gi, '').replaceAll(/(_\/)/gi, '/')
879
706
  }
880
707
 
881
- function replaceBackslash(s: string) {
882
- return s.replaceAll(/\\/gi, '/')
883
- }
884
-
885
708
  function removeGroups(s: string) {
886
709
  return s.replace(possiblyNestedRouteGroupPatternRegex, '')
887
710
  }
888
711
 
889
- function removeTrailingSlash(s: string) {
890
- return s.replace(/\/$/, '')
891
- }
892
-
893
- function determineInitialRoutePath(routePath: string) {
894
- return cleanPath(`/${routePath.split('.').join('/')}`) || ''
895
- }
896
-
897
712
  /**
898
713
  * The `node.path` is used as the `id` in the route definition.
899
714
  * This function checks if the given node has a parent and if so, it determines the correct path for the given node.
@@ -902,7 +717,7 @@ function determineInitialRoutePath(routePath: string) {
902
717
  */
903
718
  function determineNodePath(node: RouteNode) {
904
719
  return (node.path = node.parent
905
- ? node.routePath?.replace(node.parent.routePath!, '') || '/'
720
+ ? node.routePath?.replace(node.parent.routePath ?? '', '') || '/'
906
721
  : node.routePath)
907
722
  }
908
723
 
@@ -986,7 +801,7 @@ export const inferPath = (routeNode: RouteNode): string => {
986
801
  : (routeNode.cleanedPath?.replace(/\/$/, '') ?? '')
987
802
  }
988
803
 
989
- function getFilePathIdAndRouteIdFromPath(pathname: string) {
804
+ function getFilePathIdAndRouteIdFromPath(pathname?: string) {
990
805
  const filePathId = removeTrailingUnderscores(pathname)
991
806
  const id = removeGroups(filePathId ?? '')
992
807
 
@@ -1044,13 +859,14 @@ export type StartAPIRoutePathSegment = {
1044
859
  */
1045
860
  export function startAPIRouteSegmentsFromTSRFilePath(
1046
861
  src: string,
862
+ config: Config,
1047
863
  ): Array<StartAPIRoutePathSegment> {
1048
864
  const routePath = determineInitialRoutePath(src)
1049
865
 
1050
866
  const parts = routePath
1051
867
  .replaceAll('.', '/')
1052
868
  .split('/')
1053
- .filter((p) => !!p && p !== 'index')
869
+ .filter((p) => !!p && p !== config.indexToken)
1054
870
  const segments: Array<StartAPIRoutePathSegment> = parts.map((part) => {
1055
871
  if (part.startsWith('$')) {
1056
872
  if (part === '$') {
package/src/types.ts ADDED
@@ -0,0 +1,28 @@
1
+ export type RouteNode = {
2
+ filePath: string
3
+ fullPath: string
4
+ variableName: string
5
+ routePath?: string
6
+ cleanedPath?: string
7
+ path?: string
8
+ isNonPath?: boolean
9
+ isLayout?: boolean
10
+ isVirtualParentRequired?: boolean
11
+ isVirtualParentRoute?: boolean
12
+ isRoute?: boolean
13
+ isAPIRoute?: boolean
14
+ isLoader?: boolean
15
+ isComponent?: boolean
16
+ isErrorComponent?: boolean
17
+ isPendingComponent?: boolean
18
+ isVirtual?: boolean
19
+ isLazy?: boolean
20
+ isRoot?: boolean
21
+ children?: Array<RouteNode>
22
+ parent?: RouteNode
23
+ }
24
+
25
+ export interface GetRouteNodesResult {
26
+ rootRouteNode?: RouteNode
27
+ routeNodes: Array<RouteNode>
28
+ }
package/src/utils.ts CHANGED
@@ -26,3 +26,46 @@ export function logging(config: { disabled: boolean }) {
26
26
  },
27
27
  }
28
28
  }
29
+
30
+ export function removeLeadingSlash(path: string): string {
31
+ return path.replace(/^\//, '')
32
+ }
33
+
34
+ export function removeTrailingSlash(s: string) {
35
+ return s.replace(/\/$/, '')
36
+ }
37
+
38
+ export function determineInitialRoutePath(routePath: string) {
39
+ return cleanPath(`/${routePath.split('.').join('/')}`) || ''
40
+ }
41
+
42
+ export function replaceBackslash(s: string) {
43
+ return s.replaceAll(/\\/gi, '/')
44
+ }
45
+
46
+ export function routePathToVariable(routePath: string): string {
47
+ return (
48
+ removeUnderscores(routePath)
49
+ ?.replace(/\/\$\//g, '/splat/')
50
+ .replace(/\$$/g, 'splat')
51
+ .replace(/\$/g, '')
52
+ .split(/[/-]/g)
53
+ .map((d, i) => (i > 0 ? capitalize(d) : d))
54
+ .join('')
55
+ .replace(/([^a-zA-Z0-9]|[.])/gm, '')
56
+ .replace(/^(\d)/g, 'R$1') ?? ''
57
+ )
58
+ }
59
+
60
+ export function removeUnderscores(s?: string) {
61
+ return s?.replaceAll(/(^_|_$)/gi, '').replaceAll(/(\/_|_\/)/gi, '/')
62
+ }
63
+
64
+ export function capitalize(s: string) {
65
+ if (typeof s !== 'string') return ''
66
+ return s.charAt(0).toUpperCase() + s.slice(1)
67
+ }
68
+
69
+ export function removeExt(d: string, keepExtension: boolean = false) {
70
+ return keepExtension ? d : d.substring(0, d.lastIndexOf('.')) || d
71
+ }