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

dwc-gcode-core

v1.23.0

Published

RepRapFirmware-faithful G-code parsing for DuetWebControl plugins: an allocation-light line tokeniser, parameter parsing and in-place rewriting, conditional-G-code (if/var/while/...) recognition, a line-preserving config.g/macro directive editor, firmware

Readme

dwc-gcode-core

G-code parsing for DuetWebControl plugins that follows what RepRapFirmware actually does, not what a G-code line looks like it does.

  • An allocation-light line tokeniser that never splits a comment out of a quoted string (M291 P"done; resuming"), handles RRF's "" escapes and {…} expressions, and skips N line numbers.
  • Parameter parsing and in-place rewriting on index spans, so rewriting one value leaves the rest of the line byte-identical.
  • A small table of what specific commands mean, each entry checked against RRF source at a named release: G10's three forms (tool settings, workplace offset, retraction), M568/G10 tool temperatures including per-heater colon lists, and which commands' P is a tool number.
  • A line-preserving config.g/macro directive editor (dwc-gcode-core/edit) — find a directive (active or commented-out), replace one parameter or the whole line, append one if it's missing, or edit across config.g and its M98 includes, always keeping the rest of the file byte-identical.
  • Conditional G-code (RRF's if/var/while/…) recognitionclassifyLine tells a command apart from a meta-command, a comment or blank line, with RRF's own block-nesting indent computed alongside it; parseAssignment reads all four of RRF's variable-mutating forms (var/global declarations, set var./set global. assignments).
  • Firmware version comparison and a small "what changed since when" tableparseFirmwareVersion/ compareFirmwareVersions handle RRF's actual version format (the STM32 port's parenthesised suffix, and its +N build-number tiebreaker for same-prerelease releases); FEATURES/supports() gate on a cited RRF version rather than a magic string repeated at every call site.

Pure TypeScript, zero runtime dependencies, no Vue, Pinia or DWC imports. It is bundled into each plugin that uses it.

Install

npm install dwc-gcode-core

Use

import {
	classifyLine, firmwareAtLeast, g10Form, parseAssignment, parseParams, paramNumber,
	readToolTemperatureSetting, setParam, supports, tokenise, withBody,
} from "dwc-gcode-core";

const token = tokenise("G10 P1 S205:200 R150 ; tool 1");
token.code;                                      // "G10"
g10Form(token.body);                             // "toolSettings" — not a retraction
readToolTemperatureSetting(token.code, token.body);
// { tool: 1, active: [205, 200], standby: [150], setsTemperatures: true, heaterState: null }

paramNumber(parseParams("M116 P"), "P");         // null — a bare P waits for every tool; it is not tool 0
withBody(token, setParam(token.body, "P", "2")); // "G10 P2 S205:200 R150 ; tool 1"

classifyLine("if sensors.gpIn[0].value = 1");    // { kind: "meta", meta: "if", indent: 0 }
classifyLine("  G28 Z");                         // { kind: "command", command: {...}, indent: 2 }
parseAssignment("set global.T1heat=heat.heaters[1].active");
// { form: "set", scope: "global", name: "T1heat", expression: "heat.heaters[1].active", ... }

supports(board.firmwareVersion, "m568");           // true from RRF 3.3 onward
firmwareAtLeast(board.firmwareVersion, "3.7.0-rc.1"); // strips a real board's "3.7.0-rc.1(CAN0)" first

