@1agh/maude 0.42.0 → 0.44.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// Token-file import (DDR-172). Pure-function coverage: the value-grammar
|
|
2
|
+
// allowlist (the load-bearing CSS-injection closure), the whole-value-only
|
|
3
|
+
// alias resolver, prototype-pollution/structural-depth guards on the JSON
|
|
4
|
+
// flattener, the bespoke CSS tokenizer/locator/patcher, and the two
|
|
5
|
+
// PERMANENT regression requirements the DDR names explicitly: (1) the
|
|
6
|
+
// alias-resolver module import-ban, (2) the theme-block-scoped patch tested
|
|
7
|
+
// against a real dual-theme fixture.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
mkdtempSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
symlinkSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
assertValidDsName,
|
|
23
|
+
flattenJsonTokens,
|
|
24
|
+
ImportTokensError,
|
|
25
|
+
importTokens,
|
|
26
|
+
KNOWN_VARIABLES,
|
|
27
|
+
locateThemeBlock,
|
|
28
|
+
mapTokenNameToVariable,
|
|
29
|
+
normalizeColorspace,
|
|
30
|
+
parseTokenFile,
|
|
31
|
+
patchDeclarationInBlock,
|
|
32
|
+
readTokenFileCapped,
|
|
33
|
+
TOKENS_MAX_BYTES,
|
|
34
|
+
tokenizeCssCustomProperties,
|
|
35
|
+
validateTokenValue,
|
|
36
|
+
} from '../bin/_import-tokens.mjs';
|
|
37
|
+
import { resolveAliases } from '../bin/_import-tokens-alias-resolver.mjs';
|
|
38
|
+
|
|
39
|
+
function assertBlock(block: { bodyStart: number; bodyEnd: number } | null) {
|
|
40
|
+
if (!block) throw new Error('expected locateThemeBlock to return a block');
|
|
41
|
+
return block;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tmpDesignRoot() {
|
|
45
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-import-tokens-'));
|
|
46
|
+
mkdirSync(join(root, '.design', 'system', 'maude', 'preview'), { recursive: true });
|
|
47
|
+
mkdirSync(join(root, '.design', '_history'), { recursive: true });
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DUAL_THEME_CSS = `/* header comment */
|
|
52
|
+
:root,
|
|
53
|
+
.maude[data-theme="dark"] {
|
|
54
|
+
/* ─── Surfaces ─── */
|
|
55
|
+
--bg-0: oklch(0.165 0.012 255); /* canvas bg */
|
|
56
|
+
--accent: oklch(0.680 0.180 268); /* brand accent */
|
|
57
|
+
--space-3: 8px;
|
|
58
|
+
--dur-flip: 140ms;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.maude[data-theme="light"] {
|
|
62
|
+
--bg-0: oklch(0.975 0.004 255);
|
|
63
|
+
--accent: oklch(0.520 0.195 268);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
:root,
|
|
68
|
+
.maude[data-theme="dark"],
|
|
69
|
+
.maude[data-theme="light"] {
|
|
70
|
+
--dur-flip: 1ms;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
function writeConfig(root, { colorSpace } = {}) {
|
|
76
|
+
const config = {
|
|
77
|
+
designRoot: '.design',
|
|
78
|
+
rootClass: 'maude',
|
|
79
|
+
defaultDesignSystem: 'maude',
|
|
80
|
+
...(colorSpace ? { colorSpace } : {}),
|
|
81
|
+
designSystems: [
|
|
82
|
+
{
|
|
83
|
+
name: 'maude',
|
|
84
|
+
path: 'system/maude',
|
|
85
|
+
tokensCssRel: 'system/maude/colors_and_type.css',
|
|
86
|
+
rootClass: 'maude',
|
|
87
|
+
themeDefault: 'dark',
|
|
88
|
+
themes: ['dark', 'light'],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
writeFileSync(join(root, '.design', 'config.json'), JSON.stringify(config, null, 2));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================================
|
|
96
|
+
// Decision 4 — value-grammar allowlist (the load-bearing control)
|
|
97
|
+
// ============================================================================
|
|
98
|
+
|
|
99
|
+
describe('validateTokenValue', () => {
|
|
100
|
+
test('accepts valid hex/oklch/rgb/hsl colors', () => {
|
|
101
|
+
expect(validateTokenValue('--bg-0', '#fff').ok).toBe(true);
|
|
102
|
+
expect(validateTokenValue('--bg-0', '#ffffff').ok).toBe(true);
|
|
103
|
+
expect(validateTokenValue('--accent', 'oklch(0.680 0.180 268)').ok).toBe(true);
|
|
104
|
+
expect(validateTokenValue('--accent', 'rgba(0, 0, 0, 0.5)').ok).toBe(true);
|
|
105
|
+
expect(validateTokenValue('--accent', 'hsl(268 50% 40%)').ok).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('rejects malformed/invalid-length hex', () => {
|
|
109
|
+
expect(validateTokenValue('--bg-0', '#ff').ok).toBe(false);
|
|
110
|
+
expect(validateTokenValue('--bg-0', '#fffff').ok).toBe(false); // 5 digits — invalid CSS hex length
|
|
111
|
+
expect(validateTokenValue('--bg-0', '#gggggg').ok).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('accepts valid dimensions within magnitude bounds', () => {
|
|
115
|
+
expect(validateTokenValue('--space-3', '8px').ok).toBe(true);
|
|
116
|
+
expect(validateTokenValue('--space-3', '0').ok).toBe(true);
|
|
117
|
+
expect(validateTokenValue('--radius-md', '7px').ok).toBe(true);
|
|
118
|
+
expect(validateTokenValue('--type-base', '1rem').ok).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('rejects a grammar-valid-SHAPED but pathological magnitude (Round-1 W3)', () => {
|
|
122
|
+
expect(validateTokenValue('--space-3', '99999999px').ok).toBe(false);
|
|
123
|
+
expect(validateTokenValue('--type-base', '99999rem').ok).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('rejects the shadow-grammar unbalanced-paren bypass PoC (Round-1 B1/F1)', () => {
|
|
127
|
+
const poc = '0px 0px rgba(0,0,0,0), 0px 0px rgba(0(0';
|
|
128
|
+
expect(validateTokenValue('--shadow-md', poc).ok).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('accepts a real multi-layer shadow value', () => {
|
|
132
|
+
expect(validateTokenValue('--shadow-lg', '0 14px 38px rgba(0, 0, 0, 0.56)').ok).toBe(true);
|
|
133
|
+
expect(
|
|
134
|
+
validateTokenValue('--shadow-md', '1px 1px 1px rgba(0,0,0,0.4), 2px 2px 4px rgba(0,0,0,0.2)')
|
|
135
|
+
.ok
|
|
136
|
+
).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('rejects a value containing a newline before grammar matching (Round-1 F2 chain)', () => {
|
|
140
|
+
expect(validateTokenValue('--bg-0', '#fff\n};@import url(//evil)').ok).toBe(false);
|
|
141
|
+
expect(validateTokenValue('--bg-0', '#fff
};@import url(//evil)').ok).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('rejects non-ASCII values outright (printable-ASCII-only pre-filter)', () => {
|
|
145
|
+
expect(validateTokenValue('--font-body', '"Inter "').ok).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('duration and easing grammars', () => {
|
|
149
|
+
expect(validateTokenValue('--dur-flip', '140ms').ok).toBe(true);
|
|
150
|
+
expect(validateTokenValue('--dur-flip', '999999ms').ok).toBe(false); // > 60000 bound
|
|
151
|
+
expect(validateTokenValue('--ease-out', 'cubic-bezier(0.2, 0, 0, 1)').ok).toBe(true);
|
|
152
|
+
expect(validateTokenValue('--ease-out', 'ease-in-out').ok).toBe(true);
|
|
153
|
+
expect(validateTokenValue('--ease-out', 'javascript:alert(1)').ok).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('font-stack grammar rejects unquoted names and CSS-special characters', () => {
|
|
157
|
+
expect(validateTokenValue('--font-body', '"Inter Tight"').ok).toBe(true);
|
|
158
|
+
expect(validateTokenValue('--font-body', 'sans-serif').ok).toBe(true);
|
|
159
|
+
expect(validateTokenValue('--font-body', 'Arial, Helvetica').ok).toBe(false); // intentional — unquoted rejected
|
|
160
|
+
expect(validateTokenValue('--font-body', '"a\\"; } body { background: url(x) "').ok).toBe(
|
|
161
|
+
false
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('an ungoverned family is a hard reject, not a silent pass-through', () => {
|
|
166
|
+
expect(validateTokenValue('--totally-unknown-family', '#fff').ok).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Decision 5 — heuristic mapping
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
describe('mapTokenNameToVariable', () => {
|
|
175
|
+
test('maps common surface/text/accent/status naming patterns', () => {
|
|
176
|
+
expect(mapTokenNameToVariable('color.background.primary', 'color')).toBe('--bg-0');
|
|
177
|
+
expect(mapTokenNameToVariable('color.background.card', 'color')).toBe('--bg-1');
|
|
178
|
+
expect(mapTokenNameToVariable('color.text.secondary', 'color')).toBe('--fg-1');
|
|
179
|
+
expect(mapTokenNameToVariable('color.brand', 'color')).toBe('--accent');
|
|
180
|
+
expect(mapTokenNameToVariable('color.brand.hover', 'color')).toBe('--accent-hover');
|
|
181
|
+
expect(mapTokenNameToVariable('color.status.error', 'color')).toBe('--status-error');
|
|
182
|
+
expect(mapTokenNameToVariable('spacing.3', 'dimension')).toBe('--space-3');
|
|
183
|
+
expect(mapTokenNameToVariable('border-radius.pill', null)).toBe('--radius-pill');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('returns null (unmapped) for an unrecognized path — never guesses', () => {
|
|
187
|
+
expect(mapTokenNameToVariable('acme.proprietary.widget.zorp', null)).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('every mapped result is a KNOWN_VARIABLES member', () => {
|
|
191
|
+
const candidates = [
|
|
192
|
+
'color.bg.0',
|
|
193
|
+
'color.fg.primary',
|
|
194
|
+
'color.accent',
|
|
195
|
+
'spacing.4',
|
|
196
|
+
'radius.lg',
|
|
197
|
+
'font.display',
|
|
198
|
+
];
|
|
199
|
+
for (const c of candidates) {
|
|
200
|
+
const v = mapTokenNameToVariable(c, null);
|
|
201
|
+
if (v) expect(KNOWN_VARIABLES.has(v)).toBe(true);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Decision 2 — whole-value-only alias resolution (pure module)
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
describe('resolveAliases', () => {
|
|
211
|
+
test('resolves a simple whole-value alias chain', () => {
|
|
212
|
+
const { resolved } = resolveAliases({ a: '{b}', b: '{c}', c: '#fff' });
|
|
213
|
+
expect(resolved.a).toBe('#fff');
|
|
214
|
+
expect(resolved.b).toBe('#fff');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('does NOT resolve a partial-string multi-reference value (structural DoS closure)', () => {
|
|
218
|
+
const { resolved, statuses } = resolveAliases({ a: '{b} solid', b: '#fff' });
|
|
219
|
+
expect(resolved.a).toBe('{b} solid'); // passed through as literal, unresolved
|
|
220
|
+
expect(statuses.a).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('detects a direct cycle', () => {
|
|
224
|
+
const { statuses } = resolveAliases({ a: '{b}', b: '{a}' });
|
|
225
|
+
expect(statuses.a).toBe('circular-alias');
|
|
226
|
+
expect(statuses.b).toBe('circular-alias');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('detects a self-reference', () => {
|
|
230
|
+
const { statuses } = resolveAliases({ a: '{a}' });
|
|
231
|
+
expect(statuses.a).toBe('circular-alias');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('rejects an unresolved alias target rather than crashing', () => {
|
|
235
|
+
const { statuses } = resolveAliases({ a: '{nonexistent.path}' });
|
|
236
|
+
expect(statuses.a).toBe('unresolved-alias');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('rejects a chain deeper than maxDepth', () => {
|
|
240
|
+
const tokens = {};
|
|
241
|
+
for (let i = 0; i < 20; i++) tokens[`t${i}`] = `{t${i + 1}}`;
|
|
242
|
+
tokens.t20 = '#fff';
|
|
243
|
+
const { statuses } = resolveAliases(tokens, { maxDepth: 16 });
|
|
244
|
+
expect(statuses.t0).toBe('alias-chain-too-deep');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('does not attempt to open a file/network location for an unresolved alias (module purity — see the import-ban test below)', () => {
|
|
248
|
+
const { statuses } = resolveAliases({ a: '{../../../etc/passwd}' });
|
|
249
|
+
expect(statuses.a).toBe('unresolved-alias');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// PERMANENT test (DDR-172 Decision 2, ethical-hacker F4 / DDR-167's own
|
|
255
|
+
// lesson): the alias-resolver module must import NONE of fs/net/http(s)/
|
|
256
|
+
// dns/child_process — with AND without the `node:` prefix — and must not
|
|
257
|
+
// reference the global fetch/XMLHttpRequest. Enforced by grepping the
|
|
258
|
+
// module's own source, not by trusting the doc comment.
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
describe('alias-resolver module purity (permanent regression test)', () => {
|
|
262
|
+
const BANNED_SPECIFIERS = ['fs', 'net', 'http', 'https', 'dns', 'child_process'];
|
|
263
|
+
|
|
264
|
+
// A real "banned call shape" grep must check CODE, not prose — the
|
|
265
|
+
// module's own doc comment legitimately documents what's banned, using
|
|
266
|
+
// the exact banned words, and that documentation must not trip the check
|
|
267
|
+
// that verifies the ban itself. Mirrors the shape of what a lint-rule-based
|
|
268
|
+
// check would see (comments stripped) more closely than a naive full-text
|
|
269
|
+
// grep would.
|
|
270
|
+
function codeWithoutComments(src: string) {
|
|
271
|
+
return src.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
test('imports none of the banned I/O modules, prefixed or not', () => {
|
|
275
|
+
const src = codeWithoutComments(
|
|
276
|
+
readFileSync(join(import.meta.dir, '..', 'bin', '_import-tokens-alias-resolver.mjs'), 'utf8')
|
|
277
|
+
);
|
|
278
|
+
for (const spec of BANNED_SPECIFIERS) {
|
|
279
|
+
const patterns = [
|
|
280
|
+
new RegExp(`from\\s+['"]node:${spec}['"]`),
|
|
281
|
+
new RegExp(`from\\s+['"]${spec}['"]`),
|
|
282
|
+
new RegExp(`require\\(\\s*['"]node:${spec}['"]\\s*\\)`),
|
|
283
|
+
new RegExp(`require\\(\\s*['"]${spec}['"]\\s*\\)`),
|
|
284
|
+
];
|
|
285
|
+
for (const re of patterns) {
|
|
286
|
+
expect(re.test(src)).toBe(false);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('does not reference fetch or XMLHttpRequest', () => {
|
|
292
|
+
const src = codeWithoutComments(
|
|
293
|
+
readFileSync(join(import.meta.dir, '..', 'bin', '_import-tokens-alias-resolver.mjs'), 'utf8')
|
|
294
|
+
);
|
|
295
|
+
expect(/\bfetch\s*\(/.test(src)).toBe(false);
|
|
296
|
+
expect(/XMLHttpRequest/.test(src)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Decision 3 — prototype-pollution + structural depth guards
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
describe('flattenJsonTokens', () => {
|
|
305
|
+
test('flattens W3C design-tokens ($value/$type, group $type inheritance)', () => {
|
|
306
|
+
const { tokens, types } = flattenJsonTokens({
|
|
307
|
+
color: { $type: 'color', brand: { $value: '#6b6bf0' } },
|
|
308
|
+
});
|
|
309
|
+
expect(tokens['color.brand']).toBe('#6b6bf0');
|
|
310
|
+
expect(types['color.brand']).toBe('color');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('flattens Style-Dictionary (value/type)', () => {
|
|
314
|
+
const { tokens, types } = flattenJsonTokens({
|
|
315
|
+
color: { brand: { value: '#6b6bf0', type: 'color' } },
|
|
316
|
+
});
|
|
317
|
+
expect(tokens['color.brand']).toBe('#6b6bf0');
|
|
318
|
+
expect(types['color.brand']).toBe('color');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('never lets __proto__/constructor/prototype keys pollute the flattened map', () => {
|
|
322
|
+
const evil = JSON.parse(
|
|
323
|
+
'{"__proto__": {"polluted": true}, "color": {"$type":"color","$value":"#fff"}}'
|
|
324
|
+
);
|
|
325
|
+
const { tokens } = flattenJsonTokens(evil);
|
|
326
|
+
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
|
327
|
+
expect(Object.keys(tokens)).not.toContain('__proto__');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('$description and other $-prefixed metadata is never captured (Decision 8 sink elimination)', () => {
|
|
331
|
+
const { tokens } = flattenJsonTokens({
|
|
332
|
+
color: { $description: 'attacker text', brand: { $value: '#fff' } },
|
|
333
|
+
});
|
|
334
|
+
expect(
|
|
335
|
+
Object.values(tokens).some((v) => typeof v === 'string' && v.includes('attacker text'))
|
|
336
|
+
).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('rejects structure nested deeper than the 32-level cap', () => {
|
|
340
|
+
const deep: unknown[] = [];
|
|
341
|
+
let cur = deep;
|
|
342
|
+
for (let i = 0; i < 40; i++) {
|
|
343
|
+
const next: unknown[] = [];
|
|
344
|
+
cur.push(next);
|
|
345
|
+
cur = next;
|
|
346
|
+
}
|
|
347
|
+
expect(() => flattenJsonTokens(deep)).toThrow(/structure-too-deep/);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('a non-string $value (composite token) is left unflattened, not guessed', () => {
|
|
351
|
+
const { tokens } = flattenJsonTokens({
|
|
352
|
+
shadow: { $type: 'shadow', $value: { offsetX: '1px', offsetY: '1px' } },
|
|
353
|
+
});
|
|
354
|
+
expect(tokens.shadow).toBeUndefined();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// Decision 1 — bespoke CSS tokenizer (raw-CSS input format)
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
describe('tokenizeCssCustomProperties', () => {
|
|
363
|
+
test('extracts simple declarations', () => {
|
|
364
|
+
const { tokens } = tokenizeCssCustomProperties(':root { --bg-0: #fff; --fg-0: #111; }');
|
|
365
|
+
expect(tokens['--bg-0']).toBe('#fff');
|
|
366
|
+
expect(tokens['--fg-0']).toBe('#111');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('a `;` or `}` inside a quoted string is not mistaken for a declaration terminator', () => {
|
|
370
|
+
const { tokens } = tokenizeCssCustomProperties(':root { --font-body: "a;b}c"; --fg-0: #111; }');
|
|
371
|
+
expect(tokens['--font-body']).toBe('"a;b}c"');
|
|
372
|
+
expect(tokens['--fg-0']).toBe('#111');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('a `;` or `}` inside a comment is not mistaken for a declaration terminator', () => {
|
|
376
|
+
const { tokens } = tokenizeCssCustomProperties(
|
|
377
|
+
':root { /* has a ; and a } inside */ --bg-0: #fff; }'
|
|
378
|
+
);
|
|
379
|
+
expect(tokens['--bg-0']).toBe('#fff');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('tolerates a trailing per-declaration comment (the real maude DS file shape)', () => {
|
|
383
|
+
const { tokens } = tokenizeCssCustomProperties(
|
|
384
|
+
':root { --bg-0: #fff; /* page bg */\n --bg-1: #eee; /* card bg */\n }'
|
|
385
|
+
);
|
|
386
|
+
expect(tokens['--bg-0']).toBe('#fff');
|
|
387
|
+
expect(tokens['--bg-1']).toBe('#eee');
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test('an escaped quote inside a string does not prematurely end the string', () => {
|
|
391
|
+
const { tokens } = tokenizeCssCustomProperties(
|
|
392
|
+
String.raw`:root { --font-body: "a\"b;c"; --fg-0: #111; }`
|
|
393
|
+
);
|
|
394
|
+
expect(tokens['--fg-0']).toBe('#111');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// Decision 7 — theme-block-scoped locate + patch (PERMANENT dual-theme fixture)
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
describe('locateThemeBlock (Decision 7 — dual-theme + @media-duplication PERMANENT fixture)', () => {
|
|
403
|
+
test('locates the grouped :root + [data-theme="dark"] top-level block', () => {
|
|
404
|
+
const block = locateThemeBlock(DUAL_THEME_CSS, {
|
|
405
|
+
rootClass: 'maude',
|
|
406
|
+
theme: 'dark',
|
|
407
|
+
singleTheme: false,
|
|
408
|
+
});
|
|
409
|
+
expect(block).not.toBeNull();
|
|
410
|
+
const body = DUAL_THEME_CSS.slice(assertBlock(block).bodyStart, assertBlock(block).bodyEnd);
|
|
411
|
+
expect(body).toContain('--accent: oklch(0.680 0.180 268)');
|
|
412
|
+
expect(body).not.toContain('0.520 0.195 268'); // the LIGHT theme's accent value
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test('locates the standalone [data-theme="light"] block, not the dark one', () => {
|
|
416
|
+
const block = locateThemeBlock(DUAL_THEME_CSS, {
|
|
417
|
+
rootClass: 'maude',
|
|
418
|
+
theme: 'light',
|
|
419
|
+
singleTheme: false,
|
|
420
|
+
});
|
|
421
|
+
const body = DUAL_THEME_CSS.slice(assertBlock(block).bodyStart, assertBlock(block).bodyEnd);
|
|
422
|
+
expect(body).toContain('0.520 0.195 268');
|
|
423
|
+
expect(body).not.toContain('0.680 0.180 268');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('excludes the @media-nested duplicate selector (ethical-hacker Round-2 finding)', () => {
|
|
427
|
+
const block = locateThemeBlock(DUAL_THEME_CSS, {
|
|
428
|
+
rootClass: 'maude',
|
|
429
|
+
theme: 'dark',
|
|
430
|
+
singleTheme: false,
|
|
431
|
+
});
|
|
432
|
+
const body = DUAL_THEME_CSS.slice(assertBlock(block).bodyStart, assertBlock(block).bodyEnd);
|
|
433
|
+
// The small top-level block, not the large span including @media's body.
|
|
434
|
+
expect(body.length).toBeLessThan(300);
|
|
435
|
+
expect(body).not.toContain('prefers-reduced-motion');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('single-theme DS falls back to the bare :root block', () => {
|
|
439
|
+
const css = ':root { --bg-0: #fff; }';
|
|
440
|
+
const block = locateThemeBlock(css, { rootClass: 'maude', theme: 'dark', singleTheme: true });
|
|
441
|
+
expect(block).not.toBeNull();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('HARD REJECTS (returns null) when the target theme has no matching block — no fallback', () => {
|
|
445
|
+
const block = locateThemeBlock(DUAL_THEME_CSS, {
|
|
446
|
+
rootClass: 'maude',
|
|
447
|
+
theme: 'midnight',
|
|
448
|
+
singleTheme: false,
|
|
449
|
+
});
|
|
450
|
+
expect(block).toBeNull();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('does not fall back to :root when a multi-theme DS names an unmatched theme', () => {
|
|
454
|
+
// singleTheme=false + no match — even though :root exists in the grouped
|
|
455
|
+
// selector, it must NOT be treated as a fallback target here.
|
|
456
|
+
const block = locateThemeBlock(DUAL_THEME_CSS, {
|
|
457
|
+
rootClass: 'maude',
|
|
458
|
+
theme: 'sepia',
|
|
459
|
+
singleTheme: false,
|
|
460
|
+
});
|
|
461
|
+
expect(block).toBeNull();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('patchDeclarationInBlock', () => {
|
|
466
|
+
test('replaces ONLY the value, preserving name/colon-spacing/comment/semicolon', () => {
|
|
467
|
+
const css = ':root { --accent: oklch(0.680 0.180 268); /* keep me */ }';
|
|
468
|
+
const block = { bodyStart: css.indexOf('{') + 1, bodyEnd: css.lastIndexOf('}') };
|
|
469
|
+
const { css: patched } = patchDeclarationInBlock(
|
|
470
|
+
css,
|
|
471
|
+
block,
|
|
472
|
+
'--accent',
|
|
473
|
+
'oklch(0.500 0.100 100)'
|
|
474
|
+
);
|
|
475
|
+
expect(patched).toBe(':root { --accent: oklch(0.500 0.100 100); /* keep me */ }');
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("force-insert anchors immediately before the block's own closing brace", () => {
|
|
479
|
+
const css = ':root { --bg-0: #fff; }';
|
|
480
|
+
const block = { bodyStart: css.indexOf('{') + 1, bodyEnd: css.lastIndexOf('}') };
|
|
481
|
+
const { css: patched, patched: didPatch } = patchDeclarationInBlock(
|
|
482
|
+
css,
|
|
483
|
+
block,
|
|
484
|
+
'--accent',
|
|
485
|
+
'#123456',
|
|
486
|
+
{
|
|
487
|
+
forceInsert: true,
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
expect(didPatch).toBe(true);
|
|
491
|
+
expect(patched).toContain('--accent: #123456;');
|
|
492
|
+
expect(patched.indexOf('--accent')).toBeLessThan(patched.lastIndexOf('}'));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('without --force-insert, a missing declaration is reported as not-patched, never invented', () => {
|
|
496
|
+
const css = ':root { --bg-0: #fff; }';
|
|
497
|
+
const block = { bodyStart: css.indexOf('{') + 1, bodyEnd: css.lastIndexOf('}') };
|
|
498
|
+
const { patched } = patchDeclarationInBlock(css, block, '--accent', '#123456');
|
|
499
|
+
expect(patched).toBe(false);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('sequential patches in the same block use the returned (shifted) span, not a stale one', () => {
|
|
503
|
+
const css = ':root { --bg-0: #fff; --accent: #000000; }';
|
|
504
|
+
let block = { bodyStart: css.indexOf('{') + 1, bodyEnd: css.lastIndexOf('}') };
|
|
505
|
+
let text = css;
|
|
506
|
+
const r1 = patchDeclarationInBlock(text, block, '--bg-0', '#f0f0f0f0'); // longer value shifts offsets
|
|
507
|
+
text = r1.css;
|
|
508
|
+
block = r1.block;
|
|
509
|
+
const r2 = patchDeclarationInBlock(text, block, '--accent', '#111111');
|
|
510
|
+
expect(r2.patched).toBe(true);
|
|
511
|
+
expect(r2.css).toContain('--bg-0: #f0f0f0f0;');
|
|
512
|
+
expect(r2.css).toContain('--accent: #111111;');
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// ============================================================================
|
|
517
|
+
// Decision 1 — input read (realpath/symlink/size discipline)
|
|
518
|
+
// ============================================================================
|
|
519
|
+
|
|
520
|
+
describe('readTokenFileCapped', () => {
|
|
521
|
+
test('reads a real file', () => {
|
|
522
|
+
const dir = mkdtempSync(join(tmpdir(), 'maude-tokens-read-'));
|
|
523
|
+
const file = join(dir, 'tokens.json');
|
|
524
|
+
writeFileSync(file, '{"a":1}');
|
|
525
|
+
expect(readTokenFileCapped(file)).toBe('{"a":1}');
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('rejects an oversized file', () => {
|
|
529
|
+
const dir = mkdtempSync(join(tmpdir(), 'maude-tokens-read-'));
|
|
530
|
+
const file = join(dir, 'big.json');
|
|
531
|
+
writeFileSync(file, 'x'.repeat(TOKENS_MAX_BYTES + 1));
|
|
532
|
+
expect(() => readTokenFileCapped(file)).toThrow(/cap/);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('rejects a nonexistent path', () => {
|
|
536
|
+
expect(() => readTokenFileCapped('/nonexistent/path/tokens.json')).toThrow();
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('rejects a path containing a symlink component', () => {
|
|
540
|
+
const dir = mkdtempSync(join(tmpdir(), 'maude-tokens-read-'));
|
|
541
|
+
const real = join(dir, 'real.json');
|
|
542
|
+
writeFileSync(real, '{}');
|
|
543
|
+
const link = join(dir, 'link.json');
|
|
544
|
+
symlinkSync(real, link);
|
|
545
|
+
expect(() => readTokenFileCapped(link)).toThrow(/symlink/);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// ============================================================================
|
|
550
|
+
// Decision 8 — new-DS name charset
|
|
551
|
+
// ============================================================================
|
|
552
|
+
|
|
553
|
+
describe('assertValidDsName', () => {
|
|
554
|
+
test('accepts a valid kebab-case name', () => {
|
|
555
|
+
expect(() => assertValidDsName('my-brand-01')).not.toThrow();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
test('rejects a name with control characters / path traversal shapes', () => {
|
|
559
|
+
expect(() => assertValidDsName('evil\n../../x')).toThrow(/invalid-ds-name/);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test('rejects uppercase, spaces, and special characters', () => {
|
|
563
|
+
expect(() => assertValidDsName('My Brand!')).toThrow();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ============================================================================
|
|
568
|
+
// Decision 6 — colorspace normalization
|
|
569
|
+
// ============================================================================
|
|
570
|
+
|
|
571
|
+
describe('normalizeColorspace', () => {
|
|
572
|
+
test('converts hex to oklch', () => {
|
|
573
|
+
const { value } = normalizeColorspace('#6b6bf0', 'oklch');
|
|
574
|
+
expect(value).toMatch(/^oklch\(/);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('passes oklch through unchanged when target is oklch', () => {
|
|
578
|
+
const { value } = normalizeColorspace('oklch(0.680 0.180 268)', 'oklch');
|
|
579
|
+
expect(value).toBe('oklch(0.680 0.180 268)');
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test('reports an unsupported conversion rather than silently passing through (hsl->oklch, Decision 6 named gap)', () => {
|
|
583
|
+
const result = normalizeColorspace('hsl(268 50% 40%)', 'oklch');
|
|
584
|
+
expect(result.skip).toMatch(/unsupported-colorspace-conversion/);
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// ============================================================================
|
|
589
|
+
// End-to-end: importTokens() orchestration against a real dual-theme fixture
|
|
590
|
+
// ============================================================================
|
|
591
|
+
|
|
592
|
+
describe('importTokens (end-to-end, patch mode)', () => {
|
|
593
|
+
test('maps, converts colorspace, patches only the target theme, reports the other theme as skipped', async () => {
|
|
594
|
+
const root = tmpDesignRoot();
|
|
595
|
+
writeConfig(root);
|
|
596
|
+
writeFileSync(join(root, '.design', 'system', 'maude', 'colors_and_type.css'), DUAL_THEME_CSS);
|
|
597
|
+
const tokensFile = join(root, 'tokens.json');
|
|
598
|
+
writeFileSync(
|
|
599
|
+
tokensFile,
|
|
600
|
+
JSON.stringify({
|
|
601
|
+
color: { brand: { $type: 'color', $value: '#123456' } },
|
|
602
|
+
space: { '3': { $type: 'dimension', $value: '12px' } },
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
const result = await importTokens({ inputPath: tokensFile, root, designRootRel: '.design' });
|
|
607
|
+
expect(result.mappedCount).toBe(2);
|
|
608
|
+
|
|
609
|
+
const finalCss = readFileSync(
|
|
610
|
+
join(root, '.design', 'system', 'maude', 'colors_and_type.css'),
|
|
611
|
+
'utf8'
|
|
612
|
+
);
|
|
613
|
+
expect(finalCss).toContain('--space-3: 12px;');
|
|
614
|
+
// Light theme's accent must be untouched.
|
|
615
|
+
expect(finalCss).toContain('--accent: oklch(0.520 0.195 268)');
|
|
616
|
+
|
|
617
|
+
const otherThemeSkips = result.tokens.filter((t: { reason?: string }) =>
|
|
618
|
+
String(t.reason ?? '').includes('themed-token-not-patched')
|
|
619
|
+
);
|
|
620
|
+
expect(otherThemeSkips.length).toBeGreaterThan(0);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('HARD REJECTS with no write when the target theme block cannot be located', async () => {
|
|
624
|
+
const root = tmpDesignRoot();
|
|
625
|
+
writeConfig(root);
|
|
626
|
+
writeFileSync(join(root, '.design', 'system', 'maude', 'colors_and_type.css'), DUAL_THEME_CSS);
|
|
627
|
+
const tokensFile = join(root, 'tokens.json');
|
|
628
|
+
writeFileSync(
|
|
629
|
+
tokensFile,
|
|
630
|
+
JSON.stringify({ color: { brand: { $type: 'color', $value: '#123456' } } })
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
const before = readFileSync(
|
|
634
|
+
join(root, '.design', 'system', 'maude', 'colors_and_type.css'),
|
|
635
|
+
'utf8'
|
|
636
|
+
);
|
|
637
|
+
await expect(
|
|
638
|
+
importTokens({ inputPath: tokensFile, root, designRootRel: '.design', theme: 'midnight' })
|
|
639
|
+
).rejects.toThrow(ImportTokensError);
|
|
640
|
+
const after = readFileSync(
|
|
641
|
+
join(root, '.design', 'system', 'maude', 'colors_and_type.css'),
|
|
642
|
+
'utf8'
|
|
643
|
+
);
|
|
644
|
+
expect(after).toBe(before); // no write occurred
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test('a malformed input file is a clean rejection, not a crash', async () => {
|
|
648
|
+
const root = tmpDesignRoot();
|
|
649
|
+
writeConfig(root);
|
|
650
|
+
writeFileSync(join(root, '.design', 'system', 'maude', 'colors_and_type.css'), DUAL_THEME_CSS);
|
|
651
|
+
const tokensFile = join(root, 'bad.json');
|
|
652
|
+
writeFileSync(tokensFile, '{not valid json');
|
|
653
|
+
await expect(
|
|
654
|
+
importTokens({ inputPath: tokensFile, root, designRootRel: '.design' })
|
|
655
|
+
).rejects.toThrow(ImportTokensError);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
describe('importTokens (end-to-end, --new-ds scaffold mode)', () => {
|
|
660
|
+
test('scaffolds a minimal DS, patches it, and writes a charset-validated config.json entry', async () => {
|
|
661
|
+
const root = tmpDesignRoot();
|
|
662
|
+
writeConfig(root);
|
|
663
|
+
const tokensFile = join(root, 'tokens.json');
|
|
664
|
+
writeFileSync(
|
|
665
|
+
tokensFile,
|
|
666
|
+
JSON.stringify({ color: { brand: { $type: 'color', $value: '#123456' } } })
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
const result = await importTokens({
|
|
670
|
+
inputPath: tokensFile,
|
|
671
|
+
root,
|
|
672
|
+
designRootRel: '.design',
|
|
673
|
+
newDs: 'my-new-brand',
|
|
674
|
+
});
|
|
675
|
+
expect(result.mappedCount).toBe(1);
|
|
676
|
+
expect(existsSync(join(root, '.design', 'system', 'my-new-brand', 'colors_and_type.css'))).toBe(
|
|
677
|
+
true
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
const config = JSON.parse(readFileSync(join(root, '.design', 'config.json'), 'utf8'));
|
|
681
|
+
const entry = config.designSystems.find((d: { name: string }) => d.name === 'my-new-brand');
|
|
682
|
+
expect(entry).toBeDefined();
|
|
683
|
+
// No token-file-derived free text in the description (Decision 8 sink elimination).
|
|
684
|
+
expect(entry.description).not.toContain('123456');
|
|
685
|
+
expect(entry.description).toMatch(/Imported via maude design import-tokens/);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('rejects an invalid --new-ds name before touching the filesystem', async () => {
|
|
689
|
+
const root = tmpDesignRoot();
|
|
690
|
+
writeConfig(root);
|
|
691
|
+
const tokensFile = join(root, 'tokens.json');
|
|
692
|
+
writeFileSync(
|
|
693
|
+
tokensFile,
|
|
694
|
+
JSON.stringify({ color: { brand: { $type: 'color', $value: '#fff' } } })
|
|
695
|
+
);
|
|
696
|
+
await expect(
|
|
697
|
+
importTokens({ inputPath: tokensFile, root, designRootRel: '.design', newDs: 'Evil Name!' })
|
|
698
|
+
).rejects.toThrow(/invalid-ds-name/);
|
|
699
|
+
expect(existsSync(join(root, '.design', 'system', 'Evil Name!'))).toBe(false);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// ============================================================================
|
|
704
|
+
// parseTokenFile — format detection, error messages never echo input content
|
|
705
|
+
// ============================================================================
|
|
706
|
+
|
|
707
|
+
describe('parseTokenFile', () => {
|
|
708
|
+
test('detects JSON vs raw CSS by content', () => {
|
|
709
|
+
expect(parseTokenFile('{"a": {"$value": "#fff", "$type": "color"}}').format).toBe('json');
|
|
710
|
+
expect(parseTokenFile(':root { --bg-0: #fff; }').format).toBe('css');
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test('a parse-error message never echoes the raw input content back', () => {
|
|
714
|
+
const secret = 'TOP_SECRET_MARKER_XYZ';
|
|
715
|
+
try {
|
|
716
|
+
parseTokenFile(`{"a": ${secret} this is not valid json`);
|
|
717
|
+
throw new Error('expected parseTokenFile to throw');
|
|
718
|
+
} catch (err) {
|
|
719
|
+
expect(String((err as Error).message)).not.toContain(secret);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
});
|