screenshot for date-input crash fix
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
import { execSync } from 'node:child_process';
|
||||
import { resolve, join } from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import https from 'node:https';
|
||||
|
||||
const TARGET_NODE_VERSION = '20.19.1';
|
||||
const env = { ...process.env, ROLLUP_USE_NODE_JS: 'true' };
|
||||
const viteBin = resolve(process.cwd(), 'node_modules', 'vite', 'bin', 'vite.js');
|
||||
|
||||
async function ensureNodeBinary(version) {
|
||||
const platformMap = {
|
||||
linux: 'linux',
|
||||
darwin: 'darwin',
|
||||
win32: 'win'
|
||||
};
|
||||
const archMap = {
|
||||
x64: 'x64',
|
||||
arm64: 'arm64'
|
||||
};
|
||||
|
||||
const platform = platformMap[process.platform];
|
||||
const arch = archMap[process.arch];
|
||||
|
||||
if (!platform || !arch) {
|
||||
throw new Error(`Unsupported platform/architecture combination: ${process.platform} ${process.arch}`);
|
||||
}
|
||||
|
||||
if (platform === 'win') {
|
||||
throw new Error('Automatic Node.js download is not supported on Windows runners. Please upgrade Node.js to >=20.19 manually.');
|
||||
}
|
||||
|
||||
const cacheDir = join(process.cwd(), 'node_modules', '.cache', `node-v${version}-${platform}-${arch}`);
|
||||
const nodeBinary = join(cacheDir, `node-v${version}-${platform}-${arch}`, 'bin', 'node');
|
||||
|
||||
try {
|
||||
await fs.access(nodeBinary);
|
||||
return nodeBinary;
|
||||
} catch {
|
||||
// continue with download
|
||||
}
|
||||
|
||||
await fs.mkdir(cacheDir, { recursive: true });
|
||||
const archiveExt = platform === 'win' ? 'zip' : 'tar.xz';
|
||||
const archiveName = `node-v${version}-${platform}-${arch}.${archiveExt}`;
|
||||
const archivePath = join(cacheDir, archiveName);
|
||||
const downloadUrl = `https://nodejs.org/dist/v${version}/${archiveName}`;
|
||||
|
||||
await downloadFile(downloadUrl, archivePath);
|
||||
|
||||
if (archiveExt === 'tar.xz') {
|
||||
execSync(`tar -xf "${archivePath}" -C "${cacheDir}"`, { stdio: 'inherit' });
|
||||
} else {
|
||||
throw new Error('ZIP extraction not implemented. Please upgrade Node.js manually.');
|
||||
}
|
||||
|
||||
await fs.rm(archivePath, { force: true });
|
||||
return nodeBinary;
|
||||
}
|
||||
|
||||
async function downloadFile(url, destination) {
|
||||
await new Promise((resolvePromise, rejectPromise) => {
|
||||
const fileStream = createWriteStream(destination);
|
||||
https.get(url, (response) => {
|
||||
if (response.statusCode && response.statusCode >= 400) {
|
||||
rejectPromise(new Error(`Failed to download ${url}: HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
pipeline(response, fileStream).then(resolvePromise).catch(rejectPromise);
|
||||
}).on('error', rejectPromise);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Node.js ${process.version} detected; forcing Rollup's JavaScript fallback for compatibility.`);
|
||||
|
||||
if (!process.env.USE_DOWNLOADED_NODE) {
|
||||
const [major] = process.versions.node.split('.').map(Number);
|
||||
if (major < 20) {
|
||||
const nodeBinary = await ensureNodeBinary(TARGET_NODE_VERSION);
|
||||
const childEnv = { ...env, USE_DOWNLOADED_NODE: '1' };
|
||||
execSync(`"${nodeBinary}" "${viteBin}" build`, { stdio: 'inherit', env: childEnv });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
execSync(`node "${viteBin}" build`, { stdio: 'inherit', env });
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,251 @@
|
||||
import type { ExtractedKey, Plugin } from 'i18next-cli'
|
||||
import * as ts from 'typescript'
|
||||
import { vol } from 'memfs'
|
||||
import * as path from 'path'
|
||||
|
||||
// based on https://github.com/i18next/i18next-cli/blob/49188621ae5534d940c7cc86eca1e3ece99506e7/test/plugin.typescript.test.ts
|
||||
|
||||
// --- HELPER FUNCTIONS FOR PLUGIN ---
|
||||
function isTranslationFunction (node: ts.CallExpression): boolean {
|
||||
const expr = node.expression
|
||||
// Matches t('...')
|
||||
if (ts.isIdentifier(expr) && expr.text === 't') return true
|
||||
|
||||
// Matches i18n.t('...')
|
||||
if (ts.isPropertyAccessExpression(expr) && expr.name.text === 't') return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function extractStringsFromType (type: ts.Type): string[] {
|
||||
if (type.isStringLiteral()) {
|
||||
return [type.value]
|
||||
}
|
||||
if (type.isUnion()) {
|
||||
return type.types.flatMap(t => extractStringsFromType(t))
|
||||
}
|
||||
if (type.isIntersection()) {
|
||||
return type.types.flatMap(t => extractStringsFromType(t))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function typescriptPlugin (
|
||||
entryPoints: string[],
|
||||
options: { defaultNS?: string } = {}
|
||||
): Plugin {
|
||||
const defaultNS = options.defaultNS
|
||||
|
||||
return {
|
||||
name: 'typescript-resolver',
|
||||
async onEnd (keys: Map<string, ExtractedKey>) {
|
||||
// 1. Setup Compiler Options
|
||||
const compilerOptions: ts.CompilerOptions = {
|
||||
allowJs: true,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
||||
esModuleInterop: true
|
||||
}
|
||||
|
||||
// 2. Create a custom CompilerHost that reads from memfs (vol)
|
||||
const host = ts.createCompilerHost(compilerOptions)
|
||||
const originalReadFile = host.readFile
|
||||
const originalFileExists = host.fileExists
|
||||
|
||||
host.readFile = (fileName: string) => {
|
||||
if (vol.existsSync(fileName)) {
|
||||
return vol.readFileSync(fileName, 'utf-8') as string
|
||||
}
|
||||
return originalReadFile(fileName)
|
||||
}
|
||||
|
||||
host.fileExists = (fileName: string) => {
|
||||
if (vol.existsSync(fileName)) return true
|
||||
return originalFileExists(fileName)
|
||||
}
|
||||
|
||||
// Override module resolution to look in memfs
|
||||
host.resolveModuleNameLiterals = (moduleLiterals, containingFile, redirectedReference, options, containingSourceFile, reusedNames) => {
|
||||
return moduleLiterals.map(moduleLiteral => {
|
||||
const moduleName = moduleLiteral.text
|
||||
// Simple resolution for test: resolve relative paths against the containing file's directory
|
||||
if (moduleName.startsWith('.')) {
|
||||
const dir = path.dirname(containingFile)
|
||||
const candidates: [string, ts.Extension][] = [
|
||||
[path.join(dir, moduleName + '.tsx'), ts.Extension.Tsx],
|
||||
[path.join(dir, moduleName + '.ts'), ts.Extension.Ts],
|
||||
]
|
||||
for (const [resolvedPath, ext] of candidates) {
|
||||
if (vol.existsSync(resolvedPath)) {
|
||||
return {
|
||||
resolvedModule: {
|
||||
resolvedFileName: resolvedPath,
|
||||
extension: ext,
|
||||
isExternalLibraryImport: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to standard resolution (for node_modules etc)
|
||||
return ts.resolveModuleName(moduleName, containingFile, options, host)
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Create Program
|
||||
const program = ts.createProgram(entryPoints, compilerOptions, host)
|
||||
const checker = program.getTypeChecker()
|
||||
|
||||
// 4. Visit AST
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
if (sourceFile.isDeclarationFile) continue
|
||||
ts.forEachChild(sourceFile, node => { visit(node, undefined) })
|
||||
}
|
||||
|
||||
// Extract namespace from useTranslation call
|
||||
function getUseTranslationNamespace (node: ts.CallExpression): string | undefined {
|
||||
const expr = node.expression
|
||||
if (ts.isIdentifier(expr) && expr.text === 'useTranslation') {
|
||||
const arg = node.arguments[0]
|
||||
if (arg && ts.isStringLiteral(arg)) {
|
||||
return arg.text
|
||||
}
|
||||
// Handle array of namespaces: useTranslation(['ns1', 'ns2']) - use the first one
|
||||
if (arg && ts.isArrayLiteralExpression(arg)) {
|
||||
const firstElement = arg.elements[0]
|
||||
if (firstElement && ts.isStringLiteral(firstElement)) {
|
||||
return firstElement.text
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function visit (node: ts.Node, scopeNs: string | undefined) {
|
||||
// Track useTranslation namespace in current scope
|
||||
let currentScopeNs = scopeNs
|
||||
|
||||
// Check if this is a function with useTranslation
|
||||
if (ts.isFunctionDeclaration(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isArrowFunction(node) ||
|
||||
ts.isMethodDeclaration(node)) {
|
||||
const body = 'body' in node ? node.body : undefined
|
||||
if (body) {
|
||||
// Look for useTranslation in this function
|
||||
const searchForUseTranslation = (n: ts.Node): string | undefined => {
|
||||
if (ts.isVariableDeclaration(n) && n.initializer &&
|
||||
ts.isCallExpression(n.initializer)) {
|
||||
const ns = getUseTranslationNamespace(n.initializer)
|
||||
if (ns) return ns
|
||||
}
|
||||
if (ts.isCallExpression(n)) {
|
||||
const ns = getUseTranslationNamespace(n)
|
||||
if (ns) return ns
|
||||
}
|
||||
let result: string | undefined
|
||||
ts.forEachChild(n, child => {
|
||||
if (!result) result = searchForUseTranslation(child)
|
||||
})
|
||||
return result
|
||||
}
|
||||
currentScopeNs = searchForUseTranslation(body) ?? scopeNs
|
||||
}
|
||||
}
|
||||
|
||||
if (ts.isCallExpression(node) && isTranslationFunction(node)) {
|
||||
const arg = node.arguments[0]
|
||||
if (arg) {
|
||||
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
||||
ts.forEachChild(node, child => { visit(child, currentScopeNs) })
|
||||
return
|
||||
}
|
||||
|
||||
let values: string[] = []
|
||||
|
||||
// Handle function arguments (e.g. t(() => ...)) by checking return type
|
||||
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) {
|
||||
const signature = checker.getSignatureFromDeclaration(arg)
|
||||
if (signature) {
|
||||
const returnType = signature.getReturnType()
|
||||
values = extractStringsFromType(returnType)
|
||||
}
|
||||
} else {
|
||||
// Try standard type resolution first
|
||||
const type = checker.getTypeAtLocation(arg)
|
||||
values = extractStringsFromType(type)
|
||||
}
|
||||
|
||||
// Fallback: If type resolution failed (generic string) but it's a TemplateExpression,
|
||||
// try to manually resolve the parts. This helps in test environments where full type inference is flaky.
|
||||
if (values.length === 0 && ts.isTemplateExpression(arg)) {
|
||||
const head = arg.head.text
|
||||
// Only handle simple case: `prefix.${var}`
|
||||
if (arg.templateSpans.length === 1) {
|
||||
const span = arg.templateSpans[0]
|
||||
const spanType = checker.getTypeAtLocation(span.expression)
|
||||
const spanValues = extractStringsFromType(spanType)
|
||||
|
||||
if (spanValues.length > 0) {
|
||||
values = spanValues.map(v => head + v + span.literal.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract namespace from options (second argument)
|
||||
let optionsNs: string | undefined
|
||||
const optionsArg = node.arguments[1]
|
||||
if (optionsArg && ts.isObjectLiteralExpression(optionsArg)) {
|
||||
const nsProp = optionsArg.properties.find(
|
||||
p => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'ns'
|
||||
)
|
||||
if (nsProp && ts.isPropertyAssignment(nsProp)) {
|
||||
if (ts.isStringLiteral(nsProp.initializer)) {
|
||||
optionsNs = nsProp.initializer.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
values.forEach(val => {
|
||||
let key = val
|
||||
// Priority: 1. options ns, 2. key contains ns, 3. useTranslation ns, 4. default 'all'
|
||||
let ns = optionsNs
|
||||
|
||||
// Check if key contains namespace separator (e.g., "namespace:key")
|
||||
const separatorIndex = val.indexOf(':')
|
||||
if (separatorIndex > 0) {
|
||||
ns = val.substring(0, separatorIndex)
|
||||
key = val.substring(separatorIndex + 1)
|
||||
}
|
||||
|
||||
let uniqueKey = key
|
||||
|
||||
// Fallback to useTranslation namespace or default
|
||||
if (!ns) {
|
||||
if(currentScopeNs) {
|
||||
ns = currentScopeNs
|
||||
} else if (defaultNS) {
|
||||
ns = defaultNS
|
||||
}
|
||||
}
|
||||
if (ns) {
|
||||
uniqueKey = `${ns}:${key}`
|
||||
}
|
||||
|
||||
if (!keys.has(uniqueKey)) {
|
||||
keys.set(uniqueKey, {
|
||||
key,
|
||||
defaultValue: key,
|
||||
nsIsImplicit: !optionsNs && !val.includes(':') && !currentScopeNs,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, child => { visit(child, currentScopeNs) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user