tailwindcss 3.2.4 → 3.2.6

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 (98) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/README.md +1 -1
  3. package/lib/cli/build/index.js +5 -1
  4. package/lib/cli/build/plugin.js +39 -34
  5. package/lib/cli/index.js +231 -10
  6. package/lib/cli/init/index.js +2 -2
  7. package/lib/cli.js +4 -226
  8. package/lib/corePlugins.js +45 -27
  9. package/lib/featureFlags.js +8 -8
  10. package/lib/index.js +4 -46
  11. package/lib/lib/collapseAdjacentRules.js +2 -2
  12. package/lib/lib/collapseDuplicateDeclarations.js +2 -2
  13. package/lib/lib/content.js +16 -16
  14. package/lib/lib/defaultExtractor.js +10 -5
  15. package/lib/lib/detectNesting.js +7 -1
  16. package/lib/lib/evaluateTailwindFunctions.js +4 -4
  17. package/lib/lib/expandApplyAtRules.js +2 -2
  18. package/lib/lib/expandTailwindAtRules.js +34 -9
  19. package/lib/lib/findAtConfigPath.js +3 -3
  20. package/lib/lib/generateRules.js +105 -50
  21. package/lib/lib/offsets.js +88 -1
  22. package/lib/lib/remap-bitfield.js +87 -0
  23. package/lib/lib/resolveDefaultsAtRules.js +4 -4
  24. package/lib/lib/setupContextUtils.js +121 -80
  25. package/lib/lib/setupTrackingContext.js +25 -4
  26. package/lib/lib/sharedState.js +19 -1
  27. package/lib/oxide/cli/build/deps.js +81 -0
  28. package/lib/oxide/cli/build/index.js +47 -0
  29. package/lib/oxide/cli/build/plugin.js +364 -0
  30. package/lib/oxide/cli/build/utils.js +77 -0
  31. package/lib/oxide/cli/build/watching.js +177 -0
  32. package/lib/oxide/cli/help/index.js +70 -0
  33. package/lib/oxide/cli/index.js +220 -0
  34. package/lib/oxide/cli/init/index.js +35 -0
  35. package/lib/oxide/cli.js +5 -0
  36. package/lib/oxide/postcss-plugin.js +2 -0
  37. package/lib/plugin.js +98 -0
  38. package/lib/postcss-plugins/nesting/plugin.js +2 -2
  39. package/lib/util/cloneNodes.js +2 -2
  40. package/lib/util/color.js +20 -6
  41. package/lib/util/createUtilityPlugin.js +2 -2
  42. package/lib/util/dataTypes.js +26 -2
  43. package/lib/util/defaults.js +4 -4
  44. package/lib/util/escapeClassName.js +3 -3
  45. package/lib/util/formatVariantSelector.js +171 -105
  46. package/lib/util/getAllConfigs.js +2 -2
  47. package/lib/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +2 -2
  48. package/lib/util/negateValue.js +2 -2
  49. package/lib/util/normalizeConfig.js +22 -22
  50. package/lib/util/pluginUtils.js +38 -40
  51. package/lib/util/prefixSelector.js +22 -8
  52. package/lib/util/resolveConfig.js +8 -10
  53. package/package.json +30 -19
  54. package/peers/index.js +61 -42
  55. package/resolveConfig.d.ts +11 -2
  56. package/scripts/swap-engines.js +40 -0
  57. package/src/cli/build/index.js +6 -2
  58. package/src/cli/build/plugin.js +14 -9
  59. package/src/cli/index.js +234 -3
  60. package/src/cli.js +4 -220
  61. package/src/corePlugins.js +31 -3
  62. package/src/index.js +4 -46
  63. package/src/lib/content.js +12 -17
  64. package/src/lib/defaultExtractor.js +9 -3
  65. package/src/lib/detectNesting.js +9 -1
  66. package/src/lib/expandTailwindAtRules.js +35 -6
  67. package/src/lib/generateRules.js +90 -28
  68. package/src/lib/offsets.js +104 -1
  69. package/src/lib/remap-bitfield.js +82 -0
  70. package/src/lib/setupContextUtils.js +97 -55
  71. package/src/lib/setupTrackingContext.js +31 -6
  72. package/src/lib/sharedState.js +17 -0
  73. package/src/oxide/cli/build/deps.ts +91 -0
  74. package/src/oxide/cli/build/index.ts +47 -0
  75. package/src/oxide/cli/build/plugin.ts +436 -0
  76. package/src/oxide/cli/build/utils.ts +74 -0
  77. package/src/oxide/cli/build/watching.ts +225 -0
  78. package/src/oxide/cli/help/index.ts +69 -0
  79. package/src/oxide/cli/index.ts +212 -0
  80. package/src/oxide/cli/init/index.ts +32 -0
  81. package/src/oxide/cli.ts +1 -0
  82. package/src/oxide/postcss-plugin.ts +1 -0
  83. package/src/plugin.js +107 -0
  84. package/src/util/color.js +17 -2
  85. package/src/util/dataTypes.js +29 -4
  86. package/src/util/formatVariantSelector.js +215 -122
  87. package/src/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +1 -1
  88. package/src/util/negateValue.js +1 -1
  89. package/src/util/pluginUtils.js +22 -19
  90. package/src/util/prefixSelector.js +28 -10
  91. package/src/util/resolveConfig.js +0 -2
  92. package/stubs/defaultConfig.stub.js +149 -165
  93. package/types/config.d.ts +7 -2
  94. package/types/generated/default-theme.d.ts +77 -77
  95. package/lib/cli/shared.js +0 -12
  96. package/scripts/install-integrations.js +0 -27
  97. package/scripts/rebuildFixtures.js +0 -68
  98. package/src/cli/shared.js +0 -5
