mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 09:09:36 +02:00
Merge pull request #32594 from overleaf/rh-cio-migration-mapping
Add customer.io fields for migration comms and marketing initiatives GitOrigin-RevId: f11ffee255d9582cbfd4c7e285bd6690c0cf1e3c
This commit is contained in:
@@ -7,5 +7,6 @@ module.exports = {
|
||||
'require-script-runner': require('./require-script-runner'),
|
||||
'require-vi-doMock-valid-path': require('./require-vi-doMock-valid-path'),
|
||||
'require-loading-label': require('./require-loading-label'),
|
||||
'require-cio-snake-case-properties': require('./require-cio-snake-case-properties'),
|
||||
},
|
||||
}
|
||||
|
||||
111
libraries/eslint-plugin/require-cio-snake-case-properties.js
Normal file
111
libraries/eslint-plugin/require-cio-snake-case-properties.js
Normal file
@@ -0,0 +1,111 @@
|
||||
'use strict'
|
||||
|
||||
const SNAKE_CASE_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
|
||||
|
||||
function isSnakeCase(name) {
|
||||
return SNAKE_CASE_RE.test(name)
|
||||
}
|
||||
|
||||
function getStaticKeyName(property) {
|
||||
if (property.computed) return null
|
||||
if (property.key.type === 'Identifier') return property.key.name
|
||||
if (property.key.type === 'Literal' && typeof property.key.value === 'string')
|
||||
return property.key.value
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is a call to CustomerIoHandler.updateUserAttributes()
|
||||
* and return the attributes argument (2nd argument)
|
||||
*/
|
||||
function getUpdateUserAttributesArg(node) {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name === 'CustomerIoHandler' &&
|
||||
node.callee.property.name === 'updateUserAttributes' &&
|
||||
node.arguments[1]?.type === 'ObjectExpression'
|
||||
) {
|
||||
return node.arguments[1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node is a call to Modules[.promises].hooks.fire('setUserProperties', ...)
|
||||
* and return the attributes argument (3rd argument)
|
||||
*/
|
||||
function getSetUserPropertiesArg(node) {
|
||||
const callee = node.callee
|
||||
if (callee.type !== 'MemberExpression' || callee.property.name !== 'fire') {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check first argument is 'setUserProperties'
|
||||
if (
|
||||
!node.arguments[0] ||
|
||||
node.arguments[0].type !== 'Literal' ||
|
||||
node.arguments[0].value !== 'setUserProperties'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Match: Modules.hooks.fire or Modules.promises.hooks.fire
|
||||
const obj = callee.object
|
||||
if (obj.type === 'MemberExpression' && obj.property.name === 'hooks') {
|
||||
const parent = obj.object
|
||||
// Modules.hooks
|
||||
if (parent.type === 'Identifier' && parent.name === 'Modules') {
|
||||
if (node.arguments[2]?.type === 'ObjectExpression') {
|
||||
return node.arguments[2]
|
||||
}
|
||||
}
|
||||
// Modules.promises.hooks
|
||||
if (
|
||||
parent.type === 'MemberExpression' &&
|
||||
parent.property.name === 'promises' &&
|
||||
parent.object.type === 'Identifier' &&
|
||||
parent.object.name === 'Modules'
|
||||
) {
|
||||
if (node.arguments[2]?.type === 'ObjectExpression') {
|
||||
return node.arguments[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description:
|
||||
'Enforce snake_case for Customer.io user property attribute names',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
CallExpression(node) {
|
||||
const attrsNode =
|
||||
getUpdateUserAttributesArg(node) || getSetUserPropertiesArg(node)
|
||||
if (!attrsNode) return
|
||||
|
||||
for (const property of attrsNode.properties) {
|
||||
if (property.type === 'SpreadElement') continue
|
||||
|
||||
const keyName = getStaticKeyName(property)
|
||||
if (keyName === null) continue // skip computed/dynamic keys
|
||||
|
||||
if (!isSnakeCase(keyName)) {
|
||||
context.report({
|
||||
node: property.key,
|
||||
message: `Customer.io attribute '{{name}}' must be in snake_case.`,
|
||||
data: { name: keyName },
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -4,6 +4,7 @@ const noUnnecessaryTrans = require('./no-unnecessary-trans')
|
||||
const shouldUnescapeTrans = require('./should-unescape-trans')
|
||||
const noGeneratedEditorThemes = require('./no-generated-editor-themes')
|
||||
const viDoMockValidPath = require('./require-vi-doMock-valid-path')
|
||||
const requireCioSnakeCaseProperties = require('./require-cio-snake-case-properties')
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
@@ -166,3 +167,103 @@ ruleTester.run('domock-require-valid-path', viDoMockValidPath, {
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
ruleTester.run(
|
||||
'require-cio-snake-case-properties',
|
||||
requireCioSnakeCaseProperties,
|
||||
{
|
||||
valid: [
|
||||
// updateUserAttributes with snake_case keys
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { plan_type: 'free', group_size: 10 })`,
|
||||
},
|
||||
// Modules.promises.hooks.fire with snake_case keys
|
||||
{
|
||||
code: `Modules.promises.hooks.fire('setUserProperties', userId, { plan_type: 'free', last_active: 123 })`,
|
||||
},
|
||||
// Modules.hooks.fire with snake_case keys
|
||||
{
|
||||
code: `Modules.hooks.fire('setUserProperties', userId, { plan_type: 'free' })`,
|
||||
},
|
||||
// Single-word keys are valid snake_case
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { email: 'a@b.com', role: 'admin' })`,
|
||||
},
|
||||
// Computed/dynamic keys are skipped
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { [dynamicKey]: true })`,
|
||||
},
|
||||
// Spread elements are skipped
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { ...existingAttrs })`,
|
||||
},
|
||||
// Unrelated function calls are not checked
|
||||
{
|
||||
code: `SomeOtherHandler.updateUserAttributes(userId, { camelCase: true })`,
|
||||
},
|
||||
// fire() with a different event name is not checked
|
||||
{
|
||||
code: `Modules.promises.hooks.fire('someOtherEvent', userId, { camelCase: true })`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
// camelCase key in updateUserAttributes
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'planType' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
// kebab-case string key
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { 'plan-type': 'free' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'plan-type' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
// PascalCase key
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { PlanType: 'free' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'PlanType' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
// camelCase in Modules.promises.hooks.fire
|
||||
{
|
||||
code: `Modules.promises.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'planType' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
// camelCase in Modules.hooks.fire
|
||||
{
|
||||
code: `Modules.hooks.fire('setUserProperties', userId, { planType: 'free' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'planType' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
// Multiple invalid keys report multiple errors
|
||||
{
|
||||
code: `CustomerIoHandler.updateUserAttributes(userId, { planType: 'free', groupSize: 10, plan_term: 'annual' })`,
|
||||
errors: [
|
||||
{
|
||||
message: `Customer.io attribute 'planType' must be in snake_case.`,
|
||||
},
|
||||
{
|
||||
message: `Customer.io attribute 'groupSize' must be in snake_case.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user