You've already forked AstralRinth
forked from didirus/AstralRinth
fix: various fixes (#4998)
* feat: check imports using ast * fix: lint * fix: loadericon * fix: lint * feat: remove usd warning * fix: error.vue * fix: lint
This commit is contained in:
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@@ -11,21 +11,21 @@
|
|||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.organizeImports": "always"
|
"source.organizeImports": "always"
|
||||||
},
|
},
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint",
|
||||||
"[vue]": {
|
"[vue]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
},
|
},
|
||||||
"[scss]": {
|
"[scss]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
},
|
},
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "rvest.vs-code-prettier-eslint"
|
||||||
},
|
},
|
||||||
"[rust]": {
|
"[rust]": {
|
||||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
|||||||
@@ -15,27 +15,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<Transition
|
|
||||||
enter-active-class="transition-all duration-300 ease-out"
|
|
||||||
enter-from-class="opacity-0 max-h-0"
|
|
||||||
enter-to-class="opacity-100 max-h-40"
|
|
||||||
leave-active-class="transition-all duration-200 ease-in"
|
|
||||||
leave-from-class="opacity-100 max-h-40"
|
|
||||||
leave-to-class="opacity-0 max-h-0"
|
|
||||||
>
|
|
||||||
<div v-if="shouldShowUsdWarning" class="overflow-hidden">
|
|
||||||
<Admonition type="warning" :header="formatMessage(messages.usdPaypalWarningHeader)">
|
|
||||||
<IntlFormatted :message-id="messages.usdPaypalWarningMessage">
|
|
||||||
<template #direct-paypal-link="{ children }">
|
|
||||||
<span class="cursor-pointer text-link" @click="switchToDirectPaypal">
|
|
||||||
<component :is="() => normalizeChildren(children)" />
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</IntlFormatted>
|
|
||||||
</Admonition>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
<div v-if="!showGiftCardSelector && selectedMethodDisplay" class="flex flex-col gap-2.5">
|
<div v-if="!showGiftCardSelector && selectedMethodDisplay" class="flex flex-col gap-2.5">
|
||||||
<label>
|
<label>
|
||||||
<span class="text-md font-semibold text-contrast">
|
<span class="text-md font-semibold text-contrast">
|
||||||
@@ -373,19 +352,11 @@ import { computed, onMounted, ref, watch } from 'vue'
|
|||||||
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
import RevenueInputField from '@/components/ui/dashboard/RevenueInputField.vue'
|
||||||
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
import WithdrawFeeBreakdown from '@/components/ui/dashboard/WithdrawFeeBreakdown.vue'
|
||||||
import { useAuth } from '@/composables/auth.js'
|
import { useAuth } from '@/composables/auth.js'
|
||||||
import { useBaseFetch } from '@/composables/fetch.js'
|
import { useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
||||||
import { type PayoutMethod, useWithdrawContext } from '@/providers/creator-withdraw.ts'
|
|
||||||
|
|
||||||
const debug = useDebugLogger('TremendousDetailsStage')
|
const debug = useDebugLogger('TremendousDetailsStage')
|
||||||
const {
|
const { withdrawData, maxWithdrawAmount, availableMethods, paymentOptions, calculateFees } =
|
||||||
withdrawData,
|
useWithdrawContext()
|
||||||
maxWithdrawAmount,
|
|
||||||
availableMethods,
|
|
||||||
paymentOptions,
|
|
||||||
calculateFees,
|
|
||||||
setStage,
|
|
||||||
paymentMethodsCache,
|
|
||||||
} = useWithdrawContext()
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
|
|
||||||
@@ -410,12 +381,6 @@ const showPayPalCurrencySelector = computed(() => {
|
|||||||
return method === 'paypal'
|
return method === 'paypal'
|
||||||
})
|
})
|
||||||
|
|
||||||
const shouldShowUsdWarning = computed(() => {
|
|
||||||
const method = withdrawData.value.selection.method
|
|
||||||
const currency = selectedCurrency.value
|
|
||||||
return method === 'paypal' && currency === 'USD'
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedMethodDisplay = computed(() => {
|
const selectedMethodDisplay = computed(() => {
|
||||||
const method = withdrawData.value.selection.method
|
const method = withdrawData.value.selection.method
|
||||||
if (!method) return null
|
if (!method) return null
|
||||||
@@ -964,47 +929,6 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
async function switchToDirectPaypal() {
|
|
||||||
withdrawData.value.selection.country = {
|
|
||||||
id: 'US',
|
|
||||||
name: 'United States',
|
|
||||||
}
|
|
||||||
|
|
||||||
let usMethods = paymentMethodsCache.value['US']
|
|
||||||
|
|
||||||
if (!usMethods) {
|
|
||||||
try {
|
|
||||||
usMethods = (await useBaseFetch('payout/methods', {
|
|
||||||
apiVersion: 3,
|
|
||||||
query: { country: 'US' },
|
|
||||||
})) as PayoutMethod[]
|
|
||||||
|
|
||||||
paymentMethodsCache.value['US'] = usMethods
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch US payment methods:', error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
availableMethods.value = usMethods
|
|
||||||
|
|
||||||
const directPaypal = usMethods.find((m) => m.type === 'paypal')
|
|
||||||
|
|
||||||
if (directPaypal) {
|
|
||||||
withdrawData.value.selection.provider = 'paypal'
|
|
||||||
withdrawData.value.selection.method = directPaypal.id
|
|
||||||
withdrawData.value.selection.methodId = directPaypal.id
|
|
||||||
|
|
||||||
withdrawData.value.providerData = {
|
|
||||||
type: 'paypal',
|
|
||||||
}
|
|
||||||
|
|
||||||
await setStage('paypal-details', true)
|
|
||||||
} else {
|
|
||||||
console.error('An error occured - no paypal in US region??')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unverifiedEmailHeader: {
|
unverifiedEmailHeader: {
|
||||||
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header',
|
id: 'dashboard.creator-withdraw-modal.tremendous-details.unverified-email-header',
|
||||||
|
|||||||
@@ -7,10 +7,11 @@
|
|||||||
>
|
>
|
||||||
<LoaderIcon
|
<LoaderIcon
|
||||||
:loader="loader.name"
|
:loader="loader.name"
|
||||||
class="[&&]:size-6"
|
class="size-6"
|
||||||
:class="isCurrentLoader ? 'text-brand' : ''"
|
:class="isCurrentLoader ? 'text-brand' : ''"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<h1 class="m-0 text-xl font-bold leading-none text-contrast">
|
<h1 class="m-0 text-xl font-bold leading-none text-contrast">
|
||||||
@@ -20,20 +21,16 @@
|
|||||||
v-if="isCurrentLoader"
|
v-if="isCurrentLoader"
|
||||||
class="hidden items-center gap-1 rounded-full bg-bg-green p-1 px-1.5 text-xs font-semibold text-brand sm:flex"
|
class="hidden items-center gap-1 rounded-full bg-bg-green p-1 px-1.5 text-xs font-semibold text-brand sm:flex"
|
||||||
>
|
>
|
||||||
<CheckIcon class="h-4 w-4" />
|
<CheckIcon class="h-4 w-4" /> Current
|
||||||
Current
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="isCurrentLoader" class="m-0 text-xs text-secondary">
|
|
||||||
{{ loaderVersion }}
|
<p v-if="isCurrentLoader" class="m-0 text-xs text-secondary">{{ loaderVersion }}</p>
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ButtonStyled>
|
<ButtonStyled>
|
||||||
<button :disabled="isInstalling" @click="onSelect">
|
<button :disabled="isInstalling" @click="onSelect">
|
||||||
<DownloadIcon class="h-5 w-5" />
|
<DownloadIcon class="h-5 w-5" /> {{ isCurrentLoader ? 'Reinstall' : 'Install' }}
|
||||||
{{ isCurrentLoader ? 'Reinstall' : 'Install' }}
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,9 +38,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CheckIcon, DownloadIcon } from '@modrinth/assets'
|
import { CheckIcon, DownloadIcon } from '@modrinth/assets'
|
||||||
import { ButtonStyled } from '@modrinth/ui'
|
import { ButtonStyled, LoaderIcon } from '@modrinth/ui'
|
||||||
|
|
||||||
import LoaderIcon from './icons/LoaderIcon.vue'
|
|
||||||
|
|
||||||
interface LoaderInfo {
|
interface LoaderInfo {
|
||||||
name: 'Vanilla' | 'Fabric' | 'Forge' | 'Quilt' | 'Paper' | 'NeoForge' | 'Purpur'
|
name: 'Vanilla' | 'Fabric' | 'Forge' | 'Quilt' | 'Paper' | 'NeoForge' | 'Purpur'
|
||||||
|
|||||||
@@ -24,17 +24,17 @@
|
|||||||
<IntlFormatted :message-id="item">
|
<IntlFormatted :message-id="item">
|
||||||
<template #status-link="{ children }">
|
<template #status-link="{ children }">
|
||||||
<a href="https://status.modrinth.com" target="_blank" rel="noopener">
|
<a href="https://status.modrinth.com" target="_blank" rel="noopener">
|
||||||
<component :is="() => children" />
|
<component :is="() => normalizeChildren(children)" />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template #discord-link="{ children }">
|
<template #discord-link="{ children }">
|
||||||
<a href="https://discord.modrinth.com" target="_blank" rel="noopener">
|
<a href="https://discord.modrinth.com" target="_blank" rel="noopener">
|
||||||
<component :is="() => children" />
|
<component :is="() => normalizeChildren(children)" />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<template #tou-link="{ children }">
|
<template #tou-link="{ children }">
|
||||||
<nuxt-link :to="`/legal/terms`" target="_blank" rel="noopener">
|
<nuxt-link :to="`/legal/terms`" target="_blank" rel="noopener">
|
||||||
<component :is="() => children" />
|
<component :is="() => normalizeChildren(children)" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</IntlFormatted>
|
</IntlFormatted>
|
||||||
@@ -55,6 +55,7 @@ import { SadRinthbot } from '@modrinth/assets'
|
|||||||
import {
|
import {
|
||||||
defineMessage,
|
defineMessage,
|
||||||
IntlFormatted,
|
IntlFormatted,
|
||||||
|
normalizeChildren,
|
||||||
NotificationPanel,
|
NotificationPanel,
|
||||||
provideModrinthClient,
|
provideModrinthClient,
|
||||||
provideNotificationManager,
|
provideNotificationManager,
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modrinth/tooling-config": "workspace:*",
|
"@modrinth/tooling-config": "workspace:*",
|
||||||
|
"@vue/compiler-sfc": "^3.5.26",
|
||||||
|
"chalk": "^5.6.2",
|
||||||
"if-ci": "^3.0.0",
|
"if-ci": "^3.0.0",
|
||||||
"prettier": "^3.3.2",
|
"prettier": "^3.3.2",
|
||||||
"turbo": "^2.5.4",
|
"turbo": "^2.5.4",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -19,6 +19,12 @@ importers:
|
|||||||
'@modrinth/tooling-config':
|
'@modrinth/tooling-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/tooling-config
|
version: link:packages/tooling-config
|
||||||
|
'@vue/compiler-sfc':
|
||||||
|
specifier: ^3.5.26
|
||||||
|
version: 3.5.26
|
||||||
|
chalk:
|
||||||
|
specifier: ^5.6.2
|
||||||
|
version: 5.6.2
|
||||||
if-ci:
|
if-ci:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
|
|||||||
379
scripts/i18n-import-check.ts
Normal file
379
scripts/i18n-import-check.ts
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import { parse as parseVue } from '@vue/compiler-sfc'
|
||||||
|
import { parse as parseTs, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'
|
||||||
|
import type { TSESTree } from '@typescript-eslint/typescript-estree'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
// i18n symbols that should be imported from @modrinth/ui
|
||||||
|
const I18N_SYMBOLS = ['useVIntl', 'defineMessage', 'defineMessages', 'IntlFormatted'] as const
|
||||||
|
type I18nSymbol = (typeof I18N_SYMBOLS)[number]
|
||||||
|
|
||||||
|
// formatMessage is special - it's destructured from useVIntl(), not directly imported
|
||||||
|
const FORMAT_MESSAGE = 'formatMessage'
|
||||||
|
|
||||||
|
// Valid import sources for i18n symbols
|
||||||
|
const VALID_IMPORT_SOURCES = ['@modrinth/ui']
|
||||||
|
|
||||||
|
// Directories to exclude from scanning
|
||||||
|
const EXCLUDED_DIRS = new Set(['node_modules', '.output', '.nuxt', 'dist', '.git', '.turbo'])
|
||||||
|
|
||||||
|
interface FileIssue {
|
||||||
|
file: string
|
||||||
|
symbol: string
|
||||||
|
line: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportInfo {
|
||||||
|
symbol: string
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Usage {
|
||||||
|
symbol: string
|
||||||
|
line: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = {
|
||||||
|
warning: chalk.yellow,
|
||||||
|
error: chalk.red,
|
||||||
|
success: chalk.green,
|
||||||
|
muted: chalk.gray,
|
||||||
|
highlight: chalk.white.bold,
|
||||||
|
file: chalk.cyan,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively find all .vue and .ts files in directories
|
||||||
|
*/
|
||||||
|
function findFiles(dirs: string[]): string[] {
|
||||||
|
const files: string[] = []
|
||||||
|
|
||||||
|
function walk(dir: string) {
|
||||||
|
if (!fs.existsSync(dir)) return
|
||||||
|
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name)
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Skip excluded directories and hidden directories
|
||||||
|
if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
||||||
|
walk(fullPath)
|
||||||
|
}
|
||||||
|
} else if (entry.isFile()) {
|
||||||
|
if (entry.name.endsWith('.vue') || entry.name.endsWith('.ts')) {
|
||||||
|
// Skip .d.ts files
|
||||||
|
if (!entry.name.endsWith('.d.ts')) {
|
||||||
|
files.push(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
walk(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse TypeScript/JavaScript content into AST
|
||||||
|
*/
|
||||||
|
function parseTsContent(content: string, isJsx: boolean = false): TSESTree.Program | null {
|
||||||
|
try {
|
||||||
|
return parseTs(content, {
|
||||||
|
jsx: isJsx,
|
||||||
|
loc: true,
|
||||||
|
range: true,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract script content from Vue SFC
|
||||||
|
*/
|
||||||
|
function extractVueScript(content: string): { script: string; isTs: boolean } | null {
|
||||||
|
try {
|
||||||
|
const { descriptor } = parseVue(content)
|
||||||
|
const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content
|
||||||
|
if (!scriptContent) return null
|
||||||
|
|
||||||
|
const lang = descriptor.scriptSetup?.lang || descriptor.script?.lang
|
||||||
|
const isTs = lang === 'ts' || lang === 'tsx'
|
||||||
|
|
||||||
|
return { script: scriptContent, isTs }
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk AST and call visitor for each node
|
||||||
|
*/
|
||||||
|
function walkAst(node: TSESTree.Node, visitor: (node: TSESTree.Node) => void) {
|
||||||
|
visitor(node)
|
||||||
|
|
||||||
|
for (const key of Object.keys(node)) {
|
||||||
|
const child = (node as Record<string, unknown>)[key]
|
||||||
|
if (child && typeof child === 'object') {
|
||||||
|
if (Array.isArray(child)) {
|
||||||
|
for (const item of child) {
|
||||||
|
if (item && typeof item === 'object' && 'type' in item) {
|
||||||
|
walkAst(item as TSESTree.Node, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ('type' in child) {
|
||||||
|
walkAst(child as TSESTree.Node, visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract import information from AST
|
||||||
|
*/
|
||||||
|
function extractImports(ast: TSESTree.Program): ImportInfo[] {
|
||||||
|
const imports: ImportInfo[] = []
|
||||||
|
|
||||||
|
for (const node of ast.body) {
|
||||||
|
if (node.type === AST_NODE_TYPES.ImportDeclaration) {
|
||||||
|
const source = node.source.value as string
|
||||||
|
for (const specifier of node.specifiers) {
|
||||||
|
if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
|
||||||
|
imports.push({
|
||||||
|
symbol: specifier.imported.type === AST_NODE_TYPES.Identifier
|
||||||
|
? specifier.imported.name
|
||||||
|
: String(specifier.imported.value),
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
} else if (specifier.type === AST_NODE_TYPES.ImportDefaultSpecifier) {
|
||||||
|
imports.push({
|
||||||
|
symbol: specifier.local.name,
|
||||||
|
source,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imports
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find usages of i18n symbols in AST
|
||||||
|
*/
|
||||||
|
function findUsages(ast: TSESTree.Program): Usage[] {
|
||||||
|
const usages: Usage[] = []
|
||||||
|
const localVariables = new Set<string>()
|
||||||
|
|
||||||
|
// First pass: collect locally declared variables to avoid false positives
|
||||||
|
walkAst(ast, (node) => {
|
||||||
|
if (node.type === AST_NODE_TYPES.VariableDeclarator && node.id.type === AST_NODE_TYPES.Identifier) {
|
||||||
|
localVariables.add(node.id.name)
|
||||||
|
}
|
||||||
|
if (node.type === AST_NODE_TYPES.FunctionDeclaration && node.id) {
|
||||||
|
localVariables.add(node.id.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second pass: find usages
|
||||||
|
walkAst(ast, (node) => {
|
||||||
|
// Check for call expressions: useVIntl(), defineMessage(), defineMessages(), formatMessage()
|
||||||
|
if (node.type === AST_NODE_TYPES.CallExpression) {
|
||||||
|
const callee = node.callee
|
||||||
|
if (callee.type === AST_NODE_TYPES.Identifier) {
|
||||||
|
const name = callee.name
|
||||||
|
if (I18N_SYMBOLS.includes(name as I18nSymbol) || name === FORMAT_MESSAGE) {
|
||||||
|
usages.push({
|
||||||
|
symbol: name,
|
||||||
|
line: callee.loc.start.line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for JSX elements: <IntlFormatted>
|
||||||
|
if (node.type === AST_NODE_TYPES.JSXOpeningElement) {
|
||||||
|
const name = node.name
|
||||||
|
if (name.type === AST_NODE_TYPES.JSXIdentifier && name.name === 'IntlFormatted') {
|
||||||
|
usages.push({
|
||||||
|
symbol: 'IntlFormatted',
|
||||||
|
line: name.loc.start.line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for component references in Vue (e.g., components: { IntlFormatted })
|
||||||
|
if (node.type === AST_NODE_TYPES.Property) {
|
||||||
|
if (node.key.type === AST_NODE_TYPES.Identifier && node.key.name === 'IntlFormatted') {
|
||||||
|
// This is defining IntlFormatted as a component, check if it's imported
|
||||||
|
usages.push({
|
||||||
|
symbol: 'IntlFormatted',
|
||||||
|
line: node.key.loc.start.line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return usages
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if import source is valid for i18n symbols
|
||||||
|
*/
|
||||||
|
function isValidImportSource(source: string, filePath: string): boolean {
|
||||||
|
// Direct import from @modrinth/ui
|
||||||
|
if (VALID_IMPORT_SOURCES.includes(source)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative imports within packages/ui are valid
|
||||||
|
if (filePath.includes('packages/ui/') || filePath.includes('packages\\ui\\')) {
|
||||||
|
if (source.startsWith('./') || source.startsWith('../')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze a single file for missing i18n imports
|
||||||
|
*/
|
||||||
|
function analyzeFile(filePath: string): FileIssue[] {
|
||||||
|
const issues: FileIssue[] = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8')
|
||||||
|
let ast: TSESTree.Program | null = null
|
||||||
|
|
||||||
|
if (filePath.endsWith('.vue')) {
|
||||||
|
const scriptInfo = extractVueScript(content)
|
||||||
|
if (!scriptInfo) return []
|
||||||
|
ast = parseTsContent(scriptInfo.script, true)
|
||||||
|
} else {
|
||||||
|
ast = parseTsContent(content, filePath.endsWith('.tsx'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ast) return []
|
||||||
|
|
||||||
|
const imports = extractImports(ast)
|
||||||
|
const usages = findUsages(ast)
|
||||||
|
|
||||||
|
// Build a map of imported symbols from valid sources
|
||||||
|
const validImports = new Map<string, string>()
|
||||||
|
for (const imp of imports) {
|
||||||
|
if (isValidImportSource(imp.source, filePath)) {
|
||||||
|
validImports.set(imp.symbol, imp.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if useVIntl is imported (for formatMessage validation)
|
||||||
|
const hasUseVIntl = validImports.has('useVIntl')
|
||||||
|
|
||||||
|
// Check each usage
|
||||||
|
for (const usage of usages) {
|
||||||
|
const symbol = usage.symbol
|
||||||
|
|
||||||
|
if (symbol === FORMAT_MESSAGE) {
|
||||||
|
// formatMessage is valid if useVIntl is imported
|
||||||
|
if (!hasUseVIntl) {
|
||||||
|
issues.push({
|
||||||
|
file: filePath,
|
||||||
|
symbol: `${symbol} (useVIntl not imported)`,
|
||||||
|
line: usage.line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (!validImports.has(symbol)) {
|
||||||
|
issues.push({
|
||||||
|
file: filePath,
|
||||||
|
symbol,
|
||||||
|
line: usage.line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Silent fail for unparseable files
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function
|
||||||
|
*/
|
||||||
|
function main() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const verbose = args.includes('--verbose') || args.includes('-v')
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
const dirsToScan = [
|
||||||
|
path.join(rootDir, 'apps/frontend/src'),
|
||||||
|
path.join(rootDir, 'apps/app-frontend/src'),
|
||||||
|
path.join(rootDir, 'packages'),
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
process.stdout.write(theme.muted(' Scanning for i18n import issues... '))
|
||||||
|
|
||||||
|
const files = findFiles(dirsToScan)
|
||||||
|
console.log(theme.success(`found ${files.length} files`))
|
||||||
|
|
||||||
|
const allIssues: FileIssue[] = []
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const issues = analyzeFile(file)
|
||||||
|
allIssues.push(...issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
if (allIssues.length === 0) {
|
||||||
|
console.log(theme.success(' No missing i18n imports found!'))
|
||||||
|
console.log()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group issues by file
|
||||||
|
const issuesByFile = new Map<string, FileIssue[]>()
|
||||||
|
for (const issue of allIssues) {
|
||||||
|
const existing = issuesByFile.get(issue.file) || []
|
||||||
|
existing.push(issue)
|
||||||
|
issuesByFile.set(issue.file, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print issues
|
||||||
|
for (const [file, issues] of issuesByFile) {
|
||||||
|
const relativePath = path.relative(rootDir, file)
|
||||||
|
console.log(theme.warning(` ${relativePath}`))
|
||||||
|
|
||||||
|
for (const issue of issues) {
|
||||||
|
console.log(theme.muted(` Line ${issue.line}: `) + theme.highlight(issue.symbol) + theme.muted(' is used but not imported'))
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log(theme.muted(' ─'.repeat(30)))
|
||||||
|
console.log(
|
||||||
|
theme.warning(` Summary: ${issuesByFile.size} file(s) with ${allIssues.length} missing i18n import(s)`)
|
||||||
|
)
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
console.log(theme.muted(' Tip: Import these symbols from @modrinth/ui'))
|
||||||
|
console.log(theme.muted(' Example: import { useVIntl, defineMessages } from \'@modrinth/ui\''))
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit with 0 (warn only, don't block CI)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user