@shapeshift-labs/frontier-lang-compiler 0.2.154 → 0.2.155

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.
@@ -0,0 +1,84 @@
1
+ import { readStaticMemberLiteral } from './staticMemberLiteral.js';
2
+ import { escapeRegExp, isIdentifierChar } from './projectSymbolGraphCssModuleUtils.js';
3
+
4
+ const IdentifierPattern = /^[A-Za-z_$][\w$]*$/;
5
+
6
+ function cssModuleMemberAccess(sourceText, receiverEnd) {
7
+ let index = receiverEnd;
8
+ while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
9
+ if (sourceText[index] === '?' && sourceText[index + 1] === '.') index += 2;
10
+ else if (sourceText[index] === '.') index += 1;
11
+ else if (sourceText[index] === '[') return cssModuleComputedAccess(sourceText, index);
12
+ else return undefined;
13
+ while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
14
+ const match = /^[A-Za-z_$][\w$]*/.exec(sourceText.slice(index));
15
+ return match ? { accessKind: 'dot', memberName: match[0], end: index + match[0].length } : undefined;
16
+ }
17
+
18
+ function cssModuleComputedAccess(sourceText, open) {
19
+ const close = findMatchingBracket(sourceText, open);
20
+ if (close === -1) return { status: 'blocked', reasonCode: 'css-module-dynamic-member-access-unproved', end: open + 1 };
21
+ let index = open + 1;
22
+ while (index < close && /\s/.test(sourceText[index])) index += 1;
23
+ const literal = readStaticMemberLiteral(sourceText, index, close);
24
+ if (!literal || !IdentifierPattern.test(literal.value)) {
25
+ return { status: 'blocked', reasonCode: 'css-module-dynamic-member-access-unproved', end: close + 1, expressionText: sourceText.slice(open, close + 1) };
26
+ }
27
+ return { accessKind: literal.literalKind === 'static-template-literal' ? 'static-template' : 'static-bracket', memberName: literal.value, end: close + 1 };
28
+ }
29
+
30
+ function cssModuleStaticExpressionAccess(expressionText, localName) {
31
+ const value = String(expressionText ?? '').trim();
32
+ if (!value.startsWith(localName)) return undefined;
33
+ if (isIdentifierChar(value[localName.length])) return undefined;
34
+ const access = cssModuleMemberAccess(value, localName.length);
35
+ if (!access || access.status === 'blocked' || access.end !== value.length) return undefined;
36
+ return {
37
+ accessKind: access.accessKind,
38
+ memberName: access.memberName,
39
+ optional: /\?\./.test(value.slice(0, access.end))
40
+ };
41
+ }
42
+
43
+ function cssModuleExpressionHasCall(expressionText) {
44
+ const value = String(expressionText ?? '');
45
+ return /\?\.\s*\(/.test(value) || /\b[A-Za-z_$][\w$]*(?:\s*(?:\.|\?\.)\s*[A-Za-z_$][\w$]*)*\s*\(/.test(value);
46
+ }
47
+
48
+ function cssModuleExpressionHasBlockedAccess(expressionText, localName) {
49
+ const value = String(expressionText ?? '');
50
+ for (const occurrence of identifierOccurrences(value, localName)) {
51
+ const access = cssModuleMemberAccess(value, occurrence.end);
52
+ if (access?.status === 'blocked') return true;
53
+ }
54
+ return false;
55
+ }
56
+
57
+ function identifierOccurrences(sourceText, name) {
58
+ const result = [];
59
+ const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`, 'g');
60
+ for (const match of sourceText.matchAll(pattern)) {
61
+ const start = match.index;
62
+ const end = start + name.length;
63
+ if (isIdentifierChar(sourceText[start - 1]) || isIdentifierChar(sourceText[end])) continue;
64
+ result.push({ start, end });
65
+ }
66
+ return result;
67
+ }
68
+
69
+ function findMatchingBracket(sourceText, open) {
70
+ let depth = 0;
71
+ for (let index = open; index < sourceText.length; index += 1) {
72
+ if (sourceText[index] === '[') depth += 1;
73
+ else if (sourceText[index] === ']' && --depth === 0) return index;
74
+ }
75
+ return -1;
76
+ }
77
+
78
+ export {
79
+ cssModuleExpressionHasBlockedAccess,
80
+ cssModuleExpressionHasCall,
81
+ cssModuleMemberAccess,
82
+ cssModuleStaticExpressionAccess,
83
+ identifierOccurrences
84
+ };
@@ -1,8 +1,6 @@
1
- import { readStaticMemberLiteral } from './staticMemberLiteral.js';
2
1
  import {
3
2
  bindingsForSourcePath,
4
3
  escapeRegExp,
5
- isIdentifierChar,
6
4
  localKey,
7
5
  sourceSpanForRange
8
6
  } from './projectSymbolGraphCssModuleUtils.js';
@@ -11,8 +9,13 @@ import {
11
9
  cssModulePropBlocker,
12
10
  cssModuleUseSiteRecord
13
11
  } from './projectSymbolGraphCssModuleRecords.js';
14
-
15
- const IdentifierPattern = /^[A-Za-z_$][\w$]*$/;
12
+ import {
13
+ cssModuleExpressionHasBlockedAccess,
14
+ cssModuleExpressionHasCall,
15
+ cssModuleMemberAccess,
16
+ cssModuleStaticExpressionAccess,
17
+ identifierOccurrences
18
+ } from './projectSymbolGraphCssModuleMemberAccess.js';
16
19
 
17
20
  function cssModuleLexicalUseSites(importBindings, sourceTextsByPath) {
18
21
  const useSites = [];
@@ -99,13 +102,15 @@ function cssModuleJsxUseSites(bindingsByLocal, jsxPropRecords) {
99
102
  useSites.push(...cssModuleJsxStaticUseSites(sourceBindings, prop));
100
103
  const classNameBindings = bindingsForSourcePath(bindingsByLocal, prop.sourcePath);
101
104
  if (prop.propName !== 'className' || !classNameBindings.length) continue;
105
+ useSites.push(...cssModuleJsxStaticComputedUseSites(classNameBindings, prop));
106
+ useSites.push(...cssModuleJsxHelperUseSites(classNameBindings, prop));
102
107
  blockers.push(...cssModuleJsxBlockers(classNameBindings, prop));
103
108
  }
104
109
  return { useSites, blockers };
105
110
  }
106
111
 
107
112
  function cssModuleJsxStaticUseSites(sourceBindings, prop) {
108
- if (prop.propName !== 'className' || prop.propValueKind !== 'reference') return [];
113
+ if (prop.propName !== 'className' || (prop.propValueKind !== 'reference' && prop.propValueKind !== 'optional-reference')) return [];
109
114
  const referencePath = prop.propValueReferencePath ?? [];
110
115
  if (referencePath.length < 2) return [];
111
116
  return sourceBindings.map((binding) => cssModuleUseSiteRecord(binding, {
@@ -117,43 +122,75 @@ function cssModuleJsxStaticUseSites(sourceBindings, prop) {
117
122
  sourcePath: prop.sourcePath,
118
123
  sourceHash: prop.sourceHash,
119
124
  sourceSpan: prop.sourceSpan,
120
- jsxPropRecordId: prop.id
125
+ jsxPropRecordId: prop.id,
126
+ conditionalRuntimePresence: prop.propValueKind === 'optional-reference' || undefined
121
127
  }));
122
128
  }
123
129
 
124
- function cssModuleJsxBlockers(classNameBindings, prop) {
125
- if (prop.propValueKind === 'string') {
126
- return classNameBindings.map((binding) => cssModulePropBlocker(binding, prop, 'css-module-string-literal-classname-unproved'));
127
- }
128
- if (!prop.propValueDynamicBlockerReasonCode) return [];
130
+ function cssModuleJsxStaticComputedUseSites(classNameBindings, prop) {
131
+ if (prop.propValueKind === 'reference' || prop.propValueKind === 'optional-reference') return [];
129
132
  const expressionText = prop.propValueExpressionText ?? prop.propValueDynamicText ?? '';
130
- return classNameBindings
131
- .filter((binding) => expressionText.includes(binding.localName))
132
- .map((binding) => cssModulePropBlocker(binding, prop, jsxDynamicReason(prop.propValueDynamicBlockerReasonCode)));
133
+ return classNameBindings.flatMap((binding) => {
134
+ const access = cssModuleStaticExpressionAccess(expressionText, binding.localName);
135
+ if (!access) return [];
136
+ return [cssModuleUseSiteRecord(binding, {
137
+ useSiteKind: 'jsx-className',
138
+ accessKind: access.accessKind,
139
+ exportName: access.memberName,
140
+ receiverLocalName: binding.localName,
141
+ expressionText,
142
+ sourcePath: prop.sourcePath,
143
+ sourceHash: prop.sourceHash,
144
+ sourceSpan: prop.sourceSpan,
145
+ jsxPropRecordId: prop.id,
146
+ conditionalRuntimePresence: access.optional || undefined
147
+ })];
148
+ });
133
149
  }
134
150
 
135
- function cssModuleMemberAccess(sourceText, receiverEnd) {
136
- let index = receiverEnd;
137
- while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
138
- if (sourceText[index] === '?' && sourceText[index + 1] === '.') index += 2;
139
- else if (sourceText[index] === '.') index += 1;
140
- else if (sourceText[index] === '[') return cssModuleComputedAccess(sourceText, index);
141
- else return undefined;
142
- while (index < sourceText.length && /\s/.test(sourceText[index])) index += 1;
143
- const match = /^[A-Za-z_$][\w$]*/.exec(sourceText.slice(index));
144
- return match ? { accessKind: 'dot', memberName: match[0], end: index + match[0].length } : undefined;
151
+ function cssModuleJsxHelperUseSites(classNameBindings, prop) {
152
+ const expressionText = prop.propValueExpressionText ?? prop.propValueDynamicText ?? '';
153
+ if (!cssModuleExpressionHasCall(expressionText)) return [];
154
+ const useSites = [];
155
+ for (const binding of classNameBindings) {
156
+ for (const occurrence of identifierOccurrences(expressionText, binding.localName)) {
157
+ const access = cssModuleMemberAccess(expressionText, occurrence.end);
158
+ if (!access || access.status === 'blocked') continue;
159
+ if (memberWriteOperation(expressionText, occurrence.start, access.end)) continue;
160
+ useSites.push(cssModuleUseSiteRecord(binding, {
161
+ useSiteKind: 'jsx-className-helper',
162
+ accessKind: access.accessKind,
163
+ exportName: access.memberName,
164
+ receiverLocalName: binding.localName,
165
+ expressionText: expressionText.slice(occurrence.start, access.end),
166
+ sourcePath: prop.sourcePath,
167
+ sourceHash: prop.sourceHash,
168
+ sourceSpan: prop.sourceSpan,
169
+ jsxPropRecordId: prop.id,
170
+ conditionalRuntimePresence: cssModuleHelperArgumentIsConditional(expressionText, occurrence.start) || undefined
171
+ }));
172
+ }
173
+ }
174
+ return useSites;
145
175
  }
146
176
 
147
- function cssModuleComputedAccess(sourceText, open) {
148
- const close = findMatchingBracket(sourceText, open);
149
- if (close === -1) return { status: 'blocked', reasonCode: 'css-module-dynamic-member-access-unproved', end: open + 1 };
150
- let index = open + 1;
151
- while (index < close && /\s/.test(sourceText[index])) index += 1;
152
- const literal = readStaticMemberLiteral(sourceText, index, close);
153
- if (!literal || !IdentifierPattern.test(literal.value)) {
154
- return { status: 'blocked', reasonCode: 'css-module-dynamic-member-access-unproved', end: close + 1, expressionText: sourceText.slice(open, close + 1) };
177
+ function cssModuleJsxBlockers(classNameBindings, prop) {
178
+ if (prop.propValueKind === 'string') {
179
+ return classNameBindings.map((binding) => cssModulePropBlocker(binding, prop, 'css-module-string-literal-classname-unproved'));
155
180
  }
156
- return { accessKind: literal.literalKind === 'static-template-literal' ? 'static-template' : 'static-bracket', memberName: literal.value, end: close + 1 };
181
+ const expressionText = prop.propValueExpressionText ?? prop.propValueDynamicText ?? '';
182
+ if (!expressionText) return [];
183
+ return classNameBindings.flatMap((binding) => {
184
+ if (!expressionText.includes(binding.localName)) return [];
185
+ const reasonCodes = [];
186
+ if (cssModuleExpressionHasCall(expressionText)) reasonCodes.push('css-module-helper-call-unproved');
187
+ if (cssModuleExpressionHasBlockedAccess(expressionText, binding.localName)) reasonCodes.push('css-module-dynamic-member-access-unproved');
188
+ if (!reasonCodes.length && cssModuleStaticExpressionAccess(expressionText, binding.localName)) return [];
189
+ if (!reasonCodes.length && prop.propValueDynamicBlockerReasonCode) {
190
+ reasonCodes.push(jsxDynamicReason(prop.propValueDynamicBlockerReasonCode));
191
+ }
192
+ return [...new Set(reasonCodes)].map((reasonCode) => cssModulePropBlocker(binding, prop, reasonCode));
193
+ });
157
194
  }
158
195
 
159
196
  function memberWriteOperation(sourceText, start, end) {
@@ -183,15 +220,6 @@ function hasPrefixUpdateOperator(code, tokenStart) {
183
220
  return before < 0 || !/[A-Za-z0-9_$)\]]/.test(code[before]);
184
221
  }
185
222
 
186
- function findMatchingBracket(sourceText, open) {
187
- let depth = 0;
188
- for (let index = open; index < sourceText.length; index += 1) {
189
- if (sourceText[index] === '[') depth += 1;
190
- else if (sourceText[index] === ']' && --depth === 0) return index;
191
- }
192
- return -1;
193
- }
194
-
195
223
  function destructuringProperty(text) {
196
224
  const value = String(text ?? '').trim();
197
225
  const [rawName, rawLocal] = value.split(':').map((part) => part.trim());
@@ -221,16 +249,14 @@ function splitTopLevelComma(text, baseOffset) {
221
249
  return parts;
222
250
  }
223
251
 
224
- function identifierOccurrences(sourceText, name) {
225
- const result = [];
226
- const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`, 'g');
227
- for (const match of sourceText.matchAll(pattern)) {
228
- const start = match.index;
229
- const end = start + name.length;
230
- if (isIdentifierChar(sourceText[start - 1]) || isIdentifierChar(sourceText[end])) continue;
231
- result.push({ start, end });
232
- }
233
- return result;
252
+ function cssModuleHelperArgumentIsConditional(expressionText, occurrenceStart) {
253
+ const prefix = expressionText.slice(0, occurrenceStart);
254
+ const suffix = expressionText.slice(occurrenceStart);
255
+ const lastComma = prefix.lastIndexOf(',');
256
+ const argumentPrefix = prefix.slice(lastComma + 1);
257
+ const nextComma = suffix.indexOf(',');
258
+ const argumentText = `${argumentPrefix}${nextComma === -1 ? suffix : suffix.slice(0, nextComma)}`;
259
+ return /(?:&&|\|\||\?)/.test(argumentText);
234
260
  }
235
261
 
236
262
  function jsxDynamicReason(reasonCode) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shapeshift-labs/frontier-lang-compiler",
3
- "version": "0.2.154",
3
+ "version": "0.2.155",
4
4
  "description": "Compiler facade for Frontier Lang source documents and language projection adapters.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",