@shapeshift-labs/frontier-lang-css 0.1.8 → 0.1.10
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/README.md +3 -3
- package/dist/dependency-graph.d.ts +60 -0
- package/dist/dependency-graph.js +191 -0
- package/dist/index.d.ts +19 -15
- package/dist/index.js +12 -176
- package/dist/postcss-parser-evidence.js +22 -0
- package/dist/semantic-merge-selector-targets.js +50 -10
- package/dist/semantic-merge.js +9 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -232,11 +232,11 @@ const merge = safeMergeCssSource({
|
|
|
232
232
|
});
|
|
233
233
|
```
|
|
234
234
|
|
|
235
|
-
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, cascade keys, statement-form at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, preserves unchanged statement-form at-rules such as `@layer reset, components;`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts.
|
|
235
|
+
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, `var()` fallback references, animation/keyframe links, font-face links, URL asset references, cascade keys, statement-form at-rules, CSS Modules exports, ICSS edges, scoped cascade graph proof hashes, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent declaration edits by cascade key, including existing scoped `@media` / `@supports` / `@container` / `@layer` declarations when a scoped cascade graph proof hash is supplied, carries source-bound dependency graph evidence into the merge result, preserves unchanged statement-form at-rules such as `@layer reset, components;`, and for `.module.css` files it classifies exported local classes, `composes`, and ICSS import/export records as explicit merge contracts.
|
|
236
236
|
|
|
237
237
|
## Support Boundary
|
|
238
238
|
|
|
239
|
-
- Ready evidence: style rules, selectors, specificity, declarations, custom properties, statement-form at-rules, CSS Modules local exports, generated class-name map coverage, JS/TS use-site graph hashes, composition graph hashes, ICSS graph hashes, scoped cascade graph hashes, source spans, stable hashes.
|
|
239
|
+
- Ready evidence: style rules, selectors, specificity, declarations, source-bound dependency graph hashes for custom properties, `var()` fallbacks, animations/keyframes, font faces, and URL assets, statement-form at-rules, CSS Modules local exports, generated class-name map coverage, JS/TS use-site graph hashes, composition graph hashes, ICSS graph hashes, scoped cascade graph hashes, source spans, stable hashes.
|
|
240
240
|
- Safe merge: independent declarations with non-overlapping cascade keys; unchanged statement-form at-rules preserved in canonical output; existing scoped declaration edits when scoped cascade graph proof is supplied; explicit CSS Modules export additions/deletions when generated class-name and JS/TS use-site graph proof is supplied; composition edits when composition graph proof is supplied; ICSS edits when ICSS graph proof is supplied. Output is a canonical CSS render and not a byte/trivia-preserving claim.
|
|
241
241
|
- Review-only gaps: incomplete generated class-name maps, unproved CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthands without longhand expansion, statement-form at-rule order/condition changes, one-sided or structurally changed scoped cascade under `@media` / `@supports` / `@container` / `@layer`, `@keyframes`, `@font-face`, `@page`, browser layout and render equivalence.
|
|
242
|
-
- Claims: `autoMergeClaim`, `semanticEquivalenceClaim`, `browserCascadeEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false.
|
|
242
|
+
- Claims: dependency graph evidence is an admission/review signal only; `autoMergeClaim`, `semanticEquivalenceClaim`, `browserCascadeEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CssSourceSpan } from './index.js';
|
|
2
|
+
|
|
3
|
+
export interface CssDependencyGraphRecord {
|
|
4
|
+
readonly kind: string;
|
|
5
|
+
readonly cascadeKey?: string;
|
|
6
|
+
readonly property?: string;
|
|
7
|
+
readonly name?: string;
|
|
8
|
+
readonly family?: string;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
readonly sourceKind?: string;
|
|
11
|
+
readonly hasFallback?: boolean;
|
|
12
|
+
readonly fallbackHash?: string;
|
|
13
|
+
readonly targetDefined?: boolean;
|
|
14
|
+
readonly declarationHash?: string;
|
|
15
|
+
readonly ruleHash?: string;
|
|
16
|
+
readonly atRuleHash?: string;
|
|
17
|
+
readonly selectors?: readonly string[];
|
|
18
|
+
readonly scopes?: readonly string[];
|
|
19
|
+
readonly sourceSpan?: CssSourceSpan;
|
|
20
|
+
readonly sourceHash?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CssDependencyGraphRecordSets {
|
|
24
|
+
readonly customPropertyDefinitions?: readonly CssDependencyGraphRecord[];
|
|
25
|
+
readonly customPropertyReferences?: readonly CssDependencyGraphRecord[];
|
|
26
|
+
readonly keyframes?: readonly CssDependencyGraphRecord[];
|
|
27
|
+
readonly animationNameLinks?: readonly CssDependencyGraphRecord[];
|
|
28
|
+
readonly fontFaces?: readonly CssDependencyGraphRecord[];
|
|
29
|
+
readonly fontFaceLinks?: readonly CssDependencyGraphRecord[];
|
|
30
|
+
readonly urlAssetReferences?: readonly CssDependencyGraphRecord[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CssDependencyGraphEvidence {
|
|
34
|
+
readonly kind: 'frontier.lang.cssDependencyGraphEvidence' | 'frontier.lang.cssSafeMergeDependencyGraphEvidence' | string;
|
|
35
|
+
readonly version: 1;
|
|
36
|
+
readonly sourcePath?: string;
|
|
37
|
+
readonly sourceHash?: string;
|
|
38
|
+
readonly hasDependencySurface: boolean;
|
|
39
|
+
readonly dependencySurfaceCount: number;
|
|
40
|
+
readonly dependencyGraphHashPresent: boolean;
|
|
41
|
+
readonly cssDependencyGraphHashPresent?: boolean;
|
|
42
|
+
readonly dependencyGraphHash?: string;
|
|
43
|
+
readonly cssDependencyGraphHash?: string;
|
|
44
|
+
readonly changedDependencySurfaceCount?: number;
|
|
45
|
+
readonly customPropertyDefinitions?: number;
|
|
46
|
+
readonly customPropertyReferences?: number;
|
|
47
|
+
readonly varReferences?: number;
|
|
48
|
+
readonly varFallbackReferences?: number;
|
|
49
|
+
readonly keyframeDefinitions?: number;
|
|
50
|
+
readonly animationNameLinks?: number;
|
|
51
|
+
readonly keyframeLinks?: number;
|
|
52
|
+
readonly fontFaceDefinitions?: number;
|
|
53
|
+
readonly fontFaceLinks?: number;
|
|
54
|
+
readonly urlAssetReferences?: number;
|
|
55
|
+
readonly records?: CssDependencyGraphRecordSets;
|
|
56
|
+
readonly sides?: Readonly<Record<string, CssDependencyGraphEvidence>>;
|
|
57
|
+
readonly browserCascadeEquivalenceClaim?: false;
|
|
58
|
+
readonly browserRenderEquivalenceClaim?: false;
|
|
59
|
+
readonly semanticEquivalenceClaim: false;
|
|
60
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
function createCssDependencyGraphEvidence(records = [], options = {}) {
|
|
2
|
+
const hash = options.hashSemanticValue;
|
|
3
|
+
const definitions = [];
|
|
4
|
+
const references = [];
|
|
5
|
+
const animations = [];
|
|
6
|
+
const keyframes = [];
|
|
7
|
+
const fontFaces = [];
|
|
8
|
+
const fonts = [];
|
|
9
|
+
const assets = [];
|
|
10
|
+
for (const record of records) {
|
|
11
|
+
if (record.kind === 'at-rule' && record.atRuleName === 'keyframes') keyframes.push(keyframeDefinition(record));
|
|
12
|
+
if (record.kind === 'at-rule' && record.atRuleName === 'font-face') {
|
|
13
|
+
for (const family of record.dependencyTokens?.fontFamilies ?? []) fontFaces.push(fontFaceDefinition(record, family));
|
|
14
|
+
for (const url of record.dependencyTokens?.urls ?? []) assets.push(assetReference(record, undefined, url, 'font-face-src'));
|
|
15
|
+
}
|
|
16
|
+
for (const declaration of record.declarations ?? []) {
|
|
17
|
+
if (declaration.property?.startsWith('--')) definitions.push(customPropertyDefinition(record, declaration));
|
|
18
|
+
for (const item of cssVarReferences(declaration.value)) references.push(customPropertyReference(record, declaration, item));
|
|
19
|
+
if (declaration.property === 'animation' || declaration.property === 'animation-name') {
|
|
20
|
+
for (const name of animationNames(declaration.value)) animations.push(animationReference(record, declaration, name));
|
|
21
|
+
}
|
|
22
|
+
if (declaration.property === 'font' || declaration.property === 'font-family') {
|
|
23
|
+
for (const family of fontFamilyNames(declaration.value)) fonts.push(fontReference(record, declaration, family));
|
|
24
|
+
}
|
|
25
|
+
for (const url of cssUrlReferences(declaration.value)) assets.push(assetReference(record, declaration, url, 'declaration-url'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const keyframeNames = new Set(keyframes.map((entry) => entry.name));
|
|
29
|
+
const fontNames = new Set(fontFaces.map((entry) => entry.family));
|
|
30
|
+
const linkedAnimations = animations.map((entry) => ({ ...entry, targetDefined: keyframeNames.has(entry.name) }));
|
|
31
|
+
const linkedFonts = fonts.map((entry) => ({ ...entry, targetDefined: fontNames.has(entry.family) }));
|
|
32
|
+
const dependencySurfaceCount = definitions.length + references.length + linkedAnimations.length + keyframes.length + fontFaces.length + linkedFonts.length + assets.length;
|
|
33
|
+
const graphHash = dependencySurfaceCount ? hash?.({
|
|
34
|
+
kind: 'frontier.lang.css.dependencyGraph.v1',
|
|
35
|
+
definitions,
|
|
36
|
+
references,
|
|
37
|
+
animations: linkedAnimations,
|
|
38
|
+
keyframes,
|
|
39
|
+
fontFaces,
|
|
40
|
+
fonts: linkedFonts,
|
|
41
|
+
assets
|
|
42
|
+
}) : undefined;
|
|
43
|
+
return compactRecord({
|
|
44
|
+
kind: 'frontier.lang.cssDependencyGraphEvidence',
|
|
45
|
+
version: 1,
|
|
46
|
+
sourcePath: options.sourcePath,
|
|
47
|
+
sourceHash: options.sourceHash,
|
|
48
|
+
hasDependencySurface: dependencySurfaceCount > 0,
|
|
49
|
+
dependencySurfaceCount,
|
|
50
|
+
dependencyGraphHashPresent: Boolean(graphHash),
|
|
51
|
+
cssDependencyGraphHashPresent: Boolean(graphHash),
|
|
52
|
+
dependencyGraphHash: graphHash,
|
|
53
|
+
cssDependencyGraphHash: graphHash,
|
|
54
|
+
customPropertyDefinitions: definitions.length,
|
|
55
|
+
customPropertyReferences: references.length,
|
|
56
|
+
varReferences: references.length,
|
|
57
|
+
varFallbackReferences: references.filter((entry) => entry.hasFallback).length,
|
|
58
|
+
keyframeDefinitions: keyframes.length,
|
|
59
|
+
animationNameLinks: linkedAnimations.length,
|
|
60
|
+
keyframeLinks: linkedAnimations.filter((entry) => entry.targetDefined).length,
|
|
61
|
+
fontFaceDefinitions: fontFaces.length,
|
|
62
|
+
fontFaceLinks: linkedFonts.length,
|
|
63
|
+
urlAssetReferences: assets.length,
|
|
64
|
+
records: { customPropertyDefinitions: definitions, customPropertyReferences: references, keyframes, animationNameLinks: linkedAnimations, fontFaces, fontFaceLinks: linkedFonts, urlAssetReferences: assets },
|
|
65
|
+
browserCascadeEquivalenceClaim: false,
|
|
66
|
+
browserRenderEquivalenceClaim: false,
|
|
67
|
+
semanticEquivalenceClaim: false
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergeCssDependencyGraphEvidence(sheets, changed = {}) {
|
|
72
|
+
const sides = Object.fromEntries(Object.entries(sheets).map(([side, sheet]) => [side, sheet.dependencyGraphEvidence ?? emptyGraphEvidence(sheet)]));
|
|
73
|
+
const sideValues = Object.values(sides);
|
|
74
|
+
const hasDependencySurface = sideValues.some((side) => side.hasDependencySurface === true);
|
|
75
|
+
const changedKeys = new Set([...(changed.worker ?? []), ...(changed.head ?? [])].flatMap((change) => [change.before?.key, change.after?.key].filter(Boolean)));
|
|
76
|
+
const dependencyKeys = new Set(sideValues.flatMap(dependencySurfaceKeys));
|
|
77
|
+
return {
|
|
78
|
+
kind: 'frontier.lang.cssSafeMergeDependencyGraphEvidence',
|
|
79
|
+
version: 1,
|
|
80
|
+
hasDependencySurface,
|
|
81
|
+
dependencySurfaceCount: Math.max(0, ...sideValues.map((side) => side.dependencySurfaceCount ?? 0)),
|
|
82
|
+
dependencyGraphHashPresent: hasDependencySurface && sideValues.every((side) => side.hasDependencySurface !== true || side.dependencyGraphHashPresent === true),
|
|
83
|
+
cssDependencyGraphHashPresent: hasDependencySurface && sideValues.every((side) => side.hasDependencySurface !== true || side.cssDependencyGraphHashPresent === true),
|
|
84
|
+
changedDependencySurfaceCount: [...changedKeys].filter((key) => dependencyKeys.has(key)).length,
|
|
85
|
+
sides,
|
|
86
|
+
browserCascadeEquivalenceClaim: false,
|
|
87
|
+
browserRenderEquivalenceClaim: false,
|
|
88
|
+
semanticEquivalenceClaim: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function dependencySurfaceKeys(evidence) {
|
|
93
|
+
const records = evidence.records ?? {};
|
|
94
|
+
return [
|
|
95
|
+
...(records.customPropertyDefinitions ?? []),
|
|
96
|
+
...(records.customPropertyReferences ?? []),
|
|
97
|
+
...(records.animationNameLinks ?? []),
|
|
98
|
+
...(records.fontFaceLinks ?? []),
|
|
99
|
+
...(records.urlAssetReferences ?? [])
|
|
100
|
+
].map((entry) => entry.cascadeKey).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function customPropertyDefinition(record, declaration) {
|
|
104
|
+
return baseDependencyRecord(record, declaration, { kind: 'custom-property-definition', name: declaration.property, valueHash: declaration.valueHash });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function customPropertyReference(record, declaration, item) {
|
|
108
|
+
return baseDependencyRecord(record, declaration, { kind: 'custom-property-reference', name: item.name, fallbackHash: item.fallbackHash, hasFallback: item.hasFallback });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function animationReference(record, declaration, name) {
|
|
112
|
+
return baseDependencyRecord(record, declaration, { kind: 'animation-name-link', name });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fontReference(record, declaration, family) {
|
|
116
|
+
return baseDependencyRecord(record, declaration, { kind: 'font-face-link', family });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assetReference(record, declaration, url, sourceKind) {
|
|
120
|
+
return baseDependencyRecord(record, declaration, { kind: 'url-asset-reference', url, sourceKind });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function keyframeDefinition(record) {
|
|
124
|
+
return { kind: 'keyframes-definition', name: firstCssIdent(record.conditionText), atRuleHash: record.atRuleHash, sourceSpan: record.sourceSpan, sourceHash: record.sourceHash };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function fontFaceDefinition(record, family) {
|
|
128
|
+
return { kind: 'font-face-definition', family, atRuleHash: record.atRuleHash, sourceSpan: record.sourceSpan, sourceHash: record.sourceHash };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function baseDependencyRecord(record, declaration, extra) {
|
|
132
|
+
return compactRecord({
|
|
133
|
+
...extra,
|
|
134
|
+
cascadeKey: declaration?.cascadeKey,
|
|
135
|
+
property: declaration?.property,
|
|
136
|
+
declarationHash: declaration?.declarationHash,
|
|
137
|
+
ruleHash: record.ruleHash,
|
|
138
|
+
selectors: record.selectors,
|
|
139
|
+
scopes: record.scopes ?? [],
|
|
140
|
+
sourceSpan: declaration?.sourceSpan ?? record.sourceSpan,
|
|
141
|
+
sourceHash: record.sourceHash
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cssVarReferences(value) {
|
|
146
|
+
return [...String(value ?? '').matchAll(/\bvar\(\s*(--[-_A-Za-z][\w-]*)(?:\s*,\s*([^)]*))?\)/g)].map((match) => ({
|
|
147
|
+
name: match[1],
|
|
148
|
+
hasFallback: match[2] !== undefined,
|
|
149
|
+
fallbackHash: match[2] === undefined ? undefined : stableTextHash(match[2].trim())
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cssUrlReferences(value) {
|
|
154
|
+
return [...String(value ?? '').matchAll(/\burl\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/g)]
|
|
155
|
+
.map((match) => (match[1] ?? match[2] ?? match[3] ?? '').trim())
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function animationNames(value) {
|
|
160
|
+
const normalized = String(value ?? '').replace(/\bvar\(\s*--[-_A-Za-z][\w-]*(?:\s*,\s*([^)]*))?\)/g, (_, fallback) => fallback ?? '');
|
|
161
|
+
return unique(normalized.split(/[\s,]+/).map((part) => part.trim().replace(/[()]+$/g, '')).filter((part) => part && !AnimationKeywords.has(part) && !/^[\d.]+m?s$/.test(part)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function fontFamilyNames(value) {
|
|
165
|
+
return unique(String(value ?? '').split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter((part) => part && !FontKeywords.has(part) && !/^[\d.]+/.test(part)));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function firstCssIdent(value) {
|
|
169
|
+
return /^[-_A-Za-z][\w-]*/.exec(String(value ?? '').trim())?.[0];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emptyGraphEvidence(sheet) {
|
|
173
|
+
return { kind: 'frontier.lang.cssDependencyGraphEvidence', version: 1, sourceHash: sheet.sourceHash, hasDependencySurface: false, dependencySurfaceCount: 0, dependencyGraphHashPresent: false, cssDependencyGraphHashPresent: false, semanticEquivalenceClaim: false };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function stableTextHash(text) {
|
|
177
|
+
let hash = 2166136261;
|
|
178
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
179
|
+
hash ^= text.charCodeAt(index);
|
|
180
|
+
hash = Math.imul(hash, 16777619);
|
|
181
|
+
}
|
|
182
|
+
return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, '0')}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
186
|
+
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
|
|
187
|
+
|
|
188
|
+
const AnimationKeywords = new Set(['none', 'initial', 'inherit', 'unset', 'revert', 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'infinite', 'alternate', 'forwards', 'backwards', 'both', 'normal', 'reverse', 'running', 'paused']);
|
|
189
|
+
const FontKeywords = new Set(['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'inherit', 'initial', 'unset', 'revert']);
|
|
190
|
+
|
|
191
|
+
export { createCssDependencyGraphEvidence, mergeCssDependencyGraphEvidence };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel';
|
|
1
|
+
import type { FrontierLangDocument } from '@shapeshift-labs/frontier-lang-kernel'; import type { CssDependencyGraphEvidence } from './dependency-graph.js';
|
|
2
2
|
|
|
3
3
|
export interface CssProjectionOptions {
|
|
4
4
|
readonly banner?: string;
|
|
@@ -216,7 +216,7 @@ export interface CssSemanticSheet {
|
|
|
216
216
|
readonly sourcePath?: string;
|
|
217
217
|
readonly sourceHash: string;
|
|
218
218
|
readonly records: readonly CssSemanticRecord[];
|
|
219
|
-
readonly cssModules?: CssModuleEvidence;
|
|
219
|
+
readonly cssModules?: CssModuleEvidence; readonly dependencyGraphEvidence: CssDependencyGraphEvidence;
|
|
220
220
|
readonly sheetHash: string;
|
|
221
221
|
readonly summary: Readonly<Record<string, number>>;
|
|
222
222
|
readonly proofGaps: readonly CssSemanticProofGap[];
|
|
@@ -227,7 +227,7 @@ export interface CssSemanticMergeEvidence {
|
|
|
227
227
|
readonly kind: 'frontier.lang.cssSemanticMergeEvidence'; readonly version: 1;
|
|
228
228
|
readonly status: 'ready' | 'needs-review' | string; readonly sourcePath?: string; readonly sourceHash: string; readonly sheetHash: string;
|
|
229
229
|
readonly records: readonly CssSemanticRecord[];
|
|
230
|
-
readonly cssModules?: CssModuleEvidence;
|
|
230
|
+
readonly cssModules?: CssModuleEvidence; readonly dependencyGraphEvidence: CssDependencyGraphEvidence;
|
|
231
231
|
readonly proofGaps: readonly CssSemanticProofGap[];
|
|
232
232
|
readonly autoMergeClaim: false; readonly semanticEquivalenceClaim: false;
|
|
233
233
|
readonly cssModuleGeneratedNameEquivalenceClaim: false; readonly cssModuleUseSiteEquivalenceClaim: false;
|
|
@@ -235,9 +235,7 @@ export interface CssSemanticMergeEvidence {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
export interface CssSafeMergeConflict {
|
|
238
|
-
readonly code: string;
|
|
239
|
-
readonly gateId: 'css-semantic-merge' | string;
|
|
240
|
-
readonly sourcePath?: string;
|
|
238
|
+
readonly code: string; readonly gateId: 'css-semantic-merge' | string; readonly sourcePath?: string;
|
|
241
239
|
readonly details: Readonly<Record<string, unknown>> & { readonly reasonCode: string; readonly conflictKey: string };
|
|
242
240
|
}
|
|
243
241
|
|
|
@@ -256,28 +254,26 @@ export interface CssSafeMergeResult {
|
|
|
256
254
|
readonly baseSheetHash?: string; readonly workerSheetHash?: string; readonly headSheetHash?: string;
|
|
257
255
|
readonly workerChangedDeclarations?: number; readonly headChangedDeclarations?: number;
|
|
258
256
|
readonly workerChangedCssModuleContracts?: number; readonly headChangedCssModuleContracts?: number;
|
|
259
|
-
readonly parserEvidence?: CssSafeMergeParserEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence;
|
|
257
|
+
readonly parserEvidence?: CssSafeMergeParserEvidence; readonly selectorTargetEvidence?: CssSafeMergeSelectorTargetEvidence; readonly dependencyGraphEvidence?: CssDependencyGraphEvidence;
|
|
260
258
|
}
|
|
261
259
|
|
|
262
260
|
export interface CssSafeMergeParserEvidence {
|
|
263
261
|
readonly kind: 'frontier.lang.cssSafeMergeParserEvidence'; readonly version: 1; readonly parserNames: readonly string[];
|
|
264
262
|
readonly sourceCodeLocationInfo: boolean; readonly parserBackedSourceSpans: boolean; readonly parserBackedDeclarationSpans: boolean; readonly parserBackedTriviaHashes: boolean;
|
|
265
|
-
readonly scopedCascadeGraphHashPresent: boolean; readonly parseErrors: number;
|
|
266
|
-
readonly sides: Readonly<Record<string, CssSafeMergeParserSideEvidence>>;
|
|
263
|
+
readonly scopedCascadeGraphHashPresent: boolean; readonly parseErrors: number; readonly sides: Readonly<Record<string, CssSafeMergeParserSideEvidence>>;
|
|
267
264
|
}
|
|
268
265
|
|
|
269
266
|
export interface CssSafeMergeParserSideEvidence {
|
|
270
|
-
readonly parserName: string;
|
|
271
|
-
readonly sourceCodeLocationInfo: boolean; readonly parserBackedSourceSpans: boolean; readonly parserBackedDeclarationSpans: boolean; readonly parserBackedTriviaHashes: boolean;
|
|
267
|
+
readonly parserName: string; readonly sourceCodeLocationInfo: boolean; readonly parserBackedSourceSpans: boolean; readonly parserBackedDeclarationSpans: boolean; readonly parserBackedTriviaHashes: boolean;
|
|
272
268
|
readonly scopedCascadeGraphHashPresent: boolean; readonly parseErrors: number; readonly recordCount: number; readonly declarationCount: number;
|
|
273
269
|
}
|
|
274
270
|
|
|
275
271
|
export interface CssSafeMergeSelectorTargetEvidence {
|
|
276
|
-
readonly kind: 'frontier.lang.cssSafeMergeSelectorTargetEvidence'; readonly version: 1;
|
|
277
|
-
readonly selectorTargetGraphHashPresent: boolean; readonly parserBackedRuleSpans: boolean;
|
|
272
|
+
readonly kind: 'frontier.lang.cssSafeMergeSelectorTargetEvidence'; readonly version: 1; readonly selectorTargetGraphHashPresent: boolean; readonly parserBackedRuleSpans: boolean;
|
|
278
273
|
readonly selectorMoveCount: number; readonly workerSelectorMoves: number; readonly headSelectorMoves: number;
|
|
279
274
|
readonly sides: Readonly<Record<string, CssSafeMergeSelectorTargetSideEvidence>>;
|
|
280
275
|
readonly moves: Readonly<Record<'worker' | 'head', readonly CssSafeMergeSelectorMove[]>>;
|
|
276
|
+
readonly rebasedChangeCount?: number; readonly rebaseProofs?: readonly CssSafeMergeSelectorTargetRebaseProof[];
|
|
281
277
|
}
|
|
282
278
|
|
|
283
279
|
export interface CssSafeMergeSelectorTargetSideEvidence {
|
|
@@ -288,7 +284,15 @@ export interface CssSafeMergeSelectorTargetSideEvidence {
|
|
|
288
284
|
export interface CssSafeMergeSelectorMove {
|
|
289
285
|
readonly side: string; readonly property: string; readonly beforeRuleKey: string; readonly afterRuleKey: string;
|
|
290
286
|
readonly beforeSelectors?: readonly string[]; readonly afterSelectors?: readonly string[]; readonly beforeScopes?: readonly string[]; readonly afterScopes?: readonly string[];
|
|
291
|
-
readonly declarationHash: string; readonly selectorTargetGraphHashPresent: boolean;
|
|
287
|
+
readonly declarationHash: string; readonly beforeSelectorTargetGraphHash?: string; readonly afterSelectorTargetGraphHash?: string; readonly selectorTargetGraphHashPresent: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface CssSafeMergeSelectorTargetRebaseProof {
|
|
291
|
+
readonly kind: 'css-selector-target-rebase'; readonly side: string; readonly fromRuleKey: string; readonly toRuleKey: string; readonly property: string; readonly cascadeKey: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface CssSelectorTargetEquivalence {
|
|
295
|
+
readonly fromRuleKey?: string; readonly toRuleKey?: string; readonly fromSelectors?: readonly string[]; readonly toSelectors?: readonly string[]; readonly graphHash?: string;
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
export interface CssSafeMergeInput {
|
|
@@ -296,7 +300,7 @@ export interface CssSafeMergeInput {
|
|
|
296
300
|
readonly cssModule?: boolean; readonly cssModules?: boolean;
|
|
297
301
|
readonly generatedClassNameMap?: Readonly<Record<string, string>>;
|
|
298
302
|
readonly generatedClassNameMapHash?: string; readonly jsTsUseSiteGraphHash?: string; readonly cssModuleCompositionGraphHash?: string; readonly icssGraphHash?: string; readonly scopedCascadeGraphHash?: string;
|
|
299
|
-
readonly selectorTargetGraphHash?: string;
|
|
303
|
+
readonly selectorTargetGraphHash?: string; readonly selectorTargetEquivalences?: readonly CssSelectorTargetEquivalence[];
|
|
300
304
|
readonly baseGeneratedClassNameMap?: Readonly<Record<string, string>>; readonly workerGeneratedClassNameMap?: Readonly<Record<string, string>>; readonly headGeneratedClassNameMap?: Readonly<Record<string, string>>;
|
|
301
305
|
readonly baseGeneratedClassNameMapHash?: string; readonly workerGeneratedClassNameMapHash?: string; readonly headGeneratedClassNameMapHash?: string;
|
|
302
306
|
readonly baseJsTsUseSiteGraphHash?: string; readonly workerJsTsUseSiteGraphHash?: string; readonly headJsTsUseSiteGraphHash?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { hashSemanticValue } from '@shapeshift-labs/frontier-lang-kernel';
|
|
2
2
|
import { createCssModuleEvidence } from './css-modules.js';
|
|
3
|
+
import { createCssDependencyGraphEvidence } from './dependency-graph.js';
|
|
3
4
|
import { parsePostcssSemanticRecords } from './postcss-parser-evidence.js';
|
|
4
5
|
import { safeMergeCssSource as safeMergeCssSourceImpl } from './semantic-merge.js';
|
|
5
6
|
|
|
6
|
-
const ShorthandProperties = new Set(['all', 'animation', 'background', 'border', 'border-block', 'border-color', 'border-image', 'border-inline', 'border-radius', 'border-style', 'border-width', 'columns', 'flex', 'font', 'gap', 'grid', 'grid-area', 'grid-column', 'grid-row', 'inset', 'list-style', 'margin', 'offset', 'outline', 'overflow', 'padding', 'place-content', 'place-items', 'place-self', 'text-decoration', 'transition']);
|
|
7
|
-
const RuntimeAtRules = new Set(['keyframes', 'font-face', 'page', 'property']);
|
|
8
|
-
const ScopeAtRules = new Set(['media', 'supports', 'container', 'layer', 'scope']);
|
|
9
|
-
|
|
10
7
|
export function toCssAst(document, options = {}) {
|
|
11
8
|
const banner = options.banner ?? 'Generated by @shapeshift-labs/frontier-lang-css.';
|
|
12
9
|
const rules = [];
|
|
@@ -62,6 +59,7 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
|
62
59
|
const parsed = parsePostcssSemanticRecords(sourceText, sourceHash, options);
|
|
63
60
|
const records = parsed.records;
|
|
64
61
|
const cssModules = createCssModuleEvidence(records, options, sourceHash);
|
|
62
|
+
const dependencyGraphEvidence = createCssDependencyGraphEvidence(records, { ...options, sourceHash, hashSemanticValue });
|
|
65
63
|
const proofGaps = [
|
|
66
64
|
...parsed.proofGaps,
|
|
67
65
|
...records.flatMap((record) => record.proofGaps ?? []),
|
|
@@ -74,7 +72,8 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
|
74
72
|
sourceHash,
|
|
75
73
|
records,
|
|
76
74
|
cssModules,
|
|
77
|
-
|
|
75
|
+
dependencyGraphEvidence,
|
|
76
|
+
sheetHash: hashSemanticValue({ kind: 'frontier.lang.cssSemanticSheet.records.v1', records: records.map(hashableCssRecord), cssModuleHash: cssModules?.moduleHash, dependencyGraphHash: dependencyGraphEvidence.dependencyGraphHash }),
|
|
78
77
|
summary: {
|
|
79
78
|
rules: records.filter((record) => record.kind === 'rule').length,
|
|
80
79
|
declarations: records.reduce((sum, record) => sum + (record.declarations?.length ?? 0), 0),
|
|
@@ -83,6 +82,13 @@ export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
|
83
82
|
cssModuleCompositions: cssModules?.compositions.length ?? 0,
|
|
84
83
|
icssImports: cssModules?.icssImports.length ?? 0,
|
|
85
84
|
icssExports: cssModules?.icssExports.length ?? 0,
|
|
85
|
+
dependencySurfaceCount: dependencyGraphEvidence.dependencySurfaceCount,
|
|
86
|
+
customPropertyDefinitions: dependencyGraphEvidence.customPropertyDefinitions,
|
|
87
|
+
customPropertyReferences: dependencyGraphEvidence.customPropertyReferences,
|
|
88
|
+
animationNameLinks: dependencyGraphEvidence.animationNameLinks,
|
|
89
|
+
keyframeDefinitions: dependencyGraphEvidence.keyframeDefinitions,
|
|
90
|
+
fontFaceLinks: dependencyGraphEvidence.fontFaceLinks,
|
|
91
|
+
urlAssetReferences: dependencyGraphEvidence.urlAssetReferences,
|
|
86
92
|
proofGaps: proofGaps.length,
|
|
87
93
|
parseErrors: parsed.parser.parseErrors.length
|
|
88
94
|
},
|
|
@@ -102,6 +108,7 @@ export function createCssSemanticMergeEvidence(sourceText, options = {}) {
|
|
|
102
108
|
sheetHash: sheet.sheetHash,
|
|
103
109
|
records: sheet.records,
|
|
104
110
|
cssModules: sheet.cssModules,
|
|
111
|
+
dependencyGraphEvidence: sheet.dependencyGraphEvidence,
|
|
105
112
|
proofGaps: sheet.proofGaps,
|
|
106
113
|
autoMergeClaim: false,
|
|
107
114
|
semanticEquivalenceClaim: false,
|
|
@@ -116,163 +123,6 @@ export function safeMergeCssSource(input = {}) {
|
|
|
116
123
|
return safeMergeCssSourceImpl(input, { parseCssSemanticSheet, hashSemanticValue });
|
|
117
124
|
}
|
|
118
125
|
|
|
119
|
-
function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash, options) {
|
|
120
|
-
const records = [];
|
|
121
|
-
const blockRanges = [];
|
|
122
|
-
let index = start;
|
|
123
|
-
while (index < end) {
|
|
124
|
-
const open = sourceText.indexOf('{', index);
|
|
125
|
-
if (open < 0 || open >= end) break;
|
|
126
|
-
const close = matchingBrace(sourceText, open, end);
|
|
127
|
-
if (close < 0) break;
|
|
128
|
-
const preludeStart = previousBoundary(sourceText, index, open);
|
|
129
|
-
blockRanges.push([preludeStart, close + 1]);
|
|
130
|
-
const prelude = sourceText.slice(preludeStart, open).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
131
|
-
const body = sourceText.slice(open + 1, close);
|
|
132
|
-
if (prelude.startsWith('@')) {
|
|
133
|
-
const at = parseAtRule(prelude, preludeStart, close + 1, lineStarts, sourceHash, scopes, options);
|
|
134
|
-
records.push(at);
|
|
135
|
-
if (ScopeAtRules.has(at.atRuleName)) records.push(...parseCssBlocks(sourceText, open + 1, close, [...scopes, at.scopeKey], lineStarts, sourceHash, options));
|
|
136
|
-
} else if (prelude) {
|
|
137
|
-
records.push(cssRuleRecord(prelude, body, preludeStart, close + 1, lineStarts, sourceHash, scopes, options));
|
|
138
|
-
}
|
|
139
|
-
index = close + 1;
|
|
140
|
-
}
|
|
141
|
-
records.push(...parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges));
|
|
142
|
-
return records.sort((left, right) => left.sourceSpan.startOffset - right.sourceSpan.startOffset);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes, options) {
|
|
146
|
-
const selectors = prelude.split(',').map((selector) => selector.trim()).filter(Boolean);
|
|
147
|
-
const declarations = parseDeclarations(body);
|
|
148
|
-
const proofGaps = [
|
|
149
|
-
...declarations.filter((declaration) => ShorthandProperties.has(declaration.property)).map((declaration) => proofGap('css-shorthand-expansion-unproved', `CSS shorthand ${declaration.property} needs longhand expansion evidence.`)),
|
|
150
|
-
...scopes.length && !options.scopedCascadeGraphHash ? [proofGap('css-scoped-cascade-equivalence-unproved', 'Scoped cascade equivalence requires browser/style evidence.')] : []
|
|
151
|
-
];
|
|
152
|
-
return compactRecord({
|
|
153
|
-
kind: 'rule',
|
|
154
|
-
selectors,
|
|
155
|
-
selectorHash: hashSemanticValue({ kind: 'frontier.lang.css.selectors.v1', selectors }),
|
|
156
|
-
specificity: selectors.map(selectorSpecificity),
|
|
157
|
-
scopes,
|
|
158
|
-
declarations: declarations.map((declaration, ordinal) => ({
|
|
159
|
-
...declaration,
|
|
160
|
-
ordinal,
|
|
161
|
-
cascadeKey: [...scopes, selectors.join(','), declaration.property].join('::'),
|
|
162
|
-
declarationHash: hashSemanticValue({ kind: 'frontier.lang.css.declaration.v1', scopes, selectors, declaration })
|
|
163
|
-
})),
|
|
164
|
-
customProperties: declarations.filter((declaration) => declaration.property.startsWith('--')).map((declaration) => declaration.property),
|
|
165
|
-
scopedCascadeGraphHash: scopes.length ? options.scopedCascadeGraphHash : undefined,
|
|
166
|
-
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
167
|
-
sourceHash,
|
|
168
|
-
ruleHash: hashSemanticValue({ kind: 'frontier.lang.css.rule.v1', selectors, scopes, declarations }),
|
|
169
|
-
proofGaps: proofGaps.length ? proofGaps : undefined
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function parseAtRule(prelude, start, end, lineStarts, sourceHash, scopes, options) {
|
|
174
|
-
const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
|
|
175
|
-
const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
|
|
176
|
-
const conditionText = match?.[2]?.trim() ?? '';
|
|
177
|
-
const proofGaps = [];
|
|
178
|
-
if (RuntimeAtRules.has(atRuleName)) proofGaps.push(proofGap(`css-${atRuleName}-runtime-equivalence-unproved`, `CSS @${atRuleName} semantics require browser evidence.`));
|
|
179
|
-
if (ScopeAtRules.has(atRuleName) && !options.scopedCascadeGraphHash) proofGaps.push(proofGap(`css-${atRuleName}-cascade-scope-unproved`, `CSS @${atRuleName} scoped cascade requires condition evaluation evidence.`));
|
|
180
|
-
return compactRecord({
|
|
181
|
-
kind: 'at-rule',
|
|
182
|
-
atRuleName,
|
|
183
|
-
conditionText,
|
|
184
|
-
scopeKey: `@${atRuleName} ${conditionText}`.trim(),
|
|
185
|
-
scopes,
|
|
186
|
-
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
187
|
-
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
188
|
-
sourceHash,
|
|
189
|
-
atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRule.v1', atRuleName, conditionText, scopes }),
|
|
190
|
-
proofGaps: proofGaps.length ? proofGaps : undefined
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function parseAtRuleStatements(sourceText, start, end, scopes, lineStarts, sourceHash, options, blockRanges) {
|
|
195
|
-
const records = [];
|
|
196
|
-
let index = start;
|
|
197
|
-
while (index < end) {
|
|
198
|
-
const semicolon = sourceText.indexOf(';', index);
|
|
199
|
-
if (semicolon < 0 || semicolon >= end) break;
|
|
200
|
-
const range = blockRanges.find(([rangeStart, rangeEnd]) => semicolon >= rangeStart && semicolon < rangeEnd);
|
|
201
|
-
if (range) {
|
|
202
|
-
index = range[1];
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
const statementStart = previousBoundary(sourceText, index, semicolon);
|
|
206
|
-
const statementText = sourceText.slice(statementStart, semicolon + 1).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
207
|
-
if (statementText.startsWith('@') && !statementText.includes('{')) records.push(parseAtRuleStatement(statementText, statementStart, semicolon + 1, lineStarts, sourceHash, scopes, options));
|
|
208
|
-
index = semicolon + 1;
|
|
209
|
-
}
|
|
210
|
-
return records;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function parseAtRuleStatement(statementText, start, end, lineStarts, sourceHash, scopes, options) {
|
|
214
|
-
const body = statementText.replace(/;$/, '').trim();
|
|
215
|
-
const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(body);
|
|
216
|
-
const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
|
|
217
|
-
const conditionText = match?.[2]?.trim() ?? '';
|
|
218
|
-
const proofGaps = [];
|
|
219
|
-
if (atRuleName === 'layer') proofGaps.push(proofGap('css-layer-order-statement-unsupported', 'CSS @layer statement order requires cascade order evidence.'));
|
|
220
|
-
else proofGaps.push(proofGap(`css-${atRuleName}-statement-equivalence-unproved`, `CSS @${atRuleName} statement semantics require host evidence.`));
|
|
221
|
-
return compactRecord({
|
|
222
|
-
kind: 'at-rule-statement',
|
|
223
|
-
atRuleName,
|
|
224
|
-
conditionText,
|
|
225
|
-
statementText,
|
|
226
|
-
scopes,
|
|
227
|
-
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
228
|
-
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
229
|
-
sourceHash,
|
|
230
|
-
atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRuleStatement.v1', atRuleName, conditionText, scopes, statementText }),
|
|
231
|
-
proofGaps
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function parseDeclarations(body) {
|
|
236
|
-
return body
|
|
237
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
238
|
-
.split(';')
|
|
239
|
-
.map((part) => part.trim())
|
|
240
|
-
.filter(Boolean)
|
|
241
|
-
.flatMap((part) => {
|
|
242
|
-
const separator = part.indexOf(':');
|
|
243
|
-
if (separator <= 0) return [];
|
|
244
|
-
const rawProperty = part.slice(0, separator).trim();
|
|
245
|
-
const property = rawProperty.toLowerCase();
|
|
246
|
-
const value = part.slice(separator + 1).trim();
|
|
247
|
-
return [{ property, rawProperty, value, important: /!important\s*$/i.test(value), valueHash: hashSemanticValue({ kind: 'frontier.lang.css.value.v1', value }) }];
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function selectorSpecificity(selector) {
|
|
252
|
-
const withoutStrings = selector.replace(/"[^"]*"|'[^']*'/g, '');
|
|
253
|
-
const ids = (withoutStrings.match(/#[\w-]+/g) ?? []).length;
|
|
254
|
-
const classes = (withoutStrings.match(/\.[\w-]+|\[[^\]]+\]|:(?!:)[\w-]+(?:\([^)]*\))?/g) ?? []).length;
|
|
255
|
-
const elements = (withoutStrings.replace(/#[\w-]+|\.[\w-]+|\[[^\]]+\]|:{1,2}[\w-]+(?:\([^)]*\))?/g, ' ').match(/\b[A-Za-z][\w-]*\b/g) ?? []).length;
|
|
256
|
-
return [ids, classes, elements];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function matchingBrace(text, open, end) {
|
|
260
|
-
let depth = 0;
|
|
261
|
-
for (let index = open; index < end; index += 1) {
|
|
262
|
-
if (text[index] === '{') depth += 1;
|
|
263
|
-
if (text[index] === '}') {
|
|
264
|
-
depth -= 1;
|
|
265
|
-
if (depth === 0) return index;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
return -1;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function previousBoundary(text, start, open) {
|
|
272
|
-
const candidates = [text.lastIndexOf('}', open - 1), text.lastIndexOf(';', open - 1)].filter((value) => value >= start);
|
|
273
|
-
return (candidates.length ? Math.max(...candidates) + 1 : start);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
126
|
function sourceMapMapping(rule, index, startLine, endLine, options) {
|
|
277
127
|
return compactRecord({
|
|
278
128
|
id: `map_${idFragment(rule.sourceRef.semanticNodeId)}_${index}`,
|
|
@@ -290,22 +140,8 @@ function sourceMapEnvelope(ast, mappings, options) {
|
|
|
290
140
|
return compactRecord({ kind: 'frontier.lang.sourceMap', version: 1, id: options.sourceMapId ?? `sourcemap_${idFragment(ast.kind)}_css`, sourcePath: options.sourcePath, sourceHash: options.sourceHash, target: { language: 'css' }, targetPath: options.targetPath, semanticIndexId: options.semanticIndexId, mappings, evidence: options.evidence ?? [], metadata: { precision: 'rule-block', generatedSpanStrategy: 'css-renderer-rule-block' } });
|
|
291
141
|
}
|
|
292
142
|
|
|
293
|
-
function sourceSpan(start, end, lineStarts) {
|
|
294
|
-
const from = positionAt(start, lineStarts);
|
|
295
|
-
const to = positionAt(end, lineStarts);
|
|
296
|
-
return { startOffset: start, endOffset: end, startLine: from.line, startColumn: from.column, endLine: to.line, endColumn: to.column };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function positionAt(offset, lineStarts) {
|
|
300
|
-
let line = 0;
|
|
301
|
-
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= offset) line += 1;
|
|
302
|
-
return { line: line + 1, column: offset - lineStarts[line] + 1 };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
143
|
function sourceRef(node, extra = {}) { return { semanticNodeId: node.id, semanticNodeKind: node.kind, semanticNodeName: node.name, ...extra }; }
|
|
306
|
-
function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
|
|
307
144
|
function hashableCssRecord(record) { return { kind: record.kind, selectors: record.selectors, specificity: record.specificity, scopes: record.scopes, atRuleName: record.atRuleName, conditionText: record.conditionText, statementText: record.statementText, declarations: record.declarations?.map((item) => ({ property: item.property, value: item.value, important: item.important })), proofGaps: record.proofGaps?.map((gap) => gap.code) }; }
|
|
308
|
-
function computeLineStarts(text) { const starts = [0]; for (let index = 0; index < text.length; index += 1) if (text[index] === '\n') starts.push(index + 1); return starts; }
|
|
309
145
|
function cssIdentifier(value) { return String(value ?? 'unknown').replace(/[^A-Za-z0-9_-]/g, '-').replace(/^-+/, '') || 'unknown'; }
|
|
310
146
|
function cssString(value) { return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); }
|
|
311
147
|
function typeName(type) { return typeof type === 'string' ? type : type?.name ?? type?.kind ?? 'unknown'; }
|
|
@@ -102,6 +102,7 @@ function postcssAtRuleRecord(node, scopes, sourceHash, options) {
|
|
|
102
102
|
statementText: kind === 'at-rule-statement' ? rawText : undefined,
|
|
103
103
|
scopeKey: postcssAtRuleScopeKey(node),
|
|
104
104
|
scopes,
|
|
105
|
+
dependencyTokens: atRuleDependencyTokens(node, atRuleName),
|
|
105
106
|
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
106
107
|
sourceSpan: sourceSpanFromPostcss(node.source, options.sourcePath),
|
|
107
108
|
sourceHash,
|
|
@@ -116,6 +117,26 @@ function postcssAtRuleScopeKey(node) {
|
|
|
116
117
|
return `@${String(node.name ?? 'unknown').toLowerCase()} ${String(node.params ?? '').trim()}`.trim();
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
function atRuleDependencyTokens(node, atRuleName) {
|
|
121
|
+
if (atRuleName !== 'font-face') return undefined;
|
|
122
|
+
const declarations = (node.nodes ?? []).filter((child) => child.type === 'decl');
|
|
123
|
+
const fontFamilies = declarations
|
|
124
|
+
.filter((declaration) => String(declaration.prop ?? '').toLowerCase() === 'font-family')
|
|
125
|
+
.flatMap((declaration) => fontFamilyNames(declaration.value));
|
|
126
|
+
const urls = declarations.flatMap((declaration) => cssUrlReferences(declaration.value));
|
|
127
|
+
return compactRecord({ fontFamilies: unique(fontFamilies), urls: unique(urls) });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function fontFamilyNames(value) {
|
|
131
|
+
return String(value ?? '').split(',').map((part) => part.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cssUrlReferences(value) {
|
|
135
|
+
return [...String(value ?? '').matchAll(/\burl\(\s*(?:"([^"]*)"|'([^']*)'|([^)]*?))\s*\)/g)]
|
|
136
|
+
.map((match) => (match[1] ?? match[2] ?? match[3] ?? '').trim())
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
119
140
|
function sourceSpanFromPostcss(source, fallbackPath) {
|
|
120
141
|
const start = source?.start;
|
|
121
142
|
const end = source?.end;
|
|
@@ -147,6 +168,7 @@ function selectorSpecificity(selector) {
|
|
|
147
168
|
}
|
|
148
169
|
|
|
149
170
|
function proofGap(code, summary) { return { code, status: 'not-claimed', summary, failClosed: true, semanticEquivalenceClaim: false }; }
|
|
171
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
150
172
|
function compactRecord(record) { return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== undefined)); }
|
|
151
173
|
|
|
152
174
|
export { parsePostcssSemanticRecords };
|
|
@@ -46,26 +46,64 @@ function selectorTargetMoves(changes, side) {
|
|
|
46
46
|
beforeScopes: deletion.before.scopes,
|
|
47
47
|
afterScopes: addition.after.scopes,
|
|
48
48
|
declarationHash: addition.after.declarationHash,
|
|
49
|
+
beforeSelectorTargetGraphHash: deletion.before.selectorTargetGraphHash,
|
|
50
|
+
afterSelectorTargetGraphHash: addition.after.selectorTargetGraphHash,
|
|
49
51
|
selectorTargetGraphHashPresent: Boolean(deletion.before.selectorTargetGraphHash && addition.after.selectorTargetGraphHash)
|
|
50
52
|
});
|
|
51
53
|
}
|
|
52
54
|
return moves;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
function planSelectorTargetRebase(id, sourcePath, selectorTargetEvidence, changed, options = {}) {
|
|
58
|
+
const planned = { worker: [...changed.worker], head: [...changed.head] };
|
|
59
|
+
const worker = selectorTargetMoveSidePlan(id, sourcePath, selectorTargetEvidence.moves.worker, selectorTargetEvidence.moves.head, planned.head, options);
|
|
60
|
+
const head = selectorTargetMoveSidePlan(id, sourcePath, selectorTargetEvidence.moves.head, selectorTargetEvidence.moves.worker, planned.worker, options);
|
|
61
|
+
return {
|
|
62
|
+
changed: planned,
|
|
63
|
+
conflicts: [...worker.conflicts, ...head.conflicts],
|
|
64
|
+
evidence: { ...selectorTargetEvidence, rebasedChangeCount: worker.rebaseProofs.length + head.rebaseProofs.length, rebaseProofs: [...worker.rebaseProofs, ...head.rebaseProofs] }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function selectorTargetMoveSidePlan(id, sourcePath, moves, oppositeMoves, oppositeChanges, options) {
|
|
69
|
+
const conflicts = [];
|
|
70
|
+
const rebaseProofs = [];
|
|
71
|
+
for (const move of moves) {
|
|
72
|
+
if (oppositeMoves.some((oppositeMove) => sameSelectorMove(move, oppositeMove))) continue;
|
|
73
|
+
for (let index = 0; index < oppositeChanges.length; index += 1) {
|
|
74
|
+
const change = oppositeChanges[index];
|
|
75
|
+
if (!selectorMoveTouchesChange(move, change)) continue;
|
|
76
|
+
if (canRebaseChange(move, change, options)) {
|
|
77
|
+
const rebased = rebaseChangeToSelectorMove(change, move);
|
|
78
|
+
oppositeChanges[index] = rebased.change;
|
|
79
|
+
rebaseProofs.push(rebased.proof);
|
|
80
|
+
} else conflicts.push(conflict(id, sourcePath, change.key, move, change));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { conflicts, rebaseProofs };
|
|
60
84
|
}
|
|
61
85
|
|
|
62
|
-
function
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
function canRebaseChange(move, change, options) {
|
|
87
|
+
return change.kind === 'add' && change.after && hasSelectorTargetEquivalence(move, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasSelectorTargetEquivalence(move, options) {
|
|
91
|
+
return (options.selectorTargetEquivalences ?? []).some((entry) => {
|
|
92
|
+
const ruleKeysMatch = entry.fromRuleKey === move.beforeRuleKey && entry.toRuleKey === move.afterRuleKey;
|
|
93
|
+
const selectorsMatch = selectorListKey(entry.fromSelectors) === selectorListKey(move.beforeSelectors) && selectorListKey(entry.toSelectors) === selectorListKey(move.afterSelectors);
|
|
94
|
+
const graphMatches = !entry.graphHash || entry.graphHash === move.beforeSelectorTargetGraphHash || entry.graphHash === move.afterSelectorTargetGraphHash;
|
|
95
|
+
return graphMatches && (ruleKeysMatch || selectorsMatch);
|
|
66
96
|
});
|
|
67
97
|
}
|
|
68
98
|
|
|
99
|
+
function rebaseChangeToSelectorMove(change, move) {
|
|
100
|
+
const after = { ...change.after, ruleKey: move.afterRuleKey, selectors: move.afterSelectors, scopes: move.afterScopes ?? [], key: cascadeKey(move.afterScopes, move.afterSelectors, change.after.property), rebasedFromRuleKey: move.beforeRuleKey };
|
|
101
|
+
return {
|
|
102
|
+
change: { ...change, key: after.key, after },
|
|
103
|
+
proof: { kind: 'css-selector-target-rebase', side: change.side, fromRuleKey: move.beforeRuleKey, toRuleKey: move.afterRuleKey, property: change.after.property, cascadeKey: after.key }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
69
107
|
function conflict(id, sourcePath, cascadeKey, selectorMove, change) {
|
|
70
108
|
return {
|
|
71
109
|
code: 'css-selector-target-conflict',
|
|
@@ -94,6 +132,8 @@ function sameSelectorMove(left, right) {
|
|
|
94
132
|
return left.property === right.property && left.beforeRuleKey === right.beforeRuleKey && left.afterRuleKey === right.afterRuleKey && left.declarationHash === right.declarationHash;
|
|
95
133
|
}
|
|
96
134
|
|
|
135
|
+
function cascadeKey(scopes = [], selectors = [], property) { return [...scopes, selectors.join(','), property].join('::'); }
|
|
136
|
+
function selectorListKey(value = []) { return Array.isArray(value) ? value.join(',') : undefined; }
|
|
97
137
|
function changeDetails(change) { return { kind: change.kind, property: (change.after ?? change.before)?.property, value: change.after?.value, important: change.after?.important }; }
|
|
98
138
|
|
|
99
|
-
export { mergeSelectorTargetEvidence,
|
|
139
|
+
export { mergeSelectorTargetEvidence, planSelectorTargetRebase };
|
package/dist/semantic-merge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeConflicts } from './semantic-merge-css-modules.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mergeCssDependencyGraphEvidence } from './dependency-graph.js';
|
|
3
|
+
import { mergeSelectorTargetEvidence, planSelectorTargetRebase } from './semantic-merge-selector-targets.js';
|
|
3
4
|
|
|
4
5
|
function safeMergeCssSource(input = {}, context = {}) {
|
|
5
6
|
const parseSheet = context.parseCssSemanticSheet;
|
|
@@ -31,11 +32,11 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
31
32
|
const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
|
|
32
33
|
const sourceShapeConflicts = unsupportedSourceShapeConflicts(id, sourcePath, sheets, changed, hash);
|
|
33
34
|
const parserEvidence = mergeParserEvidence(sheets);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts, ...
|
|
37
|
-
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, selectorTargetEvidence });
|
|
38
|
-
const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, changed.head), changed.worker);
|
|
35
|
+
const dependencyGraphEvidence = mergeCssDependencyGraphEvidence(sheets, changed);
|
|
36
|
+
const selectorTargetPlan = planSelectorTargetRebase(id, sourcePath, mergeSelectorTargetEvidence(sheets, changed), changed, input);
|
|
37
|
+
const conflicts = [...parserConflicts, ...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts, ...selectorTargetPlan.conflicts];
|
|
38
|
+
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts, { parserEvidence, dependencyGraphEvidence, selectorTargetEvidence: selectorTargetPlan.evidence });
|
|
39
|
+
const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, selectorTargetPlan.changed.head), selectorTargetPlan.changed.worker);
|
|
39
40
|
return merged(id, sourcePath, renderDeclarationIndex(mergedIndex), 'semantic-declaration-merge', hash, {
|
|
40
41
|
baseSheetHash: sheets.base.sheetHash,
|
|
41
42
|
workerSheetHash: sheets.worker.sheetHash,
|
|
@@ -45,7 +46,8 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
45
46
|
workerChangedCssModuleContracts: moduleChanges.worker.length,
|
|
46
47
|
headChangedCssModuleContracts: moduleChanges.head.length,
|
|
47
48
|
parserEvidence,
|
|
48
|
-
|
|
49
|
+
dependencyGraphEvidence,
|
|
50
|
+
selectorTargetEvidence: selectorTargetPlan.evidence
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
|
package/package.json
CHANGED