sveltejs / kit

web development, streamlined
https://svelte.dev/docs/kit
MIT License
18.78k stars 1.96k forks source link

Programmatically get all routes #5793

Open Tommertom opened 2 years ago

Tommertom commented 2 years ago

Describe the problem

It is not a frustration, it is a feature request -

but in a project I would like to progammatically generate menu items using the routes available. This way, whenever I have new routes introduced, I immediately have them in the menu (with a bit of chances to show in a nice way).

Describe the proposed solution

I would like to see, as a suggestion that $app-navigation's api get extended with a routes object that has a list or tree structure the represents the routes as currently present within the app. Then I can use that object to generate my menu or whatever feature I want to build

Alternatives considered

Just create a json file or static js object in the project that has all the menu items.

Importance

nice to have

Additional Information

No response

Conduitry commented 2 years ago

Not exactly a duplicate, but it feels like this could be folded into #1142.

Tommertom commented 2 years ago

Seems indeed - except that I don't hope I need to parse xml programmatically then :)

benmccann commented 2 years ago

Can you use https://vitejs.dev/guide/features.html#glob-import ? That's how I've seen this done in the past

larrybotha commented 2 years ago

@Tommertom as a temporary solution, you can use the following as a basis for what you're trying:

src/routes/data/navigation.json.ts ```typescript import {Buffer} from 'buffer'; import {fileURLToPath} from 'url'; import {dirname} from 'path'; import rehypeParse from 'rehype-parse'; import {select} from 'unist-util-select'; import {unified} from 'unified'; import {visit} from 'unist-util-visit'; import dree from 'dree'; import type {Literal} from 'unist'; import type {Node} from 'unist-util-visit'; import type {ScanOptions, Dree} from 'dree'; import type {RequestHandler} from '@sveltejs/kit'; import type {Page} from '@sveltejs/kit'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const paths = _dirname.split('/'); const docsRootIndex = paths.indexOf('docs'); const docsPath = paths.slice(0, docsRootIndex + 1).join('/'); interface ElementNode extends Node { tagName: string; } interface Entry { name: string; title: string; order?: Page['stuff']['frontmatter']['navOrder']; } export interface NavTree { children?: NavTree[]; depth: number; name: string; order?: number; pathname: string; title: string; } /** * Stuff's interface, as referenced by Page['stuff'] interface Stuff { frontmatter: { //... other properties navOrder?: number; }; navigation: Locals.navTree; } */ interface RawTree extends Omit { type: dree.Type; children?: RawTree[]; order?: Page['stuff']['frontmatter']['navOrder']; } interface ViteModule { default: {render: () => {html: string}}; metadata?: Page['stuff']['frontmatter']; } const treeCache: Record = {}; function createNavTree(tree: Dree, depth = 0): RawTree { const {children, type, path} = tree; const name = path.replace(docsPath, '').replace('/src/routes', ''); const indexRegex = /index\..+$/; const pathname = type === dree.Type.DIRECTORY ? name : [name] .map((x) => { const delimiter = indexRegex.test(x) ? '/' : '.'; return x.split(delimiter).slice(0, -1).join(delimiter); }) .join(''); const newTree: RawTree = {name, type, pathname, depth, title: name}; if (children) { newTree.children = children.map((child) => createNavTree(child, depth + 1)); } return newTree; } function sortTree(tree: RawTree | NavTree) { const indexRegex = /index\..+$/; const {children} = tree; const safeChildren = children || []; const isDir = (tree as RawTree).type === dree.Type.DIRECTORY; const hasChildAsIndex = safeChildren.some((child) => indexRegex.test(child.name)); // if: // is a directory, and, // has a child as index // set the child's order on the tree [{useChildOrder: isDir && hasChildAsIndex, tree}] .filter(({useChildOrder}) => useChildOrder) .map(({tree}) => tree) .map((treeItem) => { (treeItem.children || []) .filter((child) => indexRegex.test(child.name)) .slice(0, 1) .map((child) => { treeItem.order = child.order; treeItem.title = child.title; }); return treeItem; }); // sort the children [tree] .filter((treeItem) => Array.isArray(treeItem.children)) .map((treeItem) => { treeItem.children = (tree.children || []) .sort((a, b) => { const [orderA, orderB] = [a.order, b.order].map((order) => { return typeof order === 'number' ? order : Number.MAX_VALUE; }); const sortByOrder = orderA - orderB; const sortByTitle = a.title > b.title ? 1 : -1; return sortByOrder || sortByTitle; }) .map((child) => sortTree(child)); return treeItem; }); return tree; } function addEntryToTree(tree: RawTree | NavTree, entry: Entry): NavTree { const isFile = (tree as RawTree).type === dree.Type.FILE; const hasSameFilename = tree.name === entry.name; const isMatch = [isFile, hasSameFilename].every(Boolean); const config = {isMatch, tree}; // if not a match, pass entry to children of current tree [config] .filter(({isMatch}) => !isMatch) .map(({tree}) => tree.children) .filter((children): children is [] => Array.isArray(children)) .flatMap((children) => children.map((child) => addEntryToTree(child, entry))); // if a match, set the tree's title and order using the entry [config] .filter(({isMatch}) => isMatch) .map(({tree}) => tree) .map((treeItem) => { treeItem.title = entry.title; treeItem.order = entry.order; }); return tree; } function createEntry({name, module}: {name: string; module: ViteModule}): Entry { const rendered = module.default.render(); const markupTree = unified().use(rehypeParse, {fragment: true}).parse(rendered.html); const frontmatter: Page['stuff']['frontmatter'] = module.metadata || {}; let elements: ElementNode[] = []; visit(markupTree, 'element', (node: ElementNode) => { elements = [...elements, node]; }); const headings = elements .filter((element) => ['h1', 'h2'].indexOf(element.tagName) > -1) .sort((a, b) => (a.tagName > b.tagName ? 1 : -1)); const heading = headings.find(Boolean); const headingText: Literal | null | undefined = [heading] .map((element) => select('text', element)) .find(Boolean) as Literal; const title = headingText ? (headingText.value as string) : name; return { name: name.replace('/src/routes', ''), order: frontmatter.navOrder, title, }; } const GET: RequestHandler = async function GET() { const options: ScanOptions = { excludeEmptyDirectories: true, extensions: ['md', 'svx'], followLinks: false, hash: false, showHidden: false, size: false, skipErrors: false, stat: false, }; const tree = dree.scan('./src/routes', options); const encodedTree = Buffer.from(JSON.stringify(tree), 'base64').toString(); let navTree = treeCache[encodedTree]; if (!navTree) { navTree = createNavTree(tree); treeCache[encodedTree] = navTree; } const modules = import.meta.glob('/src/routes/**/*.{md,svx}'); const files = await Promise.all( Object.entries(modules).map(async ([name, fn]) => { const module = await fn(); return {name, module}; }), ); const entries: Entry[] = files.map(createEntry); const navigation = entries.reduce((acc, entry) => { return addEntryToTree(acc, entry); }, navTree); const sortedNavigation = sortTree(navigation); return {body: JSON.stringify(sortedNavigation)}; }; export {GET}; ```

