{"id":25382586,"url":"https://github.com/midzdotdev/path-master","last_synced_at":"2025-10-03T18:36:46.940Z","repository":{"id":260979101,"uuid":"868528977","full_name":"midzdotdev/path-master","owner":"midzdotdev","description":"🧭 File structures made easy","archived":false,"fork":false,"pushed_at":"2024-11-08T16:02:53.000Z","size":115,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T13:43:57.165Z","etag":null,"topics":["storage","typescript","utility"],"latest_commit_sha":null,"homepage":"https://jsr.io/@midzdotdev/path-master","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/midzdotdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-10-06T16:05:35.000Z","updated_at":"2024-11-08T16:02:56.000Z","dependencies_parsed_at":"2025-04-09T13:39:24.262Z","dependency_job_id":"16ba5fcf-fbc7-4624-bd5f-8063a239a6b4","html_url":"https://github.com/midzdotdev/path-master","commit_stats":null,"previous_names":["midzdotdev/path-master"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/midzdotdev/path-master","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/midzdotdev%2Fpath-master","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/midzdotdev%2Fpath-master/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/midzdotdev%2Fpath-master/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/midzdotdev%2Fpath-master/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/midzdotdev","download_url":"https://codeload.github.com/midzdotdev/path-master/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/midzdotdev%2Fpath-master/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266521704,"owners_count":23942492,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["storage","typescript","utility"],"created_at":"2025-02-15T07:37:30.577Z","updated_at":"2025-10-03T18:36:41.901Z","avatar_url":"https://github.com/midzdotdev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![JSR](https://jsr.io/badges/@midzdotdev/path-master)](https://jsr.io/@midzdotdev/path-master)\n[![Release Workflow](https://github.com/midzdotdev/path-master/actions/workflows/release.yml/badge.svg)](https://github.com/midzdotdev/path-master/actions/workflows/release.yml)\n\n`path-master` is a TypeScript library that simplifies working with dynamic file structures.\n\nYou can think of `path-master` as providing structure to files in the same way that JSON provides structure to data.\n\nPath Master provides a whole host of benefits:\n\n- 🏡 **Centralised:** a single source of truth for defining paths\n- 📊 **Parameterised:** paths are as dynamic as you need\n- ✨ **Type-safe:** parameters are typed, and path types are inferred\n- 📈 **Incremental:** model a complete file structure or just parts of it\n- 🧬 **Consistent:** standardises your path generation\n\n\u003e This library does not modify any filesystem directly,\n\u003e it's solely for modelling and getting paths.\n\nIt can be used anywhere paths are used, such as:\n\n- ☁️ **S3 Buckets:** for remote storage in the cloud\n- 💾 **Local Filesystem:** for local storage (e.g. via Node.js `fs` module)\n- 🧭 **Origin Private File System:** for local storage in web apps\n- 🌎 **Relative URLs:** between resources on the web\n\n## Quickstart Guide\n\n1. **Model** your file structure with `dir` and `file`\n2. **Get paths**, either:\n   - **to a file/dir** with `getPath`\n   - **between files/dirs** with `getRelativeFsPath` or `getRelativeUrlPath`\n\nHere is a model to represent a HLS video package file structure and how to get paths with `getPath` and `getRelativeUrlPath`.\n\n```ts\nimport { dir, file, getPath, getRelativeUrlPath } from '@midzdotdev/path-master'\n\n/* This is the file structure we're modelling:\n\n    .\n    └── videos\n        └── [videoId]\n            ├── master.m3u8\n            └── stream_[quality]\n                ├── playlist.m3u8\n                └── segment_[segmentId].ts\n*/\n\nconst hlsPackageModel = dir(\n  ({ videoId }: { videoId: number }) =\u003e `videos/${videoId}`,\n  {\n    manifest: file(`master.m3u8`),\n    variantStream: dir(\n      ({ quality }: { quality: 720 | 1080 }) =\u003e `stream_${quality}`,\n      {\n        playlist: file(`playlist.m3u8`),\n        segment: file(\n          ({ segmentId }: { segmentId: number }) =\u003e `segment_${segmentId}.ts`\n        ),\n      }\n    ),\n  }\n)\n\nconst hlsPackagePath = getPath(hlsPackageModel, '', { videoId: 42 })\n// result: \"videos/42/\"\n\nconst streamPlaylist = getPath(hlsPackageModel, 'variantStream.playlist', {\n  videoId: 42,\n  quality: 720,\n})\n// result: \"videos/42/stream_720/playlist.m3u8\"\n\nconst masterPlaylistToVariantPlaylist = getRelativeUrlPath(\n  hlsPackageModel,\n  ['manifest', { videoId: 42 }],\n  ['variantStream.playlist', { videoId: 42, quality: 720 }]\n)\n// result: \"stream_720/playlist.m3u8\"\n```\n\n\u003e As you can see in above (`videos/${videoId}`), the path of a node in the model can span across multiple directories.\n\u003e This can help to produce a more concise model when dealing with deeply nested structures.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExeTJxYmk3YnZyOXc1c2U5Y2cxdjdjbGJlanpmbGx1M3l5cGc0YWtraCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/a0h7sAqON67nO/giphy.gif\" style=\"max-width: 300px\"/\u003e\n\u003c/p\u003e\n\n\u003e If the Quickstart Guide doesn't make sense, don't worry! The rest of this README will cover everything you need to know.\n\n# Table of Contents\n\n- [Introduction](#introduction)\n- [Installation](#installation)\n- [Conceptualising Models](#conceptualising-models)\n- [Create a Model](#create-a-model)\n  - [Abstracting large models](#abstracting-large-models)\n- [Get Paths](#get-paths)\n- [Get Relative Paths](#get-relative-paths)\n- [Contributing](#contributing)\n\n## Introduction\n\nMaking storage paths in any language is a painful process.\n\nAny application that persists data requires specifying paths for how it should be structured. Previously this involved working with lots of template strings, one for each path. These would probably be copied around your codebase to be used multiple times.\n\nIf you're smart, then you'd make a helper function for each path, typing the function parameters with the parameterised parts of the path. Each of these functions has to assume a root for the path if it's relative, or might give absolute paths.\n\nIf one of these helper functions gives relative paths, you've got to join the result by prepending a parent path before you can use it.\n\nIt's a lot to think about, and if you've ever dealt with complex file structures, you'll know it can get very messy very quickly.\n\n`path-master` aims to solve all these problems. It provides you with the ability to model a file structure as a tree of directories and files, so you can easily and safely get the paths you need.\n\n## Installation\n\nMake sure to install the `@midzdotdev/path-master` package from [JSR](https://jsr.io/@midzdotdev/path-master).\n\n## Conceptualising Models\n\nIn order to get paths, we need to model the shape of our file structure. Before diving straight into the code, let's make sure we understand this kind of modelling as a concept.\n\nAs an example, we're going to be modelling a collection of HTTP Live Streaming (HLS) video packages.\n\nIn our HLS package, there are playlists (`.m3u8`) and video segments (`.ts`). We have a master playlist defining the available variants, and each of those variants has a number of video segments and a playlist to index them.\n\n\u003cp id=\"psuedo-model\"\u003eThe following is our psuedo-model to understand what we're aiming for.\u003c/p\u003e\n\n```\n.\n└── videos\n    └── [videoId]\n        ├── master.m3u8\n        └── stream_[quality]\n            ├── playlist.m3u8\n            └── segment_[segmentId].ts\n```\n\n\u003e As you're probably already aware, a file structure is a tree of file and directory nodes. Later on you'll notice that our model definition in code follows the exact same shape.\n\nNotice the square brackets above (`[]`), where we've parameterised the parts of the paths that are variable.\n\nThe name of the `videos` directory is static so there will only be one, but there could be any number of directories within `videos` because of the parameterised `[videoId]` part. The same idea applies to the `master.m3u8` and `segment_[segmentId].ts` files.\n\nIn `path-master`, these parameters are referred to as _dependencies_ and are always named so we know which values to use when forming a path.\n\nThe following table gives us some examples of concrete paths for different nodes in the tree, each with the required dependencies.\n\n| Target Node              | Dependencies                                   | Path                                 |\n| ------------------------ | ---------------------------------------------- | ------------------------------------ |\n| `[videoId]`              | `{ videoId: 42 }`                              | `videos/42/`                         |\n| `master.m3u8`            | `{ videoId: 42 }`                              | `videos/42/master.m3u8`              |\n| `stream_[quality]`       | `{ videoId: 42, quality: 720 }`                | `videos/42/stream_720/`              |\n| `playlist.m3u8`          | `{ videoId: 42, quality: 720 }`                | `videos/42/stream_720/playlist.m3u8` |\n| `segment_[segmentId].ts` | `{ videoId: 42, quality: 720, segmentId: 11 }` | `videos/42/stream_720/segment_11.ts` |\n\n\u003e Note that the path is relative to the root of the model (represented by `.` [in the psuedo-model](#psuedo-model)).\n\n## Create a Model\n\nThe library exposes two helper functions `file` and `dir` for us to build our model.\n\nBoth of these start with a _path_ parameter, which behaves identically between the two.\n\nThe path can be either:\n\n- **static** as a plain string, or\n- **dynamic** as a callback with a _dependencies_ param\n\n```ts\nconst staticFile = file('foo.ext')\n\nconst dynamicFile = file(\n  ({ myParam }: { myParam: string }) =\u003e `foo_${myParam}.ext`\n)\n```\n\n\u003e You have to type dependencies manually and they must be assignable to `Record\u003cstring, any\u003e`.\n\u003e\n\u003e Keep in mind that values interpolated in a template string must be serialisable, and non-primitives can serialise in strange ways. For example, interpolating an object like `` `${someObject}` `` gives `'[object Object]'` as the resultant string.\n\nThe `dir` modelling function has an additional parameter `children` so you can recursively nest directories and files inside it.\n\n```ts\nconst myDir = dir('my_dir', {\n  staticFile,\n})\n\nconst deepDir = dir('deep_dir', {\n  surfaceFile: file('foo.ext'),\n  level1: dir('level_1', {\n    level2: dir('level_2', {\n      deepFile: file('bar.ext'),\n    }),\n  }),\n})\n```\n\n\u003e Ideally a model should fully describe the structure of a storage destination. This way your model's root aligns with the storage's root and the paths given by `path-master` can be used directly as absolute paths.\n\n## Abstracting large models\n\nIf you're working with a very large model, it might be clearer to define parts of a model separately, then join them together into one main model.\n\nIf all the parts are together in a single file, then you can `export` the main model to remove any ambiguity about which model should be used in other parts of your application.\n\n```ts\nimport { dir, file } from '@midzdotdev/path-master'\n\nconst variantStream = dir(\n  ({ quality }: { quality: 720 | 1080 }) =\u003e `stream_${quality}`,\n  {\n    playlist: file(`playlist.m3u8`),\n    segment: file(\n      ({ segmentId }: { segmentId: number }) =\u003e `segment_${segmentId}.ts`\n    ),\n  }\n)\n\nexport const hlsPackageModel = dir(\n  ({ videoId }: { videoId: number }) =\u003e `videos/${videoId}`,\n  {\n    manifest: file(`master.m3u8`),\n    variantStream,\n  }\n)\n```\n\n## Get Paths\n\nNow that we have a model, let's address the reason that we're here in the first place! Let's get some paths.\n\nWe can get the path to a node by calling the `getPath` function.\n\n```ts\ndeclare const getPath: (\n  model: FileNode | DirNode,\n  keypath: string,\n  dependencies: {}\n) =\u003e string\n```\n\nThe parameters are:\n\n- _model_: the model\n- _keypath_: a string to define which node we're getting the path for\n- _dependencies_: an object with all the dependencies from the node and it's ancestors\n\n```ts\nimport { dir, file, getPath } from '@midzdotdev/path-master'\n\nconst hlsPackageModel = dir(\n  ({ videoId }: { videoId: number }) =\u003e `videos/${videoId}`,\n  {\n    manifest: file(`master.m3u8`),\n    variantStream: dir(\n      ({ quality }: { quality: 720 | 1080 }) =\u003e `stream_${quality}`,\n      {\n        playlist: file(`playlist.m3u8`),\n        segment: file(\n          ({ segmentId }: { segmentId: number }) =\u003e `segment_${segmentId}.ts`\n        ),\n      }\n    ),\n  }\n)\n\nconst hlsPackagePath = getPath(hlsPackageModel, '', { videoId: 42 })\n// result: \"videos/42/\"\n// type: `videos/${number}`\n\nconst streamPlaylist = getPath(hlsPackageModel, 'variantStream.playlist', {\n  videoId: 42,\n  quality: 720,\n})\n// result: \"videos/42/stream_720/playlist.m3u8\"\n// type: `videos/${number}/stream_${number}/playlist.m3u8`\n\nconst segment11 = getPath(hlsPackageModel, 'variantStream.segment', {\n  videoId: 42,\n  quality: 720,\n  segmentId: 11,\n})\n// result: \"videos/42/stream_720/segment_11.ts\"\n// type: `videos/${number}/stream_${number}/segment_${number}.ts`\n```\n\n\u003e Notice that the type of the path is properly inferred from the model definition.\n\n## Get Relative Paths\n\nYou can get relative paths between nodes, so long as they're in the same model.\n\nIt's important to recognise that relative paths behave differently in URLs as opposed to filesystem-like paths.\n\nThe relative path from `a/b` to `a/c/d`:\n\n- in a filesystem is `../c/d`\n- in a URL is `c/d`\n\n\u003e You can have a play around with this yourself to better understand the difference.\n\u003e\n\u003e - Use the Node REPL with `path.relative(from, to)` for filesystem paths\n\u003e - Use the browser console with `new URL(relativePath, fromUrl)` for URL paths\n\nAs a result we have two separate functions for each use-case:\n\n- filesystem paths: `getRelativeFsPath`\n- URL paths: `getRelativeUrlPath`\n\nBoth of these functions have an identical signature that looks like this.\n\n```ts\ndeclare const x: (\n  model: DirNode,\n  from: string | [keypath: string, dependencies: {}],\n  to: string | [keypath: string, dependencies: {}]\n) =\u003e string\n```\n\n\u003e When the node specified for _from_ or _to_ has no dependencies, then you can just pass the keypath string.\n\nSince our example model being a HLS package only concerns itself with URLs, we'll demonstrate with `getRelativeUrlPath`.\n\n```ts\nimport { dir, file, getRelativeUrlPath } from '@midzdotdev/path-master'\n\nconst hlsPackageModel = dir(\n  ({ videoId }: { videoId: number }) =\u003e `videos/${videoId}`,\n  {\n    manifest: file(`master.m3u8`),\n    variantStream: dir(\n      ({ quality }: { quality: 720 | 1080 }) =\u003e `stream_${quality}`,\n      {\n        playlist: file(`playlist.m3u8`),\n        segment: file(\n          ({ segmentId }: { segmentId: number }) =\u003e `segment_${segmentId}.ts`\n        ),\n      }\n    ),\n  }\n)\n\nconst masterPlaylistToVariantPlaylist = getRelativeUrlPath(\n  hlsPackageModel,\n  ['manifest', { videoId: 42 }],\n  ['variantStream.playlist', { videoId: 42, quality: 720 }]\n)\n// result: \"stream_720/playlist.m3u8\"\n\nconst variantPlaylistToSegment = getRelativeUrlPath(\n  hlsPackageModel,\n  ['variantStream.playlist', { videoId: 42, quality: 720 }],\n  ['variantStream.segment', { videoId: 42, quality: 720, segmentId: 11 }]\n)\n// result: \"segment_11.ts\"\n```\n\n## Contributing\n\nIf you have any ideas for improvements or new features, please file an issue or open a pull request.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmidzdotdev%2Fpath-master","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmidzdotdev%2Fpath-master","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmidzdotdev%2Fpath-master/lists"}