Skip to content

Libraries: import / export with pluggable cached import resolvers - #458

Merged
alexgrover merged 5 commits into
devfrom
feature/library-imports
Oct 22, 2026
Merged

alexgrover merged 5 commits into
devfrom
feature/library-imports

Conversation

@alexgrover

@alexgrover alexgrover commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Adds Pine Script library support: import <user>/<library>/<version> [as <alias>], export, and the library() declaration. Library sources come from a pluggable import resolver, cached per import path. Also fixes two existing named-argument bugs that this work exposed (separate commit).

Opened as a draft for review of the resolver API and of the linking approach.

Usage

Import resolvers follow the data provider pattern: a registry of ready instances, or direct instantiation.

import { PineTS, Provider, ImportResolver, GitHubImportResolver } from 'pinets';

// Singleton, like Provider.FMP.configure({ apiKey })
ImportResolver.GitHub.configure({ url: 'https://github.com/<owner>/<repo>/tree/main/libraries', token });

const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '1h', 500);
pineTS.setImportResolver(ImportResolver.GitHub);
await pineTS.run(scriptThatImports);

// Direct instantiation, like new FMPProvider({ apiKey })
pineTS.setImportResolver(new GitHubImportResolver({ url, token }));
Resolver Registry Source
GitHubImportResolver ImportResolver.GitHub repository / folder / file URL; public, or private with a token
FileImportResolver ImportResolver.File local files (Node.js only, stubbed in browser builds like MockProvider)
UrlImportResolver ImportResolver.Url any URL template
MapImportResolver ImportResolver.Map sources held in memory
ChainImportResolver - several resolvers in turn
FunctionImportResolver - a plain function (path, info) => source; setImportResolver(fn) wraps one

Custom resolvers implement IImportResolver or extend BaseImportResolver. Docs: docs/libraries.md.

How it works

1. Resolution (async, before transpiling). PineTS.run() / stream() call Indicator.resolveImports(resolver), which reads the script's imports from its tokens (no full parse), asks the resolver for each, and recurses into each library's own imports. The result ({ path: source }) is passed to the synchronous transpile(source, { libraries }). An Indicator run with another resolver resolves again, and transpiles again if the sources differ.

The resolver classes mirror the providers: an IImportResolver interface (resolve, configure), and an abstract BaseImportResolver<TConfig> whose resolve() handles caching and the fail-early configuration check, then delegates to _resolveNative() (as BaseProvider.getMarketData() delegates to _getMarketDataNative()). The cache keeps one promise per import path: concurrent requests share one call, sources and "unknown library" (null) results are kept (optional cacheDuration), a failure is retried on the next run, and configure() / clearCache() empty it. Sharing a resolver instance shares its cache. request.security / request.security_lower_tf secondary contexts inherit the parent's resolver in markAsSecondary(parent), so they never fetch again.

