├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── commitlint.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .huskyrc ├── .lintstagedrc ├── .nvmrc ├── .nycrc.json ├── .prettierrc ├── README.md ├── adonis-typings ├── index.ts └── stardust-middleware.ts ├── buildClient.js ├── commitlint.config.js ├── japaFile.js ├── middleware └── Stardust.ts ├── package-lock.json ├── package.json ├── providers └── StardustProvider.ts ├── release.config.js ├── src ├── .eslintrc.json ├── client │ ├── Stardust.ts │ ├── UrlBuilder.ts │ └── index.ts └── tsconfig.json ├── test ├── Stardust.spec.ts ├── StardustProvider.spec.ts └── utils.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*] 3 | indent_style = space 4 | indent_size = 2 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.json] 11 | insert_final_newline = ignore 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | test 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:adonis/typescriptApp", "prettier"], 3 | "ignorePatterns": ["src/*", "client/*"], 4 | "rules": { 5 | "no-console": "error" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Commit Messages 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - uses: wagoid/commitlint-github-action@v2 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | lint-and-test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | - run: npm ci 15 | - run: npm run lint 16 | - run: npm test 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | - run: npm ci 14 | - run: npm run build 15 | - name: Semantic Release 16 | uses: cycjimmy/semantic-release-action@v2 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | client 4 | !src/client 5 | coverage 6 | .vscode 7 | .DS_STORE 8 | .env 9 | tmp 10 | .nyc_output 11 | yarn-error.log 12 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": ["lint-staged"] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,d.ts}": ["eslint --fix"], 3 | "*.{json,md}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "reporter": ["text", "lcov"], 4 | "exclude": ["build/**", "japaFile.js", "test/", "providers/InertiaProvider/index.ts"], 5 | "branches": 50, 6 | "lines": 70, 7 | "functions": 70, 8 | "statements": 70 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | endOfLine: lf 2 | printWidth: 120 3 | singleQuote: true 4 | trailingComma: all 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adonis Stardust 2 | 3 | ![](https://img.shields.io/npm/types/typescript?style=for-the-badge) 4 | 5 | 6 | 7 | 8 | code style: prettier 9 | 10 | 11 | 12 | 13 | 14 | 15 | # ⭐ Adonis Stardust ⭐ 16 | 17 | Use your adonis named stardust in the client. 18 | 19 | ## Installation 20 | 21 | ```shell 22 | npm i @eidellev/adonis-stardust 23 | 24 | node ace configure @eidellev/adonis-stardust 25 | ``` 26 | 27 | ## Setup 28 | 29 | ### Register Middleware 30 | 31 | Add the Stardust middleware to `start/kernel.ts`: 32 | 33 | ```typescript 34 | Server.middleware.register([ 35 | () => import('@ioc:Adonis/Core/BodyParser'), 36 | () => import('@ioc:EidelLev/Stardust/Middleware'), 37 | ]); 38 | ``` 39 | 40 | ### Register a Named Route 41 | 42 | Create a named route in your stardust file: 43 | 44 | ```typescript 45 | Route.get('users/:id', () => { 46 | ... 47 | }).as('users.show'); 48 | ``` 49 | 50 | ### In Your View 51 | 52 | Add the `@routes` Edge tag to your main layout (before your application's JavaScript). 53 | 54 | ```blade 55 | @routes 56 | @entryPointStyles('app') 57 | @entryPolintScripts('app') 58 | ``` 59 | 60 | ## Client-Side Usage 61 | 62 | ### Client Setup 63 | 64 | Stardust should be initialized as early as possible, e.g. in your application's entrypoint 65 | 66 | ```typescript 67 | import { initRoutes } from '@eidellev/adonis-stardust/client'; 68 | 69 | initRoutes(); 70 | ``` 71 | 72 | Now you can use the `stardust` helper to access your adonis routes: 73 | 74 | ```typescript 75 | import { stardust } from '@eidellev/adonis-stardust/client'; 76 | 77 | stardust.route('users.show', { id: 1 }); // => `/users/1` 78 | 79 | /** 80 | * You can also pass path params as an array and they will populated 81 | * according to their order: 82 | */ 83 | stardust.route('users.show', [1]); // => `/users/1` 84 | ``` 85 | 86 | You can also pass query parameters like so: 87 | 88 | ```typescript 89 | stardust.route('tasks.index', undefined, { qs: { tags: ['work', 'personal'] } }); 90 | // `/tasks?tags=work,personal 91 | ``` 92 | 93 | ### Checking the Current Route 94 | 95 | ```typescript 96 | stardust.current; // => 'tasks.index' 97 | stardust.isCurrent('tasks.index'); // => true 98 | ``` 99 | -------------------------------------------------------------------------------- /adonis-typings/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /adonis-typings/stardust-middleware.ts: -------------------------------------------------------------------------------- 1 | declare module '@ioc:EidelLev/Stardust/Middleware' { 2 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 3 | 4 | export default class StardustMiddleware { 5 | public handle(ctx: HttpContextContract, next: () => Promise); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /buildClient.js: -------------------------------------------------------------------------------- 1 | require('esbuild').buildSync({ 2 | entryPoints: ['src/client/index.ts'], 3 | outfile: 'client/index.js', 4 | allowOverwrite: true, 5 | bundle: true, 6 | platform: 'node', 7 | target: ['chrome91', 'edge90', 'firefox90', 'safari13'], 8 | }); 9 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /japaFile.js: -------------------------------------------------------------------------------- 1 | require('@adonisjs/require-ts/build/register'); 2 | 3 | const { configure } = require('japa'); 4 | 5 | configure({ 6 | files: ['test/**/*.spec.ts'], 7 | }); 8 | -------------------------------------------------------------------------------- /middleware/Stardust.ts: -------------------------------------------------------------------------------- 1 | import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; 2 | 3 | export default class StardustMiddleware { 4 | public async handle({ request }: HttpContextContract, next: () => Promise) { 5 | const { pathname } = new URL(request.completeUrl()); 6 | 7 | globalThis.stardust = { 8 | ...globalThis.stardust, 9 | pathname, 10 | }; 11 | 12 | await next(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eidellev/adonis-stardust", 3 | "version": "1.1.0", 4 | "private": false, 5 | "description": "Use adonis routes in the client", 6 | "repository": "https://github.com/eidellev/adonis-stardust", 7 | "bugs": "https://github.com/eidellev/adonis-stardust/issues", 8 | "main": "build/providers/StardustProvider.js", 9 | "types": "build/client/index.d.ts", 10 | "typings": "./build/adonis-typings/index.d.ts", 11 | "files": [ 12 | "build/providers", 13 | "build/adonis-typings", 14 | "build/middleware", 15 | "client" 16 | ], 17 | "adonisjs": { 18 | "providers": [ 19 | "@eidellev/adonis-stardust" 20 | ] 21 | }, 22 | "license": "MIT", 23 | "scripts": { 24 | "lint": "tsc --noEmit && eslint . --ext=ts", 25 | "lint:fix": "eslint . --ext=ts --fix", 26 | "clean": "rimraf build", 27 | "build": "cross-env npm run clean && npm run build:node && npm run build:client", 28 | "build:node": "tsc", 29 | "build:client": "node buildClient && tsc -p ./src/tsconfig.json", 30 | "watch": "cross-env npm run clean && tsc -w", 31 | "test": "nyc node japaFile.js" 32 | }, 33 | "peerDependencies": { 34 | "@adonisjs/core": ">=5" 35 | }, 36 | "devDependencies": { 37 | "@adonisjs/core": "^5.3.4", 38 | "@adonisjs/mrm-preset": "^4.1.2", 39 | "@adonisjs/require-ts": "^2.0.8", 40 | "@adonisjs/view": "^6.1.1", 41 | "@commitlint/cli": "^13.1.0", 42 | "@commitlint/config-conventional": "^13.1.0", 43 | "@commitlint/prompt-cli": "^13.1.0", 44 | "@poppinss/dev-utils": "^1.1.5", 45 | "@typescript-eslint/parser": "4.33.0", 46 | "adonis-preset-ts": "^2.1.0", 47 | "copyfiles": "^2.4.1", 48 | "cross-env": "^7.0.3", 49 | "esbuild": "^0.12.28", 50 | "eslint": "^7.32.0", 51 | "eslint-config-prettier": "^8.3.0", 52 | "eslint-plugin-adonis": "^1.3.3", 53 | "eslint-plugin-prettier": "^4.0.0", 54 | "husky": "^7.0.2", 55 | "japa": "^3.1.1", 56 | "lint-staged": "^11.1.2", 57 | "nyc": "^15.1.0", 58 | "prettier": "^2.4.0", 59 | "rimraf": "^3.0.2", 60 | "semantic-release": "^17.4.7", 61 | "supertest": "^6.1.6", 62 | "typescript": "4.8.2" 63 | }, 64 | "dependencies": { 65 | "@poppinss/matchit": "^3.1.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /providers/StardustProvider.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationContract } from '@ioc:Adonis/Core/Application'; 2 | import { RouterContract } from '@ioc:Adonis/Core/Route'; 3 | import { ViewContract } from '@ioc:Adonis/Core/View'; 4 | import StardustMiddleware from '../middleware/Stardust'; 5 | 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Stardust Provider 9 | |-------------------------------------------------------------------------- 10 | */ 11 | export default class StardustProvider { 12 | constructor(protected app: ApplicationContract) {} 13 | public static needsApplication = true; 14 | 15 | /** 16 | * Returns list of named routes 17 | */ 18 | private getNamedRoutes(Route: RouterContract) { 19 | /** 20 | * Only sharing the main domain routes. Subdomains are 21 | * ignored for now. Let's see if many people need it 22 | */ 23 | const mainDomainRoutes = Route.toJSON()?.['root'] ?? []; 24 | 25 | return mainDomainRoutes.reduce>((routes, route) => { 26 | if (route.name) { 27 | routes[route.name] = route.pattern; 28 | } else if (typeof route.handler === 'string') { 29 | routes[route.handler] = route.pattern; 30 | } 31 | 32 | return routes; 33 | }, {}); 34 | } 35 | 36 | /** 37 | * Register the `@routes()` tag 38 | */ 39 | private registerStardustTag(View: ViewContract) { 40 | View.registerTag({ 41 | block: false, 42 | tagName: 'routes', 43 | seekable: false, 44 | compile(_, buffer, token) { 45 | buffer.writeExpression( 46 | `\n 47 | out += template.sharedState.routes(template.sharedState.cspNonce) 48 | `, 49 | token.filename, 50 | token.loc.start.line, 51 | ); 52 | }, 53 | }); 54 | } 55 | 56 | private registerRoutesGlobal(View: ViewContract, namedRoutes: Record) { 57 | View.global('routes', (cspNonce?: string) => { 58 | return ` 59 | 60 | (globalThis || window).stardust = {namedRoutes: ${JSON.stringify(namedRoutes)}}; 61 | 62 | `; 63 | }); 64 | } 65 | 66 | /** 67 | * Registers named routes on the global scope in order to seamlessly support 68 | * stardust's functionality on the server 69 | * @param namedRoutes 70 | */ 71 | private registerSsrRoutes(namedRoutes: Record) { 72 | globalThis.stardust = { namedRoutes }; 73 | } 74 | 75 | public ready() { 76 | this.app.container.bind('EidelLev/Stardust/Middleware', () => StardustMiddleware); 77 | 78 | this.app.container.withBindings(['Adonis/Core/View', 'Adonis/Core/Route'], (View, Route) => { 79 | const namedRoutes = this.getNamedRoutes(Route); 80 | 81 | this.registerRoutesGlobal(View, namedRoutes); 82 | this.registerStardustTag(View); 83 | this.registerSsrRoutes(namedRoutes); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['main'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@typescript-eslint/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "rules": { 9 | "no-console": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/client/Stardust.ts: -------------------------------------------------------------------------------- 1 | import { match, parse } from '@poppinss/matchit'; 2 | import { UrlBuilder } from './UrlBuilder'; 3 | 4 | /** 5 | * Options accepted by the route method 6 | */ 7 | interface RouteOptions { 8 | qs?: Record; 9 | prefixUrl?: string; 10 | } 11 | 12 | export class Stardust { 13 | private routes: Record = {}; 14 | private reverseRoutes: Record = {}; 15 | private parsedRoutePatterns: any[]; 16 | 17 | constructor(namedRoutes: Record) { 18 | if (!namedRoutes) { 19 | console.error('Routes could not be found. Please make sure you use the `@routes()` tag in your view!'); 20 | return; 21 | } 22 | 23 | const parsedRoutePatterns = Object.entries(namedRoutes).map(([, pattern]) => parse(pattern)); 24 | 25 | this.routes = namedRoutes; 26 | this.reverseRoutes = Object.fromEntries(Object.entries(namedRoutes).map(([key, value]) => [value, key])); 27 | this.parsedRoutePatterns = parsedRoutePatterns; 28 | } 29 | 30 | /** 31 | * Returns all AdonisJS named routes 32 | * @example 33 | * ```typescript 34 | * import { stardust } from '@eidellev/adonis-stardust'; 35 | * ... 36 | * stardust.getRoutes(); 37 | * ``` 38 | */ 39 | public getRoutes() { 40 | return this.routes; 41 | } 42 | 43 | /** 44 | * Get URL builder instance to make the URL 45 | * @returns Instance of URL builder 46 | */ 47 | public builder() { 48 | return new UrlBuilder(this.routes); 49 | } 50 | 51 | /** 52 | * Resolve Adonis route 53 | * @param route Route name 54 | * @param params Route path params 55 | * @param options Make url options 56 | * @returns Full path with params 57 | */ 58 | public route(route: string, params?: any[] | Record, options?: RouteOptions): string { 59 | return new UrlBuilder(this.routes).params(params).qs(options?.qs).prefixUrl(options?.prefixUrl).make(route); 60 | } 61 | 62 | /** 63 | * Current route. 64 | * If the current route doesn't match any named routes, the returned value will be `null` 65 | * @example 66 | * ```typescript 67 | * import { stardust } from '@eidellev/adonis-stardust'; 68 | * ... 69 | * stardust.current; // => 'users.index' 70 | * ``` 71 | */ 72 | public get current(): string | null { 73 | const [matchedRoute] = match(this.pathname, this.parsedRoutePatterns); 74 | 75 | if (!matchedRoute) { 76 | return null; 77 | } 78 | 79 | const { old: pattern } = matchedRoute; 80 | return this.reverseRoutes[pattern]; 81 | } 82 | 83 | private get pathname() { 84 | /** 85 | * When rendering on the server 86 | */ 87 | if (globalThis.stardust.pathname) { 88 | return globalThis.stardust.pathname; 89 | } 90 | 91 | const { pathname } = new URL((window ?? globalThis).location.href); 92 | return pathname; 93 | } 94 | 95 | /** 96 | * Checks if a given route is the current route 97 | * @example 98 | * ```typescript 99 | * import { stardust } from '@eidellev/adonis-stardust'; 100 | * ... 101 | * stardust.isCurrent('users.index'); // => true/false 102 | * ``` 103 | */ 104 | public isCurrent(route: string): boolean { 105 | return route === this.current; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/client/UrlBuilder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy of https://github.com/adonisjs/http-server/blob/develop/src/Router/LookupStore.ts#L26 3 | * with a few modifications like remove the `makeSigned` method. 4 | */ 5 | export class UrlBuilder { 6 | /** 7 | * Params to be used for building the URL 8 | */ 9 | private routeParams: any[] | Record; 10 | 11 | /** 12 | * A custom query string to append to the URL 13 | */ 14 | private queryString: Record = {}; 15 | 16 | /** 17 | * BaseURL to prefix to the endpoint 18 | */ 19 | private baseUrl: string; 20 | 21 | constructor(private routes: Record) {} 22 | 23 | /** 24 | * Processes the pattern with the route params 25 | */ 26 | private processPattern(pattern: string): string { 27 | let url: string[] = []; 28 | const isParamsAnArray = Array.isArray(this.routeParams); 29 | 30 | /* 31 | * Split pattern when route has dynamic segments 32 | */ 33 | const tokens = pattern.split('/'); 34 | let paramsIndex = 0; 35 | 36 | for (const token of tokens) { 37 | /** 38 | * Expected wildcard param to be at the end always and hence 39 | * we must break out from the loop 40 | */ 41 | if (token === '*') { 42 | const wildcardParams = isParamsAnArray ? this.routeParams.slice(paramsIndex) : this.routeParams['*']; 43 | if (!Array.isArray(wildcardParams)) { 44 | throw new Error('Wildcard param must pass an array of values'); 45 | } 46 | 47 | if (!wildcardParams.length) { 48 | throw new Error(`Wildcard param is required to make URL for "${pattern}" route`); 49 | } 50 | 51 | url = url.concat(wildcardParams); 52 | break; 53 | } 54 | 55 | /** 56 | * Token is a static value 57 | */ 58 | if (!token.startsWith(':')) { 59 | url.push(token); 60 | } else { 61 | const isOptional = token.endsWith('?'); 62 | const paramName = token.replace(/^:/, '').replace(/\?$/, ''); 63 | const param = isParamsAnArray ? this.routeParams[paramsIndex] : this.routeParams[paramName]; 64 | 65 | paramsIndex++; 66 | 67 | /* 68 | * A required param is always required to make the complete URL 69 | */ 70 | if (!param && !isOptional) { 71 | throw new Error(`"${param}" param is required to make URL for "${pattern}" route`); 72 | } 73 | 74 | url.push(param); 75 | } 76 | } 77 | 78 | return url.join('/'); 79 | } 80 | 81 | /** 82 | * Finds the route inside the list of registered routes and 83 | * raises exception when unable to 84 | */ 85 | private findRouteOrFail(identifier: string) { 86 | const route = this.routes[identifier]; 87 | if (!route) { 88 | throw new Error(`Cannot find route for "${identifier}"`); 89 | } 90 | 91 | return route; 92 | } 93 | 94 | /** 95 | * Suffix the query string to the URL 96 | */ 97 | private suffixQueryString(url: string): string { 98 | if (this.queryString) { 99 | const params = new URLSearchParams(); 100 | 101 | for (const [key, value] of Object.entries(this.queryString)) { 102 | if (Array.isArray(value)) { 103 | value.forEach((item) => params.append(key, item)); 104 | } else { 105 | params.set(key, value); 106 | } 107 | } 108 | 109 | const encoded = params.toString(); 110 | url = encoded ? `${url}?${encoded}` : url; 111 | } 112 | 113 | return url; 114 | } 115 | 116 | /** 117 | * Prefix a custom url to the final URI 118 | */ 119 | public prefixUrl(url?: string): this { 120 | if (url) { 121 | this.baseUrl = url; 122 | } 123 | return this; 124 | } 125 | 126 | /** 127 | * Append query string to the final URI 128 | */ 129 | public qs(queryString?: Record): this { 130 | if (queryString) { 131 | this.queryString = queryString; 132 | } 133 | return this; 134 | } 135 | 136 | /** 137 | * Define required params to resolve the route 138 | */ 139 | public params(params?: any[] | Record): this { 140 | if (params) { 141 | this.routeParams = params; 142 | } 143 | return this; 144 | } 145 | 146 | /** 147 | * Generate url for the given route identifier 148 | */ 149 | public make(identifier: string) { 150 | const route = this.findRouteOrFail(identifier); 151 | const url = this.processPattern(route); 152 | return this.suffixQueryString(this.baseUrl ? `${this.baseUrl}${url}` : url); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import { Stardust } from './Stardust'; 2 | 3 | declare global { 4 | interface Window { 5 | stardust: { namedRoutes: Record }; 6 | } 7 | } 8 | 9 | export let stardust: Stardust; 10 | 11 | /** 12 | * Initialize stardust 13 | */ 14 | export function initRoutes() { 15 | const { namedRoutes } = (globalThis ?? window).stardust; 16 | stardust = new Stardust(namedRoutes); 17 | } 18 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "skipLibCheck": true, 10 | "emitDeclarationOnly": true, 11 | "declaration": true, 12 | "outDir": "../client" 13 | }, 14 | "include": ["./client"] 15 | } 16 | -------------------------------------------------------------------------------- /test/Stardust.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import { Stardust } from '../src/client/Stardust'; 3 | 4 | test.group('Client', () => { 5 | test('should return all routes', (assert) => { 6 | const namedRoutes = { 'tasks.show': '/tasks/:id' }; 7 | const stardust = new Stardust(namedRoutes); 8 | 9 | assert.equal(stardust.getRoutes(), namedRoutes); 10 | }); 11 | 12 | test('should resolve route with object params', (assert) => { 13 | const stardust = new Stardust({ 'tasks.show': '/tasks/:id' }); 14 | 15 | assert.equal(stardust.route('tasks.show', { id: 1 }), '/tasks/1'); 16 | }); 17 | 18 | test('should resolve route with array of params', (assert) => { 19 | const stardust = new Stardust({ 'tasks.show': '/tasks/:id' }); 20 | 21 | assert.equal(stardust.route('tasks.show', [1]), '/tasks/1'); 22 | }); 23 | 24 | test('should resolve route with query string', (assert) => { 25 | const stardust = new Stardust({ 'tasks.show': '/tasks' }); 26 | 27 | assert.equal(stardust.route('tasks.show', undefined, { qs: { status: 'done' } }), '/tasks?status=done'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/StardustProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'japa'; 2 | import { setup, teardown } from './utils'; 3 | 4 | test.group('Server', (group) => { 5 | group.afterEach(async () => { 6 | await teardown(); 7 | }); 8 | 9 | test('Should handle empty router gracefully', async (assert) => { 10 | const app = await setup(); 11 | await app.start(); 12 | 13 | const view = app.container.use('Adonis/Core/View'); 14 | 15 | view.registerTemplate('dummy', { template: '@routes()' }); 16 | 17 | assert.equal( 18 | (await view.render('dummy')).trim(), 19 | ``, 22 | ); 23 | }); 24 | 25 | test('Should render nonce', async (assert) => { 26 | const app = await setup(); 27 | await app.start(); 28 | 29 | const view = app.container.use('Adonis/Core/View'); 30 | const renderer = view.share({ cspNonce: 'test' }); 31 | 32 | assert.equal( 33 | (await renderer.renderRaw('@routes')).trim(), 34 | ``, 37 | ); 38 | }); 39 | 40 | test('Should render named routes', async (assert) => { 41 | const app = await setup(); 42 | const router = app.container.use('Adonis/Core/Route'); 43 | const view = app.container.use('Adonis/Core/View'); 44 | 45 | router.get('/', async () => {}).as('index'); 46 | router.post('/users', async () => {}).as('users.store'); 47 | router.commit(); 48 | 49 | await app.start(); 50 | 51 | view.registerTemplate('dummy', { template: '@routes()' }); 52 | const dummy = await view.render('dummy'); 53 | 54 | assert.equal( 55 | dummy.trim(), 56 | ``, 59 | ); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { Filesystem } from '@poppinss/dev-utils'; 2 | import { join } from 'path'; 3 | import { Application } from '@adonisjs/core/build/standalone'; 4 | 5 | export const fs = new Filesystem(join(__dirname, 'app')); 6 | 7 | export async function setup() { 8 | await fs.add( 9 | 'config/app.ts', 10 | `export const appKey = '${Math.random().toFixed(36).substring(2, 38)}', 11 | export const http = { 12 | cookie: {}, 13 | trustProxy: () => true, 14 | }`, 15 | ); 16 | 17 | const app = new Application(fs.basePath, 'web', { 18 | providers: ['@adonisjs/core', '@adonisjs/view', '../../providers/StardustProvider'], 19 | }); 20 | 21 | await app.setup(); 22 | await app.registerProviders(); 23 | await app.bootProviders(); 24 | 25 | return app; 26 | } 27 | 28 | export async function teardown() { 29 | await fs.cleanup(); 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@adonisjs/mrm-preset/_tsconfig", 3 | "compilerOptions": { 4 | "lib": ["dom", "es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"], 5 | "skipLibCheck": true 6 | }, 7 | "files": [ 8 | "./node_modules/@adonisjs/core/build/adonis-typings/index.d.ts", 9 | "./node_modules/@adonisjs/view/build/adonis-typings/index.d.ts" 10 | ], 11 | "exclude": ["build/*", "node_modules/*", "src/*", "test/*"] 12 | } 13 | --------------------------------------------------------------------------------