@navecss/tokens 0.1.0 → 0.1.1

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.
@@ -2,7 +2,7 @@
2
2
  "formatVersion": 1,
3
3
  "producer": {
4
4
  "name": "@navecss/core",
5
- "version": "0.1.0"
5
+ "version": "0.1.1"
6
6
  },
7
7
  "tokens": [
8
8
  "--nave-border-width-focus",
package/dist/lib/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
- import { build, DuplicateTokenNameRefusal, MissingContractTokensError, SeedIngestRefusal, TokenCollisionRefusal, UsageError, validate, } from "./facade.js";
3
+ import { build, DuplicateTokenNameRefusal, formatVersionSkewFact, MissingContractTokensError, SeedIngestRefusal, TokenCollisionRefusal, UsageError, validate, } from "./facade.js";
4
4
  function normalizationQualifier(seedNormalization) {
5
5
  switch (seedNormalization) {
6
6
  case 'chroma-reduced': {
@@ -78,6 +78,12 @@ async function main() {
78
78
  }
79
79
  const result = await build(parseBuildArgs(rest));
80
80
  console.log(`Built from seed ${result.resolvedSeed}${normalizationQualifier(result.seedNormalization)}: ${result.files.join(', ')}`);
81
+ if (result.versionSkew) {
82
+ const { producerName, ...skew } = result.versionSkew;
83
+ console.error(`Version skew: ${formatVersionSkewFact(producerName, skew)}. The generated file may ` +
84
+ `not carry every custom-property name the installed ${producerName} reads. Reinstall ` +
85
+ `matching versions, then build again.`);
86
+ }
81
87
  return 0;
82
88
  }
83
89
  if (subcommand === 'validate') {
@@ -1,3 +1,4 @@
1
+ import type { CoreContractManifest, ManifestVersionSkew } from './theming/core-contract.ts';
1
2
  export type CoreVersionProbe = {
2
3
  status: 'resolved';
3
4
  version: string;
@@ -9,4 +10,7 @@ export type CoreVersionProbe = {
9
10
  message: string;
10
11
  status: 'unreadable';
11
12
  };
12
- export declare function resolveInstalledCoreVersion(): Promise<CoreVersionProbe>;
13
+ export declare function detectVersionSkew(manifest: CoreContractManifest): Promise<{
14
+ probe: CoreVersionProbe;
15
+ skew: ManifestVersionSkew | undefined;
16
+ }>;
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { checkManifestVersionSkew } from "./theming/core-contract.js";
3
4
  function canResolveCoreDefaultEntry() {
4
5
  try {
5
6
  import.meta.resolve('@navecss/core');
@@ -9,7 +10,7 @@ function canResolveCoreDefaultEntry() {
9
10
  return false;
10
11
  }
11
12
  }
12
- export async function resolveInstalledCoreVersion() {
13
+ async function resolveInstalledCoreVersion() {
13
14
  let resolved;
14
15
  try {
15
16
  resolved = import.meta.resolve('@navecss/core/package.json');
@@ -38,3 +39,8 @@ export async function resolveInstalledCoreVersion() {
38
39
  return { message: error.message, status: 'unreadable' };
39
40
  }
40
41
  }
42
+ export async function detectVersionSkew(manifest) {
43
+ const probe = await resolveInstalledCoreVersion();
44
+ const skew = probe.status === 'resolved' ? checkManifestVersionSkew(manifest, probe.version) : undefined;
45
+ return { probe, skew };
46
+ }
@@ -1,8 +1,10 @@
1
1
  import type { SeedNormalization } from './theming/color-math.ts';
2
+ import type { ManifestVersionSkew } from './theming/core-contract.ts';
2
3
  export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from './errors.ts';
3
4
  export type { SeedNormalization } from './theming/color-math.ts';
4
5
  export type { PerStepOverrides } from './theming/pipeline.ts';
5
6
  export { SeedIngestRefusal } from './theming/seed-ingest.ts';
7
+ export { formatVersionSkewFact } from './validate-report.ts';
6
8
  export interface TokensBuildOptions {
7
9
  seed: string;
8
10
  source?: string;
@@ -14,6 +16,9 @@ export interface TokensBuildResult {
14
16
  seedNormalization: SeedNormalization;
15
17
  files: string[];
16
18
  collidingNames: string[];
19
+ versionSkew?: (ManifestVersionSkew & {
20
+ producerName: string;
21
+ }) | undefined;
17
22
  }
18
23
  export declare function build(options: TokensBuildOptions): Promise<TokensBuildResult>;
19
24
  export interface TokensValidateOptions {
@@ -3,14 +3,14 @@ import { readFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { writeOutputs } from "./builder.js";
5
5
  import { detectCollidingNames, refuseOnCollision } from "./collision.js";
6
- import { resolveInstalledCoreVersion } from "./core-version.js";
6
+ import { detectVersionSkew } from "./core-version.js";
7
7
  import { composeDtcgOutputs } from "./dtcg-outputs.js";
8
8
  import { MissingContractTokensError, UsageError } from "./errors.js";
9
9
  import { readOverrides } from "./overrides.js";
10
10
  import { findPackageRoot } from "./package-root.js";
11
11
  import { formatOklch } from "./theming/color-math.js";
12
12
  import { composeConsumerBuild } from "./theming/consumer-build.js";
13
- import { checkManifestVersionSkew, validateAgainstManifest } from "./theming/core-contract.js";
13
+ import { validateAgainstManifest } from "./theming/core-contract.js";
14
14
  import { shippedThemingPropertyNames } from "./theming/emit.js";
15
15
  import { parseSeed } from "./theming/seed-ingest.js";
16
16
  import { SHIPPED_SEEDS } from "./theming/shipped-seeds.js";
@@ -18,6 +18,7 @@ import { checkManifestFormatSupport, formatValidateReport } from "./validate-rep
18
18
  import { computeMissing, detectSourceKind, namesFromSource, splitMissingBySupply, } from "./validate.js";
19
19
  export { DuplicateTokenNameRefusal, MissingContractTokensError, TokenCollisionRefusal, UsageError, } from "./errors.js";
20
20
  export { SeedIngestRefusal } from "./theming/seed-ingest.js";
21
+ export { formatVersionSkewFact } from "./validate-report.js";
21
22
  const PACKAGE_ROOT = findPackageRoot(import.meta.url);
22
23
  async function readManifest() {
23
24
  const raw = await readFile(path.join(PACKAGE_ROOT, 'dist', 'core-contract.json'), 'utf8');
@@ -37,6 +38,7 @@ export async function build(options) {
37
38
  const missing = validateAgainstManifest(manifest, emittedNames);
38
39
  if (missing.length > 0)
39
40
  throw new MissingContractTokensError(missing);
41
+ const { skew } = await detectVersionSkew(manifest);
40
42
  const theming = composeConsumerBuild({
41
43
  seedInputs: { primary },
42
44
  seeds: {
@@ -63,6 +65,7 @@ export async function build(options) {
63
65
  .map((file) => path.relative(options.outDir, file.destination))
64
66
  .toSorted((a, b) => a.localeCompare(b)),
65
67
  collidingNames,
68
+ versionSkew: skew === undefined ? undefined : { ...skew, producerName: manifest.producer.name },
66
69
  };
67
70
  }
68
71
  export async function validate(options) {
@@ -77,10 +80,7 @@ export async function validate(options) {
77
80
  const content = await readFile(options.source, 'utf8');
78
81
  const notDeclared = computeMissing(manifest, kind, content, { path: options.source });
79
82
  const { missing, supplied } = splitMissingBySupply(kind, notDeclared);
80
- const coreProbe = await resolveInstalledCoreVersion();
81
- const versionSkew = coreProbe.status === 'resolved'
82
- ? checkManifestVersionSkew(manifest, coreProbe.version)
83
- : undefined;
83
+ const { probe: coreProbe, skew: versionSkew } = await detectVersionSkew(manifest);
84
84
  const formatRefusal = checkManifestFormatSupport(manifest);
85
85
  const output = formatValidateReport(manifest, missing, versionSkew, { ...coreProbe, supplied });
86
86
  let exitCode;
@@ -1,15 +1,35 @@
1
1
  export function linesThatBeginInsideAnUnclosedComment(lines) {
2
2
  const beginningsInsideAnUnclosedComment = new Set();
3
- let isInsideAnOpenComment = false;
4
- for (const [index, line] of lines.entries()) {
5
- if (isInsideAnOpenComment)
6
- beginningsInsideAnUnclosedComment.add(index);
7
- isInsideAnOpenComment = isLineStillInsideAnOpenCommentAfterScanning(line, isInsideAnOpenComment);
3
+ const readings = [
4
+ AS_THE_TOKENIZER_READS_IT,
5
+ RESUMING_PAST_AN_UNCLOSED_QUOTE,
6
+ RESUMING_WITH_NO_FURTHER_STRINGS,
7
+ ];
8
+ for (const reading of readings) {
9
+ let isInsideAnOpenComment = false;
10
+ for (const [index, line] of lines.entries()) {
11
+ if (isInsideAnOpenComment)
12
+ beginningsInsideAnUnclosedComment.add(index);
13
+ isInsideAnOpenComment = isLineStillInsideAnOpenComment(line, isInsideAnOpenComment, reading);
14
+ }
8
15
  }
9
16
  return beginningsInsideAnUnclosedComment;
10
17
  }
11
- function isLineStillInsideAnOpenCommentAfterScanning(line, wasInsideAnOpenComment) {
18
+ const AS_THE_TOKENIZER_READS_IT = {
19
+ resumesAtEndOfLineAfterAnUnclosedQuote: true,
20
+ aLaterQuoteCanStillOpenAString: false,
21
+ };
22
+ const RESUMING_PAST_AN_UNCLOSED_QUOTE = {
23
+ resumesAtEndOfLineAfterAnUnclosedQuote: false,
24
+ aLaterQuoteCanStillOpenAString: true,
25
+ };
26
+ const RESUMING_WITH_NO_FURTHER_STRINGS = {
27
+ resumesAtEndOfLineAfterAnUnclosedQuote: false,
28
+ aLaterQuoteCanStillOpenAString: false,
29
+ };
30
+ function isLineStillInsideAnOpenComment(line, wasInsideAnOpenComment, reading) {
12
31
  let isInsideAnOpenComment = wasInsideAnOpenComment;
32
+ let hasPassedAnUnclosedQuoteOnThisLine = false;
13
33
  let cursor = 0;
14
34
  while (cursor < line.length) {
15
35
  if (isInsideAnOpenComment) {
@@ -21,8 +41,17 @@ function isLineStillInsideAnOpenCommentAfterScanning(line, wasInsideAnOpenCommen
21
41
  continue;
22
42
  }
23
43
  const character = line[cursor];
24
- if (character === '"' || character === "'") {
25
- cursor = indexAfterAStringLiteral(line, cursor, character);
44
+ const canThisQuoteOpenAString = isAQuoteCharacter(character) &&
45
+ (reading.aLaterQuoteCanStillOpenAString || !hasPassedAnUnclosedQuoteOnThisLine);
46
+ if (canThisQuoteOpenAString) {
47
+ const afterTheString = indexAfterAStringLiteral(line, cursor, character);
48
+ if (afterTheString === -1) {
49
+ hasPassedAnUnclosedQuoteOnThisLine = true;
50
+ cursor = reading.resumesAtEndOfLineAfterAnUnclosedQuote ? line.length : cursor + 1;
51
+ }
52
+ else {
53
+ cursor = afterTheString;
54
+ }
26
55
  continue;
27
56
  }
28
57
  if (line.startsWith('/*', cursor)) {
@@ -34,6 +63,9 @@ function isLineStillInsideAnOpenCommentAfterScanning(line, wasInsideAnOpenCommen
34
63
  }
35
64
  return isInsideAnOpenComment;
36
65
  }
66
+ function isAQuoteCharacter(character) {
67
+ return character === '"' || character === "'";
68
+ }
37
69
  function indexAfterAStringLiteral(line, quoteIndex, quote) {
38
70
  let cursor = quoteIndex + 1;
39
71
  while (cursor < line.length) {
@@ -45,5 +77,5 @@ function indexAfterAStringLiteral(line, quoteIndex, quote) {
45
77
  return cursor + 1;
46
78
  cursor += 1;
47
79
  }
48
- return line.length;
80
+ return -1;
49
81
  }
@@ -1,13 +1,136 @@
1
1
  import { readdirSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  const NAVE_VAR_PATTERN = /var\((--nave-[a-z0-9-]+)/g;
4
- const BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
4
+ function isIdentifierChar(ch) {
5
+ return ch !== undefined && /[A-Za-z0-9_$-]/.test(ch);
6
+ }
7
+ function findSpanEnd(content, found, offset) {
8
+ return found === -1 ? content.length : found + offset;
9
+ }
10
+ function findUrlTokenEnd(content, start) {
11
+ let end = start;
12
+ while (end < content.length) {
13
+ const ch = content[end];
14
+ if (ch === '\\' && end + 1 < content.length && content[end + 1] !== '\n') {
15
+ end += 2;
16
+ }
17
+ else if (ch === ')') {
18
+ return end + 1;
19
+ }
20
+ else if (['\n', '"', "'"].includes(ch)) {
21
+ return end;
22
+ }
23
+ else {
24
+ end++;
25
+ }
26
+ }
27
+ return end;
28
+ }
29
+ function matchUrlToken(content, index) {
30
+ if (!/^url\(/i.test(content.slice(index, index + 4)))
31
+ return undefined;
32
+ if (isIdentifierChar(content[index - 1]))
33
+ return undefined;
34
+ let i = index + 4;
35
+ while (i < content.length && /\s/.test(content[i]))
36
+ i++;
37
+ if (content[i] === "'" || content[i] === '"')
38
+ return undefined;
39
+ return findUrlTokenEnd(content, index + 4);
40
+ }
41
+ function stepInString(content, index, stack) {
42
+ const frame = stack.at(-1);
43
+ const ch = content[index];
44
+ if (ch === '\\' && index + 1 < content.length) {
45
+ return { index: index + 2, out: ch + content[index + 1] };
46
+ }
47
+ if (ch === '\n' || ch === frame.quote)
48
+ stack.pop();
49
+ return { index: index + 1, out: ch };
50
+ }
51
+ function stepInTemplateText(content, index, stack) {
52
+ const ch = content[index];
53
+ if (ch === '\\' && index + 1 < content.length) {
54
+ return { index: index + 2, out: ch + content[index + 1] };
55
+ }
56
+ if (ch === '`') {
57
+ stack.pop();
58
+ return { index: index + 1, out: ch };
59
+ }
60
+ if (ch === '$' && content[index + 1] === '{') {
61
+ stack.push({ kind: 'interp', depth: 0 });
62
+ return { index: index + 2, out: '${' };
63
+ }
64
+ return { index: index + 1, out: ch };
65
+ }
66
+ function stepBraceInCode(ch, frame, stack) {
67
+ if (frame.kind !== 'interp')
68
+ return;
69
+ if (ch === '{')
70
+ frame.depth++;
71
+ else if (ch === '}' && frame.depth === 0)
72
+ stack.pop();
73
+ else if (ch === '}')
74
+ frame.depth--;
75
+ }
76
+ function didOpenQuoteOrTemplate(ch, stack) {
77
+ if (ch === '`') {
78
+ stack.push({ kind: 'template' });
79
+ return true;
80
+ }
81
+ if (ch === "'" || ch === '"') {
82
+ stack.push({ kind: 'string', quote: ch });
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+ function trySkipCommentOrUrl(content, index) {
88
+ const ch = content[index];
89
+ if (ch === '/' && content[index + 1] === '*') {
90
+ return { index: findSpanEnd(content, content.indexOf('*/', index + 2), 2), out: '' };
91
+ }
92
+ if (ch === '/' && content[index + 1] === '/') {
93
+ return { index: findSpanEnd(content, content.indexOf('\n', index), 0), out: '' };
94
+ }
95
+ const urlEnd = matchUrlToken(content, index);
96
+ if (urlEnd !== undefined)
97
+ return { index: urlEnd, out: content.slice(index, urlEnd) };
98
+ return undefined;
99
+ }
100
+ function stepInCode(content, index, stack) {
101
+ const ch = content[index];
102
+ if (didOpenQuoteOrTemplate(ch, stack))
103
+ return { index: index + 1, out: ch };
104
+ const skipped = trySkipCommentOrUrl(content, index);
105
+ if (skipped)
106
+ return skipped;
107
+ stepBraceInCode(ch, stack.at(-1), stack);
108
+ return { index: index + 1, out: ch };
109
+ }
110
+ function step(content, index, stack) {
111
+ const frame = stack.at(-1);
112
+ if (frame.kind === 'string')
113
+ return stepInString(content, index, stack);
114
+ if (frame.kind === 'template')
115
+ return stepInTemplateText(content, index, stack);
116
+ return stepInCode(content, index, stack);
117
+ }
118
+ function stripComments(content) {
119
+ const stack = [{ kind: 'code' }];
120
+ let out = '';
121
+ let i = 0;
122
+ while (i < content.length) {
123
+ const result = step(content, i, stack);
124
+ out += result.out;
125
+ i = result.index;
126
+ }
127
+ return out;
128
+ }
5
129
  const CONTRACT_AUDIT_PATTERN = /var\((--nave-[a-z0-9-]+)/g;
6
- const CONTRACT_AUDIT_BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
7
130
  export function scanCoreContract(fileContents) {
8
131
  const found = new Set();
9
132
  for (const content of fileContents) {
10
- const stripped = content.replaceAll(BLOCK_COMMENT_RE, '');
133
+ const stripped = stripComments(content);
11
134
  for (const match of stripped.matchAll(NAVE_VAR_PATTERN)) {
12
135
  found.add(match[1]);
13
136
  }
@@ -50,7 +173,7 @@ export function assertContractPreconditions(names, rawSourceText) {
50
173
  if (unprefixed.length > 0) {
51
174
  throw new StaleContractPreconditionError(`these scanned names are not yet prefixed --nave-: ${unprefixed.join(', ')}`);
52
175
  }
53
- const stripped = rawSourceText.replaceAll(CONTRACT_AUDIT_BLOCK_COMMENT_RE, '');
176
+ const stripped = stripComments(rawSourceText);
54
177
  const referenced = new Set(stripped.matchAll(CONTRACT_AUDIT_PATTERN).map((match) => match[1]));
55
178
  const scanned = new Set(names);
56
179
  const unscanned = referenced.difference(scanned);
@@ -1,4 +1,4 @@
1
- import { ADJACENCY, assertNoOrphanedSemanticSlot } from "./adjacency.js";
1
+ import { ADJACENCY, assertCoverageFloor, assertNoFocusableAdjacentToActionFill, assertNoForbiddenAdjacency, assertNoOrphanedSemanticSlot, } from "./adjacency.js";
2
2
  import { assertContrastFloors, assertFloorProvenance, assertNoSameStepViolations, checkSameStepLint, runContrastHarness, } from "./contrast.js";
3
3
  import { assertNoticeIsEmitted, RETHEMING_NOTICE } from "./copy-lint.js";
4
4
  import { TINT_SEED_COMMENT_STEM } from "./emit.js";
@@ -59,6 +59,23 @@ export const ACCESSIBILITY_GUARD_MESSAGE_PROBES = [
59
59
  label: 'the orphaned-semantic-slot output',
60
60
  capture: () => capturedFailureMessage(() => assertNoOrphanedSemanticSlot(['content.brandNew'])),
61
61
  },
62
+ {
63
+ label: 'the adjacency coverage-floor output',
64
+ capture: () => capturedFailureMessage(() => assertCoverageFloor(ADJACENCY.filter((a) => a.subject !== 'border.control'))),
65
+ },
66
+ {
67
+ label: 'the forbidden-adjacency output',
68
+ capture: () => capturedFailureMessage(() => assertNoForbiddenAdjacency([
69
+ { subject: 'content.secondary', against: 'surface.inverse', class: 'text' },
70
+ ])),
71
+ },
72
+ {
73
+ label: 'the focusable-adjacent-to-action-fill output',
74
+ capture: () => capturedFailureMessage(() => assertNoFocusableAdjacentToActionFill([
75
+ ...ADJACENCY,
76
+ { subject: 'border.focus', against: 'action.primary', class: 'non-text' },
77
+ ])),
78
+ },
62
79
  {
63
80
  label: "the same-step lint's real violation output (build-step.ts's runResultGuards)",
64
81
  capture: () => {
@@ -14,6 +14,7 @@ export type CoreVersionReportStatus = {
14
14
  message: string;
15
15
  status: 'unreadable';
16
16
  };
17
+ export declare function formatVersionSkewFact(producerName: string, versionSkew: ManifestVersionSkew): string;
17
18
  export declare function formatValidateReport(manifest: CoreContractManifest, missing: readonly string[], versionSkew: ManifestVersionSkew | undefined, coreVersionStatus?: CoreVersionReportStatus & {
18
19
  supplied?: readonly string[];
19
20
  }): string[];
@@ -7,6 +7,11 @@ export function checkManifestFormatSupport(manifest) {
7
7
  const NAMESPACE_PREAMBLE = 'Contract names below are the emitted CSS custom-property spelling (for example ' +
8
8
  '--nave-color-surface-base). Your DTCG 2025.10 source names the same node with the "--nave-" ' +
9
9
  'prefix dropped and hyphens read as a dotted path (color.surface.base).';
10
+ export function formatVersionSkewFact(producerName, versionSkew) {
11
+ return (`the core contract this @navecss/tokens ships was recorded against ` +
12
+ `${producerName}@${versionSkew.recorded}, but the installed ${producerName} is ` +
13
+ `@${versionSkew.installed}`);
14
+ }
10
15
  const SCOPE_STATEMENT = 'This check is one-directional: it reports every core contract name missing from your ' +
11
16
  'source. A token in your source beyond the contract is never an error.';
12
17
  const NO_INSTALLED_CORE_DISCLAIMER = 'No installed @navecss/core was found, so no version-skew check ran.';
@@ -79,9 +84,8 @@ export function formatValidateReport(manifest, missing, versionSkew, coreVersion
79
84
  }
80
85
  if (versionSkew) {
81
86
  return [
82
- `Version skew: this manifest was recorded against ${manifest.producer.name}@` +
83
- `${versionSkew.recorded}, but the installed ${manifest.producer.name} is @` +
84
- `${versionSkew.installed}. Reinstall matching versions before trusting this result.`,
87
+ `Version skew: ${formatVersionSkewFact(manifest.producer.name, versionSkew)}. Reinstall ` +
88
+ `matching versions before trusting this result.`,
85
89
  ];
86
90
  }
87
91
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navecss/tokens",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "DTCG 2025.10 design tokens for Nave: seed-derived semantic colours, typography, spacing, and the rest of the token layer, built by a first-party DTCG 2025.10 reader.",
5
5
  "keywords": [
6
6
  "css",