2. Linking (sync, in pineToJS, on the Pine AST). src/transpiler/pineToJS/linker.ts parses each library and splices its declarations into the importing script's AST where its import statements stood. The rest of the pipeline then only sees ordinary user functions, methods, types and enums, so library code gets per-call-site var / ta.* state, collision renames and method dispatch with no change downstream.

  • Every library declaration is renamed <user>$<library>$<version>$<name>. $ is not a Pine identifier character, so linked names cannot collide with the script's own or another library's.
  • alias.member references are rewritten: alias.f(), alias.Type.new() / .copy(), alias.Type in annotations (alias.Type p, array<alias.Type>), alias.Enum.field, and alias.method(obj, ...) → obj.method(...).
  • Exported methods keep their Pine name (the importer calls them with method syntax, dispatched on the receiver type); non-exported methods are renamed and stay private, unless the name is a built-in method's.
  • Only declarations reachable from the importer are linked (the library's own script code, e.g. demo plots, never is). A library's import that the resolver does not know is therefore only an error when the script uses code that needs it: TradingView's ta library imports TradingView/RelativeValue/3; without it every function except relativeVolume works, and ta.relativeVolume fails with Library "TradingView/ta/14" uses TVrv.averageAtTime() from "TradingView/RelativeValue/3", which could not be resolved.
  • An alias named like a built-in namespace (import TradingView/ta/12 as ta, used by 31 scripts of the last community sweep) reaches the library for the members it exports and the built-ins for everything else.
  • Libraries importing libraries are linked once each, emitted before their importers.

Why not append the transpiled library functions to the script and rely on hoisting: functions would hoist, but types and enums compile to const statements that do not, and linking before codegen is what gives library code the per-call-site state and renames of script code.

Parser. import / export are top-level statements (in a local scope: "import" is only allowed in the global scope); import = 1 and export(x) => x still give TradingView's reserved-name error. Exported function names now count as top-level function names, so float ema2 = ema2(...) inside export ema2(...) no longer shadows the function. library() runs as an indicator declaration, so a library script also runs on its own.

Named-argument fixes (first commit)

Found while comparing a toolkit's linked and inlined runs: a library method called with named arguments returned the wrong value on every bar. Both bugs also affect plain scripts.

Call Expected (Pine binding rules) Before After
trig.enc(a = false, b = true, c = true) with method enc(map<string, bool> m, bool a, bool b, bool c) => (a ? 4 : 0) + (b ? 2 : 0) + (c ? 1 : 0) 3 4 (the named arguments arrived as one positional value) 3
trig.enc(false, c = true, b = false) 1 2 1
f(10, z = 7) with f(float base, float x = 1, float y = 2, float z = 3) => base + x * 100 + y * 10 + z 137 NaN (skipped x, y read as na) 137
a.total(z = 7), same shape as a method 137 NaN 137

Codegen now maps named arguments of obj.method(...) onto the method's parameters after the receiver (not for built-in method names or namespace receivers like label.new(text = ...)), and fills a skipped parameter with its default value — Pine restricts defaults to literals and built-in variables, so evaluating it at the call site is equivalent. A string parameter defaulting to na gets "", as in the signature.

Verification

  • Tests (all fail or do not run on dev):
    • tests/transpiler/import-export-parsing.test.ts: import paths and aliases, invalid paths, local-scope errors, export forms, a library running on its own.
    • tests/imports/library-linking.test.ts: per-call-site state (two calls of a library EMA vs the built-in, two var counters), types / enums / methods both syntaxes, named arguments and defaults, no collisions with the script's own names, tree shaking, built-in-namespace alias, imported enum in input.enum, nested imports, unresolved nested import, error cases, request.security (slice path and full-script fallback, library fetched once).
    • tests/imports/import-resolver.test.ts: BaseImportResolver cache (dedup, null kept, errors retried, cacheDuration, configure / clearCache) and configuration check, the registry, Map lookup order, Chain, Url, GitHub URL forms and configure merging with a stubbed fetch, File, Function, recursive resolveImports, a resolver shared by two instances fetching once, an Indicator run with two resolvers.
    • tests/transpiler/method-named-args.test.ts: the table above.
  • Real libraries (scratch runs, not committed):
    • TradingView's public ta library imported as ta: dema, tema, trima, aroon, changePercent, highestSince, stochFull equal the same formulas written with built-ins on all 577 bars.
    • Two production libraries (a DSL runtime and a trigger-encoding library) used by three toolkit indicators and a backtester: the linked run equals the same script with the library code pasted inline, every plot on every bar (29 194 + 37 841 + 15 140 points; drawing objects equal apart from their id counters). A DSL script exercising comparisons, crossovers, lags, sequences and @valuewhen matches the inlined run on all bars, and its simple conditions match the same conditions written directly in Pine.
    • GitHub resolver live: a public repository via raw.githubusercontent.com (default branch), a private repository through the contents API with a token, a single-file /blob/ URL.
  • Full suite: all committed tests pass. tsc --noEmit reports the same 881 errors on dev and on this branch, none in the changed files; tsc -p tsconfig.dts.json (src) is clean.
  • Bundles: all four dev formats build (same rollup warnings as dev); the CJS and ES bundles link imports with MapImportResolver, FileImportResolver and ImportResolver.File; the browser bundles contain no fs and get the FileImportResolver stub.

Docs

  • docs/libraries.md (new page): import resolvers (registry vs direct instantiation, one section per resolver with its config table, chaining, caching, custom resolvers via IImportResolver / BaseImportResolver), what is supported, limitations.
  • docs/initialization-and-usage.md: a Libraries section (setImportResolver).
  • README.md: libraries in the features, documentation links and roadmap.
  • docs/lang-coverage.md: Imports → done; docs/api-coverage: library() ✅.
  • AGENTS.md: the linker in the Stage 1 pipeline, src/imports/ and tests/imports/ in the file tree; src/transpiler/pineToJS/README.md: the linking step.
  • CHANGELOG.md: Unreleased entries.

Limitations

  • A library runs with the language semantics of the importing script's Pine version. v5 and v6 differ in a few cases (lazy and / or, const int division), which matters when a v6 script imports a v5 library.
  • Library sources are not fetched from TradingView; they come from the resolver.
  • A function declared with a library type as its return type (export alias.Type f() =>) does not parse yet.

Found while testing, unrelated to imports (they fail the same way with the library code inlined): a UDT named like a built-in namespace (type strategy) breaks the strategy() declaration call, and one toolkit's tuple-returning function called inside request.security throws Cannot read properties of undefined (reading '1').


Note

Medium Risk
Large transpiler/runtime surface (linking, resolution, caching) affects any script using imports; named-argument codegen changes touch all user methods/functions with named args.

Overview
Adds Pine Script libraries: import / export, library(), and a new linker step in pineToJS that splices resolved library sources into the script AST (prefixed renames, reachability-based linking, nested imports). Before transpile, PineTS.setImportResolver and resolveImports fetch sources via pluggable, cached resolvers (GitHub, file, URL, map, chain, or a function), mirroring the market-data provider pattern; secondary request.security contexts inherit the same resolver.

Codegen fixes for named arguments on user method calls (obj.m(a = 1)) and for skipped parameters using defaults instead of na.

Docs, API coverage, browser stubs for Node-only FileImportResolver, and tests under tests/imports/ and transpiler suites.

Reviewed by Cursor Bugbot for commit a51b0a1. Bugbot is set up for automated code reviews on this repo. Configure here.

alexgrover and others added 4 commits October 20, 2026 16:29
…rs take their defaults

A user method called with method syntax and named arguments, `obj.m(a = x)`, received the named arguments as one positional value; they now bind to the method's parameters after the receiver, as in the function-syntax call `m(obj, a = x)`. A built-in method name or a namespace receiver (`label.new(text = ...)`) is left alone.

A parameter a named-argument call skips (`f(10, z = 7)` with a defaulted `y`) read as na: the gap was passed as an explicit `undefined`, which reaches the function wrapped as a series and bypasses the default. Codegen now fills it with the parameter's default value, which Pine restricts to a literal or a built-in variable, so it evaluates the same at the call site. A string parameter defaulting to `na` gets the empty string, as in the signature.

Expected values are computed by hand from Pine's binding rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
`import <user>/<library>/<version> [as <alias>]` is parsed (the alias defaults to the library name), and so are `export` before functions, methods, types and enums, and the `library()` declaration, which now also runs as an indicator would.

Library sources come from an import resolver set with `PineTS.setImportResolver`: a function `(path, info) => source`. Before transpiling, `Indicator.resolveImports` collects the script's libraries and theirs; a `CachedImportResolver` requests each path once, shared across runs and with request.security contexts. Ready-made resolvers: `ImportResolver.fromMap`, `fromFiles` (Node.js), `fromUrl`, `fromGitHub` (raw.githubusercontent.com, or the contents API with a token for private repositories) and `chain`.

The linker (pineToJS/linker.ts) splices each library's declarations into the importing script's Pine AST where its imports stood, renamed with a `<user>$<library>$<version>$` prefix (`$` is not a Pine identifier character), and rewrites `alias.member` references: functions, `alias.Type.new()`, type annotations, `alias.Enum.field`, and `alias.method(obj)` as `obj.method()`. The rest of the pipeline sees ordinary user code, so each call site keeps its own `var` / `ta.*` state. Exported methods keep their name for method-syntax dispatch; private ones are renamed. Only declarations reachable from the script are linked, so a library's unresolved import only fails when the script uses code that needs it. An alias named like a built-in namespace keeps reaching the built-ins the library does not export.

Docs: docs/libraries.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
The resolver API was closures returned by an `ImportResolver` object of factories, typed as a function-or-`{ resolve }` union, with caching added silently by an outside wrapper. It is rebuilt like the data providers:

- `IImportResolver` interface (`resolve`, `configure`), mirroring `IProvider`.
- `BaseImportResolver<TConfig>`: `resolve()` drives the cache (one request per import path, concurrent requests shared, errors not kept, optional `cacheDuration`, emptied by `configure()` / `clearCache()`) and the fail-early configuration check, and delegates to `_resolveNative()`, as `getMarketData()` delegates to `_getMarketDataNative()`.
- One class per source, each in its own folder with a typed config and `configure()`: `GitHubImportResolver`, `FileImportResolver`, `UrlImportResolver`, `MapImportResolver`, `ChainImportResolver`, and `FunctionImportResolver` for a plain function.
- `ImportResolver` registry of ready instances (`ImportResolver.GitHub.configure({ url, token })`) and `registerImportResolver`, mirroring `Provider`.
- `FileImportResolver` reads files with `fs` and is stubbed in browser builds, as `MockProvider` is (the rollup plugin now handles both).

Two wiring fixes come with it. A `request.*` secondary context inherits its parent's resolver in `markAsSecondary(parent)` instead of at each call site. An `Indicator` run with another resolver resolves its imports again, and transpiles again if the sources differ; it kept the first resolver's libraries.

Co-authored-by: Cursor <cursoragent@cursor.com>
…S README

The usage guide gets a Libraries section (setImportResolver, the ImportResolver registry and classes, link to the Libraries page); the README lists libraries in its features, documentation links and roadmap; AGENTS.md shows the linker in the Stage 1 pipeline and src/imports/ and tests/imports/ in the file tree; the pineToJS README describes the linking step. The Libraries page tables are aligned.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexgrover
alexgrover marked this pull request as ready for review October 22, 2026 11:35
@alexgrover
alexgrover merged commit f221889 into dev Oct 22, 2026
2 checks passed
@cursor

cursor Bot commented Oct 22, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: fd101a66-4505-430c-ab27-45115136160f)

@github-actions github-actions Bot locked and limited conversation to collaborators Oct 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant