Skip to content

Use the engine in code

GuidePartial2026-11-09

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.

Terminal window
git clone https://github.com/syntope-eu/syntope-tokens.git
cd syntope-tokens
npm ci
npm run build -w @syntope/token-core
npm pack -w packages/engine

This writes syntope-token-core-0.3.0.tgz to the repository root. Install it in your project:

Terminal window
npm install /path/to/syntope-token-core-0.3.0.tgz

The 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 ids factory created, and its value is a context name. A modifier without a default needs an explicit choice.
  • In TypeScript, wrap the strings in the factory with asTokenSourceId, asTokenId, asResolverSetId and asResolverModifierId.
  • Failures are data. Both calls return ok: false with diagnostics instead of throwing. Each diagnostic has a code and a message.
  • One broken reference refuses the whole input. To get the tokens that do resolve, call resolveCanonicalTokenSystemWithBrokenReferences. Its result adds broken and failed lists.

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:

Terminal window
npm run build -w @syntope/import-tokens-studio
npm pack -w packages/import-tokens-studio
npm install /path/to/syntope-token-core-0.3.0.tgz /path/to/syntope-import-tokens-studio-0.1.0.tgz
import {
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.