@@ -1,3 +1,4 @@
1
+ import fs from 'fs'
1
2
  import LRU from 'quick-lru'
2
3
  import * as sharedState from './sharedState'
3
4
  import { generateRules } from './generateRules'
@@ -124,15 +125,32 @@ export default function expandTailwindAtRules(context) {
124
125
  // ---
125
126
 
126
127
  // Find potential rules in changed files
127
- let candidates = new Set([sharedState.NOT_ON_DEMAND])
128
+ let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND])
128
129
  let seen = new Set()
129
130
 
130
131
  env.DEBUG && console.time('Reading changed files')
131
132
 
132
- for (let { content, extension } of context.changedContent) {
133
- let transformer = getTransformer(context.tailwindConfig, extension)
134
- let extractor = getExtractor(context, extension)
135
- getClassCandidates(transformer(content), extractor, candidates, seen)
133
+ if (env.OXIDE) {
134
+ // TODO: Pass through or implement `extractor`
135
+ for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
136
+ context.changedContent
137
+ // Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
138
+ )) {
139
+ candidates.add(candidate)
140
+ }
141
+
142
+ // for (let { file, content, extension } of context.changedContent) {
143
+ // let transformer = getTransformer(context.tailwindConfig, extension)
144
+ // let extractor = getExtractor(context, extension)
145
+ // getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
146
+ // }
147
+ } else {
148
+ for (let { file, content, extension } of context.changedContent) {
149
+ let transformer = getTransformer(context.tailwindConfig, extension)
150
+ let extractor = getExtractor(context, extension)
151
+ content = file ? fs.readFileSync(file, 'utf8') : content
152
+ getClassCandidates(transformer(content), extractor, candidates, seen)
153
+ }
136
154
  }
137
155
 
138
156
  env.DEBUG && console.timeEnd('Reading changed files')
