generate route to href function

This commit is contained in:
Ryan Haskell-Glatz 2021-01-16 13:04:32 -06:00
parent 2f3b68db4c
commit 7befcbba86
5 changed files with 110 additions and 11 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store .DS_Store
.elm-spa
elm-stuff elm-stuff
node_modules node_modules
dist dist

View File

@ -9,6 +9,7 @@ import MsgTemplate from '../templates/msg'
import ParamsTemplate from '../templates/params' import ParamsTemplate from '../templates/params'
import * as Process from '../process' import * as Process from '../process'
import { bold, underline, colors, reset, check, dim } from "../terminal" import { bold, underline, colors, reset, check, dim } from "../terminal"
import { isStaticPage } from "../templates/utils"
export const build = (env : Environment) => () => export const build = (env : Environment) => () =>
createMissingDefaultFiles() createMissingDefaultFiles()
@ -58,7 +59,7 @@ const createMissingDefaultFiles = async () => {
const scanForStaticPages = async (entries: PageEntry[]) : Promise<string[][]> => { const scanForStaticPages = async (entries: PageEntry[]) : Promise<string[][]> => {
const contents = await Promise.all(entries.map(e => File.read(e.filepath))) const contents = await Promise.all(entries.map(e => File.read(e.filepath)))
return contents return contents
.map((content, i) => content.includes('exposing (page)') ? i : undefined) .map((content, i) => isStaticPage(content) ? i : undefined)
.filter(a => typeof a === 'number') .filter(a => typeof a === 'number')
.map((i : any) => entries[i].segments) .map((i : any) => entries[i].segments)
} }

View File

@ -1,10 +1,10 @@
import { routeTypeDefinition, indent, routeParserList, paramsImports, Options } from "./utils" import { routeTypeDefinition, indent, routeParserList, paramsImports, Options, routeToHref } from "./utils"
export default (pages : string[][], _options : Options) : string => ` export default (pages : string[][], _options : Options) : string => `
module Gen.Route exposing module Gen.Route exposing
( Route(..) ( Route(..)
, fromUrl , fromUrl
-- , toUrl , toHref
) )
${paramsImports(pages)} ${paramsImports(pages)}
@ -25,8 +25,13 @@ routes =
${indent(routeParserList(pages), 1)} ${indent(routeParserList(pages), 1)}
-- toUrl : Route -> Url toHref : Route -> String
-- toUrl route = toHref route =
-- Debug.todo "Gen.Route.toUrl" let
joinAsHref : List String -> String
joinAsHref segments =
"/" ++ String.join "/" segments
in
${indent(routeToHref(pages), 1)}
`.trimLeft() `.trimLeft()

View File

@ -12,13 +12,18 @@ const isHomepage = (path: string[]) =>
const isNotFoundPage = (path: string[]) => const isNotFoundPage = (path: string[]) =>
path.join('') === config.reserved.notFound path.join('') === config.reserved.notFound
// [ 'Users', 'Name_', 'Settings' ] => [ 'Name_' ] // [ 'Users', 'Name_', 'Settings' ] => [ 'Name' ]
const dynamicRouteSegments = (path : string[]) : string[] => const dynamicRouteSegments = (path : string[]) : string[] =>
isHomepage(path) || isNotFoundPage(path) isHomepage(path) || isNotFoundPage(path)
? [] ? []
: path.filter(segment => segment.endsWith('_')) : path.filter(isDynamicSegment)
.map(segment => segment.substr(0, segment.length - 1)) .map(segment => segment.substr(0, segment.length - 1))
const isDynamicSegment = (segment : string) : boolean =>
segment !== config.reserved.homepage
&& segment !== config.reserved.notFound
&& segment.endsWith('_')
// "AboutUs" => "aboutUs" // "AboutUs" => "aboutUs"
const fromPascalToCamelCase = (str : string) : string => const fromPascalToCamelCase = (str : string) : string =>
str[0].toLowerCase() + str.substring(1) str[0].toLowerCase() + str.substring(1)
@ -127,6 +132,29 @@ export const routeTypeDefinition = (paths: string[][]) : string =>
export const routeParserList = (paths: string[][]) : string => export const routeParserList = (paths: string[][]) : string =>
multilineList(paths.map(routeParserMap)) multilineList(paths.map(routeParserMap))
export const routeToHref = (paths: string[][]) : string =>
caseExpression(paths, {
variable: 'route',
condition: (path) =>
(dynamicRouteSegments(path).length === 0)
? routeVariant(path)
: `${routeVariant(path)} params`,
result: (path) => `joinAsHref ${routeToHrefSegments(path)}`
})
export const routeToHrefSegments = (path: string[]) : string => {
const segments = path.filter(p => p !== config.reserved.homepage)
const hrefFragments =
segments.map(segment =>
isDynamicSegment(segment)
? `params.${fromPascalToCamelCase(segment.substring(0, segment.length - 1))}`
: `"${fromPascalToSlugCase(segment)}"`
)
return hrefFragments.length === 0
? `[]`
: `[ ${hrefFragments.join(', ')} ]`
}
export const paramsImports = (paths: string[][]) : string => export const paramsImports = (paths: string[][]) : string =>
paths.map(path => `import Gen.Params.${path.join('.')}`).join('\n') paths.map(path => `import Gen.Params.${path.join('.')}`).join('\n')
@ -193,7 +221,6 @@ const msgVariant = (path: string[]) : string =>
const msg = (path: string[]) : string => const msg = (path: string[]) : string =>
`Pages.${path.join('.')}.Msg` `Pages.${path.join('.')}.Msg`
export const pagesInitBody = (paths: string[][]) : string => export const pagesInitBody = (paths: string[][]) : string =>
indent(caseExpression(paths, { indent(caseExpression(paths, {
variable: 'route', variable: 'route',
@ -241,3 +268,13 @@ const pageModelArguments = (path: string[], options : Options) : string =>
options.isStatic(path) options.isStatic(path)
? `params ()` ? `params ()`
: `params model` : `params model`
// Used in place of sophisticated AST parsing
const exposes = (keyword: string) => (elmSourceCode: string): boolean =>
new RegExp(`module\\s(\\S)+\\sexposing(\\s)+\\([^\\)]*${keyword}[^\\)]*\\)`, 'm').test(elmSourceCode)
export const exposesModel = exposes('Model')
export const exposesMsg = exposes('Msg')
export const isStaticPage = (sourceCode : string) : boolean =>
!exposesModel(sourceCode) || !exposesMsg(sourceCode)

View File

@ -143,4 +143,59 @@ type Route
] ]
`.trim()) `.trim())
}) })
test.each([
[ [ config.reserved.homepage ], `[]` ],
[ [ "AboutUs" ], `[ "about-us" ]` ],
[ [ "AboutUs", "Offices" ], `[ "about-us", "offices" ]` ],
[ [ "Posts" ], `[ "posts" ]` ],
[ [ "Posts", "Id_" ], `[ "posts", params.id ]` ],
[ [ "Users", "Name_", "Settings" ], `[ "users", params.name, "settings" ]` ],
[ [ "Users", "Name_", "Posts", "Id_" ], `[ "users", params.name, "posts", params.id ]` ],
])(".routeVariant(%p)", (input, output) => {
expect(Utils.routeToHrefSegments(input)).toBe(output)
})
}) })
describe.each([['Model'], ['Msg']])
('Utils.exposes%s', (name: string) => {
const fn = (Utils as any)[`exposes${name}`] as (val: string) => boolean
test('fails for exposing all', () =>
expect(fn(`module Layout exposing (..)`)).toBe(false)
)
test(`fails if missing keyword`, () => {
expect(fn(`module Layout exposing (OtherImport)`)).toBe(false)
expect(fn(`module Layout exposing
( OtherImport
)
`)).toBe(false)
})
test(`works with single-line exposing "${name}"`, () => {
expect(fn(`module Layout exposing (${name})`)).toBe(true)
expect(fn(`module Layout exposing (OtherImport, ${name})`)).toBe(true)
expect(fn(`module Layout exposing (${name}, OtherImport)`)).toBe(true)
})
test(`works with multi-line exposing "${name}"`, () => {
expect(fn(`
module Layout exposing
( ${name}
)
`)).toBe(true)
expect(fn(`
module Layout exposing
( OtherImport
, ${name}
)
`)).toBe(true)
expect(fn(`
module Layout exposing
( ${name}
, OtherImport
)
`)).toBe(true)
})
})