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 🙏

© 2026 – Pkg Stats / Ryan Hefner

putnik

v1.5.1

Published

parse JavaScript to SQL

Readme

Putnik License NPM version Build Status Coverage Status

Putnik (Chroatian) - passenger

Putnik is a code transformation engine built on top of 🐊Putout. Instead of traversing the AST in memory with Babel, it writes the AST into a SQLite database, runs SQL-aware plugins against it, then reads the AST back and prints it with @putout/printer.

The key idea: SQL indexes beat Babel traverse at scale. A plugin that finds DebuggerStatement nodes queries one table with one index hit instead of visiting every node in the file.

Install

npm i putnik

Usage

import {
    putnik,
    parse,
    print,
} from 'putnik';

parse('src/index.js', 'const a = 1;');

const constPlugin = {
    select: `
        SELECT id, type, file, start_line, start_col
        FROM VariableDeclaration
        WHERE file = :file AND kind = 'const'
    `,
    report: `
        SELECT 'Prefer let over const' AS message, start_line AS line, start_col AS col
        FROM VariableDeclaration
        WHERE file = :file AND kind = 'const'
    `,
    fix: `
        UPDATE VariableDeclaration SET kind = 'let'
        WHERE file = :file AND kind = 'const'
    `,
};

// report mode — returns [code, places], does not mutate
const [code, places] = await putnik('src/index.js', {
    plugins: [constPlugin],
});

// places: [{message: 'Prefer let over const', line: 1, col: 0}]
// fix mode — mutates the DB, returns [newCode, places]
const [newCode] = await putnik('src/index.js', {
    plugins: [constPlugin],
    fix: true,
});

console.log(newCode);
// let a = 1;

API

parse(file, source)

Parses source with @putout/babel and writes every AST node into its typed table. Call once per file before run.

parse('src/index.js', 'const a = 1;');

Each Babel node type gets its own table. Every row shares these base columns:

| column | type | description | |--------------|---------|------------------------------------------------| | id | INTEGER | generated by the database — never set manually | | file | TEXT | source file path | | parent_id | INTEGER | id of the parent node | | parent_type | TEXT | type of the parent node | | parent_field | TEXT | field name on parent (id, init, body, …) | | start_line | INT | location | | start_col | INT | location | | end_line | INT | location | | end_col | INT | location |

Type-specific columns: kind on VariableDeclaration, name on Identifier, value on StringLiteral and NumericLiteral. Boolean-like fields (async, generator, computed, etc.) are stored as INTEGER1 for true, 0 for false.


putnik(file, source, options)

Report mode, does not mutate the DB:

const [code, places] = await putnik('src/index.js', source, {
    plugins,
    fix: false,
});

fix mode, mutates the DB:

const [newCode, places] = await putnik('src/index.js', source, {
    plugins,
    fix: true,
});

Returns [code, places] — same shape as putout. code is the transformed source in fix mode, the original source in report mode. places is an array of {message, line, col}.

The runner passes each row returned by @select as named parameters into @fix, so fix queries can reference :id, :type, :file, :parent_id, :parent_type, :parent_field, :start_line, :start_col, :end_line, :end_col, and any type-specific columns from the matched row.


print(file)

Reads all nodes for file from the DB, assembles the AST, and returns the printed source string via @putout/printer. Returns '' if the file has not been parsed.

import {print} from 'putout';

const code = print('src/index.js');
// 'let a = 1;\n'

getAst(file)

Same as print but returns the raw AST object.

const ast = putnik.getAst('src/index.js');

sql tagged template

import {sql} from 'putnik';
import {types} from 'putout';

const {file} = types;
const query = sql`SELECT id FROM VariableDeclaration WHERE file = ${file}`;

A no-op tag that enables SQL syntax highlighting in editors that support tagged templates.

Plugin shape

A plugin is a plain object with three SQL strings. The @fix query can be UPDATE, DELETE, or INSERT — all three are valid:

