{"id":15636364,"url":"https://github.com/saibotsivad/glopen","last_synced_at":"2026-04-30T10:39:06.984Z","repository":{"id":41882094,"uuid":"419052470","full_name":"saibotsivad/glopen","owner":"saibotsivad","description":"GLobbify OPENapi: Glob a folder structure into an OpenAPI definition and API driver.","archived":false,"fork":false,"pushed_at":"2022-07-04T22:27:27.000Z","size":494,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-24T02:12:51.543Z","etag":null,"topics":["generator","glob","openapi"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saibotsivad.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-19T18:48:16.000Z","updated_at":"2021-11-19T23:00:26.000Z","dependencies_parsed_at":"2022-07-09T15:02:14.245Z","dependency_job_id":null,"html_url":"https://github.com/saibotsivad/glopen","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saibotsivad%2Fglopen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saibotsivad%2Fglopen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saibotsivad%2Fglopen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saibotsivad%2Fglopen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saibotsivad","download_url":"https://codeload.github.com/saibotsivad/glopen/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246237437,"owners_count":20745348,"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","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":["generator","glob","openapi"],"created_at":"2024-10-03T11:02:39.109Z","updated_at":"2026-04-30T10:39:01.958Z","avatar_url":"https://github.com/saibotsivad.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# glopen\n\nGLobbify OPENapi: Glob folder structures into an OpenAPI definition and API driver.\n\nThe basic idea is that you create a folder structure to represent the final OpenAPI object, with some very light sugar, and use that to both generate the OpenAPI definition file and drive the API.\n\nYou can also merge multiple distinct folders together, into a single definition and driver, allowing API endpoint definitions to be published as discrete, reusable libraries.\n\n## Install\n\nThe usual way:\n\n```shell\nnpm install glopen\n```\n\n## Using\n\nAs part of the build process, for a single folder structure, simply do:\n\n```shell\nglopen --dir=./path/to/api/folder --out=./generated-file.js\n```\n\n(Use the `-w` or `--watch` flags to watch for changes and rebuild when any are detected, for easier development.)\n\nThe `--out` flag is optional, if not set the code will print, so you could also do:\n\n```shell\nglopen --dir=./path/to/api/folder \u003e ./generated-file.js\n```\n\nFor multiple merged folder structures, you can use multiple `--json` parameters, with each being a JSON object, or an array of objects:\n\n```shell\nglopen --json='{ \"dir\": \"./path1\", \"api\": \"/api/v1\" }' \\\n       --json='[ { \"dir\": \"./path1\", \"api\": \"/api/v1\" }, { \"dir\": \"./path2\", \"api\": \"/api/v2\" } ]' \\\n       --out=./generated-file.js\n```\n\nTo use it in code, pass in `{ merge: Array\u003cPart\u003e }`:\n\n```js\nimport { glopen } from 'glopen'\nimport { writeFile } from 'node:fs/promises'\n\nconst code = await glopen({\n\tmerge: [\n\t\t{\n\t\t\tdir: './path/to/api/folder',\n\t\t\tapi: '/api/v1/tasks',\n\t\t\text: '@'\n\t\t}\n\t]\n})\nawait writeFile('./generated-file.js', code, 'utf8')\n```\n\nYou can also use the `--config` (or `-c` alias) to point to a JavaScript file that exports a config object:\n\n```js\nexport default {\n\tmerge: [{\n\t\tdir: './path/to/api/folder',\n\t\tapi: '/api/v1/tasks',\n\t\text: '@'\n\t}],\n\toutput: './generated-file.js'\n}\n```\n\n## Parts\n\nWhether used in code, or in any of the modes, each API \"part\" has these parameters:\n\n- `dir` *required* - The path to the API folder.\n- `api` *optional* - The API path to prefix to the folder, e.g. if the folder is `/tasks` and `prefix` is `/api/v1` the API path becomes `/api/v1/tasks`.\n- `ext` *optional* - The extension prefix used for auto-globbing, (Default: `@`, e.g. `get.@.js`)\n\nFor example, given this folder structure:\n\n```\n/demo\n\t/tasks\n\t\t/get.@.js\n```\n\nIf, at the root, you used `glopen --dir=./demo` the OpenAPI operation would be `GET /tasks`.\n\nHowever, if you used `glopen --dir=./demo --api=/v1` the OpenAPI operation would be `GET /v1/tasks`.\n\n## API `glopen({ merge: Array\u003cPart\u003e })`\n\nWhen used in code, the input is an object with a `merge` property, which is an ordered array of \"part\" objects.\n\n```js\nconst code = await glopen({\n\tmerge: [\n\t\t{\n\t\t\tdir: './path/to/users',\n\t\t\tapi: '/api/v2/users',\n\t\t\text: '@'\n\t\t}\n\t],\n})\n```\n\n## CLI\n\nThe different modes (single, json, and config) are not mixable, you must pick one or the other.\n\n### Single Mode\n\nIn single mode, a single \"part\" is passed in as CLI args:\n\n```shell\nglopen --api=./path/to/users \\\n       --prefix=/api/v2/users \\\n       --suffix=@\n```\n\n### JSON Mode\n\nIn JSON mode, one or more \"parts\" are passed in as JSON strings, either as objects, arrays, or mixed:\n\n```shell\nglopen --json='{ \"dir\": \"./path/to/users\", \"api\": \"/api/v2/users\", \"ext\": \"@\" }' \\\n       --json='[ { \"dir\": \"./path/to/tasks\", \"api\": \"/api/v2/tasks\", \"ext\": \"$\" }, { \"dir\": \"./path/to/cars\", \"api\": \"/api/v1/cars\", \"ext\": \"@\" } ]'\n```\n\nThe order of all JSON inputs is preserved, so the above example would be the same as:\n\n```js\nawait glopen({\n\tmerge: [\n\t\t{\n\t\t\tdir: './path/to/users',\n\t\t\tapi: '/api/v2/users',\n\t\t\text: '@'\n\t\t},\n\t\t{\n\t\t\tdir: './path/to/tasks',\n\t\t\tapi: '/api/v2/tasks',\n\t\t\text: '$'\n\t\t},\n\t\t{\n\t\t\tdir: './path/to/cars',\n\t\t\tapi: '/api/v1/cars',\n\t\t\text: '$'\n\t\t},\n\t]\n})\n```\n\n### Config Mode\n\nCalling `glopen` with the `-c` or `--config` option will import the file specified, or `glopen.config.js` by default:\n\n```shell\nglopen -c # uses './glopen.config.js'\n# or\nglopen -c ./path/to/config.js\n```\n\nThe config file must export a default object containing the following properties:\n\n- `merge: Array\u003cPart\u003e` *required* - An array of \"parts\", e.g. `dir`, `api`, `ext`.\n- `output: String` *optional* - The file path to write to.\n\n## Merge Order\n\nTo be clear, all paths, components, and so on, will all be merged on top of each other, first to last. In other words, if there are overlapping paths, models, etc., the winning one will be the last.\n\nThis copies the `Object.assign` order, e.g.:\n\n```js\nObject.assign({}, { a: 1 }, { a: 2 })\n// { a: 2 }\n```\n\n## Okay... Now What?\n\nFor example, an OpenAPI definition with a single path might look like:\n\n```json\n{\n\t\"paths\": {\n\t\t\"/api/v1/tasks/{taskId}\": {\n\t\t\t\"get\": {\n\t\t\t\t\"description\": \"Get a single task.\"\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\nThat translates to the folder+file structure:\n\n```\n/paths\n\t/api\n\t\t/v1\n\t\t\t/tasks\n\t\t\t\t/{taskId}\n\t\t\t\t\t/get.@.js\n```\n\nNote that the `.@.js` suffix is configurable, but it means that we can auto-glob the files together (the sugar) and put other files next to it, e.g.:\n\n```\n/paths\n\t/api\n\t\t/v1\n\t\t\t/tasks\n\t\t\t\t/{taskId}\n\t\t\t\t\t/get.@.js\n\t\t\t\t\t/get.test.js\n\t\t\t\t\t/some-utils.js\n```\n\nInside each file, you just export named constants that match the OpenAPI property names, for example:\n\n```js\n// file: /paths/api/v1/tasks/{taskId}/get.@.js\nexport const summary = 'Get a single task.'\nexport const tags = [ 'task' ]\n```\n\nFor methods, you would export a default function as a request handler. Since this generator is simply importing and exporting, the argument parameters can be whatever you'd like. Here we're using the normal Express-like `async (request, response)` signature, but you can use any signature you like:\n\n```js\n// same file\nexport default async (request, response) =\u003e {\n\tresponse.end(`Task ID: ${request.params.taskId}`)\n}\n```\n\n## Generator Output\n\nWhat comes out of the generator is a single JS file that imports and exports the folder+file tree\ninto something useful for generating the OpenAPI JSON object, as well as for passing into API frameworks\nlike Polka, Express, and so on:\n\n```js\nimport GENERATED_ID_handler, * as GENERATED_ID from './paths/api/v1/tasks/{taskId}/get.@.js'\n\nexport const definition = {\n\tpaths: {\n\t\t'/api/v1/tasks/{taskId}': {\n\t\t\tget: {\n\t\t\t\t...GENERATED_ID,\n\t\t\t\toperationId: 'GENERATED_ID_get',\n\t\t\t},\n\t\t},\n\t}\n}\n\nexport const routes = [\n\t\t{\n\t\t\thandler: GENERATED_ID_handler,\n\t\t\texports: GENERATED_ID,\n\t\t\tmethod: 'get',\n\t\t\tpath: '/api/v1/tasks/{taskId}',\n\t\t\t// Because the `:`` prefix is so common, it is offered as an alternate\n\t\t\t// to the OpenAPI path syntax.\n\t\t\tpathAlt: '/api/v1/tasks/:taskId',\n\t\t\toperationId: 'GENERATED_ID_get',\n\t\t},\n]\n```\n\n## Underscore Filename\n\nThe special exception to a named file matching an OpenAPI object is that you still need to define some properties at the root of an object. For example, the \"Path Item Object\" has a `description` property.\n\nTo resolve this, the underscore character is reserved as a file name. Simply place a file named `_.@.js` in the folder, and those properties will be merged, e.g. for this structure:\n\n```\n/paths\n\t/api\n\t\t/v1\n\t\t\t/tasks\n\t\t\t\t/{taskId}\n\t\t\t\t\t/_.@.js\n\t\t\t\t\t/get.@.js\n```\n\nThe `_.@.js` file would be responsible for the \"Path Item Object\" properties, such as\nthe path parameter `{taskId}` definition. This looks the same as the other files:\n\n```js\nexport const parameters = [\n\t{\n\t\tname: 'taskId',\n\t\tin: 'path',\n\t\trequired: true,\n\t\tschema: {\n\t\t\ttype: 'string'\n\t\t}\n\t}\n]\n```\n\nThe generated output would expand the exported properties from `_.@.js` to the Path Item Object:\n\n```js\nimport * as GENERATED_ID0 from './paths/api/v1/tasks/{taskId}/_.@.js'\nimport GENERATED_ID1_handler, * as GENERATED_ID1 from './paths/api/v1/tasks/{taskId}/get.@.js'\n\nexport const definition = {\n\tpaths: {\n\t\t'/api/v1/tasks/{taskId}': {\n\t\t\t...GENERATED_ID0,\n\t\t\tget: {\n\t\t\t\t...GENERATED_ID1,\n\t\t\t\toperationId: 'GENERATED_ID_get',\n\t\t\t},\n\t\t},\n\t}\n}\n```\n\n## Output\n\nThe generator creates a file that exports two named properties:\n\n- `definition` *Object* - The object containing the fully constructed OpenAPI definition.\n- `routes` *Array\u003cObject\u003e* - A list of all routes, including the handler.\n\n### Output: Definition\n\nTo get the OpenAPI JSON, you simply import and stringify:\n\n```js\nimport { definition } from './generated-file.js'\nconsole.log(JSON.stringify(definition, null, 2))\n```\n\nWould output:\n\n```json\n{\n\t\"paths\": {\n\t\t\"/api/v1/tasks/{taskId}\": {\n\t\t\t\"parameters\": [\n\t\t\t\t{\n\t\t\t\t\t\"name\": \"taskId\",\n\t\t\t\t\t\"in\": \"path\",\n\t\t\t\t\t\"required\": true,\n\t\t\t\t\t\"schema\": { \"type\": \"string\" }\n\t\t\t\t}\n\t\t\t],\n\t\t\t\"get\": {\n\t\t\t\t\"summary\": \"Get a single task.\",\n\t\t\t\t\"tags\": [ \"task\" ]\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n### Output: Routes\n\nTo use the generated file in an API framework, like Polka, import the `routes` property:\n\n```js\nimport polka from 'polka'\nimport { routes } from './generated-file.js'\n\nconst api = polka()\n\nroutes.forEach(({ handler, exports, method, path, pathAlt, operationId }) =\u003e {\n\tconsole.log(' - ', method.toUpperCase(), polkaPath, '\\n   ', exports.summary)\n\tapi[method](pathAlt, handler)\n})\n\napi.listen(3000, () =\u003e {\n\tconsole.log('API running on port 3000, try opening: http://localhost:3000/api/v1/tasks/9001')\n})\n```\n\nEach route array element has the following properties:\n\n- `handler` - The `default` export of the file.\n- `exports` - Every named export of the file.\n- `method` - The lower-cased method, which comes from the filename, e.g. `get.@.js` becomes `get`.\n- `path` - The full OpenAPI path string, which comes from the folder paths.\n- `pathAlt` - The `:` prefixed path syntax is so common, it is provided for your convenience.\n- `operationId` - The generated identifier of the path+method.\n\nYou would use those to do things like secure the route, validate input against schemas, and so on.\n\n## Importing Text\n\nIt is very convenient to be able to write longer descriptions in separate markdown files, so that you get the syntax highlighting, previews, etc. that you wouldn't get if you put it directly in the JavaScript file.\n\nIn other words, this isn't a very developer nice experience:\n\n```js\nexport const description = `\nThis really long string will work just fine, so use this if you like.\n\nHoever, in most IDEs you won't get markdown syntax highlighting, and \\`escaping\\`\nthe template literals can get annoying.\n`\n```\n\nModern bundlers support importing string, so you could definitely do this:\n\n```js\nexport { default as description } from './description.md'\n```\n\nOne of the goals of this project is to output a generated file that doesn't require further bundling to function. To that end, if you name a markdown file appropriately, it'll get brought in as a string and exported, e.g. `get.description.@.md` will become:\n\n```js\n// file: generated-file.js\nconst GENERATED_ID_description = \"The text gets placed here, since it can't be imported.\"\n```\n\nThe naming convention is simply `FILENAME.PROPERTY.SUFFIX.md`:\n\n- `FILENAME` - The filename to connect to, e.g. `get` or `_`.\n- `PROPERTY` - The property name to connect to, e.g. `description`. (Note: the generator doesn't support nested text, sorry.)\n- `SUFFIX` - By default it's `@`, but that's configurable.\n\n## Schema References\n\nThese are handled the same way, so instead of `_.@.js` exporting the `taskId` parameter definition, it could export the parameter definition as a schema reference:\n\n```js\nexport const parameters = [\n\t{\n\t\t$ref: '#/components/parameters/taskId'\n\t}\n]\n```\n\nThe generator will check that all `$ref` references that use a `#/` prefix are resolvable in the final OpenAPI object, and throw an error if not.\n\n## License\n\nThis software and all example code are published to the public domain using the [Very Open License](https://veryopenlicense.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaibotsivad%2Fglopen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaibotsivad%2Fglopen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaibotsivad%2Fglopen/lists"}