Skip to content

BUG: TypeError: printer.print is not a function with Prettier 3.x #15

@dirkwa

Description

@dirkwa

Summary

The assemblyscript-prettier plugin fails with TypeError: printer.print is not a function when used with Prettier 3.x. This affects both version 3.0.1 (latest) and version 2.0.2.

Environment

  • Node.js: v20.x / v22.x
  • Prettier: 3.3.3+
  • assemblyscript-prettier: Tested with 3.0.1 and 2.0.2
  • OS: Linux (Debian 13 / Ubuntu)

Steps to Reproduce

1. Install the plugin

npm i -D [email protected]  # or @2.0.2

2. Configure Prettier (any of these approaches)

Option A - Root .prettierrc.json:

{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "plugins": ["assemblyscript-prettier"]
}

Option B - Directory-scoped .prettierrc.json:
Place the config in the AssemblyScript directory (e.g., assembly/.prettierrc.json)

3. Run Prettier

npx prettier --write .

Expected Behavior

AssemblyScript files with decorators like @inline, @lazy, @external should be formatted correctly.

Actual Behavior

[error] assembly/index.ts: TypeError: printer.print is not a function
[error]     at callPluginPrintFunction (file:///node_modules/prettier/index.mjs:16631:20)
[error]     at printAstToDoc (file:///node_modules/prettier/index.mjs:16581:22)
[error]     at async coreFormat (file:///node_modules/prettier/index.mjs:16959:14)
[error]     at async formatWithCursor (file:///node_modules/prettier/index.mjs:17172:14)
[error]     at async formatFiles (file:///node_modules/prettier/internal/legacy-cli.mjs:5831:18)

The error occurs on all .ts files processed by the plugin, not just AssemblyScript files.

Root Cause Analysis

Looking at the plugin source code in src/plugin.js:

Version 3.0.1 Issue

let as_estree = {};

async function initPrinter(jsPlugin) {
  let estree = jsPlugin.printers.estree;
  estree = typeof estree == "function" ? await estree() : estree;
  Object.assign(as_estree, {
    ...estree,
    // ...
  });
}

async function parse(text, options) {
  await initPrinter(options.plugins.find((plugin) => plugin.printers && plugin.printers.estree));
  // ...
}

export default {
  parsers: {
    typescript: {
      ...pluginTypescript.parsers.typescript,
      parse,
      astFormat: "as-estree",
      preprocess: preProcess,
    },
  },
  printers: { "as-estree": as_estree },  // Empty object at registration time!
};

Version 2.0.2 Issue

let as_estree = {};

function initPrinter(jsPlugin) {
  Object.assign(as_estree, {
    ...jsPlugin.printers.estree,
    // ...
  });
}

function parse(text, options) {
  initPrinter(options.plugins.find((plugin) => plugin.printers && plugin.printers.estree));
  // ...
}

export default {
  parsers: {
    typescript: { /* ... */ },
  },
  printers: { "as-estree": as_estree },  // Still empty at registration time!
};

The problem: The as_estree printer is registered as an empty object ({}) at module load time. The initPrinter function that populates it is called during parse(), but Prettier attempts to use the printer before the first parse completes.

When Prettier calls printer.print(), the as_estree object is still empty because:

  1. Plugin exports are evaluated at module load
  2. as_estree = {} is empty at that point
  3. initPrinter() hasn't been called yet
  4. Prettier tries to use the printer before parsing the first file

Additional Issue: Parser Scope

The plugin overrides the built-in typescript parser globally:

export default {
  parsers: {
    typescript: { /* ... */ },  // Replaces Prettier's typescript parser!
  },
  // ...
};

This means when the plugin is active, all .ts files are processed with the AssemblyScript parser, not just files in assembly/ directories. This is problematic for monorepos or projects with both regular TypeScript and AssemblyScript.

Suggested Fix

The printer should be initialized synchronously at module load time, not lazily during parse:

import pluginTypescript from "prettier/plugins/typescript";
import { magic, preProcess } from "./replace.js";
import { builders } from "prettier/doc";

// Get the estree printer synchronously at module load
const tsPlugin = pluginTypescript;

const as_estree = {
  // Copy all methods from the TypeScript estree printer
  ...(() => {
    const estree = tsPlugin.printers?.estree;
    return typeof estree === "function" ? null : estree;  // Handle lazy loading differently
  })(),

  printComment(commentPath, options) {
    const comment = commentPath.getValue().value;
    if (comment.startsWith(magic) && comment.endsWith(magic)) {
      const doc = [];
      if (commentPath.stack[commentPath.stack.length - 2] === 0) {
        doc.push(builders.hardline);
      }
      doc.push(comment.slice(magic.length, -magic.length));
      return doc;
    }
    // Fall back to original printComment
    return this.originalPrintComment?.(commentPath, options);
  },
};

// ... rest of implementation

Alternatively, use Prettier's newer plugin API that properly supports async printer initialization.

Workaround

Currently, the only workaround is to use .prettierignore to exclude AssemblyScript files:

# .prettierignore
# AssemblyScript files use decorators that prettier doesn't understand
examples/wasm-plugins/*/assembly
packages/assemblyscript-plugin-sdk/assembly

This is not ideal as it means AssemblyScript files are never formatted.

Test Configuration Used

signalk-server (monorepo)
├── .prettierrc.json              # Root config (no plugin)
├── .prettierignore               # Excludes assembly dirs
├── packages/
│   └── assemblyscript-plugin-sdk/
│       └── assembly/
│           └── *.ts              # AssemblyScript files
└── examples/
    └── wasm-plugins/
        └── */assembly/
            └── *.ts              # AssemblyScript files

We tested:

  1. Global plugin configuration - FAILED
  2. Directory-scoped .prettierrc.json with plugin - FAILED
  3. Both versions 3.0.1 and 2.0.2 - BOTH FAILED

Version Information

Tested combinations:

Both versions declare peer dependency prettier: ">=3.0.0-alpha.4" but fail to work with any Prettier 3.x version.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions