chore(repository): reset repo to new version
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const CLEAN_CONTENT_REGEX = {
|
||||
comments: /\/\*[\s\S]*?\*\/|\/\/.*$/gm,
|
||||
templateLiterals: /`[\s\S]*?`/g,
|
||||
strings: /'[^']*'|"[^"]*"/g,
|
||||
jsxExpressions: /\{.*?\}/g,
|
||||
htmlEntities: {
|
||||
quot: /"/g,
|
||||
amp: /&/g,
|
||||
lt: /</g,
|
||||
gt: />/g,
|
||||
apos: /'/g
|
||||
}
|
||||
};
|
||||
|
||||
const EXTRACTION_REGEX = {
|
||||
// `element={<Page />}` puts a `>` inside the tag, so the attribute scan has
|
||||
// to step over brace groups instead of stopping at the first `>`.
|
||||
route: /<Route\s+(?:[^>{]|\{[^}]*\})*\/?>/g,
|
||||
path: /path=["']([^"']+)["']/,
|
||||
element: /element=\{([\s\S]*)\}/,
|
||||
elementComponent: /<(\w+)/g,
|
||||
helmet: /<Helmet[^>]*?>([\s\S]*?)<\/Helmet>/i,
|
||||
helmetTest: /<Helmet[\s\S]*?<\/Helmet>/i,
|
||||
title: /<title[^>]*?>\s*(.*?)\s*<\/title>/i,
|
||||
description: /<meta\s+name=["']description["']\s+content=["'](.*?)["']/i
|
||||
};
|
||||
|
||||
function cleanContent(content) {
|
||||
return content
|
||||
.replace(CLEAN_CONTENT_REGEX.comments, '')
|
||||
.replace(CLEAN_CONTENT_REGEX.templateLiterals, '""')
|
||||
.replace(CLEAN_CONTENT_REGEX.strings, '""');
|
||||
}
|
||||
|
||||
function cleanText(text) {
|
||||
if (!text) return text;
|
||||
|
||||
return text
|
||||
.replace(CLEAN_CONTENT_REGEX.jsxExpressions, '')
|
||||
.replace(CLEAN_CONTENT_REGEX.htmlEntities.quot, '"')
|
||||
.replace(CLEAN_CONTENT_REGEX.htmlEntities.amp, '&')
|
||||
.replace(CLEAN_CONTENT_REGEX.htmlEntities.lt, '<')
|
||||
.replace(CLEAN_CONTENT_REGEX.htmlEntities.gt, '>')
|
||||
.replace(CLEAN_CONTENT_REGEX.htmlEntities.apos, "'")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function extractRoutes(appJsxPath) {
|
||||
if (!fs.existsSync(appJsxPath)) return new Map();
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(appJsxPath, 'utf8');
|
||||
const routes = new Map();
|
||||
const routeMatches = [...content.matchAll(EXTRACTION_REGEX.route)];
|
||||
|
||||
for (const match of routeMatches) {
|
||||
const routeTag = match[0];
|
||||
const pathMatch = routeTag.match(EXTRACTION_REGEX.path);
|
||||
const elementMatch = routeTag.match(EXTRACTION_REGEX.element);
|
||||
const isIndex = routeTag.includes('index');
|
||||
|
||||
if (elementMatch) {
|
||||
// A guarded route reads `element={<ProtectedRoute><SettingsPage /></ProtectedRoute>}`,
|
||||
// so the page is the innermost component rather than the first one.
|
||||
const componentNames = [...elementMatch[1].matchAll(EXTRACTION_REGEX.elementComponent)];
|
||||
const componentName = componentNames.at(-1)?.[1];
|
||||
|
||||
if (!componentName) continue;
|
||||
|
||||
let routePath;
|
||||
|
||||
if (isIndex) {
|
||||
routePath = '/';
|
||||
} else if (pathMatch) {
|
||||
routePath = pathMatch[1].startsWith('/') ? pathMatch[1] : `/${pathMatch[1]}`;
|
||||
}
|
||||
|
||||
routes.set(componentName, routePath);
|
||||
}
|
||||
}
|
||||
|
||||
return routes;
|
||||
} catch (error) {
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
function findReactFiles(dir) {
|
||||
return fs.readdirSync(dir).map(item => path.join(dir, item));
|
||||
}
|
||||
|
||||
function extractHelmetData(content, filePath, routes) {
|
||||
const cleanedContent = cleanContent(content);
|
||||
|
||||
if (!EXTRACTION_REGEX.helmetTest.test(cleanedContent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const helmetMatch = content.match(EXTRACTION_REGEX.helmet);
|
||||
if (!helmetMatch) return null;
|
||||
|
||||
const helmetContent = helmetMatch[1];
|
||||
const titleMatch = helmetContent.match(EXTRACTION_REGEX.title);
|
||||
const descMatch = helmetContent.match(EXTRACTION_REGEX.description);
|
||||
|
||||
const title = cleanText(titleMatch?.[1]);
|
||||
const description = cleanText(descMatch?.[1]);
|
||||
|
||||
const fileName = path.basename(filePath, path.extname(filePath));
|
||||
const url = routes.size && routes.has(fileName)
|
||||
? routes.get(fileName)
|
||||
: generateFallbackUrl(fileName);
|
||||
|
||||
return {
|
||||
url,
|
||||
title: title || 'Untitled Page',
|
||||
description: description || 'No description available'
|
||||
};
|
||||
}
|
||||
|
||||
function generateFallbackUrl(fileName) {
|
||||
const cleanName = fileName.replace(/Page$/, '').toLowerCase();
|
||||
return cleanName === 'app' ? '/' : `/${cleanName}`;
|
||||
}
|
||||
|
||||
function generateLlmsTxt(pages) {
|
||||
const sortedPages = pages.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const pageEntries = sortedPages.map(page =>
|
||||
`- [${page.title}](${page.url}): ${page.description}`
|
||||
).join('\n');
|
||||
|
||||
return `## Pages\n${pageEntries}`;
|
||||
}
|
||||
|
||||
function ensureDirectoryExists(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function processPageFile(filePath, routes) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return extractHelmetData(content, filePath, routes);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error processing ${filePath}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const pagesDir = path.join(process.cwd(), 'src', 'pages');
|
||||
const appJsxPath = path.join(process.cwd(), 'src', 'App.jsx');
|
||||
|
||||
let pages = [];
|
||||
|
||||
if (!fs.existsSync(pagesDir)) {
|
||||
pages.push(processPageFile(appJsxPath, new Map()))
|
||||
pages = pages.filter(Boolean);
|
||||
} else {
|
||||
const routes = extractRoutes(appJsxPath);
|
||||
const reactFiles = findReactFiles(pagesDir);
|
||||
|
||||
pages = reactFiles
|
||||
.map(filePath => processPageFile(filePath, routes))
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
console.error('❌ No pages with Helmet components found!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
const llmsTxtContent = generateLlmsTxt(pages);
|
||||
const outputPath = path.join(process.cwd(), 'public', 'llms.txt');
|
||||
|
||||
ensureDirectoryExists(path.dirname(outputPath));
|
||||
fs.writeFileSync(outputPath, llmsTxtContent, 'utf8');
|
||||
}
|
||||
|
||||
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||
|
||||
if (isMainModule) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const APP_DIR = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
);
|
||||
|
||||
const COMPONENTS_JSON_FILE = "components.json";
|
||||
const PACKAGE_JSON_FILE = "package.json";
|
||||
const TAILWIND_CONFIG_FILE = "tailwind.config.js";
|
||||
const FILES_UPDATED_BY_SHADCN_INIT = [
|
||||
PACKAGE_JSON_FILE,
|
||||
COMPONENTS_JSON_FILE,
|
||||
TAILWIND_CONFIG_FILE,
|
||||
];
|
||||
|
||||
const JSX_EXTENSION = "jsx";
|
||||
const JS_EXTENSION = "js";
|
||||
|
||||
const COMPONENT_NAME_TO_INSTALLABLE_COMPONENT_NAME_MAP = {
|
||||
toaster: "toast",
|
||||
"use-toast": "toast",
|
||||
};
|
||||
|
||||
async function run() {
|
||||
let stdout = "";
|
||||
let error = null;
|
||||
const updatedFiles = [];
|
||||
|
||||
try {
|
||||
process.chdir(APP_DIR);
|
||||
|
||||
// Get a list of components that are imported but not installed
|
||||
const uniqueComponentNames = await findImportedComponentNames();
|
||||
const missingMask = await Promise.all(
|
||||
uniqueComponentNames.map((name) =>
|
||||
componentExists(name).then((exists) => !exists),
|
||||
),
|
||||
);
|
||||
const missingComponentNames = uniqueComponentNames.filter(
|
||||
(_, i) => missingMask[i],
|
||||
);
|
||||
const installableComponentNames = [
|
||||
...new Set(
|
||||
missingComponentNames.map(
|
||||
(name) =>
|
||||
COMPONENT_NAME_TO_INSTALLABLE_COMPONENT_NAME_MAP[name] ?? name,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
// If the components.json file doesn't exist, initialize shadcn
|
||||
const componentsJsonPath = path.join(APP_DIR, COMPONENTS_JSON_FILE);
|
||||
const componentsJsonExists = await pathExists(componentsJsonPath);
|
||||
|
||||
if (!componentsJsonExists) {
|
||||
const shadcnInitResult = await runCommand(
|
||||
"npx -y shadcn@3 init -f --src-dir --no-base-style",
|
||||
);
|
||||
|
||||
const shadcnInitOutput = [
|
||||
shadcnInitResult.stdout,
|
||||
shadcnInitResult.stderr,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
stdout = shadcnInitOutput;
|
||||
|
||||
const isErrorStatus = !!shadcnInitResult.status;
|
||||
|
||||
if (isErrorStatus) {
|
||||
error = `Shadcn failed to init: ${shadcnInitOutput}`;
|
||||
} else {
|
||||
updatedFiles.push(...FILES_UPDATED_BY_SHADCN_INIT);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are installable components, install them
|
||||
if (installableComponentNames.length && !error) {
|
||||
const shadcnAddResult = await runCommand(
|
||||
`npx -y shadcn@3 add ${installableComponentNames.join(" ")} -y -o --src-dir`,
|
||||
);
|
||||
|
||||
const shadcnAddOutput = [shadcnAddResult.stdout, shadcnAddResult.stderr]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
stdout = stdout ? `${stdout}\n${shadcnAddOutput}` : shadcnAddOutput;
|
||||
|
||||
const isErrorStatus = !!shadcnAddResult.status;
|
||||
|
||||
if (isErrorStatus) {
|
||||
error = `Shadcn failed to add: ${shadcnAddOutput}`;
|
||||
} else {
|
||||
// Example output of shadcnAddOutput:
|
||||
// - src/components/ui/button.tsx
|
||||
// - src/components/ui/input.tsx
|
||||
// The code below extracts these file paths like this: src/components/ui/button.tsx, src/components/ui/input.tsx
|
||||
const filesUpdatedByShadcnAdd = shadcnAddOutput
|
||||
.split("\n")
|
||||
.flatMap((line) => {
|
||||
const match = line.match(/^\s*-\s*(src\/\S+)/);
|
||||
return match ? [match[1].trim()] : [];
|
||||
});
|
||||
|
||||
updatedFiles.push(...filesUpdatedByShadcnAdd);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
error = error ?? err?.message ?? "unknown error";
|
||||
}
|
||||
|
||||
const uniqueUpdatedFiles = [...new Set(updatedFiles)];
|
||||
|
||||
const result = {
|
||||
stdout: stdout,
|
||||
error: error,
|
||||
updatedFiles: uniqueUpdatedFiles,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
|
||||
async function runCommand(command) {
|
||||
try {
|
||||
const { stdout, stderr } = await promisify(exec)(command);
|
||||
|
||||
return {
|
||||
stdout: stdout ?? "",
|
||||
stderr: stderr ?? "",
|
||||
status: 0,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
stdout: error.stdout ?? "",
|
||||
stderr: error.stderr ?? "",
|
||||
status: error.code ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function pathExists(filePath) {
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function scanDirectoryForUiImports(directoryPath) {
|
||||
const componentNames = new Set();
|
||||
const entries = await fs.promises.readdir(directoryPath, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullEntryPath = path.join(directoryPath, entry.name);
|
||||
const isDirectory = entry.isDirectory();
|
||||
|
||||
if (isDirectory) {
|
||||
const innerDirectoryComponentNames =
|
||||
await scanDirectoryForUiImports(fullEntryPath);
|
||||
|
||||
for (const name of innerDirectoryComponentNames) {
|
||||
componentNames.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
const isJsxFile = entry.name.endsWith(`.${JSX_EXTENSION}`);
|
||||
|
||||
if (isJsxFile) {
|
||||
try {
|
||||
const fileContent = await fs.promises.readFile(fullEntryPath, "utf8");
|
||||
|
||||
for (const [, componentName] of fileContent.matchAll(
|
||||
/@\/components\/ui\/([a-z0-9-]+)/g,
|
||||
)) {
|
||||
componentNames.add(componentName);
|
||||
}
|
||||
} catch {
|
||||
// Skip unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return componentNames;
|
||||
}
|
||||
|
||||
async function findImportedComponentNames() {
|
||||
const srcPath = path.join(APP_DIR, "src");
|
||||
const srcExists = await pathExists(srcPath);
|
||||
|
||||
if (!srcExists) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueComponentNames = await scanDirectoryForUiImports(srcPath);
|
||||
|
||||
return [...uniqueComponentNames];
|
||||
}
|
||||
|
||||
async function componentExists(componentName) {
|
||||
const componentsUiPath = path.join(APP_DIR, "src", "components", "ui");
|
||||
const hooksPath = path.join(APP_DIR, "src", "hooks");
|
||||
|
||||
const existenceChecks = await Promise.all(
|
||||
[JSX_EXTENSION, JS_EXTENSION].map(async (extension) => {
|
||||
const fileName = `${componentName}.${extension}`;
|
||||
|
||||
return (
|
||||
(await pathExists(path.join(componentsUiPath, fileName))) ||
|
||||
(await pathExists(path.join(hooksPath, fileName)))
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return existenceChecks.some(Boolean);
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in New Issue
Block a user