I've recently implemented this for a SvelteKit site using MDSvex to allow for writing in markdown.

What it does is:

You can then get the tree via the load function in your layout to build it out.

There's additional stuff like using frontmatter to allow for ordering of pages, and using unifiedjs to traverse the markdown / HTML to pull out headings etc. - useful for creating menu titles etc. in the navigation.

It's quite a chunk of code, and my use-case with markdown is likely not going to match yours, but ye... it was non-trivial to implement, so thought I'd give a heads up on what could be a good starting point for others until something official is released.

Tommertom commented 2 years ago

Hi @larrybotha and @benmccann

Thanks for both pointers.

I will use this which is sufficint for my use case -giving an array of menu items for the app

const modules = import.meta.glob('./components/*.*');
const menuItems = Object.keys(modules).map((item) =>
        item.replace('./components/', '').replace('.svelte', '')
    );
console.log('MODULES & Menu', modules, menuItems);
cdcarson commented 9 months ago

It'd be super nice to get all route details from, say, import { routes } from '$app/paths'.

Example of data I'd find useful ```js [ { id: '/', pattern: /^\/$/, params: [] }, { id: '/about', pattern: /^\/about\/?$/, params: [] }, { id: '/dashboard', pattern: /^\/dashboard\/?$/, params: [] }, { id: '/dashboard/widgets', pattern: /^\/dashboard\/widgets\/?$/, params: [] }, { id: '/dashboard/widgets/[widgetId]', pattern: /^\/dashboard\/widgets\/([^/]+?)\/?$/, params: [ { name: 'widgetId', matcher: undefined, optional: false, rest: false, chained: false } ] }, { id: '/dashboard/widgets/[widgetId]/edit/[[what]]', pattern: /^\/dashboard\/widgets\/([^/]+?)\/edit(?:\/([^/]+))?\/?$/, params: [ { name: 'widgetId', matcher: undefined, optional: false, rest: false, chained: false }, { name: 'what', matcher: undefined, optional: true, rest: false, chained: true } ] }, { id: '/help', pattern: /^\/help\/?$/, params: [] }, { id: '/help/[...slug]', pattern: /^\/help(?:\/(.*))?\/?$/, params: [ { name: 'slug', matcher: undefined, optional: false, rest: true, chained: true } ] } ] ```

