{"id":18879759,"url":"https://github.com/loilo/gyros","last_synced_at":"2025-04-14T19:23:33.957Z","repository":{"id":162920119,"uuid":"637664153","full_name":"loilo/gyros","owner":"loilo","description":"🥙 Transform PHP ASTs the easy way","archived":false,"fork":false,"pushed_at":"2024-05-06T04:44:52.000Z","size":379,"stargazers_count":4,"open_issues_count":2,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-11T05:06:03.295Z","etag":null,"topics":["ast","manipulation","php"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/loilo.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}},"created_at":"2023-05-08T06:41:06.000Z","updated_at":"2023-11-25T12:47:20.000Z","dependencies_parsed_at":"2024-04-22T05:52:25.438Z","dependency_job_id":null,"html_url":"https://github.com/loilo/gyros","commit_stats":{"total_commits":98,"total_committers":4,"mean_commits":24.5,"dds":"0.24489795918367352","last_synced_commit":"d3c025535a46c6063c2b4ad19180194cacca3ed3"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loilo%2Fgyros","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loilo%2Fgyros/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loilo%2Fgyros/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loilo%2Fgyros/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loilo","download_url":"https://codeload.github.com/loilo/gyros/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248943518,"owners_count":21186978,"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":["ast","manipulation","php"],"created_at":"2024-11-08T06:39:12.668Z","updated_at":"2025-04-14T19:23:33.906Z","avatar_url":"https://github.com/loilo.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cbr\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/loilo/gyros/main/gyros.svg\" alt=\"A quarter-circle shaped bread with Gyros stuffing\" width=\"150\" height=\"106\"\u003e\n  \u003cbr\u003e\n  \u003ch1\u003eGyros\u003c/h1\u003e\n\u003c/div\u003e\n\nTransform PHP [AST](http://en.wikipedia.org/wiki/Abstract_syntax_tree)s the easy way. Just as tasty as [Yufka](https://github.com/loilo/yufka), but with different ingredients.\n\n[![Tests](https://badgen.net/github/checks/loilo/gyros/main)](https://github.com/loilo/gyros/actions)\n[![npm](https://badgen.net/npm/v/gyros)](https://npmjs.com/package/gyros)\n\n## Installation\n\n```\nnpm install --save gyros\n```\n\n**IMPORTANT:** Gyros is ESM-only. [Read more.](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)\n\n\n## Motivation\nGyros is the ideal tool for programmatically making small \u0026 simple modifications to your PHP code in JavaScript.\n\nAs an introducing example, let's put a function wrapper around all array literals:\n\n```js\nimport { gyros } from 'gyros'\n\nconst source = `\n$xs = [1, 2, [3, 4]];\n$ys = [5, 6];\nvar_dump([$xs, $ys]);\n`\n\nconst result = gyros(source, (node, { update, source }) =\u003e {\n  if (node.kind === 'array') {\n    update(`fun(${source()})`)\n  }\n})\n\nconsole.log(result.toString())\n```\n\nOutput:\n```php\n$xs = fun([1, 2, fun([3, 4])]);\n$ys = fun([5, 6]);\nvar_dump(fun([$xs, $ys]));\n```\n\n## Usage\n### How it Works\n```ts\nfunction gyros(source, options = {}, manipulator)\n```\n\nTransform the string `source` with the function `manipulator`, returning an output object.\n\nFor every node in the AST, `manipulator(node, helpers)` fires. The recursive walk is an\nin-order traversal, so children get called before their parents. This makes it easier to write nested transforms since transforming parents often requires transforming their children first anyway.\n\nThe `gyros()` return value is an object with two properties:\n* `code` – contains the transformed source code\n* `map` – holds the resulting source map object, [as generated by `magic-string`](https://www.npmjs.com/package/magic-string#sgeneratemap-options-)\n\nCalling `.toString()` on a Gyros result object will return its source `code`.\n\n\u003e **Pro Tip:**\n\u003e Don't know how a PHP AST looks like? Have a look at [astexplorer.net](https://astexplorer.net/) to get an idea.\n\n### Options\nAll options are, as the name suggests, optional. If you want to provide an options object, its place is between the `source` code and the `manipulator` function.\n\n#### Parse Mode\nThere are two parse modes available: `code` and `eval`. The default is `eval`.\n\nThe `code` parse mode allows to parse PHP code as it appears \"in the wild\", i.e. with enclosing `\u003c?php` tags. The default `eval` mode only parses pure PHP code, with no enclosing tags.\n\n```js\ngyros('\u003c!doctype html\u003e\u003c?= \"Hello World!\" ?\u003e', { parseMode: 'code' }, (node, helpers) =\u003e {\n  // Parse the `source` as mixed HTML/PHP code\n})\n```\n\n#### PHP Parser Options\nAny options for the underlying [`php-parser`](https://npmjs.com/package/php-parser) can be passed to `options.phpParser`:\n\n```js\ngyros(source, { phpParser: { parser: { suppressErrors: true } } }, (node, helpers) =\u003e {\n  // Parse the `source` in loose mode\n})\n```\n\n#### Source Maps\nGyros uses [`magic-string`](https://www.npmjs.com/package/magic-string) under the hood to generate [source maps](https://developer.mozilla.org/docs/Tools/Debugger/How_to/Use_a_source_map) for your code modifications. You can pass its [source map options](https://www.npmjs.com/package/magic-string#sgeneratemap-options-) as `options.sourceMap`:\n\n```js\ngyros(source, { sourceMap: { hires: true } }, (node, helpers) =\u003e {\n  // Create a high-resolution source map\n})\n```\n\n### Helpers\nThe `helpers` object passed to the `manipulator` function exposes the following methods. All of these methods handle the *current AST node* (the one that's passed to the manipulator as its first argument).\n\nHowever, all of these methods take an AST node as an optional first parameter if you want to access other nodes.\n\n\u003e **Example:**\n\u003e ```js\n\u003e gyros('$x = 1', (node, { source }) =\u003e {\n\u003e   if (node.kind === 'assign') {\n\u003e     // `node` refers to the `$x = 1` Expression\n\u003e     source()           // returns \"$x = 1\"\n\u003e     source(node.right) // returns \"1\"\n\u003e   }\n\u003e })\n\u003e ```\n\n\n#### `source()`\nReturn the source code for the given node, including any modifications made to\nchild nodes:\n\n```js\ngyros('(true)', (node, { source, update }) =\u003e {\n  if (node.kind === 'boolean') {\n    source() // returns \"true\"\n    update('false')\n    source() // returns \"false\"\n  }\n})\n```\n\n#### `update(replacement)`\nReplace the source of the affected node with the `replacement` string:\n\n```js\nconst result = gyros('4 + 2', (node, { source, update }) =\u003e {\n  if (node.kind === 'bin') {\n    update(source(node.left) + source(node.right))\n  }\n})\n\nconsole.log(result.toString())\n```\n\nOutput:\n```php\n42\n```\n\n#### `parent(levels = 1)`\nFrom the starting node, climb up the syntax tree `levels` times. Getting an ancestor node of the program root yields `undefined`.\n\n```js\ngyros('$x = [1]', (node, { parent }) =\u003e {\n  if (node.kind === 'number') {\n    // `node` refers to the `1` number literal\n    parent() // same as parent(1), refers to the `1` array item\n    parent(2) // refers to the `[1]` expression\n    parent(3) // refers to the `x = [1]` assignment expression\n    parent(4) // refers to the `x = [1]` statement\n    parent(5) // refers to the program as a whole (root node)\n    parent(6) // yields `undefined`, same as parent(7), parent(8) etc.\n  }\n})\n```\n\n#### External Helper Access\n\n**Tip:** If you want to extract manipulation behavior into standalone functions, you can access the helpers directly on the `gyros` instance (e.g. `gyros.source()`) where they are not bound to a specific node:\n\n```js\n// Standalone function, increments node's value if it's a number\nconst increment = node =\u003e {\n  if (node.kind === 'number') {\n    gyros.update(node, String(Number(node.value) + 1))\n  }\n}\n\nconst result = gyros('$x = 1', node =\u003e {\n  increment(node)\n})\n\nconsole.log(result.toString())\n```\nOutput:\n```php\n$x = 2\n```\n\n### Asynchronous Manipulations\nThe `manipulator` function may return a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). If it does, Gyros will wait for that to resolve, making the whole `gyros()` function return a Promise resolving to the result object (instead of returning the result object directly):\n\n```js\nimport got from 'got' // see www.npmjs.com/package/got\n\nconst source = `\n$content = curl(\"https://example.com\")\n`\nconst deferredResult = gyros(source, async (node, { source, update }) =\u003e {\n  if (node.kind === 'call' \u0026\u0026 node.what.kind === 'name' \u0026\u0026 node.what.name === 'curl') {\n    // Replace all curl() calls with their actual content\n\n    // Get the URL (will only work for simple string literals)\n    const url = node.arguments[0].value\n\n    // Fetch the URL's contents\n    const contents = (await got(url)).body\n\n    // Replace the cUrl() call with the fetched contents\n    update(JSON.stringify(contents))\n  }\n})\n\n// Result is not available immediately, we need to await it\ndeferredResult.then(result =\u003e {\n  console.log(result.toString())\n})\n```\n\nOutput:\n```php\n$content = \"\u003c!doctype html\u003e\\n\u003chtml\u003e\\n[...]\\n\u003c/html\u003e\"\n```\n\n\u003e **Note:** You *have* to return a promise if you want to commit updates asynchronously. Once the manipulator function is done running, any `update()` calls originating from it will throw an error.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floilo%2Fgyros","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floilo%2Fgyros","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floilo%2Fgyros/lists"}