@shapeshift-labs/frontier-lang-css 0.1.2 → 0.1.4
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 +4 -4
- package/dist/css-modules.js +3 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +11 -9
- package/dist/semantic-merge-css-modules.js +207 -0
- package/dist/semantic-merge.js +32 -9
- 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, source spans, stable hashes, and fail-closed proof gaps for cascade/render-sensitive CSS surfaces. `safeMergeCssSource` admits independent
|
|
235
|
+
`sourceMap.mappings` links emitted rule blocks back to Frontier Lang semantic node ids. `createCssSemanticMergeEvidence` records selectors, specificity, declarations, custom properties, cascade keys, 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, 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, source spans, stable hashes.
|
|
240
|
-
- Safe merge: independent
|
|
241
|
-
- Review-only gaps: shorthands without longhand expansion, scoped cascade under `@media` / `@supports` / `@container` / `@layer`, `@keyframes`, `@font-face`, `@page`, browser layout and render equivalence.
|
|
239
|
+
- Ready evidence: style rules, selectors, specificity, declarations, custom properties, 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
|
+
- Safe merge: independent declarations with non-overlapping cascade keys; 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
|
+
- Review-only gaps: incomplete generated class-name maps, unproved CSS Modules use-site graphs, unproved composition or ICSS graphs, shorthands without longhand expansion, unproved or structurally changed scoped cascade under `@media` / `@supports` / `@container` / `@layer`, `@keyframes`, `@font-face`, `@page`, browser layout and render equivalence.
|
|
242
242
|
- Claims: `autoMergeClaim`, `semanticEquivalenceClaim`, `browserCascadeEquivalenceClaim`, and `browserRenderEquivalenceClaim` remain false.
|
package/dist/css-modules.js
CHANGED
|
@@ -41,6 +41,9 @@ function cssModuleProofGaps(exports, compositions, icssImports, icssExports, opt
|
|
|
41
41
|
if (exports.length && !options.generatedClassNameMapHash && !options.generatedClassNameMap) {
|
|
42
42
|
proofGaps.push(proofGap('css-module-generated-class-map-unproved', 'CSS Modules exported local classes need generated class-name map evidence from the bundler/runtime.'));
|
|
43
43
|
}
|
|
44
|
+
if (exports.length && options.generatedClassNameMap && exports.some((entry) => !entry.generatedName)) {
|
|
45
|
+
proofGaps.push(proofGap('css-module-generated-class-map-incomplete', 'CSS Modules generated class-name map evidence must cover every exported local class.'));
|
|
46
|
+
}
|
|
44
47
|
if (exports.length && !options.jsTsUseSiteGraphHash) {
|
|
45
48
|
proofGaps.push(proofGap('css-module-js-ts-use-site-graph-unproved', 'CSS Modules exported classes need JS/TS/JSX import and member-use graph evidence.'));
|
|
46
49
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface CssProjectionOptions {
|
|
|
12
12
|
readonly jsTsUseSiteGraphHash?: string;
|
|
13
13
|
readonly cssModuleCompositionGraphHash?: string;
|
|
14
14
|
readonly icssGraphHash?: string;
|
|
15
|
+
readonly scopedCascadeGraphHash?: string;
|
|
15
16
|
readonly targetPath?: string;
|
|
16
17
|
readonly semanticIndexId?: string;
|
|
17
18
|
readonly sourceSpansBySemanticNodeId?: Readonly<Record<string, CssSourceSpan>>;
|
|
@@ -124,6 +125,7 @@ export interface CssSemanticRecord {
|
|
|
124
125
|
readonly atRuleName?: string;
|
|
125
126
|
readonly conditionText?: string;
|
|
126
127
|
readonly scopeKey?: string;
|
|
128
|
+
readonly scopedCascadeGraphHash?: string;
|
|
127
129
|
readonly sourceSpan: CssSourceSpan;
|
|
128
130
|
readonly sourceHash: string;
|
|
129
131
|
readonly ruleHash?: string;
|
|
@@ -249,6 +251,8 @@ export interface CssSafeMergeResult {
|
|
|
249
251
|
readonly headSheetHash?: string;
|
|
250
252
|
readonly workerChangedDeclarations?: number;
|
|
251
253
|
readonly headChangedDeclarations?: number;
|
|
254
|
+
readonly workerChangedCssModuleContracts?: number;
|
|
255
|
+
readonly headChangedCssModuleContracts?: number;
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
export interface CssSafeMergeInput {
|
|
@@ -257,6 +261,32 @@ export interface CssSafeMergeInput {
|
|
|
257
261
|
readonly baseSourceText?: string;
|
|
258
262
|
readonly workerSourceText?: string;
|
|
259
263
|
readonly headSourceText?: string;
|
|
264
|
+
readonly cssModule?: boolean;
|
|
265
|
+
readonly cssModules?: boolean;
|
|
266
|
+
readonly generatedClassNameMap?: Readonly<Record<string, string>>;
|
|
267
|
+
readonly generatedClassNameMapHash?: string;
|
|
268
|
+
readonly jsTsUseSiteGraphHash?: string;
|
|
269
|
+
readonly cssModuleCompositionGraphHash?: string;
|
|
270
|
+
readonly icssGraphHash?: string;
|
|
271
|
+
readonly scopedCascadeGraphHash?: string;
|
|
272
|
+
readonly baseGeneratedClassNameMap?: Readonly<Record<string, string>>;
|
|
273
|
+
readonly workerGeneratedClassNameMap?: Readonly<Record<string, string>>;
|
|
274
|
+
readonly headGeneratedClassNameMap?: Readonly<Record<string, string>>;
|
|
275
|
+
readonly baseGeneratedClassNameMapHash?: string;
|
|
276
|
+
readonly workerGeneratedClassNameMapHash?: string;
|
|
277
|
+
readonly headGeneratedClassNameMapHash?: string;
|
|
278
|
+
readonly baseJsTsUseSiteGraphHash?: string;
|
|
279
|
+
readonly workerJsTsUseSiteGraphHash?: string;
|
|
280
|
+
readonly headJsTsUseSiteGraphHash?: string;
|
|
281
|
+
readonly baseCssModuleCompositionGraphHash?: string;
|
|
282
|
+
readonly workerCssModuleCompositionGraphHash?: string;
|
|
283
|
+
readonly headCssModuleCompositionGraphHash?: string;
|
|
284
|
+
readonly baseIcssGraphHash?: string;
|
|
285
|
+
readonly workerIcssGraphHash?: string;
|
|
286
|
+
readonly headIcssGraphHash?: string;
|
|
287
|
+
readonly baseScopedCascadeGraphHash?: string;
|
|
288
|
+
readonly workerScopedCascadeGraphHash?: string;
|
|
289
|
+
readonly headScopedCascadeGraphHash?: string;
|
|
260
290
|
}
|
|
261
291
|
|
|
262
292
|
export declare function toCssAst(document: FrontierLangDocument, options?: CssProjectionOptions): CssAstStylesheet;
|
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ export function emitCssWithSourceMap(document, options = {}) {
|
|
|
59
59
|
export function parseCssSemanticSheet(sourceText, options = {}) {
|
|
60
60
|
const lineStarts = computeLineStarts(sourceText);
|
|
61
61
|
const sourceHash = options.sourceHash ?? hashSemanticValue({ kind: 'frontier.lang.css.source.v1', sourceText });
|
|
62
|
-
const records = parseCssBlocks(sourceText, 0, sourceText.length, [], lineStarts, sourceHash);
|
|
62
|
+
const records = parseCssBlocks(sourceText, 0, sourceText.length, [], lineStarts, sourceHash, options);
|
|
63
63
|
const cssModules = createCssModuleEvidence(records, options, sourceHash);
|
|
64
64
|
const proofGaps = [
|
|
65
65
|
...records.flatMap((record) => record.proofGaps ?? []),
|
|
@@ -112,7 +112,7 @@ export function safeMergeCssSource(input = {}) {
|
|
|
112
112
|
return safeMergeCssSourceImpl(input, { parseCssSemanticSheet, hashSemanticValue });
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash) {
|
|
115
|
+
function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash, options) {
|
|
116
116
|
const records = [];
|
|
117
117
|
let index = start;
|
|
118
118
|
while (index < end) {
|
|
@@ -124,23 +124,23 @@ function parseCssBlocks(sourceText, start, end, scopes, lineStarts, sourceHash)
|
|
|
124
124
|
const prelude = sourceText.slice(preludeStart, open).replace(/\/\*[\s\S]*?\*\//g, '').trim();
|
|
125
125
|
const body = sourceText.slice(open + 1, close);
|
|
126
126
|
if (prelude.startsWith('@')) {
|
|
127
|
-
const at = parseAtRule(prelude, preludeStart, close + 1, lineStarts, sourceHash, scopes);
|
|
127
|
+
const at = parseAtRule(prelude, preludeStart, close + 1, lineStarts, sourceHash, scopes, options);
|
|
128
128
|
records.push(at);
|
|
129
|
-
if (ScopeAtRules.has(at.atRuleName)) records.push(...parseCssBlocks(sourceText, open + 1, close, [...scopes, at.scopeKey], lineStarts, sourceHash));
|
|
129
|
+
if (ScopeAtRules.has(at.atRuleName)) records.push(...parseCssBlocks(sourceText, open + 1, close, [...scopes, at.scopeKey], lineStarts, sourceHash, options));
|
|
130
130
|
} else if (prelude) {
|
|
131
|
-
records.push(cssRuleRecord(prelude, body, preludeStart, close + 1, lineStarts, sourceHash, scopes));
|
|
131
|
+
records.push(cssRuleRecord(prelude, body, preludeStart, close + 1, lineStarts, sourceHash, scopes, options));
|
|
132
132
|
}
|
|
133
133
|
index = close + 1;
|
|
134
134
|
}
|
|
135
135
|
return records;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes) {
|
|
138
|
+
function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes, options) {
|
|
139
139
|
const selectors = prelude.split(',').map((selector) => selector.trim()).filter(Boolean);
|
|
140
140
|
const declarations = parseDeclarations(body);
|
|
141
141
|
const proofGaps = [
|
|
142
142
|
...declarations.filter((declaration) => ShorthandProperties.has(declaration.property)).map((declaration) => proofGap('css-shorthand-expansion-unproved', `CSS shorthand ${declaration.property} needs longhand expansion evidence.`)),
|
|
143
|
-
...scopes.length ? [proofGap('css-scoped-cascade-equivalence-unproved', 'Scoped cascade equivalence requires browser/style evidence.')] : []
|
|
143
|
+
...scopes.length && !options.scopedCascadeGraphHash ? [proofGap('css-scoped-cascade-equivalence-unproved', 'Scoped cascade equivalence requires browser/style evidence.')] : []
|
|
144
144
|
];
|
|
145
145
|
return compactRecord({
|
|
146
146
|
kind: 'rule',
|
|
@@ -155,6 +155,7 @@ function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes
|
|
|
155
155
|
declarationHash: hashSemanticValue({ kind: 'frontier.lang.css.declaration.v1', scopes, selectors, declaration })
|
|
156
156
|
})),
|
|
157
157
|
customProperties: declarations.filter((declaration) => declaration.property.startsWith('--')).map((declaration) => declaration.property),
|
|
158
|
+
scopedCascadeGraphHash: scopes.length ? options.scopedCascadeGraphHash : undefined,
|
|
158
159
|
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
159
160
|
sourceHash,
|
|
160
161
|
ruleHash: hashSemanticValue({ kind: 'frontier.lang.css.rule.v1', selectors, scopes, declarations }),
|
|
@@ -162,19 +163,20 @@ function cssRuleRecord(prelude, body, start, end, lineStarts, sourceHash, scopes
|
|
|
162
163
|
});
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
function parseAtRule(prelude, start, end, lineStarts, sourceHash, scopes) {
|
|
166
|
+
function parseAtRule(prelude, start, end, lineStarts, sourceHash, scopes, options) {
|
|
166
167
|
const match = /^@([A-Za-z-]+)\s*([\s\S]*)$/.exec(prelude);
|
|
167
168
|
const atRuleName = match?.[1]?.toLowerCase() ?? 'unknown';
|
|
168
169
|
const conditionText = match?.[2]?.trim() ?? '';
|
|
169
170
|
const proofGaps = [];
|
|
170
171
|
if (RuntimeAtRules.has(atRuleName)) proofGaps.push(proofGap(`css-${atRuleName}-runtime-equivalence-unproved`, `CSS @${atRuleName} semantics require browser evidence.`));
|
|
171
|
-
if (ScopeAtRules.has(atRuleName)) proofGaps.push(proofGap(`css-${atRuleName}-cascade-scope-unproved`, `CSS @${atRuleName} scoped cascade requires condition evaluation evidence.`));
|
|
172
|
+
if (ScopeAtRules.has(atRuleName) && !options.scopedCascadeGraphHash) proofGaps.push(proofGap(`css-${atRuleName}-cascade-scope-unproved`, `CSS @${atRuleName} scoped cascade requires condition evaluation evidence.`));
|
|
172
173
|
return compactRecord({
|
|
173
174
|
kind: 'at-rule',
|
|
174
175
|
atRuleName,
|
|
175
176
|
conditionText,
|
|
176
177
|
scopeKey: `@${atRuleName} ${conditionText}`.trim(),
|
|
177
178
|
scopes,
|
|
179
|
+
scopedCascadeGraphHash: ScopeAtRules.has(atRuleName) ? options.scopedCascadeGraphHash : undefined,
|
|
178
180
|
sourceSpan: sourceSpan(start, end, lineStarts),
|
|
179
181
|
sourceHash,
|
|
180
182
|
atRuleHash: hashSemanticValue({ kind: 'frontier.lang.css.atRule.v1', atRuleName, conditionText, scopes }),
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
function sheetOptions(input, side, sourcePath) {
|
|
2
|
+
const prefix = side === 'base' ? 'base' : side === 'worker' ? 'worker' : 'head';
|
|
3
|
+
return {
|
|
4
|
+
sourcePath,
|
|
5
|
+
cssModule: input.cssModule,
|
|
6
|
+
cssModules: input.cssModules,
|
|
7
|
+
generatedClassNameMap: input[`${prefix}GeneratedClassNameMap`] ?? input.generatedClassNameMap,
|
|
8
|
+
generatedClassNameMapHash: input[`${prefix}GeneratedClassNameMapHash`] ?? input.generatedClassNameMapHash,
|
|
9
|
+
jsTsUseSiteGraphHash: input[`${prefix}JsTsUseSiteGraphHash`] ?? input.jsTsUseSiteGraphHash,
|
|
10
|
+
cssModuleCompositionGraphHash: input[`${prefix}CssModuleCompositionGraphHash`] ?? input.cssModuleCompositionGraphHash,
|
|
11
|
+
icssGraphHash: input[`${prefix}IcssGraphHash`] ?? input.icssGraphHash,
|
|
12
|
+
scopedCascadeGraphHash: input[`${prefix}ScopedCascadeGraphHash`] ?? input.scopedCascadeGraphHash
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cssModuleContractChanges(sheets, hash) {
|
|
17
|
+
const indexes = {
|
|
18
|
+
base: cssModuleContractIndex(sheets.base, hash),
|
|
19
|
+
worker: cssModuleContractIndex(sheets.worker, hash),
|
|
20
|
+
head: cssModuleContractIndex(sheets.head, hash)
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
worker: changedContracts(indexes.base, indexes.worker, 'worker', sheets.worker),
|
|
24
|
+
head: changedContracts(indexes.base, indexes.head, 'head', sheets.head)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function cssModuleContractIndex(sheet, hash) {
|
|
29
|
+
const contracts = new Map();
|
|
30
|
+
const cssModules = sheet.cssModules;
|
|
31
|
+
if (!cssModules) return { contracts, proofGaps: [], moduleHash: undefined };
|
|
32
|
+
for (const entry of cssModules.exports ?? []) {
|
|
33
|
+
const contractHash = hash?.({ kind: 'frontier.lang.css.module.export.contract.v1', name: entry.name, generatedName: entry.generatedName });
|
|
34
|
+
contracts.set(`export:${entry.name}`, {
|
|
35
|
+
key: `export:${entry.name}`,
|
|
36
|
+
contractKind: 'css-module-export',
|
|
37
|
+
name: entry.name,
|
|
38
|
+
hash: contractHash ?? entry.exportHash,
|
|
39
|
+
requiredProofGapCodes: ['css-module-generated-class-map-unproved', 'css-module-generated-class-map-incomplete', 'css-module-js-ts-use-site-graph-unproved']
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
for (const entry of cssModules.compositions ?? []) {
|
|
43
|
+
const key = ['composition', entry.localName, entry.sourceKind, entry.source ?? 'local'].join(':');
|
|
44
|
+
contracts.set(key, {
|
|
45
|
+
key,
|
|
46
|
+
contractKind: 'css-module-composition',
|
|
47
|
+
name: entry.localName,
|
|
48
|
+
hash: entry.compositionHash,
|
|
49
|
+
requiredProofGapCodes: ['css-module-composition-resolution-unproved']
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (const entry of cssModules.icssImports ?? []) {
|
|
53
|
+
const key = ['icss-import', entry.source, entry.importedName].join(':');
|
|
54
|
+
contracts.set(key, {
|
|
55
|
+
key,
|
|
56
|
+
contractKind: 'icss-import',
|
|
57
|
+
name: entry.localName,
|
|
58
|
+
hash: entry.importHash,
|
|
59
|
+
requiredProofGapCodes: ['css-module-icss-graph-unproved']
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const entry of cssModules.icssExports ?? []) {
|
|
63
|
+
const key = `icss-export:${entry.name}`;
|
|
64
|
+
contracts.set(key, {
|
|
65
|
+
key,
|
|
66
|
+
contractKind: 'icss-export',
|
|
67
|
+
name: entry.name,
|
|
68
|
+
hash: entry.exportHash,
|
|
69
|
+
requiredProofGapCodes: ['css-module-icss-graph-unproved']
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { contracts, proofGaps: cssModules.proofGaps ?? [], moduleHash: cssModules.moduleHash };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function changedContracts(baseIndex, currentIndex, side, sheet) {
|
|
76
|
+
const keys = unique([...baseIndex.contracts.keys(), ...currentIndex.contracts.keys()]);
|
|
77
|
+
return keys.flatMap((key) => {
|
|
78
|
+
const before = baseIndex.contracts.get(key);
|
|
79
|
+
const after = currentIndex.contracts.get(key);
|
|
80
|
+
if ((before?.hash ?? '') === (after?.hash ?? '')) return [];
|
|
81
|
+
return [{
|
|
82
|
+
side,
|
|
83
|
+
key,
|
|
84
|
+
before,
|
|
85
|
+
after,
|
|
86
|
+
proofGaps: uniqueProofGaps([...(baseIndex.proofGaps ?? []), ...(currentIndex.proofGaps ?? [])]),
|
|
87
|
+
sheetHash: sheet.sheetHash,
|
|
88
|
+
kind: before && after ? 'update' : before ? 'delete' : 'add'
|
|
89
|
+
}];
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cssModuleContractConflicts(id, sourcePath, changes) {
|
|
94
|
+
return [
|
|
95
|
+
...cssModuleContractProofConflicts(id, sourcePath, changes.worker),
|
|
96
|
+
...cssModuleContractProofConflicts(id, sourcePath, changes.head),
|
|
97
|
+
...cssModuleOverlapConflicts(id, sourcePath, changes.worker, changes.head)
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function cssModuleContractProofConflicts(id, sourcePath, changes) {
|
|
102
|
+
return changes.flatMap((change) => {
|
|
103
|
+
const contract = change.after ?? change.before;
|
|
104
|
+
const requiredCodes = contract?.requiredProofGapCodes ?? [];
|
|
105
|
+
return (change.proofGaps ?? [])
|
|
106
|
+
.filter((gap) => requiredCodes.includes(gap.code))
|
|
107
|
+
.map((gap) => conflict(id, sourcePath, 'css-module-proof-gap-blocked', gap.code, {
|
|
108
|
+
contractKey: change.key,
|
|
109
|
+
contractKind: contract.contractKind,
|
|
110
|
+
side: change.side,
|
|
111
|
+
changeKind: change.kind,
|
|
112
|
+
proofGap: gap
|
|
113
|
+
}));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function cssModuleOverlapConflicts(id, sourcePath, workerChanges, headChanges) {
|
|
118
|
+
const headByKey = new Map(headChanges.map((change) => [change.key, change]));
|
|
119
|
+
return workerChanges.flatMap((workerChange) => {
|
|
120
|
+
const headChange = headByKey.get(workerChange.key);
|
|
121
|
+
if (!headChange || sameContractChange(workerChange, headChange)) return [];
|
|
122
|
+
const contract = workerChange.after ?? workerChange.before ?? headChange.after ?? headChange.before;
|
|
123
|
+
return [conflict(id, sourcePath, 'css-module-contract-conflict', 'css-module-contract-conflict', {
|
|
124
|
+
contractKey: workerChange.key,
|
|
125
|
+
contractKind: contract?.contractKind,
|
|
126
|
+
worker: contractChangeDetails(workerChange),
|
|
127
|
+
head: contractChangeDetails(headChange)
|
|
128
|
+
})];
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function unsupportedSourceShapeConflicts(id, sourcePath, sheets, declarationChanges, hash) {
|
|
133
|
+
const sourceShapeChanges = {
|
|
134
|
+
worker: unsupportedSourceShapeChanges(sheets.base, sheets.worker, declarationChanges.worker, 'worker', hash),
|
|
135
|
+
head: unsupportedSourceShapeChanges(sheets.base, sheets.head, declarationChanges.head, 'head', hash)
|
|
136
|
+
};
|
|
137
|
+
return [...sourceShapeChanges.worker, ...sourceShapeChanges.head].map((change) => conflict(id, sourcePath, 'css-source-shape-unsupported', change.reasonCode, change));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function unsupportedSourceShapeChanges(baseSheet, currentSheet, declarationChanges, side, hash) {
|
|
141
|
+
const baseShape = sourceShapeIndex(baseSheet, hash);
|
|
142
|
+
const currentShape = sourceShapeIndex(currentSheet, hash);
|
|
143
|
+
const keys = unique([...baseShape.keys(), ...currentShape.keys()]);
|
|
144
|
+
const changedDeclarationRuleKeys = new Set(declarationChanges.map((change) => (change.after ?? change.before)?.ruleKey));
|
|
145
|
+
return keys.flatMap((key) => {
|
|
146
|
+
const before = baseShape.get(key);
|
|
147
|
+
const after = currentShape.get(key);
|
|
148
|
+
if ((before?.hash ?? '') === (after?.hash ?? '')) return [];
|
|
149
|
+
if (before?.representedByDeclarations || after?.representedByDeclarations) return [];
|
|
150
|
+
if (changedDeclarationRuleKeys.has(before?.ruleKey) || changedDeclarationRuleKeys.has(after?.ruleKey)) return [];
|
|
151
|
+
return [{
|
|
152
|
+
side,
|
|
153
|
+
reasonCode: 'css-source-shape-unsupported',
|
|
154
|
+
shapeKey: key,
|
|
155
|
+
before: sourceShapeDetails(before),
|
|
156
|
+
after: sourceShapeDetails(after)
|
|
157
|
+
}];
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sourceShapeIndex(sheet, hash) {
|
|
162
|
+
const result = new Map();
|
|
163
|
+
for (const record of sheet.records ?? []) {
|
|
164
|
+
if (record.kind === 'rule') {
|
|
165
|
+
const ruleKey = ruleIdentityKey(record);
|
|
166
|
+
const declarations = record.declarations ?? [];
|
|
167
|
+
const exportName = sheet.cssModules?.exports?.find((entry) => (entry.ruleHashes ?? []).includes(record.ruleHash))?.name;
|
|
168
|
+
result.set(`rule:${ruleKey}`, {
|
|
169
|
+
kind: 'rule',
|
|
170
|
+
ruleKey,
|
|
171
|
+
selectors: record.selectors,
|
|
172
|
+
representedByDeclarations: declarations.length > 0,
|
|
173
|
+
contractKey: exportName ? `export:${exportName}` : undefined,
|
|
174
|
+
hash: hash?.({ kind: 'frontier.lang.css.sourceShape.rule.v1', ruleKey, declarations: declarations.length, exportName, ruleHash: record.ruleHash })
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (record.kind === 'at-rule') {
|
|
178
|
+
const shapeKey = `at-rule:${[...(record.scopes ?? []), record.atRuleName, record.conditionText].join('::')}`;
|
|
179
|
+
result.set(shapeKey, {
|
|
180
|
+
kind: 'at-rule',
|
|
181
|
+
atRuleName: record.atRuleName,
|
|
182
|
+
conditionText: record.conditionText,
|
|
183
|
+
representedByDeclarations: false,
|
|
184
|
+
hash: record.atRuleHash
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function conflict(id, sourcePath, code, reasonCode, details = {}) {
|
|
192
|
+
const conflictTarget = details.cascadeKey ?? details.contractKey ?? details.shapeKey ?? sourcePath ?? 'source';
|
|
193
|
+
return { code, gateId: 'css-semantic-merge', sourcePath, details: { reasonCode, conflictKey: `css#${id}#${reasonCode}#${conflictTarget}`, ...details } };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function sameContractChange(left, right) { return (left.after?.hash ?? '') === (right.after?.hash ?? '') && left.kind === right.kind; }
|
|
197
|
+
function contractChangeDetails(change) { return { kind: change.kind, contractKind: (change.after ?? change.before)?.contractKind, name: (change.after ?? change.before)?.name, hash: change.after?.hash }; }
|
|
198
|
+
function sourceShapeDetails(shape) { return shape ? { kind: shape.kind, selectors: shape.selectors, atRuleName: shape.atRuleName, conditionText: shape.conditionText, representedByDeclarations: shape.representedByDeclarations } : undefined; }
|
|
199
|
+
function ruleIdentityKey(record) { return [...(record.scopes ?? []), record.selectors.join(',')].join('::'); }
|
|
200
|
+
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
201
|
+
function uniqueProofGaps(values) {
|
|
202
|
+
const byCode = new Map();
|
|
203
|
+
for (const value of values) if (value?.code && !byCode.has(value.code)) byCode.set(value.code, value);
|
|
204
|
+
return [...byCode.values()];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeConflicts };
|
package/dist/semantic-merge.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cssModuleContractChanges, cssModuleContractConflicts, sheetOptions, unsupportedSourceShapeConflicts } from './semantic-merge-css-modules.js';
|
|
2
|
+
|
|
1
3
|
function safeMergeCssSource(input = {}, context = {}) {
|
|
2
4
|
const parseSheet = context.parseCssSemanticSheet;
|
|
3
5
|
const hash = context.hashSemanticValue;
|
|
@@ -11,21 +13,24 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
11
13
|
if (worker === base) return merged(id, sourcePath, head, 'worker-unchanged', hash);
|
|
12
14
|
if (head === base) return merged(id, sourcePath, worker, 'head-unchanged', hash);
|
|
13
15
|
const sheets = {
|
|
14
|
-
base: parseSheet(base,
|
|
15
|
-
worker: parseSheet(worker,
|
|
16
|
-
head: parseSheet(head,
|
|
16
|
+
base: parseSheet(base, sheetOptions(input, 'base', sourcePath)),
|
|
17
|
+
worker: parseSheet(worker, sheetOptions(input, 'worker', sourcePath)),
|
|
18
|
+
head: parseSheet(head, sheetOptions(input, 'head', sourcePath))
|
|
17
19
|
};
|
|
18
20
|
const indexes = Object.fromEntries(Object.entries(sheets).map(([name, sheet]) => [name, declarationIndex(sheet)]));
|
|
19
21
|
const changed = {
|
|
20
22
|
worker: changedDeclarations(indexes.base, indexes.worker, 'worker'),
|
|
21
23
|
head: changedDeclarations(indexes.base, indexes.head, 'head')
|
|
22
24
|
};
|
|
25
|
+
const moduleChanges = cssModuleContractChanges(sheets, hash);
|
|
23
26
|
const proofConflicts = proofGapConflicts(id, sourcePath, changed, indexes);
|
|
24
27
|
const overlapConflicts = [
|
|
25
28
|
...overlapDeclarationConflicts(id, sourcePath, changed.worker, changed.head),
|
|
26
29
|
...shorthandOverlapConflicts(id, sourcePath, changed.worker, changed.head)
|
|
27
30
|
];
|
|
28
|
-
const
|
|
31
|
+
const moduleConflicts = cssModuleContractConflicts(id, sourcePath, moduleChanges);
|
|
32
|
+
const sourceShapeConflicts = unsupportedSourceShapeConflicts(id, sourcePath, sheets, changed, hash);
|
|
33
|
+
const conflicts = [...proofConflicts, ...overlapConflicts, ...moduleConflicts, ...sourceShapeConflicts];
|
|
29
34
|
if (conflicts.length) return blocked(id, sourcePath, 'css-semantic-merge-conflict', conflicts);
|
|
30
35
|
const mergedIndex = applyDeclarationChanges(applyDeclarationChanges(indexes.base, changed.head), changed.worker);
|
|
31
36
|
return merged(id, sourcePath, renderDeclarationIndex(mergedIndex), 'semantic-declaration-merge', hash, {
|
|
@@ -33,7 +38,9 @@ function safeMergeCssSource(input = {}, context = {}) {
|
|
|
33
38
|
workerSheetHash: sheets.worker.sheetHash,
|
|
34
39
|
headSheetHash: sheets.head.sheetHash,
|
|
35
40
|
workerChangedDeclarations: changed.worker.length,
|
|
36
|
-
headChangedDeclarations: changed.head.length
|
|
41
|
+
headChangedDeclarations: changed.head.length,
|
|
42
|
+
workerChangedCssModuleContracts: moduleChanges.worker.length,
|
|
43
|
+
headChangedCssModuleContracts: moduleChanges.head.length
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
|
|
@@ -143,18 +150,33 @@ function renderDeclarationIndex(index) {
|
|
|
143
150
|
const groups = new Map();
|
|
144
151
|
for (const key of index.order) {
|
|
145
152
|
const declaration = index.declarations.get(key);
|
|
146
|
-
if (!declaration
|
|
153
|
+
if (!declaration) continue;
|
|
147
154
|
groups.set(declaration.ruleKey, [...(groups.get(declaration.ruleKey) ?? []), declaration]);
|
|
148
155
|
}
|
|
149
156
|
const chunks = [];
|
|
150
157
|
for (const declarations of groups.values()) {
|
|
151
|
-
chunks
|
|
152
|
-
for (const declaration of declarations) chunks.push(` ${declaration.property}: ${declaration.value};`);
|
|
153
|
-
chunks.push('}', '');
|
|
158
|
+
renderDeclarationGroup(chunks, declarations);
|
|
154
159
|
}
|
|
155
160
|
return `${chunks.join('\n').trimEnd()}\n`;
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
function renderDeclarationGroup(chunks, declarations) {
|
|
164
|
+
const first = declarations[0];
|
|
165
|
+
let indent = 0;
|
|
166
|
+
for (const scope of first.scopes) {
|
|
167
|
+
chunks.push(`${spaces(indent)}${scope} {`);
|
|
168
|
+
indent += 2;
|
|
169
|
+
}
|
|
170
|
+
chunks.push(`${spaces(indent)}${first.selectors.join(', ')} {`);
|
|
171
|
+
for (const declaration of declarations) chunks.push(`${spaces(indent + 2)}${declaration.property}: ${declaration.value};`);
|
|
172
|
+
chunks.push(`${spaces(indent)}}`);
|
|
173
|
+
for (let index = first.scopes.length - 1; index >= 0; index -= 1) {
|
|
174
|
+
indent -= 2;
|
|
175
|
+
chunks.push(`${spaces(indent)}}`);
|
|
176
|
+
}
|
|
177
|
+
chunks.push('');
|
|
178
|
+
}
|
|
179
|
+
|
|
158
180
|
function merged(id, sourcePath, sourceText, operation, hash, extra = {}) {
|
|
159
181
|
return result(id, sourcePath, 'merged', {
|
|
160
182
|
operation,
|
|
@@ -202,6 +224,7 @@ function proofGapsForDeclaration(record, declaration) {
|
|
|
202
224
|
return (record.proofGaps ?? []).filter((gap) => gap.code !== 'css-shorthand-expansion-unproved' || gap.summary.includes(` ${declaration.property} `));
|
|
203
225
|
}
|
|
204
226
|
function unique(values) { return [...new Set(values.filter(Boolean))]; }
|
|
227
|
+
function spaces(count) { return ' '.repeat(Math.max(0, count)); }
|
|
205
228
|
|
|
206
229
|
function shorthandGroupForProperty(property) {
|
|
207
230
|
if (ShorthandGroups.has(property)) return property;
|
package/package.json
CHANGED