Some people are already doing this, either with Vite plugins or with import.meta.glob. Examples:

It's seems better/safer for SvelteKit to provide it, given that it is in charge of the logic (what's considered a "route", optionality and spreadability of params, etc.)

jasongitmail commented 9 months ago

It's seems better/safer for SvelteKit to provide it, given that it is in charge of the logic (what's considered a "route"

+100 especially with the addition of reroute, import.meta.glob can no longer be depended upon.

This is something super sitemap will have to deal with eventually, to move away from import.meta.glob to something else, due to i18n packages like Paraglide https://github.com/jasongitmail/super-sitemap/issues/24

ananduremanan commented 6 months ago

Any Update on this? or where could I find the updates about feature request?

ISOR3X commented 4 months ago

I also have a need for this to make something of a route tree for all routes in my project. Additionally a way to access the page data (each page for me has a related icon in the route tree) would be super useful as well. Until then, if anyone has a, hopefully temporary, solution for these it would be much appreciated.

Pierstoval commented 2 months ago

Apparently, there's a small piece of code that returns an array of RouteData objects, and it seems it could be used somehow as a route list:

(click to expand) Script code: ```javascript import { load_config } from './node_modules/@sveltejs/kit/src/core/config/index.js'; import { all } from './node_modules/@sveltejs/kit/src/core/sync/sync.js'; (async () => { const conf = await load_config(); const manifest_data = all(conf, 'development'); const routes = manifest_data.manifest_data.routes; console.info(routes); })(); ```
(click to expand) Sample output from customized `playground/basic/` directory: ``` [ { id: '/', segment: '', pattern: /^\/$/, params: [], layout: { depth: 0, child_pages: [circular array], component: '../../packages/kit/src/runtime/components/svelte-4/layout.svelte' }, error: { depth: 0, component: 'src/routes/+error.svelte' }, leaf: { depth: 0, component: 'src/routes/+page.svelte', universal: 'src/routes/+page.ts', parent: { depth: 0, child_pages: [circular array], component: '../../packages/kit/src/runtime/components/svelte-4/layout.svelte' } }, page: { layouts: [ 0 ], errors: [ 1 ], leaf: 3 }, endpoint: { file: 'src/routes/+server.ts' } }, { id: '/[slug]', segment: '[slug]', pattern: /^\/([^/]+?)\/?$/, params: [ { name: 'slug', matcher: undefined, optional: false, rest: false, chained: false } ], layout: null, error: { depth: 1, component: 'src/routes/[slug]/+error.svelte' }, leaf: { depth: 1, component: 'src/routes/[slug]/+page.svelte', universal: 'src/routes/[slug]/+page.ts', parent: { depth: 0, child_pages: [circular array], component: '../../packages/kit/src/runtime/components/svelte-4/layout.svelte' } }, page: { layouts: [ 0, undefined ], errors: [ 1, 2 ], leaf: 4 }, endpoint: { file: 'src/routes/[slug]/+server.ts' } } ] ```

Theoretically, if we could hook into SvelteKit's routing system, it could be interesting to allow people to create a routes.js file exporting a RouteData[] iterable/array that would be imported by Svelte, so that devs could have total flexibility in both project structure and routes definitions.

One would be able to define their own +page.svelte file, and even name it differently, or use a 3rd-party file for that particular route, could it be a Svelte file or a JS/TS file for server handling.