unframer 2.7.6 → 2.7.8

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.
Files changed (78) hide show
  1. package/README.md +5 -6
  2. package/dist/babel-plugin-imports.d.ts +21 -0
  3. package/dist/babel-plugin-imports.d.ts.map +1 -0
  4. package/dist/babel-plugin-imports.js +375 -0
  5. package/dist/babel-plugin-imports.js.map +1 -0
  6. package/dist/cli.d.ts +14 -0
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +9 -42
  9. package/dist/cli.js.map +1 -1
  10. package/dist/css.d.ts.map +1 -1
  11. package/dist/css.js +4 -3
  12. package/dist/css.js.map +1 -1
  13. package/dist/esbuild.d.ts +7 -0
  14. package/dist/esbuild.d.ts.map +1 -1
  15. package/dist/esbuild.js +15 -1
  16. package/dist/esbuild.js.map +1 -1
  17. package/dist/exporter.d.ts +6 -14
  18. package/dist/exporter.d.ts.map +1 -1
  19. package/dist/exporter.js +79 -38
  20. package/dist/exporter.js.map +1 -1
  21. package/dist/exporter.test.js +48 -0
  22. package/dist/exporter.test.js.map +1 -1
  23. package/dist/framer.d.ts.map +1 -1
  24. package/dist/framer.js +26 -1788
  25. package/dist/framer.js.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/renamer.d.ts +12 -0
  29. package/dist/renamer.d.ts.map +1 -0
  30. package/dist/renamer.js +169 -0
  31. package/dist/renamer.js.map +1 -0
  32. package/dist/unframer-loader.d.ts.map +1 -1
  33. package/dist/unframer-loader.js +4 -3
  34. package/dist/unframer-loader.js.map +1 -1
  35. package/esm/babel-plugin-imports.d.ts +21 -0
  36. package/esm/babel-plugin-imports.d.ts.map +1 -0
  37. package/esm/babel-plugin-imports.js +344 -0
  38. package/esm/babel-plugin-imports.js.map +1 -0
  39. package/esm/cli.d.ts +14 -0
  40. package/esm/cli.d.ts.map +1 -1
  41. package/esm/cli.js +9 -42
  42. package/esm/cli.js.map +1 -1
  43. package/esm/css.d.ts.map +1 -1
  44. package/esm/css.js +3 -2
  45. package/esm/css.js.map +1 -1
  46. package/esm/esbuild.d.ts +7 -0
  47. package/esm/esbuild.d.ts.map +1 -1
  48. package/esm/esbuild.js +13 -0
  49. package/esm/esbuild.js.map +1 -1
  50. package/esm/exporter.d.ts +6 -14
  51. package/esm/exporter.d.ts.map +1 -1
  52. package/esm/exporter.js +78 -37
  53. package/esm/exporter.js.map +1 -1
  54. package/esm/exporter.test.js +48 -0
  55. package/esm/exporter.test.js.map +1 -1
  56. package/esm/framer.d.ts.map +1 -1
  57. package/esm/framer.js +27 -1788
  58. package/esm/framer.js.map +1 -1
  59. package/esm/index.d.ts.map +1 -1
  60. package/esm/index.js.map +1 -1
  61. package/esm/renamer.d.ts +12 -0
  62. package/esm/renamer.d.ts.map +1 -0
  63. package/esm/renamer.js +140 -0
  64. package/esm/renamer.js.map +1 -0
  65. package/esm/unframer-loader.d.ts.map +1 -1
  66. package/esm/unframer-loader.js +4 -3
  67. package/esm/unframer-loader.js.map +1 -1
  68. package/package.json +6 -4
  69. package/src/babel-plugin-imports.ts +441 -0
  70. package/src/cli.tsx +11 -52
  71. package/src/css.ts +3 -2
  72. package/src/esbuild.ts +24 -2
  73. package/src/exporter.test.ts +66 -0
  74. package/src/exporter.ts +95 -42
  75. package/src/framer.js +25 -1827
  76. package/src/index.ts +2 -0
  77. package/src/renamer.ts +184 -0
  78. package/src/unframer-loader.ts +7 -3
