@teleporthq/teleport-plugin-css 0.44.0-alpha.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,9 @@ import {
5
5
  ChunkType,
6
6
  FileType,
7
7
  } from '@teleporthq/teleport-types'
8
- import { join, relative } from 'path'
8
+ import { join } from 'path'
9
+ import { GenericUtils } from '@teleporthq/teleport-shared'
10
+ import { sanitizeStylesheetSelectors } from './utils'
9
11
 
10
12
  interface StyleSheetPlugin {
11
13
  fileName?: string
@@ -64,7 +66,7 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
64
66
 
65
67
  const fontPath = relativeFontPath
66
68
  ? join(
67
- relative(join(...(uidl.outputOptions?.folderPath || [])), './'),
69
+ GenericUtils.localRelativePath(join(...(uidl.outputOptions?.folderPath || [])), './'),
68
70
  join(fontsFolder || '', font.path)
69
71
  )
70
72
  : join('/', assetIdentifier || '', 'fonts', font.path)
@@ -93,7 +95,7 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
93
95
  styleSetDefinitions,
94
96
  cssMap,
95
97
  mediaStylesMap,
96
- (styleName: string) => styleName
98
+ (styleName) => styleName
97
99
  )
98
100
  }
99
101
 
@@ -107,7 +109,11 @@ export const createStyleSheetPlugin: ComponentPluginFactory<StyleSheetPlugin> =
107
109
  name: fileName,
108
110
  type: ChunkType.STRING,
109
111
  fileType: FileType.CSS,
110
- content: cssMap.join('\n'),
112
+ // Repair unbalanced selector parens before emitting — a stray `)` in a
113
+ // single AI-authored selector otherwise breaks the consumer's production
114
+ // `next build` (cssnano selector parser: "Expected an opening parenthesis"),
115
+ // while `next dev` silently tolerates it.
116
+ content: sanitizeStylesheetSelectors(cssMap.join('\n \n')),
111
117
  linkAfter: [],
112
118
  })
113
119
 
package/src/utils.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { ASTUtils } from '@teleporthq/teleport-plugin-common'
2
+ import { PluginCSS, UIDLConditionExpressionEntry, UIDLPropDefinition } from '@teleporthq/teleport-types'
3
+
4
+ export const createConditionalStatement = (
5
+ conditions: UIDLConditionExpressionEntry[],
6
+ leftOperand: UIDLPropDefinition['defaultValue']
7
+ ) => {
8
+ return conditions.map((condition) => {
9
+ const { operation, operand } = condition
10
+
11
+ if (operand === undefined) {
12
+ return `${ASTUtils.convertToUnaryOperator(operation)}${getValueType(operand)}`
13
+ }
14
+
15
+ return `${getValueType(leftOperand)} ${ASTUtils.convertToBinaryOperator(
16
+ operation
17
+ )} ${getValueType(operand)}`
18
+ })
19
+ }
20
+
21
+ /**
22
+ * Balance parentheses inside a single CSS selector "prelude" (the text before a
23
+ * `{`). Drops unmatched `)` and closes unmatched `(`, while leaving quoted
24
+ * strings untouched. Valid balanced parens (`:not(.x)`, `:nth-child(2n+1)`,
25
+ * attribute values containing `)`) are preserved byte-for-byte.
26
+ */
27
+ const balanceSelectorParens = (prelude: string): string => {
28
+ let depth = 0
29
+ let out = ''
30
+ let quote: string | null = null
31
+ for (let i = 0; i < prelude.length; i++) {
32
+ const ch = prelude[i]
33
+ // A backslash escapes the next char (e.g. an escaped `\(` / `\)` inside a
34
+ // class name) — copy both verbatim so escaped parens are never counted.
35
+ if (ch === '\\' && i + 1 < prelude.length) {
36
+ out += ch + prelude[i + 1]
37
+ i++
38
+ continue
39
+ }
40
+ if (quote) {
41
+ out += ch
42
+ if (ch === quote) {
43
+ quote = null
44
+ }
45
+ continue
46
+ }
47
+ if (ch === '"' || ch === "'") {
48
+ quote = ch
49
+ out += ch
50
+ continue
51
+ }
52
+ if (ch === '(') {
53
+ depth++
54
+ out += ch
55
+ continue
56
+ }
57
+ if (ch === ')') {
58
+ if (depth === 0) {
59
+ // Unmatched closing paren — drop it. A stray `)` in a selector makes the
60
+ // production cssnano selector parser throw "Expected an opening
61
+ // parenthesis", failing the whole `next build` even though dev tolerates it.
62
+ continue
63
+ }
64
+ depth--
65
+ out += ch
66
+ continue
67
+ }
68
+ out += ch
69
+ }
70
+ if (depth > 0) {
71
+ out += ')'.repeat(depth)
72
+ }
73
+ return out
74
+ }
75
+
76
+ /**
77
+ * Repairs unbalanced parentheses in every selector of a generated stylesheet so a
78
+ * single malformed AI-authored selector (e.g. `.card:hover) { … }`) can't break
79
+ * the consumer's production build. Only selector preludes (text immediately
80
+ * before each `{`) are touched; declaration blocks and quoted strings are left
81
+ * exactly as-is. Idempotent and a no-op for well-formed CSS.
82
+ */
83
+ export const sanitizeStylesheetSelectors = (css: string): string => {
84
+ let out = ''
85
+ let segStart = 0
86
+ let quote: string | null = null
87
+ for (let i = 0; i < css.length; i++) {
88
+ const ch = css[i]
89
+ if (ch === '\\') {
90
+ // Skip the escaped char so an escaped `\{` / `\}` / `\"` is never read as a
91
+ // block boundary or quote delimiter.
92
+ i++
93
+ continue
94
+ }
95
+ if (quote) {
96
+ if (ch === quote) {
97
+ quote = null
98
+ }
99
+ continue
100
+ }
101
+ if (ch === '"' || ch === "'") {
102
+ quote = ch
103
+ continue
104
+ }
105
+ if (ch === '{') {
106
+ out += balanceSelectorParens(css.slice(segStart, i)) + ch
107
+ segStart = i + 1
108
+ } else if (ch === '}') {
109
+ out += css.slice(segStart, i) + ch
110
+ segStart = i + 1
111
+ }
112
+ }
113
+ out += css.slice(segStart)
114
+ return out
115
+ }
116
+
117
+ const getValueType = (value: UIDLPropDefinition['defaultValue']) => {
118
+ const valueType = typeof value
119
+ switch (valueType) {
120
+ case 'string':
121
+ return `"${value}"`
122
+ case 'number':
123
+ return value
124
+ case 'boolean':
125
+ return value
126
+ default:
127
+ throw new PluginCSS(
128
+ `Conditional node received an operand of type ${valueType} \n
129
+ Received ${JSON.stringify(value)}`
130
+ )
131
+ }
132
+ }