// UPDATE — change a value
const constToLet = {
    select: `SELECT id, type, file, start_line, start_col FROM VariableDeclaration WHERE file = :file AND kind = 'const'`,
    report: `SELECT 'Prefer let over const' AS message, start_line AS line, start_col AS col FROM VariableDeclaration WHERE file = :file AND kind = 'const'`,
    fix: `UPDATE VariableDeclaration SET kind = 'let' WHERE file = :file AND kind = 'const'`,
};

// DELETE — remove a node
const noDebugger = {
    select: `SELECT id, type, file, start_line, start_col FROM DebuggerStatement WHERE file = :file`,
    report: `SELECT 'Unexpected debugger statement' AS message, start_line AS line, start_col AS col FROM DebuggerStatement WHERE file = :file`,
    fix: `DELETE FROM DebuggerStatement WHERE file = :file`,
};

// INSERT — add a new node
// The runner passes each @select row as params, so :id and :type refer to the matched row.
// The database generates the new node's id automatically.
const addDebugger = {
    select: `SELECT id, type, file, start_line, start_col FROM BlockStatement WHERE file = :file`,
    report: `SELECT 'Missing debugger' AS message, start_line AS line, start_col AS col FROM BlockStatement WHERE file = :file`,
    fix: `
        INSERT INTO DebuggerStatement (file, parent_id, parent_type, parent_field, start_line, start_col, end_line, end_col)
        VALUES (:file, :id, :type, 'body', :start_line, :start_col, :start_line, :start_col)
    `,
};

Plugins can also be loaded from .sql files:

-- @select
SELECT id, type, file, start_line, start_col
FROM VariableDeclaration
WHERE file = :file AND kind = 'const';
-- @report
SELECT 'Prefer let over const' AS message,
       start_line AS line, start_col AS col
FROM VariableDeclaration
WHERE file = :file AND kind = 'const';
-- @fix
UPDATE VariableDeclaration SET kind = 'let'
WHERE file = :file AND kind = 'const';
import {loadSqlPlugin} from 'putnik';

const plugin = loadSqlPlugin('./plugins/const-to-let.sql');

Boolean columns

Boolean-like AST fields are stored as INTEGER. Use 0 and 1 in plugin SQL — not true or false:

-- find all async functions
SELECT id, type, file, start_line, start_col
FROM FunctionDeclaration
WHERE file = :file AND async = 1;
-- make all functions non-async
UPDATE FunctionDeclaration SET async = 0 WHERE file = :file;

Portable SQL

Plugins must use the common subset supported by both SQLite and Postgres. validatePlugin rejects non-portable constructs with a clear error:

| rejected | use instead | |-------------------|-----------------------------------| | FULL OUTER JOIN | two LEFT JOINs with UNION ALL | | RIGHT JOIN | LEFT JOIN with tables swapped | | REGEXP | LIKE or IN | | ANY / ALL | IN with a subquery | | true / false | 1 / 0 | | LATERAL | correlated subquery |

Cross-file transforms

Because all files share one DB, a plugin can query across the whole project:

import {readFileSync} from 'node:fs';
import {putnik} from 'putnik';

const [code] = putnik({
    connection: '.putnik.db',
});

const unusedExports = {
    select: `
        SELECT e.id, e.type, e.file, e.start_line, e.start_col
        FROM ExportDeclaration e
        LEFT JOIN ImportDeclaration i ON i.name = e.name AND i.file != e.file
        WHERE i.id IS NULL AND e.file = :file
    `,
    report: `
        SELECT 'Unused export' AS message, start_line AS line, start_col AS col
        FROM ExportDeclaration e
        LEFT JOIN ImportDeclaration i ON i.name = e.name AND i.file != e.file
        WHERE i.id IS NULL AND e.file = :file
    `,
};

const [, places] = await putnik(targetFile, 'const a = "hello"', {
    plugins: [unusedExports],
});

License

MIT