@@ -0,0 +1,441 @@
1
+ import * as BabelTypes from '@babel/types'
2
+
3
+ import { PluginObj } from '@babel/core'
4
+ import { ImportDeclaration, ImportSpecifier, Identifier } from '@babel/types'
5
+ import BatchRenamer from './renamer'
6
+ import { logger } from './utils'
7
+
8
+ export function babelPluginDeduplicateImports({
9
+ types: t,
10
+ }: {
11
+ types: typeof BabelTypes
12
+ }): PluginObj {
13
+ const importAliasMap = new Map<
14
+ string,
15
+ Map<
16
+ string,
17
+ { consolidated: string; importName; path: ImportSpecifier; source }
18
+ >
19
+ >()
20
+
21
+ function addImport({
22
+ source,
23
+ local,
24
+ consolidated,
25
+ importName,
26
+ path,
27
+ }: {
28
+ source: string
29
+ local: string
30
+ consolidated: string
31
+ importName: string
32
+ path
33
+ }) {
34
+ if (!importAliasMap.has(source)) {
35
+ importAliasMap.set(source, new Map())
36
+ }
37
+ importAliasMap
38
+ .get(source)
39
+ ?.set(local, { consolidated, importName, path, source })
40
+ }
41
+
42
+ function getConsolidatedName({ source, importName, defaultOne }) {
43
+ const allSpecifiers = [...importAliasMap.values()].flatMap((x) => [
44
+ ...x.values(),
45
+ ])
46
+ const first = allSpecifiers.find((x) => {
47
+ if (importName === 'default' || importName === 'namespace') {
48
+ if (source !== x.source) {
49
+ return false
50
+ }
51
+ }
52
+ return x.importName === importName
53
+ })
54
+ return first?.consolidated || defaultOne
55
+ }
56
+
57
+ return {
58
+ visitor: {
59
+ ImportDeclaration(path) {
60
+ const source = path.node.source.value
61
+
62
+ path.node.specifiers.forEach((specifier) => {
63
+ if (
64
+ t.isImportSpecifier(specifier) &&
65
+ BabelTypes.isIdentifier(specifier.imported)
66
+ ) {
67
+ const importName = specifier.imported.name
68
+
69
+ const consolidatedName = getConsolidatedName({
70
+ source,
71
+ importName,
72
+ defaultOne: specifier.local.name,
73
+ })
74
+ addImport({
75
+ source,
76
+ local: specifier.local.name,
77
+ importName,
78
+ consolidated: consolidatedName,
79
+ path,
80
+ })
81
+ } else if (t.isImportDefaultSpecifier(specifier)) {
82
+ const importName = 'default'
83
+
84
+ const consolidatedName = getConsolidatedName({
85
+ source,
86
+ importName,
87
+ defaultOne: specifier.local.name,
88
+ })
89
+ addImport({
90
+ source,
91
+ local: specifier.local.name,
92
+ importName,
93
+ consolidated: consolidatedName,
94
+ path,
95
+ })
96
+ } else if (t.isImportNamespaceSpecifier(specifier)) {
97
+ const importName = 'namespace'
98
+
99
+ const consolidatedName = getConsolidatedName({
100
+ source,
101
+ importName,
102
+ defaultOne: specifier.local.name,
103
+ })
104
+ addImport({
105
+ source,
106
+ local: specifier.local.name,
107
+ importName,
108
+ consolidated: consolidatedName,
109
+ path,
110
+ })
111
+ }
112
+ })
113
+
114
+ // Remove the current import declaration to later add the consolidated one
115
+ // path.remove()
116
+ },
117
+ Program: {
118
+ exit(path) {
119
+ console.log(`renaming imports...`)
120
+ for (const [source, modMap] of importAliasMap) {
121
+ // rename import names to consolidated names
122
+ for (let [local, { consolidated, path: p }] of modMap) {
123
+ logger.log(
124
+ `renaming ${local} to ${consolidated}...`,
125
+ )
126
+ }
127
+ }
128
+
129
+ const map = new Map<string, string>(
130
+ [...importAliasMap.values()].flatMap((x) => {
131
+ return [...x.entries()]
132
+ .map(([prev, x]) => {
133
+ if (prev === x.consolidated) {
134
+ return
135
+ }
136
+ return [prev, x.consolidated]
137
+ })
138
+ .filter((x) => x !== undefined) as Array<
139
+ [string, string]
140
+ >
141
+ }),
142
+ )
143
+ // console.log([...map.entries()])
144
+ const renamer = new BatchRenamer(path.scope, map)
145
+ renamer.rename()
146
+
147
+ const importDecs = path.node.body.filter((node) =>
148
+ t.isImportDeclaration(node),
149
+ ) as ImportDeclaration[]
150
+
151
+ const definedImports = new Set<string>()
152
+ const later = [] as Function[]
153
+ console.log(`removing duplicates...`)
154
+ for (let importDec of importDecs) {
155
+ const source = importDec.source.value
156
+
157
+ const specifiers = importDec.specifiers
158
+ for (let specifier of specifiers) {
159
+ if (
160
+ !BabelTypes.isImportSpecifier(specifier) &&
161
+ !BabelTypes.isImportDefaultSpecifier(
162
+ specifier,
163
+ ) &&
164
+ !BabelTypes.isImportNamespaceSpecifier(
165
+ specifier,
166
+ )
167
+ ) {
168
+ continue
169
+ }
170
+ let importKey = ''
171
+ if (
172
+ BabelTypes.isImportDefaultSpecifier(specifier)
173
+ ) {
174
+ importKey = source + 'default'
175
+ } else if (
176
+ BabelTypes.isImportNamespaceSpecifier(specifier)
177
+ ) {
178
+ importKey = source + 'namespace'
179
+ } else {
180
+ if (
181
+ !BabelTypes.isIdentifier(specifier.imported)
182
+ ) {
183
+ continue
184
+ }
185
+ importKey = specifier.imported.name
186
+ }
187
+
188
+ if (definedImports.has(importKey)) {
189
+ later.push(() => {
190
+ console.log(
191
+ `removing ${importKey} from ${source}...`,
192
+ )
193
+
194
+ importDec.specifiers =
195
+ importDec.specifiers.filter(
196
+ (x) => x !== specifier,
197
+ )
198
+ if (!importDec.specifiers.length) {
199
+ const index = path.node.body.findIndex(
200
+ (x) => x === importDec,
201
+ )
202
+ path.node.body.splice(index, 1)
203
+ }
204
+ })
205
+ }
206
+ definedImports.add(importKey)
207
+ }
208
+ }
209
+ for (let fn of later) {
210
+ fn()
211
+ }
212
+ },
213
+ },
214
+ // Identifier(path) {
215
+ // const name = path.node.name
216
+ // const binding = path.scope.getBinding(name)
217
+
218
+ // if (binding && t.isImportSpecifier(binding.path.node)) {
219
+ // const source = (binding.path.parent as ImportDeclaration)
220
+ // .source.value
221
+ // const localName = getLocalImportName(source, name)
222
+
223
+ // if (localName && localName !== name) {
224
+ // path.node.name = localName
225
+ // }
226
+ // }
227
+ // },
228
+ },
229
+ }
230
+ }
231
+
232
+ export function babelPluginRenameExports({
233
+ map,
234
+ }: {
235
+ map: Map<string, string>
236
+ }) {
237
+ return {
238
+ name: 'rename-exports',
239
+ visitor: {
240
+ ExportNamedDeclaration(path) {
241
+ const { specifiers, declaration } = path.node
242
+ // Handle export specifiers like: export { something as oldName }
243
+ for (const specifier of specifiers) {
244
+ if (!BabelTypes.isExportSpecifier(specifier)) continue
245
+ const exported = specifier.exported
246
+ if (!BabelTypes.isIdentifier(exported)) continue
247
+ const oldName = exported.name
248
+ const newName = map.get(oldName)
249
+ if (newName) {
250
+ exported.name = newName
251
+ }
252
+ }
253
+ // Handle function declarations like: export function oldName1() {}
254
+ if (BabelTypes.isFunctionDeclaration(declaration)) {
255
+ const oldName = declaration.id?.name
256
+ if (oldName) {
257
+ const newName = map.get(oldName)
258
+ if (newName && declaration?.id) {
259
+ declaration.id.name = newName
260
+ }
261
+ }
262
+ }
263
+ },
264
+ ExportDefaultDeclaration(path) {
265
+ const { declaration } = path.node
266
+ if (BabelTypes.isIdentifier(declaration)) {
267
+ const oldName = declaration.name
268
+ const newName = map.get(oldName)
269
+ if (newName) {
270
+ declaration.name = newName
271
+ }
272
+ }
273
+ },
274
+ },
275
+ }
276
+ }
277
+
278
+ // Set of types that don't need expression containers
279
+ const noContainerTypes = new Set([
280
+ 'JSXElement',
281
+ // 'StringLiteral',
282
+ 'NumericLiteral',
283
+ ])
284
+
285
+ export function babelPluginJsxTransform() {
286
+ return {
287
+ name: 'jsx-transform',
288
+ visitor: {
289
+ CallExpression(path) {
290
+ // Check if it's a _jsx or _jsxs call
291
+ if (
292
+ !path.node.callee ||
293
+ !path.node.callee.name?.startsWith('_jsx')
294
+ ) {
295
+ return
296
+ }
297
+
298
+ // Remove /* @__PURE__ */ comments
299
+ if (path.node.leadingComments) {
300
+ path.node.leadingComments =
301
+ path.node.leadingComments.filter(
302
+ (comment) => !comment.value.includes('@__PURE__'),
303
+ )
304
+ }
305
+
306
+ const [elementArg, propsArg] = path.node.arguments
307
+
308
+ // Get the element type name
309
+ let elementName = ''
310
+ if (elementArg.type === 'MemberExpression') {
311
+ elementName = `${elementArg.object.name}.${elementArg.property.name}`
312
+ } else if (elementArg.type === 'StringLiteral') {
313
+ elementName = elementArg.value
314
+ } else if (elementArg.type === 'Identifier') {
315
+ elementName = elementArg.name
316
+ } else {
317
+ // Skip if we can't determine element name
318
+ return
319
+ }
320
+
321
+ // Convert to JSX element
322
+ const jsxElement: BabelTypes.JSXElement = {
323
+ type: 'JSXElement',
324
+ openingElement: {
325
+ type: 'JSXOpeningElement',
326
+ name: {
327
+ type: 'JSXIdentifier',
328
+ name: elementName,
329
+ },
330
+ attributes: [],
331
+ selfClosing: !propsArg.properties.find(
332
+ (p) => p.key?.name === 'children',
333
+ ),
334
+ },
335
+ closingElement: propsArg.properties.find(
336
+ (p) => p.key?.name === 'children',
337
+ )
338
+ ? {
339
+ type: 'JSXClosingElement',
340
+ name: {
341
+ type: 'JSXIdentifier',
342
+ name: elementName,
343
+ },
344
+ }
345
+ : null,
346
+ children: [],
347
+ }
348
+
349
+ // Add attributes
350
+ if (propsArg && propsArg.properties) {
351
+ propsArg.properties.forEach((prop) => {
352
+ if (prop.type === 'SpreadElement') {
353
+ jsxElement.openingElement.attributes.push({
354
+ type: 'JSXSpreadAttribute',
355
+ argument: prop.argument,
356
+ })
357
+ } else if (prop.key?.name === 'children') {
358
+ if (prop.value.type === 'ArrayExpression') {
359
+ jsxElement.children = prop.value.elements.map(
360
+ (element) => {
361
+ if (
362
+ noContainerTypes.has(
363
+ element.type,
364
+ ) ||
365
+ (element.type ===
366
+ 'CallExpression' &&
367
+ element.callee?.name?.startsWith(
368
+ '_jsx',
369
+ ))
370
+ ) {
371
+ return element
372
+ }
373
+ return {
374
+ type: 'JSXExpressionContainer',
375
+ expression: element,
376
+ }
377
+ },
378
+ )
379
+ } else {
380
+ if (
381
+ noContainerTypes.has(prop.value.type) ||
382
+ (prop.value.type === 'CallExpression' &&
383
+ prop.value.callee?.name?.startsWith(
384
+ '_jsx',
385
+ ))
386
+ ) {
387
+ jsxElement.children = [prop.value]
388
+ } else {
389
+ jsxElement.children = [
390
+ {
391
+ type: 'JSXExpressionContainer',
392
+ expression: prop.value,
393
+ },
394
+ ]
395
+ }
396
+ }
397
+ } else {
398
+ let attrName = prop.key?.name
399
+ if (
400
+ !attrName &&
401
+ prop.key?.type === 'StringLiteral'
402
+ ) {
403
+ attrName = prop.key.value
404
+ }
405
+ if (!attrName) {
406
+ console.log(
407
+ `no prop.key?.name for ${JSON.stringify(
408
+ prop,
409
+ )}`,
410
+ )
411
+ return
412
+ }
413
+
414
+ jsxElement.openingElement.attributes.push({
415
+ type: 'JSXAttribute',
416
+ name: {
417
+ type: 'JSXIdentifier',
418
+ name: attrName,
419
+ },
420
+ value: {
421
+ type: 'JSXExpressionContainer',
422
+ expression: prop.value,
423
+ },
424
+ })
425
+ }
426
+ })
427
+ }
428
+
429
+ path.replaceWith(jsxElement)
430
+ },
431
+ },
432
+ }
433
+ }
434
+
435
+ function jsonStringifyWithMaps(map) {
436
+ return JSON.stringify(
437
+ [...map],
438
+ (key, value) => (value instanceof Map ? [...value] : value),
439
+ 2,
440
+ )
441
+ }
package/src/cli.tsx CHANGED
@@ -9,19 +9,12 @@ import path, { basename } from 'path'
9
9
  import { BreakpointSizes } from './css.js'
