Use the engine in code
You can call Syntope Engine from your own Node.js code today, with three gaps:
- It is not on npm. Do instead: build it from the repository, as described below.
- The API can change between versions. Do instead: pin the commit you build from.
- There is no API reference. Do instead: read
packages/engine/src/token-system/index.ts, which lists everything the main entry point exports. The types ship with the package.
Build and install
You need Node.js 22.12 or newer in the 22.x line, or Node.js 24 or newer.
git clone https://github.com/syntope-eu/syntope-tokens.gitcd syntope-tokensnpm cinpm run build -w @syntope/token-corenpm pack -w packages/engineThis writes syntope-token-core-0.3.0.tgz to the repository root. Install it in your project:
npm install /path/to/syntope-token-core-0.3.0.tgzThe package is ES modules only.
The two entry points
| Import | For |
|---|---|
@syntope/token-core/token-system |
The engine: parsing, validation, resolution, project operations and writing files |
@syntope/token-core/import-api |
The smaller set of functions an import adapter needs to convert another tool’s files |
Nothing else can be imported. Any other path fails with ERR_PACKAGE_PATH_NOT_EXPORTED.
Example: one reference, two themes
Save this as resolve.mjs and run node resolve.mjs.
import { parsePortableTokenProject, resolveCanonicalTokenSystem,} from '@syntope/token-core/token-system';
const resolver = { version: '2025.10', sets: { base: { sources: [{ $ref: 'tokens/base.tokens.json' }] } }, modifiers: { mode: { default: 'light', contexts: { light: [{ $ref: 'tokens/light.tokens.json' }], dark: [{ $ref: 'tokens/dark.tokens.json' }], }, }, }, resolutionOrder: [{ $ref: '#/sets/base' }, { $ref: '#/modifiers/mode' }],};
const base = { color: { $type: 'color', surface: { $value: '{palette.surface}' } },};const light = { palette: { $type: 'color', surface: { $value: { colorSpace: 'srgb', components: [1, 1, 1] } }, },};const dark = { palette: { $type: 'color', surface: { $value: { colorSpace: 'srgb', components: [0.1, 0.1, 0.1] } }, },};
// The engine tracks objects by stable IDs. Your code decides how they are made.const ids = { createTokenSourceId: (location) => `source:${location}`, createTokenId: (sourceId, pointer) => `token:${sourceId}:${pointer}`, createResolverSetId: (name) => `set:${name}`, createResolverModifierId: (name) => `modifier:${name}`,};
const parsed = parsePortableTokenProject( { entrypoint: 'design-tokens.resolver.json', files: { 'design-tokens.resolver.json': JSON.stringify(resolver), 'tokens/base.tokens.json': JSON.stringify(base), 'tokens/light.tokens.json': JSON.stringify(light), 'tokens/dark.tokens.json': JSON.stringify(dark), }, }, ids,);if (!parsed.ok) throw new Error(JSON.stringify(parsed.diagnostics, null, 2));
function surface(input) { const result = resolveCanonicalTokenSystem(parsed.project, input); if (!result.ok) throw new Error(JSON.stringify(result.diagnostics, null, 2)); return result.resolvedTokens.find((token) => token.path === 'color.surface').value;}
console.log(surface());// { colorSpace: 'srgb', components: [ 1, 1, 1 ] }
console.log(surface({ 'modifier:mode': 'dark' }));// { colorSpace: 'srgb', components: [ 0.1, 0.1, 0.1 ] }What to know about it:
- Files are strings. The engine reads nothing from disk. Your code loads the files and passes their text.
- The theme input is keyed by modifier ID, the one your
idsfactory created, and its value is a context name. A modifier without adefaultneeds an explicit choice. - In TypeScript, wrap the strings in the factory with
asTokenSourceId,asTokenId,asResolverSetIdandasResolverModifierId. - Failures are data. Both calls return
ok: falsewithdiagnosticsinstead of throwing. Each diagnostic has acodeand amessage. - One broken reference refuses the whole input. To get the tokens that do resolve, call
resolveCanonicalTokenSystemWithBrokenReferences. Its result addsbrokenandfailedlists.
Write files
Add this to the end of resolve.mjs, and add serializePortableTokenProject to the import.
const written = serializePortableTokenProject(parsed.project, 'multi-file');if (!written.ok) throw new Error(JSON.stringify(written.diagnostics, null, 2));console.log(Object.keys(written.package.files));// [ 'design-tokens.resolver.json', 'tokens/base.tokens.json', ... ]written.package.files maps each path to its JSON text. Use 'bundled' instead of 'multi-file' for one self-contained Resolver file. Written files carry Syntope’s DTCG extensions. Broken references stay as written and are listed in written.diagnostics.
Change a project
createCanonicalToken, updateCanonicalToken, deleteCanonicalToken and applyCanonicalTokenBatch take a project and return { ok, project } with a new project, or { ok: false, error }. The project you pass in is not changed.
deleteCanonicalToken refuses to break a reference. Passing { force: true } removes the token anyway and leaves those references broken.
Read other inputs
parseCanonicalImportArtifacts tries the adapters you give it. With DTCG_IMPORT_ADAPTER it accepts one DTCG Format document, a bundled Resolver project or a project package, and works out which it has. To read Tokens Studio exports as well, build that adapter in the repository and install both archives in one command:
npm run build -w @syntope/import-tokens-studionpm pack -w packages/import-tokens-studionpm install /path/to/syntope-token-core-0.3.0.tgz /path/to/syntope-import-tokens-studio-0.1.0.tgzimport { DTCG_IMPORT_ADAPTER, parseCanonicalImportArtifacts, resolveCanonicalTokenSystem,} from '@syntope/token-core/token-system';import { TOKENS_STUDIO_IMPORT_ADAPTER } from '@syntope/import-tokens-studio';
const ids = { createTokenSourceId: (location) => `source:${location}`, createTokenId: (sourceId, pointer) => `token:${sourceId}:${pointer}`, createResolverSetId: (name) => `set:${name}`, createResolverModifierId: (name) => `modifier:${name}`,};
const exported = { core: { blue: { value: '#0066ff', type: 'color' }, primary: { value: '{blue}', type: 'color' }, }, $themes: [], $metadata: { tokenSetOrder: ['core'] },};
const imported = parseCanonicalImportArtifacts( { entrypoint: 'tokens.json', files: { 'tokens.json': JSON.stringify(exported) } }, ids, [TOKENS_STUDIO_IMPORT_ADAPTER, DTCG_IMPORT_ADAPTER],);if (!imported.ok) throw new Error(JSON.stringify(imported.diagnostics, null, 2));
console.log(imported.conversion.converted, 'of', imported.conversion.sourceTokens, 'tokens converted');console.log(resolveCanonicalTokenSystem(imported.project).ok);Keep DTCG_IMPORT_ADAPTER last, because it accepts every upload. After a conversion, imported.conversion.messages lists what was left out and why.
To support another tool’s format, write an adapter with @syntope/token-core/import-api. An adapter is an object with a parse(upload) function that returns undefined when the files are not its format.