@@ -143,7 +161,18 @@ export default function expandTailwindAtRules(context) {
143
161
  let classCacheCount = context.classCache.size
144
162
 
145
163
  env.DEBUG && console.time('Generate rules')
146
- generateRules(candidates, context)
164
+ env.DEBUG && console.time('Sorting candidates')
165
+ let sortedCandidates = env.OXIDE
166
+ ? candidates
167
+ : new Set(
168
+ [...candidates].sort((a, z) => {
169
+ if (a === z) return 0
170
+ if (a < z) return -1
171
+ return 1
172
+ })
173
+ )
174
+ env.DEBUG && console.timeEnd('Sorting candidates')
175
+ generateRules(sortedCandidates, context)
147
176
  env.DEBUG && console.timeEnd('Generate rules')
148
177
 
149
178
  // We only ever add to the classCache, so if it didn't grow, there is nothing new.
@@ -10,7 +10,7 @@ import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSe
10
10
  import { asClass } from '../util/nameClass'
11
11
  import { normalize } from '../util/dataTypes'
12
12
  import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
13
- import isValidArbitraryValue from '../util/isValidArbitraryValue'
13
+ import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
14
14
  import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
15
15
  import { flagEnabled } from '../featureFlags'
16
16
 
@@ -152,10 +152,18 @@ function applyVariant(variant, matches, context) {
152
152
 
153
153
  // Retrieve "modifier"
154
154
  {
155
- let match = /(.*)\/(.*)$/g.exec(variant)
156
- if (match) {
157
- variant = match[1]
158
- args.modifier = match[2]
155
+ let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
156
+
157
+ // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
158
+ // In this case 1/10 is a value but /20 is a modifier
159
+ if (modifiers.length > 1) {
160
+ baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
161
+ modifiers = modifiers.slice(-1)
162
+ }
163
+
164
+ if (modifiers.length && !context.variantMap.has(variant)) {
165
+ variant = baseVariant
166
+ args.modifier = modifiers[0]
159
167
 
160
168
  if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
161
169
  return []
@@ -201,6 +209,7 @@ function applyVariant(variant, matches, context) {
201
209
  }
202
210
 
203
211
  if (context.variantMap.has(variant)) {
212
+ let isArbitraryVariant = isArbitraryValue(variant)
204
213
  let variantFunctionTuples = context.variantMap.get(variant).slice()
205
214
  let result = []
206
215
 
@@ -262,7 +271,10 @@ function applyVariant(variant, matches, context) {
262
271
  clone.append(wrapper)
263
272
  },
264
273
  format(selectorFormat) {
265
- collectedFormats.push(selectorFormat)
274
+ collectedFormats.push({
275
+ format: selectorFormat,
276
+ isArbitraryVariant,
277
+ })
266
278
  },
267
279
  args,
268
280
  })
@@ -288,7 +300,10 @@ function applyVariant(variant, matches, context) {
288
300
  }
289
301
 
290
302
  if (typeof ruleWithVariant === 'string') {
291
- collectedFormats.push(ruleWithVariant)
303
+ collectedFormats.push({
304
+ format: ruleWithVariant,
305
+ isArbitraryVariant,
306
+ })
292
307
  }
293
308
 
294
309
  if (ruleWithVariant === null) {
@@ -329,7 +344,10 @@ function applyVariant(variant, matches, context) {
329
344
  // modified (by plugin): .foo .foo\\:markdown > p
330
345
  // rebuiltBase (internal): .foo\\:markdown > p
331
346
  // format: .foo &
332
- collectedFormats.push(modified.replace(rebuiltBase, '&'))
347
+ collectedFormats.push({
348
+ format: modified.replace(rebuiltBase, '&'),
349
+ isArbitraryVariant,
350
+ })
333
351
  rule.selector = before
334
352
  })
335
353
  }
@@ -349,7 +367,6 @@ function applyVariant(variant, matches, context) {
349
367
  Object.assign(args, context.variantOptions.get(variant))
350
368
  ),
351
369
  collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
352
- isArbitraryVariant: isArbitraryValue(variant),
353
370
  },
354
371
  clone.nodes[0],
355
372
  ]
@@ -412,7 +429,7 @@ function isParsableNode(node) {
412
429
  let isParsable = true
413
430
 
414
431
  node.walkDecls((decl) => {
415
- if (!isParsableCssValue(decl.name, decl.value)) {
432
+ if (!isParsableCssValue(decl.prop, decl.value)) {
416
433
  isParsable = false
417
434
  return false
418
435
  }
@@ -736,24 +753,13 @@ function* resolveMatches(candidate, context, original = candidate) {
736
753
  match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
737
754
 
738
755
  // Apply final format selector
739
- if (match[0].collectedFormats) {
740
- let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
741
- let container = postcss.root({ nodes: [match[1].clone()] })
742
- container.walkRules((rule) => {
743
- if (inKeyframes(rule)) return
744
-
745
- rule.selector = finalizeSelector(finalFormat, {
746
- selector: rule.selector,
747
- candidate: original,
748
- base: candidate
749
- .split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
750
- .pop(),
751
- isArbitraryVariant: match[0].isArbitraryVariant,
752
-
753
- context,
754
- })
755
- })
756
- match[1] = container.nodes[0]
756
+ match = applyFinalFormat(match, { context, candidate, original })
757
+
758
+ // Skip rules with invalid selectors
759
+ // This will cause the candidate to be added to the "not class"
760
+ // cache skipping it entirely for future builds
761
+ if (match === null) {
762
+ continue
757
763
  }
758
764
 
759
765
  yield match
@@ -761,6 +767,62 @@ function* resolveMatches(candidate, context, original = candidate) {
761
767
  }
762
768
  }
763
769
 
770
+ function applyFinalFormat(match, { context, candidate, original }) {
771
+ if (!match[0].collectedFormats) {
772
+ return match
773
+ }
774
+
775
+ let isValid = true
776
+ let finalFormat
777
+
778
+ try {
779
+ finalFormat = formatVariantSelector(match[0].collectedFormats, {
780
+ context,
781
+ candidate,
782
+ })
783
+ } catch {
784
+ // The format selector we produced is invalid
785
+ // This could be because:
786
+ // - A bug exists
787
+ // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
788
+ // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
789
+ // Either way the build will fail because of this
790
+ // We would rather that the build pass "silently" given that this could
791
+ // happen because of picking up invalid things when scanning content
792
+ // So we'll throw out the candidate instead
793
+
794
+ return null
795
+ }
796
+
797
+ let container = postcss.root({ nodes: [match[1].clone()] })
798
+
799
+ container.walkRules((rule) => {
800
+ if (inKeyframes(rule)) {
801
+ return
802
+ }
803
+
804
+ try {
805
+ rule.selector = finalizeSelector(rule.selector, finalFormat, {
806
+ candidate: original,
807
+ context,
808
+ })
809
+ } catch {
810
+ // If this selector is invalid we also want to skip it
811
+ // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
812
+ isValid = false
813
+ return false
814
+ }
815
+ })
816
+
817
+ if (!isValid) {
818
+ return null
819
+ }
820
+
821
+ match[1] = container.nodes[0]
822
+
823
+ return match
824
+ }
825
+
764
826
  function inKeyframes(rule) {
765
827
  return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
766
828
  }
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import bigSign from '../util/bigSign'
4
+ import { remapBitfield } from './remap-bitfield.js'
4
5
 
5
6
  /**
6
7
  * @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer
@@ -12,6 +13,7 @@ import bigSign from '../util/bigSign'
12
13
  * @property {function | undefined} sort The sort function
13
14
  * @property {string|null} value The value we want to compare
14
15
  * @property {string|null} modifier The modifier that was used (if any)
16
+ * @property {bigint} variant The variant bitmask
15
17
  */
16
18
 
17
19
  /**
@@ -126,6 +128,8 @@ export class Offsets {
126
128
  * @returns {RuleOffset}
127
129
  */
128
130
  applyVariantOffset(rule, variant, options) {
131
+ options.variant = variant.variants
132
+
129
133
  return {
130
134
  ...rule,
131
135
  layer: 'variants',
@@ -190,7 +194,7 @@ export class Offsets {
190
194
 
191
195
  return {
192
196
  ...this.create('variants'),
193
- variants: 1n << this.reservedVariantBits,
197
+ variants: this.variantOffsets.get(variant),
194
198
  }
195
199
  }
196
200
 
@@ -205,11 +209,30 @@ export class Offsets {
205
209
  return this.layerPositions[a.layer] - this.layerPositions[b.layer]
206
210
  }
207
211
 
212
+ // When sorting the `variants` layer, we need to sort based on the parent layer as well within
213
+ // this variants layer.
214
+ if (a.parentLayer !== b.parentLayer) {
215
+ return this.layerPositions[a.parentLayer] - this.layerPositions[b.parentLayer]
216
+ }
217
+
208
218
  // Sort based on the sorting function
209
219
  for (let aOptions of a.options) {
210
220
  for (let bOptions of b.options) {
211
221
  if (aOptions.id !== bOptions.id) continue
212
222
  if (!aOptions.sort || !bOptions.sort) continue
223
+
224
+ let maxFnVariant = max([aOptions.variant, bOptions.variant]) ?? 0n
225
+
226
+ // Create a mask of 0s from bits 1..N where N represents the mask of the Nth bit
227
+ let mask = ~(maxFnVariant | (maxFnVariant - 1n))
228
+ let aVariantsAfterFn = a.variants & mask
229
+ let bVariantsAfterFn = b.variants & mask
230
+
231
+ // If the variants the same, we _can_ sort them
232
+ if (aVariantsAfterFn !== bVariantsAfterFn) {
233
+ continue
234
+ }
235
+
213
236
  let result = aOptions.sort(
214
237
  {
215
238
  value: aOptions.value,
@@ -243,12 +266,68 @@ export class Offsets {
243
266
  return a.index - b.index
244
267
  }
245
268
 
269
+ /**
270
+ * Arbitrary variants are recorded in the order they're encountered.
271
+ * This means that the order is not stable between environments and sets of content files.
272
+ *
273
+ * In order to make the order stable, we need to remap the arbitrary variant offsets to
274
+ * be in alphabetical order starting from the offset of the first arbitrary variant.
275
+ */
276
+ recalculateVariantOffsets() {
277
+ // Sort the variants by their name
278
+ let variants = Array.from(this.variantOffsets.entries())
279
+ .filter(([v]) => v.startsWith('['))
280
+ .sort(([a], [z]) => fastCompare(a, z))
281
+
282
+ // Sort the list of offsets
283
+ // This is not necessarily a discrete range of numbers which is why
284
+ // we're using sort instead of creating a range from min/max
285
+ let newOffsets = variants.map(([, offset]) => offset).sort((a, z) => bigSign(a - z))
286
+
287
+ // Create a map from the old offsets to the new offsets in the new sort order
288
+ /** @type {[bigint, bigint][]} */
289
+ let mapping = variants.map(([, oldOffset], i) => [oldOffset, newOffsets[i]])
290
+
291
+ // Remove any variants that will not move letting us skip
292
+ // remapping if everything happens to be in order
293
+ return mapping.filter(([a, z]) => a !== z)
294
+ }
295
+
296
+ /**
297
+ * @template T
298
+ * @param {[RuleOffset, T][]} list
299
+ * @returns {[RuleOffset, T][]}
300
+ */
301
+ remapArbitraryVariantOffsets(list) {
302
+ let mapping = this.recalculateVariantOffsets()
303
+
304
+ // No arbitrary variants? Nothing to do.
305
+ // Everyhing already in order? Nothing to do.
306
+ if (mapping.length === 0) {
307
+ return list
308
+ }
309
+
310
+ // Remap every variant offset in the list
311
+ return list.map((item) => {
312
+ let [offset, rule] = item
313
+
314
+ offset = {
315
+ ...offset,
316
+ variants: remapBitfield(offset.variants, mapping),
317
+ }
318
+
319
+ return [offset, rule]
320
+ })
321
+ }
322
+
246
323
  /**
247
324
  * @template T
248
325
  * @param {[RuleOffset, T][]} list
249
326
  * @returns {[RuleOffset, T][]}
250
327
  */
251
328
  sort(list) {
329
+ list = this.remapArbitraryVariantOffsets(list)
330
+
252
331
  return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
253
332
  }
254
333
  }
@@ -268,3 +347,27 @@ function max(nums) {
268
347
 
269
348
  return max
270
349
  }
350
+
351
+ /**
352
+ * A fast ASCII order string comparison function.
353
+ *
354
+ * Using `.sort()` without a custom compare function is faster
355
+ * But you can only use that if you're sorting an array of
356
+ * only strings. If you're sorting strings inside objects
357
+ * or arrays, you need must use a custom compare function.
358
+ *
359
+ * @param {string} a
360
+ * @param {string} b
361
+ */
362
+ function fastCompare(a, b) {
363
+ let aLen = a.length
364
+ let bLen = b.length
365
+ let minLen = aLen < bLen ? aLen : bLen
366
+
367
+ for (let i = 0; i < minLen; i++) {
368
+ let cmp = a.charCodeAt(i) - b.charCodeAt(i)
369
+ if (cmp !== 0) return cmp
370
+ }
371
+
372
+ return aLen - bLen
373
+ }
@@ -0,0 +1,82 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * We must remap all the old bits to new bits for each set variant
5
+ * Only arbitrary variants are considered as those are the only
6
+ * ones that need to be re-sorted at this time
7
+ *
8
+ * An iterated process that removes and sets individual bits simultaneously
9
+ * will not work because we may have a new bit that is also a later old bit
10
+ * This means that we would be removing a previously set bit which we don't
11
+ * want to do
12
+ *
13
+ * For example (assume `bN` = `1<<N`)
14
+ * Given the "total" mapping `[[b1, b3], [b2, b4], [b3, b1], [b4, b2]]`
15
+ * The mapping is "total" because:
16
+ * 1. Every input and output is accounted for
17
+ * 2. All combinations are unique
18
+ * 3. No one input maps to multiple outputs and vice versa
19
+ * And, given an offset with all bits set:
20
+ * V = b1 | b2 | b3 | b4
21
+ *
22
+ * Let's explore the issue with removing and setting bits simultaneously:
23
+ * V & ~b1 | b3 = b2 | b3 | b4
24
+ * V & ~b2 | b4 = b3 | b4
25
+ * V & ~b3 | b1 = b1 | b4
26
+ * V & ~b4 | b2 = b1 | b2
27
+ *
28
+ * As you can see, we end up with the wrong result.
29
+ * This is because we're removing a bit that was previously set.
30
+ * And, thus the final result is missing b3 and b4.
31
+ *
32
+ * Now, let's explore the issue with removing the bits first:
33
+ * V & ~b1 = b2 | b3 | b4
34
+ * V & ~b2 = b3 | b4
35
+ * V & ~b3 = b4
36
+ * V & ~b4 = 0
37
+ *
38
+ * And then setting the bits:
39
+ * V | b3 = b3
40
+ * V | b4 = b3 | b4
41
+ * V | b1 = b1 | b3 | b4
42
+ * V | b2 = b1 | b2 | b3 | b4
43
+ *
44
+ * We get the correct result because we're not removing any bits that were
45
+ * previously set thus properly remapping the bits to the new order
46
+ *
47
+ * To collect this into a single operation that can be done simultaneously
48
+ * we must first create a mask for the old bits that are set and a mask for
49
+ * the new bits that are set. Then we can remove the old bits and set the new
50
+ * bits simultaneously in a "single" operation like so:
51
+ * OldMask = b1 | b2 | b3 | b4
52
+ * NewMask = b3 | b4 | b1 | b2
53
+ *
54
+ * So this:
55
+ * V & ~oldMask | newMask
56
+ *
57
+ * Expands to this:
58
+ * V & ~b1 & ~b2 & ~b3 & ~b4 | b3 | b4 | b1 | b2
59
+ *
60
+ * Which becomes this:
61
+ * b1 | b2 | b3 | b4
62
+ *
63
+ * Which is the correct result!
64
+ *
65
+ * @param {bigint} num
66
+ * @param {[bigint, bigint][]} mapping
67
+ */
68
+ export function remapBitfield(num, mapping) {
69
+ // Create masks for the old and new bits that are set
70
+ let oldMask = 0n
71
+ let newMask = 0n
72
+ for (let [oldBit, newBit] of mapping) {
73
+ if (num & oldBit) {
74
+ oldMask = oldMask | oldBit
75
+ newMask = newMask | newBit
76
+ }
77
+ }
78
+
79
+ // Remove all old bits
80
+ // Set all new bits
81
+ return (num & ~oldMask) | newMask
82
+ }