ChartnautDocs

Multi-file packages and imports

A definition can be one file or several. Split it with relative ES module imports (./helpers.js) when a piece of logic is reusable or easier to read alone. Small definitions stay in one file.

imports.use is a different thing: it declares another definition whose events this one builds on. It is never for sharing code between your own files.

Package rules

  • Exactly one entry file. It calls meta({...}) and declares export function onBar(ctx) directly.

  • meta() appears only in the entry file.

  • Only the entry exports onBar, and it must be declared there, not re-exported from a helper.

  • Helpers export functions and const values. They can call events.declare, outputs.declare and indicators.declare.

  • Import paths are relative and end in .js or .mjs: import { classify } from "./classify.js".

  • Files not reachable from the entry can stay in the editor. They do not affect the definition, and lint warns.

In the builder each file has a role: main for the entry, helper for ordinary modules, and library as an optional tag for reusable helpers. A library file is still local to this definition.

Rejected

Code

Trigger

modules/bare-import

A package name or absolute path, such as import x from "lodash"

modules/missing-extension

A relative import without .js or .mjs

modules/cjs-extension

Importing a .cjs file

modules/path-escape

A path that climbs above the definition's root

modules/unresolved-import

A path that matches no file

modules/import-cycle

Files importing each other in a loop

modules/dynamic-import

import()

modules/top-level-await

await at module scope

modules/unsupported-export

export let or export var. Use export const.

runtime/onbar-entry-only

onBar exported from a file other than the entry

modules/unreachable-file

A file nothing imports (warning)

require() and module.exports are CommonJS and do not run: the bundler is strict ESM.

Example

// main.js (entry)
import { classify } from "./classify.js";

meta({ name: "Bar direction", kind: "flow", slug: "bar-direction" });

events.declare({
  id: "classified",
  intent: "signal",
  payload: { direction: { type: { enum: ["up", "down"] }, description: "Bar direction" } },
  paint: "pin",
});

export function onBar(ctx) {
  const direction = classify(ctx);
  if (!direction) return;
  ctx.emit("classified", { direction }, { label: `Bar ${direction}`, price: ctx.close });
}
// classify.js
export function classify(ctx) {
  if (ctx.close > ctx.open) return "up";
  if (ctx.close < ctx.open) return "down";
  return null;
}

A run over history uses the saved files of the whole package. Save after editing any file, helpers included.

imports.use

imports.use({
  flowId: string;    // the other definition's slug
  alias: string;     // local name, unique in this definition
  version?: number;  // pin a version of the other definition
}): void

imports.use records the dependency in the definition's manifest. Neither the chart nor a run over history passes the other definition's events to this one, so a definition cannot react to another definition's events. imports.subscribe is accepted by the runtime but does nothing and is not part of the typed API: do not use it.

For indicator values, including values another definition paints, use indicators.declare, not imports.use.

Rule

Trigger

imports/missing-flow-id

imports.use without flowId

imports/missing-alias

imports.use without alias

imports/duplicate-alias

Two imports with the same alias

Drawing and dependencies in helpers

outputs.declare and indicators.declare work the same in a helper as in the entry. See Drawing on the chart and Declaring indicator dependencies.

How the Agent edits packages

The Definition authoring agent reads the live, unsaved draft with get_flow_files and writes with apply_flow_edits, a set of per-file patches (create, replace, edit, delete). When it renames or moves a file, it updates every importer's path in the same patch set. lint_flow and typecheck_flow then check the whole module graph of the draft.