10
10
  import { logger } from './utils.js'
11
11
  const configNames = ['unframer.config.json', 'unframer.json']
12
+ import kebabCase from 'just-kebab-case'
12
13
 
13
14
  export const cli = cac('unframer')
14
15
 
15
16
  let defaultOutDir = 'framer'
16
17
 
17
- function nameToFolder(name: string) {
18
- return name
19
- .replace(/[^a-zA-Z0-9]/g, '-') // Replace non-alphanumeric with dash
20
- .replace(/-+/g, '-') // Replace multiple dashes with single dash
21
- .replace(/^-|-$/g, '') // Remove leading/trailing dashes
22
- .toLowerCase()
23
- }
24
-
25
18
  cli.command('[projectId]', 'Run unframer with optional project ID')
26
19
  .option('--outDir <dir>', 'Output directory', { default: defaultOutDir })
27
20
  .action(async function main(projectId, options) {
@@ -40,20 +33,20 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
40
33
  return
41
34
  }
42
35
  const data = await response.json()
43
- return processConfig({
36
+ logger.log('unframer data', data)
37
+ let cwd = path.resolve(process.cwd(), outDir || 'framer')
38
+ return await bundle({
44
39
  config: {
45
40
  outDir,
46
41
  components: Object.fromEntries(
47
- data.components.map((c) => [
48
- nameToFolder(c.name),
49
- c.url,
50
- ]),
42
+ data.components.map((c) => [kebabCase(c.name), c.url]),
51
43
  ),
52
44
  tokens: data.colorStyles,
45
+ framerWebPages: data.framerWebPages || [],
53
46
  },
54
47
  watch: false,
55
48
 
56
- configBasename: 'remote config',
49
+ cwd,
57
50
  signal: new AbortController().signal,
58
51
  })
59
52
  }
@@ -79,11 +72,11 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
79
72
 
80
73
  let controller = new AbortController()
81
74
  setMaxListeners(0, controller.signal)
82
- processConfig({
75
+ await bundle({
83
76
  config,
84
77
  watch: false,
85
78
  signal: controller.signal,
86
- configBasename,
79
+ cwd: path.resolve(process.cwd(), outDir || 'framer'),
87
80
  })
88
81
  })
89
82
 
@@ -154,46 +147,12 @@ function getNewNames(oldConfig: Config, newConfig: Config) {
154
147
  return newNames
155
148
  }
156
149
 
157
- type Config = {
150
+ export type Config = {
158
151
  components: {
159
152
  [name: string]: string
160
153
  }
154
+ framerWebPages?: { webPageId: string; path: string }[]
161
155
  breakpoints?: BreakpointSizes
162
156
  tokens?: StyleToken[]
163
157
  outDir?: string
164
158
  }
165
- async function processConfig({
166
- config,
167
- watch,
168
- signal,
169
- configBasename,
170
- }: {
171
- config: Config
172
- watch: boolean
173
- configBasename: string
174
- signal?: AbortSignal
175
- }) {
176
- try {
177
- const { components, breakpoints, outDir } = config || {}
178
- const installDir = path.resolve(process.cwd(), outDir || 'framer')
179
- if (!components) {
180
- logger.log(`No components found in ${configBasename}`)
181
- return
182
- }
183
-
184
- await bundle({
185
- components,
186
- breakpoints,
187
- cwd: installDir,
188
- watch,
189
- tokens: config.tokens,
190
- signal,
191
- })
192
- } catch (e: any) {
193
- if (signal) {
194
- logger.log('Error processing config', e.stack)
195
- return
196
- }
197
- throw e
198
- }
199
- }
package/src/css.ts CHANGED
@@ -1,4 +1,4 @@
1
- import dedent from 'dedent'
1
+ import dedent from 'string-dedent'
2
2
  import { ComponentFont } from './framer.js'
3
3
 
4
4
  function deduplicateByKey<T>(arr: T[], key: (k: T) => string): T[] {
@@ -92,7 +92,8 @@ export function getFontsStyles(_fontsDefs: ComponentFontBundle[]) {
92
92
  str += dedent`
93
93
  @font-face {
94
94
  font-family: '${x.family}';
95
- src: url('${x.url}');\n`
95
+ src: url('${x.url}');\n
96
+ `
96
97
  if (x.style) {
97
98
  str += ` font-style: ${x.style};\n`
98
99
  }
package/src/esbuild.ts CHANGED
@@ -12,6 +12,28 @@ export const externalPackages = [
12
12
  ]
13
13
 
14
14
  let redirectCache = new Map<string, Promise<string>>()
15
+
16
+ export const replaceWebPageIds = ({
17
+ elements,
18
+ code,
19
+ }: {
20
+ elements: { webPageId: string; path: string }[]
21
+ code: string
22
+ }) => {
23
+ // Match webPageId pattern with optional trailing comma
24
+ const pattern = /{[\s\n]*webPageId[\s\n]*:[\s\n]*(['"])(.*?)\1[\s\n]*,?[\s\n]*}/g
25
+
26
+ return code.replace(pattern, (match, quote, id) => {
27
+ const path = elements.find((e) => e.webPageId === id)?.path
28
+ if (!path) {
29
+ return match
30
+ }
31
+
32
+ logger.log(`Replacing relative link to ${id} with fixed path: ${path}`)
33
+ return `'${path}'`
34
+ })
35
+ }
36
+
15
37
  export function esbuildPluginBundleDependencies({
16
38
  signal = undefined as AbortSignal | undefined,
17
39
  externalizeNpm = false,
@@ -96,7 +118,7 @@ export function esbuildPluginBundleDependencies({
96
118
  build.onEnd(() => {
97
119
  spinner.stop()
98
120
  })
99
-
121
+
100
122
  build.onLoad({ filter: /.*/, namespace }, async (args) => {
101
123
  if (signal?.aborted) {
102
124
  throw new Error('aborted')
@@ -119,7 +141,7 @@ export function esbuildPluginBundleDependencies({
119
141
  const promise = Promise.resolve().then(async () => {
120
142
  logger.log('fetching', url.replace(/https?:\/\//, ''))
121
143
  spinner.update(`Fetching ${url.replace(/https?:\/\//, '')}`)
122
-
144
+
123
145
  const res = await fetchWithRetry(resolved, { signal })
124
146
  if (!res.ok) {
125
147
  throw new Error(
@@ -1,5 +1,71 @@
1
1
  import { describe, test, expect } from 'vitest'
2
2
  import { propCamelCase } from './exporter'
3
+ import { replaceWebPageIds } from './esbuild'
4
+
5
+ describe('replaceWebPageIds', () => {
6
+ test('replaces webPageIds with paths', () => {
7
+ const elements = [
8
+ { webPageId: 'abc123', path: '/page1' },
9
+ { webPageId: 'def456', path: '/page2' },
10
+ ]
11
+ const code = `{ webPageId: 'abc123' }`
12
+ expect(replaceWebPageIds({ elements, code })).toEqual(`'/page1'`)
13
+
14
+ const code2 = `{ webPageId: "def456" }`
15
+ expect(replaceWebPageIds({ elements, code: code2 })).toEqual(`'/page2'`)
16
+ })
17
+
18
+ test('handles whitespace variations', () => {
19
+ const elements = [{ webPageId: 'abc123', path: '/page1' }]
20
+
21
+ const code = `{webPageId:'abc123'}`
22
+ expect(replaceWebPageIds({ elements, code })).toEqual(`'/page1'`)
23
+
24
+ const code2 = `{ webPageId : 'abc123' }`
25
+ expect(replaceWebPageIds({ elements, code: code2 })).toEqual(`'/page1'`)
26
+ const code3 = `{ href: { webPageId: 'zRPFqFbvc' } }`
27
+ expect(
28
+ replaceWebPageIds({
29
+ elements: [{ webPageId: 'zRPFqFbvc', path: '/page1' }],
30
+ code: code3,
31
+ }),
32
+ ).toEqual(`{ href: '/page1' }`)
33
+ const code4 = `/* @__PURE__ */ _jsx(Link, {
34
+ href: { webPageId: 'zRPFqFbvc', },
35
+ nodeId: 'aU2SMIi6t',`
36
+ expect(
37
+ replaceWebPageIds({
38
+ elements: [{ webPageId: 'zRPFqFbvc', path: '/page1' }],
39
+ code: code4,
40
+ }),
41
+ ).toMatchInlineSnapshot(
42
+ `
43
+ "/* @__PURE__ */ _jsx(Link, {
44
+ href: '/page1',
45
+ nodeId: 'aU2SMIi6t',"
46
+ `,
47
+ )
48
+ })
49
+
50
+ test('preserves non-matching webPageIds', () => {
51
+ const elements = [{ webPageId: 'abc123', path: '/page1' }]
52
+ const code = `{ webPageId: 'xyz789' }`
53
+
54
+ expect(replaceWebPageIds({ elements, code })).toEqual(
55
+ `{ webPageId: 'xyz789' }`,
56
+ )
57
+ })
58
+
59
+ test('handles newlines in input', () => {
60
+ const elements = [{ webPageId: 'abc123', path: '/page1' }]
61
+
62
+ const code = `{\n webPageId: 'abc123'\n}`
63
+ expect(replaceWebPageIds({ elements, code })).toEqual(`'/page1'`)
64
+
65
+ const code2 = `{\n\n webPageId:\n 'abc123'\n\n}`
66
+ expect(replaceWebPageIds({ elements, code: code2 })).toEqual(`'/page1'`)
67
+ })
68
+ })
3
69
 
4
70
  describe('propCamelCase', () => {
5
71
  test('converts dashes to camelCase', () => {