npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

gyros

v1.0.6

Published

Transform PHP ASTs the easy way

Downloads

29

Readme

Transform PHP ASTs the easy way. Just as tasty as Yufka, but with different ingredients.

Tests npm

Installation

npm install --save gyros

IMPORTANT: Gyros is ESM-only. Read more.

Motivation

Gyros is the ideal tool for programmatically making small & simple modifications to your PHP code in JavaScript.

As an introducing example, let's put a function wrapper around all array literals:

import { gyros } from 'gyros'

const source = `
$xs = [1, 2, [3, 4]];
$ys = [5, 6];
var_dump([$xs, $ys]);
`

const result = gyros(source, (node, { update, source }) => {
  if (node.kind === 'array') {
    update(`fun(${source()})`)
  }
})

console.log(result.toString())

Output:

$xs = fun([1, 2, fun([3, 4])]);
$ys = fun([5, 6]);
var_dump(fun([$xs, $ys]));

Usage

How it Works

function gyros(source, options = {}, manipulator)

Transform the string source with the function manipulator, returning an output object.

For every node in the AST, manipulator(node, helpers) fires. The recursive walk is an in-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.

The gyros() return value is an object with two properties:

Calling .toString() on a Gyros result object will return its source code.

Pro Tip: Don't know how a PHP AST looks like? Have a look at astexplorer.net to get an idea.

Options

All 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.

Parse Mode

There are two parse modes available: code and eval. The default is eval.

The code parse mode allows to parse PHP code as it appears "in the wild", i.e. with enclosing <?php tags. The default eval mode only parses pure PHP code, with no enclosing tags.

gyros('<!doctype html><?= "Hello World!" ?>', { parseMode: 'code' }, (node, helpers) => {
  // Parse the `source` as mixed HTML/PHP code
})

PHP Parser Options

Any options for the underlying php-parser can be passed to options.phpParser:

gyros(source, { phpParser: { parser: { suppressErrors: true } } }, (node, helpers) => {
  // Parse the `source` in loose mode
})

Source Maps

Gyros uses magic-string under the hood to generate source maps for your code modifications. You can pass its source map options as options.sourceMap:

gyros(source, { sourceMap: { hires: true } }, (node, helpers) => {
  // Create a high-resolution source map
})

Helpers

The 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).

However, all of these methods take an AST node as an optional first parameter if you want to access other nodes.

Example:

gyros('$x = 1', (node, { source }) => {
  if (node.kind === 'assign') {
    // `node` refers to the `$x = 1` Expression
    source()           // returns "$x = 1"
    source(node.right) // returns "1"
  }
})

source()

Return the source code for the given node, including any modifications made to child nodes:

gyros('(true)', (node, { source, update }) => {
  if (node.kind === 'boolean') {
    source() // returns "true"
    update('false')
    source() // returns "false"
  }
})

update(replacement)

Replace the source of the affected node with the replacement string:

const result = gyros('4 + 2', (node, { source, update }) => {
  if (node.kind === 'bin') {
    update(source(node.left) + source(node.right))
  }
})

console.log(result.toString())

Output:

42

parent(levels = 1)

From the starting node, climb up the syntax tree levels times. Getting an ancestor node of the program root yields undefined.

gyros('$x = [1]', (node, { parent }) => {
  if (node.kind === 'number') {
    // `node` refers to the `1` number literal
    parent()  // same as parent(1), refers to the `1` as an array element
    parent(2) // refers to the `[1]` expression
    parent(3) // refers to the `$x = [1]` assignment expression
    parent(4) // refers to the `$x = [1]` statement
    parent(5) // refers to the program as a whole (root node)
    parent(6) // yields `undefined`, same as parent(6), parent(7) etc.
  }
})

External Helper Access

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:

// Standalone function, increments node's value if it's a number
const increment = node => {
  if (node.kind === 'number') {
    gyros.update(node, String(Number(node.value) + 1))
  }
}

const result = gyros('$x = 1', node => {
  increment(node)
})

console.log(result.toString())

Output:

$x = 2

Asynchronous Manipulations

The manipulator function may return a 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):

import got from 'got' // see www.npmjs.com/package/got

const source = `
$content = curl("https://example.com")
`
const deferredResult = gyros(source, async (node, { source, update }) => {
  if (node.kind === 'call' && node.what.kind === 'name' && node.what.name === 'curl') {
    // Replace all curl() calls with their actual content

    // Get the URL (will only work for simple string literals)
    const url = node.arguments[0].value

    // Fetch the URL's contents
    const contents = (await got(url)).body

    // Replace the cUrl() call with the fetched contents
    update(JSON.stringify(contents))
  }
})

// Result is not available immediately, we need to await it
deferredResult.then(result => {
  console.log(result.toString())
})

Output:

$content = "<!doctype html>\n<html>\n[...]\n</html>"

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.