Each module is also its own entry point (dwc-gcode-core/lex, /params, /meta, /document, /edit, /expr/parse, /expr/tables, /files/kinds, /files/menu, /files/heightmap, /firmware, /commands/g10, /commands/toolParams, /dictionary/commands, /dictionary/schema, /objectmodel/versions, /objectmodel/schema, /releases/schema, /releases/changes, /releases/impact, /diagnostics/schema, /diagnostics/rules, /diagnostics/diagnose, /diagnostics/monaco, /project, /compare, /rrf, /version, /stamp — see docs/api.md for every export, subpath by subpath), and the package is marked side-effect free, so a bundler keeps only what a plugin imports. /edit and /dictionary/* are subpath-only, not re-exported from the root/edit's own setParam (rewrites a parameter on a raw config.g line) is a different function from the root's setParam (rewrites a parameter on an already-tokenised command body) that happens to share a name, and /dictionary/schema's ParamKind (the dictionary's parameter-kind enum) likewise collides by name with /lex's own ParamKind (a lexed parameter's syntactic shape); import config-file editing explicitly:

import { findDirectives, parseLines, planDirectiveEdit, setParam } from "dwc-gcode-core/edit";

const plan = planDirectiveEdit(configText, "M572", { D: "0" }, (raw) => setParam(raw, "S", "0.045"), "M572 D0 S0.045", "note");
plan.after;   // the whole file's new text, or unchanged with plan.blocked set if the line isn't safe to touch
plan.diff;    // line-level diff for a preview UI

The command dictionary

What each command's parameters are — letter, kind, whether it takes a colon list or an expression, required-ness, value enums, deprecation — cited to a named RRF release rather than guessed from a pattern. 280 commands are known; every command a real slicer or config.g actually uses ("tier 1") is reviewed against RRF 3.7.0-rc.1 source, the rest are drafted from @duet3d/monacotokens pending review (dictionary/coverage.json tracks exactly which is which — see docs/tasks/10-dictionary.md).

import { commandSpec } from "dwc-gcode-core/dictionary/commands";

const spec = commandSpec("M568");
spec?.parameters.find((p) => p.letter === "P");
// { letter: "P", kind: "toolNumber", list: false, expressionAllowed: true, ... }
spec?.reviewed; // "3.7.0-rc.1" for a reviewed entry, undefined for a drafted-only one

src/commands/toolParams.ts's TOOL_PARAM_COMMANDS (which commands' parameter is a real tool number, for a tool-renumbering pass) is derived from this dictionary's reviewed toolNumber parameters, rather than hand-maintained.

The object model

Whether an object-model path used in an expression or a condition (move.axes[0].homed, heat.heaters[1].current) exists at a given RRF release, since/until which release, and whether it's deprecated — paths are added, removed and deprecated between releases, and a macro written against one release can silently break on another.

import { objectModelChanges, objectModelPath } from "dwc-gcode-core/objectmodel/schema";

objectModelPath("move.motionSystems", "3.6.3");     // { known: false } - added later
objectModelPath("move.motionSystems", "3.7.0-rc.1"); // { known: true, since: "3.7.0-beta.1" }
objectModelPath("heat.bedHeaters", "3.7.0-rc.1");    // { known: true, deprecated: "use bedHeaterMapping instead" }

objectModelChanges("3.6.3", "3.7.0-rc.1");
// [{ path: "move.motionSystems", change: "added", version: "3.7.0-beta.1" }, ...]
objectModelChanges("3.7.0-rc.1", "3.6.3"); // the same change, the other way: "removed"

709 paths are tracked across every RRF release in this package's window that @duet3d/objectmodel published a matching version for (3.6.3, 3.7.0-beta.13.7.0-rc.1); 3.7.0-alpha.2 is a known RRF tag with no usable object-model source for it (see docs/tasks/11-object-model-schema.md) and is listed in OBJECT_MODEL_VERSIONS with hasData: false rather than silently guessed at.

Evaluating expressions and simulating execution

expr/parse.ts only parses {...} expressions into an AST — it "never evaluates anything ... no object model, no variable values" (its own doc comment). expr/evaluate.ts is the practical subset of RRF's real evaluator built on top of that AST: comparisons, short-circuit &&/||, arithmetic, ^ (concatenation, not exponentiation — see its own doc comment), # (a string's length, or an array's element count — object-model-backed or a plain/variable one; anything else is a real RRF error, not a coercion), the deterministic math functions (abs, floor, pow, mod, max/min, ...), vector/take/drop/find, and exists() (special-cased like real RRF's own parser — true for a declared var/global regardless of its value, or for an object-model path when EvalContext.pathExists is supplied). It deliberately leaves out anything that needs file IO, entropy, wall-clock time or macro-call parameters (fileread, fileexists, random, datetime, param.*, ...) as clean "not supported" errors rather than guessing.

import { evaluateExpression, UnresolvedPathError, type EvalContext } from "dwc-gcode-core/expr/evaluate";
import { parseExpression } from "dwc-gcode-core/expr/parse";

const ctx: EvalContext = {
	resolvePath: (path) => { throw new UnresolvedPathError(path); }, // a live/hardware value we don't have
	resolveVariable: (scope, name) => { throw new Error(`'${scope}.${name}' is not defined`); },
};
evaluateExpression(parseExpression("sensors.gpIn[0].value > 0").ast, ctx);
// { ok: false, kind: "unresolved-path", path: "sensors.gpIn[0].value" }

execute.ts's walkExecution uses that evaluator together with document.ts's blocks tree to determine a file's REAL execution order — which if/elif/else arm actually runs, how many times a while body repeats, break/continue/abort — rather than a flat top-to-bottom line walk. It's a single-pass, synchronous, pure function: given a resolvePath that throws UnresolvedPathError for a value it doesn't have, it stops exactly at the condition that needed it and reports everything executed up to that point; re-running it with a resolvePath that now answers that path picks up from the top and gets further (re-running is simpler and no less correct than trying to resume mid-walk, since the walk is deterministic given the same inputs). This is the seam a caller uses to simulate "what if this sensor read X" without a live machine — prompt for a value, supply it, re-walk.

import { walkExecution } from "dwc-gcode-core/execute";
import { parseDocument } from "dwc-gcode-core/document";

const doc = parseDocument("if sensors.gpIn[0].value > 0\n    G1 X1\nelse\n    G1 X2\nM400\n");
const r = walkExecution(doc, { resolvePath: () => { throw new UnresolvedPathError("sensors.gpIn[0].value"); } });
// { status: "paused", line: 0, path: "sensors.gpIn[0].value", steps: [] }

walkExecution(doc, { resolvePath: (p) => (p === "sensors.gpIn[0].value" ? 1 : 0) });
// { status: "complete", steps: [{ line: 0 }, { line: 1 }, { line: 2 }, { line: 4 }] }

A while loop is capped at maxIterationsPerLoop (default 10 000) — RRF itself has none (it re-seeks the file on every iteration), but an offline simulator can't inherit that unboundedness without risking a hang. An if/elif/while CONDITION can pause a walk on an unresolved path, and so can a blocking M291 on an unanswered message box (below) — an ordinary command's own {...} parameter (e.g. G1 X{sensors.someValue}) is NOT evaluated by walkExecution at all, since unlike those two it doesn't affect which lines run next; a caller deriving machine state from each step evaluates those itself. var is properly block-scoped (a fresh frame per if/elif/else-arm or while-iteration body, popped when it ends, correctly shadowing an outer var of the same name without corrupting it) — global is unaffected, RRF doesn't block-scope it either.

Pass objectModelVersion (one of OBJECT_MODEL_VERSIONS' tracked versions, objectmodel/versions.ts) to validate every referenced path against objectmodel/schema.ts before resolvePath is even called — a path that doesn't exist at that version is a hard "error", not a "paused" (there's no reasonable value to ask for a path that isn't real), and it's what lets exists() answer for an object-model-path argument instead of a "not supported" error:

walkExecution(doc, { resolvePath: () => { throw new UnresolvedPathError("x"); }, objectModelVersion: "3.7.0-rc.1" });
// a typo'd or version-mismatched path is now an "error", not a "paused" waiting for a value

onStep(step) fires synchronously, in order, right after each step is recorded — before walkExecution itself returns. It's the seam for a caller that wants to maintain its own derived state INCREMENTALLY as the walk proceeds (rather than only replaying the final steps array afterwards), so a later condition's resolvePath can answer from something the walk has already gone past — e.g. answering move.axes[0].homed from a G28 seen earlier, instead of always prompting for it.

Blocking message boxes (M291)

messageBox.ts's parseBlockingMessageBox turns an M291 command into a structured prompt — "ok"/ "okCancel" (S2/S3), a value entry ("integer"/"float"/"string", S5/S6/S7, with L/H/F limits and default), or "choice" (S4, from a K-array — K is a full RRF expression in real RRF, so parseBlockingMessageBox alone can't finish it; walkExecution evaluates it internally, with the same live var/global scope a condition would see, before ever pausing). S0/S1 (non-blocking) return null. walkExecution pauses on a blocking one exactly like an unresolved object-model path — supply resolveMessageBox, or throw UnresolvedMessageBoxError to defer:

import { UnresolvedMessageBoxError, walkExecution } from "dwc-gcode-core/execute";

const doc = parseDocument('M291 P"How many?" S5 L0 H10\nif input > 3\n    G1 X1\n');
walkExecution(doc, { resolvePath, resolveMessageBox: () => { throw new UnresolvedMessageBoxError(); } });
// { status: "message-box", line: 0, prompt: { mode: "integer", message: "How many?", min: 0, max: 10, ... } }

walkExecution(doc, { resolvePath, resolveMessageBox: () => ({ input: 7, cancelled: false }) });
// { status: "complete", steps: [...] } - the later `if input > 3` reads the answer via the `input` constant

A choice box's prompt already carries the evaluated choices, not the raw K expression — input after answering one is the CHOSEN INDEX (0-based; this package's own convention, since real RRF just stores whatever M292's own R expression sends back, with no canonical index-vs-string rule of its own):

const doc2 = parseDocument('M291 P"Pick a mode" S4 K{"Fast","Quiet"}\nif input = 1\n    G1 X1\n');
walkExecution(doc2, { resolvePath, resolveMessageBox: () => { throw new UnresolvedMessageBoxError(); } });
// { status: "message-box", prompt: { mode: "choice", choices: ["Fast", "Quiet"], defaultIndex: null, ... } }

Cancelling an "okCancel" box aborts the walk by default — RRF's own default (shouldAbort unless the command carries J2) — matching the existing abort meta-keyword handling; with J2, execution continues instead and result reads -1. result/input/line/iterations (RRF's execution-state named constants) are always live during a walk — no option needed to turn them on.

The project model

The machine's whole SD-card configuration as one graph — which files invoke which others (M98, homing, tool changes, filament changes, pause/resume, and more, every route cited in docs/invocation-table.md), and which numbered/named resource (tool, heater, sensor, fan, axis, endstop, probe, ...) each command defines or references.

import { loadProject } from "dwc-gcode-core/project";

const project = loadProject([
	{ path: "0:/sys/config.g", text: configText },
	{ path: "0:/sys/homeall.g", text: homeallText },
	{ path: "0:/gcodes/print.gcode", text: printText },
]);

project.calls.find((c) => c.via === "G28-homeall");     // { to: "sys/homeall.g", resolved: true, ... }
project.symbols.find((s) => s.type === "tool" && s.id === "0");
// { definitions: [...], uses: [...] } - each site records whether it's inside an `if`/`while`
// (conditional) or written as an `{...}` expression (dynamic) - static analysis only, never evaluated.

Diagnostics

25 cited rules — syntax, structure, dictionary, project, release, menu, data and object-model — each naming the RRF source or wiki passage that justifies it (see docs/diagnostics.md, generated from the rule registry). diagnoseDocument checks a single parsed file; diagnoseProject adds everything that needs the whole SD-card graph (undefined/duplicate resources, missing macro files, order dependencies, menu-file and height-map errors).

import { diagnoseDocument, diagnoseProject } from "dwc-gcode-core/diagnostics/diagnose";
import { toMonacoMarkers } from "dwc-gcode-core/diagnostics/monaco";
import { parseDocument } from "dwc-gcode-core/document";

const doc = parseDocument(configText);
const diags = diagnoseDocument(doc, "0:/sys/config.g", { firmwareVersion: "3.7.0-rc.1" });
// [{ rule: "dictionary/deprecated", severity: "warning", message: "M107 is deprecated...", ... }]

toMonacoMarkers(diags, configText); // 1-based line/column, UTF-16 units - no Monaco import

Run diagnoseProject(project, options) (see "The project model" above for loadProject) to add the project-wide rules on top of every file's own.

Compare

Semantic diff, not textual — directives are matched by an identity key derived from the dictionary's own defining parameters (a tool by its P, a heater by M950's H, an axis mapping by M584's own letter, ...), so a reordered file or one split across includes reads as changed/moved, not wholesale removals and additions. diffText is the separate byte-faithful line diff for callers that want that view too.

import { compareDocuments, diffText } from "dwc-gcode-core/compare";
import { parseDocument } from "dwc-gcode-core/document";

compareDocuments(parseDocument(before), parseDocument(after), { fromVersion: "3.6.3", toVersion: "3.7.0-rc.1" });
// [{ type: "changed", code: "M950", identity: "M950:H0", params: [{ letter: "C", from: "\"out0\"", to: "\"out1\"" }], ... }]

diffText(before, after); // [{ type: "same" | "added" | "removed", text, lineA?, lineB? }, ...]

compareProjects(a, b, options) runs the same matching across a whole loadProject graph, which is what lets a directive that moved from config.g into an included file show up as moved, not a false remove-plus-add.

Release changes

A versioned catalogue of RRF changes — commands, parameters, object-model paths and a handful of expression-syntax features — queryable in either direction, plus matching those changes against a real file to find what it actually uses that changed.

import { changesBetween } from "dwc-gcode-core/releases/changes";
import { impactOf } from "dwc-gcode-core/releases/impact";
import { parseDocument } from "dwc-gcode-core/document";

changesBetween("3.6.3", "3.7.0-rc.1").find((e) => e.id === "m408-removed");
// { id: "m408-removed", version: "3.7.0-alpha.2", kind: "removed", direction: "upgrade", ... }

const doc = parseDocument(configText);
impactOf(doc, "3.6.3", "3.7.0-rc.1");
// [{ event: {...}, direction: "upgrade", line: 4, message: "M408 removed entirely; ... (will stop working ...)" }]

Built from scripts/rrf-triage.mjs's output for the GCodeBuffer/GCodes dispatch subsystems (the two most likely to affect this package) plus every dictionary/object-model entry with version history — the rest of RRF's subsystems and the wiki are deferred; see docs/tasks/12-release-model.md. firmware.ts's FEATURES/supports() are now a thin, named view over this same store.

The stamp

Every file a plugin parses can carry a stamp — one ; comment recording the RRF version it was checked against, the plugin (id + version) that checked it, and this package's own version — so a file can be flagged for re-checking when the firmware, the plugin, or this package itself changes (the user's own words: "in case we need to reparse due to plugin bugs"). One format, owned here, so every plugin reads and writes the same thing.

import { readStamp, recheckReasons, stampable, writeStamp } from "dwc-gcode-core/stamp";
import { classifyFile } from "dwc-gcode-core/files/kinds";

const { kind } = classifyFile("0:/gcodes/benchy.gcode");
if (stampable(kind)) {
	text = writeStamp(text, { rrf: "3.7.0-rc.1", pluginId: "GCodePostProcessor", pluginVersion: "1.2.1", at: new Date().toISOString() }, kind);
}
// ; dwc-gcode-core: checked rrf=3.7.0-rc.1 [email protected] core=0.5.0 at=2026-09-14T10:00:00.000Z

const stamp = readStamp(text);                                        // null if there is no stamp
recheckReasons(stamp, { rrf: currentRrf, pluginId: "GCodePostProcessor", pluginVersion: "1.3.0" });
// [{ kind: "plugin-changed", pluginId: "GCodePostProcessor", from: "1.2.1", to: "1.3.0" }]

Writing replaces an existing stamp in place rather than accumulating one, coexists with the post-processor's own ; postprocessed-by: line (in either order), and survives a BOM or CRLF line endings untouched. writeStamp throws StampNotAllowedError for a file kind that must never be stamped — heightmap.csv and probePoints.csv most importantly: their own loader in RRF requires an exact first line, so a stamp would break loading outright (verified directly against HeightMap::LoadFromFile, not assumed). A value containing a space, % or = (a DWC plugin id may contain a space) is percent-encoded; anything else, including the at timestamp's own colons, is left as plain text.

Tracking RepRapFirmware

RRF_BASELINE (and package.json's rrf.baseline) names the RRF release every citation in this package was checked against — currently 3.7.0-rc.1.

When RRF releases, list the commits that could change what this package must recognise:

npm run triage -- 3.7.0-rc.1 3.7.0 --out docs/rrf-triage/3.7.0-rc.1..3.7.0.md

That lists every commit in the range touching RRF's G-code parser (src/GCodes/GCodeBuffer/) or command dispatch (src/GCodes/GCodes*.cpp), as a checklist. It needs an authenticated gh. Each item is closed as no effect on syntax, recognition added (code + test) or semantic table updated (entry + citation + test). Only a fully closed list moves the baseline, and that commit is tagged rrf-<tag> alongside the usual vX.Y.Z release tags.

docs/wiki-discrepancies.md records where the Duet3D G-code dictionary and RRF source disagree, with evidence from both, so they can be reported upstream.

Legacy webpack/CJS builds (moduleResolution: "node")

This package is ESM-only ("type": "module") and its exports map has no "require" condition. A modern bundler (Vite, current TypeScript with moduleResolution: "bundler"/"node16"/"nodenext") handles this fine. An older webpack/Vue-CLI setup using moduleResolution: "node" (TypeScript's classic algorithm, which doesn't read exports at all) resolves both the bare root specifier and every subpath correctly as of typesVersions' current shape (task 16) — verified against a real build failure, not assumed fixed: test/packaging/legacy/ is a fixture consumer importing the root and every documented subpath under moduleResolution: "node"; npm run test:packaging (also a CI step) copies the built package into its own node_modules and runs tsc --noEmit against it.

History, for anyone hitting this again: the root specifier alone used to fail — confirmed against a real DWC 3.6 build (resonance-lab's own dual DWC 3.6/3.7 target) — with TS2307: ... types exist, but this result could not be resolved under your current 'moduleResolution' setting. The actual cause turned out to be neither the missing "require" condition nor the exports map at all (both were tested and ruled out directly against the fixture): typesVersions' own "*": {"*": ["dist/*"]} wildcard also matches the ROOT specifier under classic resolution (the "subpath" being matched is empty), rewriting it to dist/ with no filename — which fails, shadowing the perfectly good top-level types/main fields entirely. The fix is a second fallback candidate in the same wildcard entry: "*": ["dist/*", "dist/index.d.ts"] — a real subpath still resolves via the first candidate; the root specifier's empty match fails the first candidate and falls through to the second, which is exactly dist/index.d.ts. No exports or main/types change was needed once this was understood.

Licence

GPL-3.0-or-later.