@transtyle/plugin-kit 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fixture/tokens/brand.tokens.json +50 -0
- package/fixture/transtyle.config.json +6 -0
- package/package.json +26 -0
- package/src/index.js +125 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"option": {
|
|
3
|
+
"color": {
|
|
4
|
+
"$type": "color",
|
|
5
|
+
"blue": { "600": { "$value": "oklch(0.55 0.18 255)" } },
|
|
6
|
+
"gray": {
|
|
7
|
+
"50": { "$value": "oklch(0.985 0.003 255)" },
|
|
8
|
+
"200": { "$value": "oklch(0.9 0.005 255)" },
|
|
9
|
+
"900": { "$value": "oklch(0.2 0.01 255)" },
|
|
10
|
+
"950": { "$value": "oklch(0.145 0.008 255)" }
|
|
11
|
+
},
|
|
12
|
+
"white": { "$value": "oklch(1 0 0)" }
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"semantic": {
|
|
16
|
+
"color": {
|
|
17
|
+
"$type": "color",
|
|
18
|
+
"primary": { "solid": { "$value": "{option.color.blue.600}" } },
|
|
19
|
+
"elevation": {
|
|
20
|
+
"0": {
|
|
21
|
+
"surface": {
|
|
22
|
+
"$value": "{option.color.white}",
|
|
23
|
+
"$extensions": { "transtyle.modes": { "color-scheme": { "dark": "{option.color.gray.950}" } } }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"1": {
|
|
27
|
+
"surface": {
|
|
28
|
+
"$value": "{option.color.gray.50}",
|
|
29
|
+
"$extensions": { "transtyle.modes": { "color-scheme": { "dark": "{option.color.gray.900}" } } }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"text": {
|
|
34
|
+
"base": {
|
|
35
|
+
"$value": "{option.color.gray.900}",
|
|
36
|
+
"$extensions": { "transtyle.modes": { "color-scheme": { "dark": "{option.color.gray.50}" } } }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"border": {
|
|
40
|
+
"$value": "{option.color.gray.200}",
|
|
41
|
+
"$extensions": { "transtyle.modes": { "color-scheme": { "dark": "{option.color.gray.900}" } } }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"radius": { "md": { "$type": "dimension", "$value": "0.5rem" } },
|
|
45
|
+
"font": {
|
|
46
|
+
"sans": { "$type": "fontFamily", "$value": ["system-ui", "sans-serif"] },
|
|
47
|
+
"mono": { "$type": "fontFamily", "$value": ["ui-monospace", "monospace"] }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@transtyle/plugin-kit",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "The executable specification of the Transtyle exporter interface: a conformance suite any exporter (official or third-party) runs against to prove it honors the plugin contract.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@transtyle/core": "0.1.0-alpha.0"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"fixture"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/transtyle/transtyle.git",
|
|
21
|
+
"directory": "packages/plugin-kit"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/transtyle/transtyle#readme",
|
|
24
|
+
"bugs": "https://github.com/transtyle/transtyle/issues",
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @transtyle/plugin-kit — the executable specification of the exporter interface.
|
|
3
|
+
*
|
|
4
|
+
* plugins.md is prose and drifts; this suite is the contract that doesn't.
|
|
5
|
+
* `conformance(plugin)` runs a plugin against a canonical fixture design system
|
|
6
|
+
* and asserts it honors the real interface: a single `emit(normalizedIR, ctx) →
|
|
7
|
+
* { files, coverage }` hook that is deterministic, pure (never mutates the IR),
|
|
8
|
+
* and honest (every coverage class is one of the five). Passing it is what
|
|
9
|
+
* "official" means and what community exporters advertise.
|
|
10
|
+
*
|
|
11
|
+
* The interface it checks is the one all shipped exporters actually implement
|
|
12
|
+
* (ADR-0011 reconciliation) — not the richer resolve/doc/declarative-mapping
|
|
13
|
+
* design that plugins.md once aspired to and no exporter used.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { compile, formatColor, formatHslTriplet, formatHex, contrastRatio, mix } from '@transtyle/core';
|
|
19
|
+
|
|
20
|
+
const FIXTURE = join(dirname(fileURLToPath(import.meta.url)), '..', 'fixture');
|
|
21
|
+
const COVERAGE_CLASSES = new Set(['native', 'derived', 'approximated', 'dropped', 'unsupported']);
|
|
22
|
+
|
|
23
|
+
// A derivation-only loader — the fixture config declares no targets, so compile
|
|
24
|
+
// resolves the IR without ever calling this, but the signature must be present.
|
|
25
|
+
const noopLoader = async () => ({ name: 'noop', optionsSchema: { type: 'object' }, emit: () => ({ files: [], coverage: [] }) });
|
|
26
|
+
|
|
27
|
+
let _irPromise;
|
|
28
|
+
/** Resolve the bundled fixture to a normalized IR (cached across calls). */
|
|
29
|
+
export async function fixtureIR() {
|
|
30
|
+
if (!_irPromise) {
|
|
31
|
+
_irPromise = compile({ cwd: FIXTURE, targets: [], emit: false, loadExporter: noopLoader })
|
|
32
|
+
.then((r) => r.normalized);
|
|
33
|
+
}
|
|
34
|
+
return _irPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build a TargetContext equivalent to the one core passes exporters at emit time. */
|
|
38
|
+
function makeCtx() {
|
|
39
|
+
return {
|
|
40
|
+
config: { name: 'conformance-fixture', targets: {} },
|
|
41
|
+
targetConfig: { output: 'dist', options: {} },
|
|
42
|
+
formatColor, formatHslTriplet, formatHex, contrastRatio, mix,
|
|
43
|
+
projectName: 'conformance-fixture',
|
|
44
|
+
siblings: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Values-and-provenance snapshot of the IR, for the mutation check + equality. */
|
|
49
|
+
function snapshotIR(ir) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [mode, map] of Object.entries(ir.modes)) {
|
|
52
|
+
out[mode] = {};
|
|
53
|
+
for (const [slot, entry] of map) out[mode][slot] = { value: entry.value, kind: entry.provenance?.kind };
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(out);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {object} plugin the exporter's default export ({ name, emit, optionsSchema? })
|
|
60
|
+
* @param {{ manifest?: object, ir?: object }} [opts] manifest = the package.json `transtyle` key
|
|
61
|
+
* @returns {Promise<{ pass: boolean, checks: Array<{ name, pass, spec, detail? }> }>}
|
|
62
|
+
*/
|
|
63
|
+
export async function conformance(plugin, opts = {}) {
|
|
64
|
+
const checks = [];
|
|
65
|
+
const add = (name, pass, spec, detail) => checks.push({ name, pass: !!pass, spec, ...(pass ? {} : { detail }) });
|
|
66
|
+
const done = () => ({ pass: checks.every((c) => c.pass), checks });
|
|
67
|
+
|
|
68
|
+
add('interface-shape',
|
|
69
|
+
plugin && typeof plugin.name === 'string' && typeof plugin.emit === 'function',
|
|
70
|
+
'plugins.md#the-exporter-interface',
|
|
71
|
+
'default export must be { name: string, emit: function }');
|
|
72
|
+
if (!plugin || typeof plugin.emit !== 'function') return done();
|
|
73
|
+
|
|
74
|
+
const ir = opts.ir ?? await fixtureIR();
|
|
75
|
+
const ctx = makeCtx();
|
|
76
|
+
const before = snapshotIR(ir);
|
|
77
|
+
|
|
78
|
+
let out1, threw;
|
|
79
|
+
try { out1 = plugin.emit(ir, ctx); } catch (e) { threw = e; }
|
|
80
|
+
add('emit-runs', !threw, 'plugins.md#the-exporter-interface', threw && `emit() threw: ${threw.message}`);
|
|
81
|
+
if (threw) return done();
|
|
82
|
+
|
|
83
|
+
add('emit-returns-files',
|
|
84
|
+
Array.isArray(out1.files) && out1.files.every((f) => f && typeof f.path === 'string' && typeof f.contents === 'string' && typeof f.kind === 'string'),
|
|
85
|
+
'plugins.md#the-exporter-interface',
|
|
86
|
+
'emit must return files: { path, contents, kind }[]');
|
|
87
|
+
|
|
88
|
+
add('emit-returns-coverage',
|
|
89
|
+
Array.isArray(out1.coverage) && out1.coverage.every((c) => c && typeof c.variable === 'string' && typeof c.slot === 'string' && typeof c.class === 'string'),
|
|
90
|
+
'validation-and-coverage.md',
|
|
91
|
+
'emit must return coverage: { variable, slot, class }[]');
|
|
92
|
+
|
|
93
|
+
add('coverage-classes-valid',
|
|
94
|
+
Array.isArray(out1.coverage) && out1.coverage.every((c) => COVERAGE_CLASSES.has(c.class)),
|
|
95
|
+
'docs/specs/validation-and-coverage.md',
|
|
96
|
+
`every coverage.class must be one of ${[...COVERAGE_CLASSES].join(', ')}`);
|
|
97
|
+
|
|
98
|
+
const out2 = plugin.emit(ir, ctx);
|
|
99
|
+
add('deterministic',
|
|
100
|
+
JSON.stringify(out1.files) === JSON.stringify(out2.files),
|
|
101
|
+
'plugins.md ("emit must be deterministic")',
|
|
102
|
+
'two emit() runs on the same IR produced different files');
|
|
103
|
+
|
|
104
|
+
add('ir-immutable',
|
|
105
|
+
snapshotIR(ir) === before,
|
|
106
|
+
'plugins.md ("exporters receive an immutable IR snapshot")',
|
|
107
|
+
'emit() mutated the IR it was given');
|
|
108
|
+
|
|
109
|
+
if (opts.manifest) {
|
|
110
|
+
const m = opts.manifest;
|
|
111
|
+
add('manifest-valid',
|
|
112
|
+
['exporter', 'importer'].includes(m.kind) && typeof m.name === 'string' && 'irSpec' in m && 'pluginApi' in m && Array.isArray(m.capabilities),
|
|
113
|
+
'plugins.md#packaging',
|
|
114
|
+
'transtyle manifest needs kind (exporter|importer), name, irSpec, pluginApi, capabilities[]');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (plugin.optionsSchema) {
|
|
118
|
+
add('options-schema-shape',
|
|
119
|
+
plugin.optionsSchema.type === 'object',
|
|
120
|
+
'audit A8 / R3',
|
|
121
|
+
'optionsSchema must be a JSON-Schema object ({ type: "object", ... })');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return done();
|
|
125
|
+
}
|