pathpida

repository·main·Indexed 19 days ago

https://github.com/aspida/pathpida

A TypeScript-friendly static path generator for Next.js that automatically generates type-safe URL helpers for application pages and static assets in the public folder. It supports both the App Router and Pages Router, providing type checking for dynamic segments, catch-all routes, and query parameters defined via Query or OptionalQuery types.

Tokens
3.5K
Snippets
13
Records
14
Agent score
68%

What's inside pathpida

  1. Define Query types for type safety

    main

    To enable type checking for query parameters in $url(), export a Query or OptionalQuery type from your Next.js page file.

    // pages/post/create.tsx
    export type Query = {
      userId: number;
      name?: string;
    };
    
    export default () => <div />;
  2. Install pathpida

    main

    Install pathpida and npm-run-all as development dependencies to enable type-safe path generation and script orchestration.

    $ npm install pathpida npm-run-all --save-dev
    # or
    $ yarn add pathpida npm-run-all --dev
  3. Generate static files path with --enableStatic

    main

    By default, pathpida only generates paths for pages. To include assets from your public/ directory (like .json or .png files), use the --enableStatic (or -s) flag. This generates a staticPath object in your $path.ts file.

    {
      "scripts": {
        "dev:path": "pathpida --enableStatic --watch",
        "build": "pathpida --enableStatic && next build"
      }
    }
    import { pagesPath, staticPath } from '../lib/$path';
    
    // Accessing a file: public/aa.json
    console.log(staticPath.aa_json); // '/aa.json'
    
    // Accessing a nested file: public/bb/cc.png
    console.log(staticPath.bb.cc_png); // '/bb/cc.png'
    
    export default () => {
      return <img src={staticPath.bb.cc_png} />;
    };
  4. Configure pathpida in package.json

    main

    To use pathpida effectively, add scripts to your package.json. It is recommended to run pathpida in watch mode during development and as part of your build process.

    {
      "scripts": {
        "dev": "run-p dev:*",
        "dev:next": "next dev",
        "dev:path": "pathpida --ignorePath .gitignore --watch",
        "build": "pathpida --ignorePath .gitignore && next build"
      }
    }
  5. Understand the generated URL helper signature

    main

    The generated helpers for App Router routes follow a specific pattern to ensure type safety for both the path and the query parameters.

    For a route with dynamic segments, the helper is generated as a function. The signature depends on whether a Query type was detected in the source file:

    1. With Query Type: If pathpida detects a query type in the page.tsx file, the helper accepts a url object containing both the query and an optional hash.
    2. Without Query Type: The helper accepts the dynamic segments directly as arguments and returns an object containing the pathname, query, hash, and path.

    Dynamic Segment Types:

    • [slug]: Generates a string | number type.
    • [...slug]: Generates a string[] type.
    • [[...slug]]: Generates an optional parameter.

    Returned Object Shape: Every helper returns an object with:

    • pathname: The static string representation of the route.
    • query: An object containing the parsed query parameters.
    • hash: The URL hash.
    • path: A template string (e.g., /${slug}) used for actual navigation.
    // Example of what the generated code looks like for a route like /blog/[slug]
    $blog_slug: (slug: string) => ({
      pathname: '/blog/[slug]' as const,
      query: { ... },
      hash: url.hash,
      path: `/blog/${slug}`
    });
    
    // Example for a route with a Query type
    $blog_slug: (url: { query: BlogQuery, hash?: string }) => ({
      pathname: '/blog/[slug]' as const,
      query: { ...url.query },
      hash: url.hash,
      path: `/blog/${slug}`
    });
  6. How pathpida parses Next.js Pages Router directories

    main

    pathpida uses the parsePagesDir function to scan a directory (typically a Next.js pages directory) and generate a type-safe object representing the available routes.

    Key Behaviors

    • File Filtering: It ignores files starting with _, .d.ts files, and the /api route. It also respects an ignorePath via an ignore utility.
    • Dynamic Routes:
      • Single dynamic segments like [id].tsx are converted into functions that accept a string | number slug.
      • Catch-all routes like [[...slug]].tsx are converted into functions that accept an optional string[] slug.
      • Nested catch-all routes like [...slug].tsx are converted into functions that accept a required string[] slug.
    • Index Files: Files named index.tsx (or other supported extensions) are treated as the base route for a directory.
    • Query Type Extraction: The parser attempts to extract query types from TypeScript files using parseQueryFromTS to provide type safety for the query object in the generated route methods.
    • Output: It returns an object containing an array of imports required for the generated file and the generated text (the route object string).
    // The core function used by the CLI/generator to process the pages directory
    const { imports, text } = parsePagesDir(
      inputDir,
      outputDir,
      ignorePath,
      ['tsx', 'ts', 'jsx', 'js']
    );
  7. How Next.js App Router paths are parsed and transformed

    main

    When using pathpida with the Next.js App Router, the tool scans your directory structure to generate type-safe URL helpers. It applies specific transformation rules to handle Next.js-specific routing features:

    • Route Groups: Directories wrapped in parentheses, e.g., (marketing), are ignored in the resulting URL path.
    • Parallel Routes: Directories starting with @, e.g., @modal, are ignored in the resulting URL path.
    • Dynamic Segments: Segments like [slug] are converted into function parameters in the generated code.
    • Catch-all Segments: Segments like [...slug] are converted into parameters that accept an array (e.g., string[]).
    • Optional Catch-all Segments: Segments like [[...slug]] are treated as optional parameters.

    The final url generated for a route is the combination of the directory structure after stripping route groups and parallel routes.

  8. Use generated pagesPath for type-safe navigation

    main

    After running pathpida, a $path.ts file is generated in lib/ or utils/. You can import pagesPath to generate URLs for your Next.js pages. This ensures that dynamic segments (like [pid] or [...slug]) and required query parameters are type-checked.

    import Link from 'next/link';
    import { pagesPath } from '../lib/$path';
    
    // Static path
    console.log(pagesPath.post.create.$url()); // { pathname: '/post/create' }
    
    // Dynamic segment [pid]
    console.log(pagesPath.post._pid(1).$url()); // { pathname: '/post/[pid]', query: { pid: 1 }}
    
    // Catch-all segment [...slug]
    console.log(pagesPath.post._slug(['a', 'b', 'c']).$url()); // { pathname: '/post//[...slug]', query: { slug: ['a', 'b', 'c'] }}
    
    // With query parameters
    console.log(pagesPath.post._pid(1).$url({ query: { limit: 10 }, hash: 'sample' })); 
    // { pathname: '/post/[pid]', query: { pid: 1, limit: 10 }, hash: 'sample' }
    
    export default () => {
      return (
        <>
          <Link href={pagesPath.post._slug(['a', 'b', 'c']).$url()} />
        </>
      );
    };
  9. Command Line Interface Options

    main

    The pathpida CLI provides several options for controlling how paths are generated and watched.

    --enableStatic | -s    Generate static files path in $path.ts.
    --ignorePath | -p     Specify the ignore pattern file path.
    --output | -o         Specify the output directory for $path.ts.
    --watch | -w           Enable watch mode. Regenerate $path.ts.
    --version | -v         Print pathpida version.
  10. Parse query types from TypeScript files with parseQueryFromTS

    main

    The parseQueryFromTS function is a utility used to identify and extract type definitions for queries (specifically Query or OptionalQuery interfaces/types) from a TypeScript file. It generates a unique, hashed import name to prevent naming collisions when these types are imported into generated static files.

    Behavior:

    1. It searches the provided file for an exported interface Query, interface OptionalQuery, type Query, or type OptionalQuery.
    2. If found, it calculates a relative importPath from the output directory to the source file.
    3. It generates a unique importName by combining the type name with a hash of the import path.
    4. It returns an object containing the importName and a formatted importString statement.

    Returns: An object containing:

    • importName: The aliased name (e.g., Query_abc123).
    • importString: A valid TypeScript import statement (e.g., import type { Query as Query_abc123 } from './path/to/file';).

    Returns undefined if no matching Query or OptionalQuery type is found in the file.

    // Example usage (conceptual):
    const result = parseQueryFromTS('./generated/output', './src/api/user.ts');
    
    if (result) {
      console.log(result.importName);   // e.g., "Query_hash"
      console.log(result.importString); // e.g., "import type { Query as Query_hash } from './user';"
    }
  11. The Config object schema

    main

    The Config type defines the internal configuration structure used by pathpida to resolve paths for input directories, output directories, and Next.js specific settings. This schema is used to determine where pages are located and where the generated static files should be placed.

    export type Config = {
      input: string | undefined;
      appDir: { input: string } | undefined;
      staticDir: string | undefined;
      output: string;
      ignorePath: string | undefined;
      basepath?: string | undefined;
      pageExtensions?: string[] | undefined;
    };
  12. Reference pathpida CLI options

    main

    The following flags are available when running the pathpida command via the CLI:

    | Flag | Alias | Type | Description |
    |------|-------|------|-------------|
    | `--version` | `-v` | string | Show the current version |
    | `--watch` | `-w` | string | Watch input directories for changes |
    | `--enableStatic` | `-s` | string | Enable static file generation |
    | `--output` | `-o` | string | Specify the output directory |
    | `--ignorePath` | `-p` | string | Specify a path to ignore |