You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/vendor/bundle/ruby/4.0.0/gems/shakapacker-9.7.0/tools/css-modules-v9-codemod.js

180 lines
5.6 KiB

#!/usr/bin/env node
/**
* Shakapacker v9 CSS Modules Codemod
*
* This codemod helps migrate CSS module imports from v8 (default exports)
* to v9 (named exports for JS, namespace imports for TS).
*
* Usage:
* npx jscodeshift -t tools/css-modules-v9-codemod.js src/
* npx jscodeshift -t tools/css-modules-v9-codemod.js --parser tsx src/ (for TypeScript)
*
* Options:
* --dry Run in dry mode (no files modified)
* --print Print transformed files to stdout
* --parser tsx Use TypeScript parser
*/
module.exports = function transformer(fileInfo, api) {
const j = api.jscodeshift
const root = j(fileInfo.source)
let hasChanges = false
// Detect if this is a TypeScript file
const isTypeScript = fileInfo.path.match(/\.tsx?$/)
// Find all CSS module imports
root
.find(j.ImportDeclaration, {
source: {
value: (value) =>
value && value.match(/\.module\.(css|scss|sass|less)$/)
}
})
.forEach((path) => {
const importDecl = path.node
// Check if it's a default import (v8 style)
const defaultSpecifier = importDecl.specifiers.find(
(spec) => spec.type === "ImportDefaultSpecifier"
)
if (!defaultSpecifier) {
// Already using named or namespace imports, skip
return
}
const defaultImportName = defaultSpecifier.local.name
if (isTypeScript) {
// For TypeScript: Convert to namespace import (import * as styles)
const namespaceSpecifier = j.importNamespaceSpecifier(
j.identifier(defaultImportName)
)
// Replace the import specifiers
importDecl.specifiers = [namespaceSpecifier]
hasChanges = true
} else {
// For JavaScript: Convert to named imports
// First, we need to find all usages of the imported object
const usages = new Set()
// Find all member expressions using the imported default
root
.find(j.MemberExpression, {
object: {
type: "Identifier",
name: defaultImportName
}
})
.forEach((memberPath) => {
// Handle both dot notation (styles.className) and bracket notation (styles['class-name'])
if (
memberPath.node.computed &&
memberPath.node.property.type === "Literal"
) {
// Computed property access: styles['active-button']
const propertyValue = memberPath.node.property.value
if (typeof propertyValue === "string") {
usages.add(propertyValue)
}
} else if (
!memberPath.node.computed &&
memberPath.node.property.type === "Identifier"
) {
// Dot notation: styles.activeButton
const propertyName = memberPath.node.property.name
if (propertyName) {
usages.add(propertyName)
}
}
})
if (usages.size > 0) {
// Create named import specifiers
const namedSpecifiers = Array.from(usages)
.sort()
.map((name) => {
// Handle kebab-case to camelCase conversion
const camelCaseName = name.replace(/-([a-z])/g, (g) =>
g[1].toUpperCase()
)
if (camelCaseName !== name) {
// If conversion happened, we need to alias it
return j.importSpecifier(
j.identifier(camelCaseName),
j.identifier(camelCaseName) // css-loader exports it as camelCase
)
}
return j.importSpecifier(j.identifier(name))
})
// Replace the import specifiers
importDecl.specifiers = namedSpecifiers
// Update all usages in the file
root
.find(j.MemberExpression, {
object: {
type: "Identifier",
name: defaultImportName
}
})
.forEach((memberPath) => {
let propertyName
// Extract property name from both computed and dot notation
if (
memberPath.node.computed &&
memberPath.node.property.type === "Literal"
) {
propertyName = memberPath.node.property.value
} else if (
!memberPath.node.computed &&
memberPath.node.property.type === "Identifier"
) {
propertyName = memberPath.node.property.name
}
if (propertyName && typeof propertyName === "string") {
// Convert kebab-case to camelCase
const camelCaseName = propertyName.replace(/-([a-z])/g, (g) =>
g[1].toUpperCase()
)
// Replace with direct identifier
j(memberPath).replaceWith(j.identifier(camelCaseName))
}
})
hasChanges = true
} else if (usages.size === 0) {
// No usages found, might be passed as a whole object
// In this case, convert to namespace import for safety
const namespaceSpecifier = j.importNamespaceSpecifier(
j.identifier(defaultImportName)
)
importDecl.specifiers = [namespaceSpecifier]
hasChanges = true
}
}
})
if (!hasChanges) {
return null // No changes made
}
return root.toSource({
quote: "single",
trailingComma: true,
tabWidth: 2
})
}
// Export the parser to use
module.exports.parser = "tsx"