-
Notifications
You must be signed in to change notification settings - Fork 4
feat: added prototype code to make ui grey and present a banner to user #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,14 +7,26 @@ | |
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "main": "dist/cli.js", | ||
| "main": "dist/index.js", | ||
| "types": "dist/index.d.ts", | ||
| "type": "module", | ||
| "bin": { | ||
| "patternfly-cli": "./dist/cli.js", | ||
| "pf": "./dist/cli.js" | ||
| }, | ||
| "exports": { | ||
| ".": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js" | ||
| }, | ||
| "./components": { | ||
| "types": "./dist/index.d.ts", | ||
| "import": "./dist/index.js" | ||
| }, | ||
| "./prototype.css": "./dist/prototype.css" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "build": "tsc && cp src/prototype.css dist/prototype.css", | ||
| "start": "node dist/cli.js", | ||
| "test": "jest", | ||
| "semantic-release": "semantic-release", | ||
|
|
@@ -55,10 +67,17 @@ | |
| "commander": "^12.1.0", | ||
| "execa": "^9.3.0", | ||
| "fs-extra": "^11.2.0", | ||
| "glob": "^11.0.0", | ||
| "inquirer": "^9.3.5" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": "^18.0.0", | ||
| "react-dom": "^18.0.0", | ||
| "@patternfly/react-core": "^5.0.0 || ^6.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@eslint/js": "^10.0.1", | ||
| "@patternfly/react-core": "^6.4.3", | ||
| "@semantic-release/changelog": "^6.0.3", | ||
| "@semantic-release/commit-analyzer": "^13.0.1", | ||
| "@semantic-release/git": "^10.0.1", | ||
|
|
@@ -71,8 +90,12 @@ | |
| "@types/inquirer": "^9.0.9", | ||
| "@types/jest": "^30.0.0", | ||
| "@types/node": "^24.10.1", | ||
| "@types/react": "^18.3.0", | ||
| "@types/react-dom": "^18.3.0", | ||
| "eslint": "^10.0.2", | ||
| "jest": "^29.7.0", | ||
| "react": "^18.3.0", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If these are in peerDependencies, they shouldn't also be needed in devDependencies. Same for @patternfly/react-core |
||
| "react-dom": "^18.3.0", | ||
| "semantic-release": "^24.2.0", | ||
| "ts-jest": "^29.4.5", | ||
| "typescript": "^5.9.3", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import * as React from "react"; | ||
| import { Banner, Bullseye } from "@patternfly/react-core"; | ||
|
|
||
| const ProtoBanner: React.FC = () => { | ||
| return ( | ||
| <Banner default isSticky> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: In the current version of PatternFly (v6 as of 2026), the Citations:
🏁 Script executed: find . -name "package.json" -type f | head -5 | xargs grep -l "@patternfly/react-core" 2>/dev/nullRepository: patternfly/patternfly-cli Length of output: 84 🏁 Script executed: cat package.json | grep -A 2 -B 2 "@patternfly/react-core" 2>/dev/null || echo "Not found in root package.json"Repository: patternfly/patternfly-cli Length of output: 364 Remove invalid In PatternFly v6, the 🤖 Prompt for AI Agents |
||
| <Bullseye> | ||
| <strong>This application is a design prototype</strong> | ||
| </Bullseye> | ||
| </Banner> | ||
| ); | ||
| }; | ||
|
|
||
| export default ProtoBanner; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| // Export React components | ||
| export { default as ProtoBanner } from './components/protoBanner.js'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| html { | ||
| filter: grayscale(100%); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import path from 'path'; | ||
| import fs from 'fs-extra'; | ||
| import { glob } from 'glob'; | ||
|
|
||
| const CSS_IMPORT_STATEMENT = "import '@patternfly/patternfly-cli/prototype.css';"; | ||
|
|
||
| /** | ||
| * Find the main index.tsx or index.jsx file in a React application. | ||
| * Searches common locations like src/index.tsx, src/index.jsx, index.tsx, index.jsx. | ||
| */ | ||
| async function findMainIndexFile(cwd: string): Promise<string | null> { | ||
| // Common patterns for React app entry points | ||
| const patterns = [ | ||
| 'src/index.tsx', | ||
| 'src/index.jsx', | ||
| 'index.tsx', | ||
| 'index.jsx', | ||
| ]; | ||
|
|
||
| // Try exact matches first | ||
| for (const pattern of patterns) { | ||
| const fullPath = path.join(cwd, pattern); | ||
| if (await fs.pathExists(fullPath)) { | ||
| return fullPath; | ||
| } | ||
| } | ||
|
|
||
| // If no exact match, search with glob for any index.tsx or index.jsx | ||
| const globPattern = '**/index.{tsx,jsx}'; | ||
| const matches = await glob(globPattern, { | ||
| cwd, | ||
| ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'], | ||
| absolute: true, | ||
| }); | ||
|
|
||
| if (matches.length > 0) { | ||
| // Prefer files in src directory | ||
| const srcMatch = matches.find(m => m.includes('/src/')); | ||
| return srcMatch ?? matches[0] ?? null; | ||
|
Comment on lines
+39
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify hardcoded POSIX separator checks that are platform-fragile
rg -n "includes\\('/src/'\\)" src/prototype.tsRepository: patternfly/patternfly-cli Length of output: 198 🏁 Script executed: sed -n '20,50p' src/prototype.tsRepository: patternfly/patternfly-cli Length of output: 876 🏁 Script executed: sed -n '95,120p' src/prototype.tsRepository: patternfly/patternfly-cli Length of output: 690 🏁 Script executed: sed -n '1,30p' src/prototype.ts | head -20Repository: patternfly/patternfly-cli Length of output: 654 🏁 Script executed: cat > /tmp/verify_glob.js << 'EOF'
// Quick verification of how the 'glob' package returns paths
console.log("path.sep on this system:", require('path').sep);
console.log("Example absolute path on this system would use:", require('path').sep);
console.log("\nOn Windows: C:\\Users\\project\\src\\index.tsx would NOT match /src/");
console.log("On Unix: /home/user/project/src/index.tsx WOULD match /src/");
EOF
node /tmp/verify_glob.jsRepository: patternfly/patternfly-cli Length of output: 275 🏁 Script executed: # Verify that matches come from glob with absolute: true
rg -A 8 "glob\(globPattern" src/prototype.ts | head -20Repository: patternfly/patternfly-cli Length of output: 620 Use path-segment checks instead of hardcoded The Proposed fix+function isUnderSrc(cwd: string, filePath: string): boolean {
+ const rel = path.relative(cwd, filePath);
+ return rel.split(path.sep).includes('src');
+}
...
- const srcMatch = matches.find(m => m.includes('/src/'));
+ const srcMatch = matches.find(m => isUnderSrc(cwd, m));
return srcMatch ?? matches[0] ?? null;
...
- const srcMatch = matches.find(m => m.includes('/src/'));
+ const srcMatch = matches.find(m => isUnderSrc(cwd, m));
return srcMatch ?? matches[0] ?? null;Also applies to: 107-108 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Check if the CSS import already exists in the file content. | ||
| */ | ||
| function hasImport(content: string): boolean { | ||
| return content.includes(CSS_IMPORT_STATEMENT); | ||
| } | ||
|
|
||
| /** | ||
| * Add the CSS import to the top of the file, after any existing imports. | ||
| */ | ||
| function addImport(content: string): string { | ||
| if (hasImport(content)) { | ||
| return content; | ||
| } | ||
|
|
||
| const lines = content.split('\n'); | ||
| let lastImportIndex = -1; | ||
|
|
||
| // Find the last import statement | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| if (line && line.trim().startsWith('import ')) { | ||
| lastImportIndex = i; | ||
| } | ||
| } | ||
|
|
||
| // Insert after the last import, or at the beginning if no imports found | ||
| const insertIndex = lastImportIndex >= 0 ? lastImportIndex + 1 : 0; | ||
| lines.splice(insertIndex, 0, CSS_IMPORT_STATEMENT); | ||
|
|
||
| return lines.join('\n'); | ||
| } | ||
|
|
||
| /** | ||
| * Run the prototype command: find the main index file and add the CSS import. | ||
| */ | ||
| export async function runPrototype(cwd: string): Promise<void> { | ||
| console.log('🔍 Searching for main index file...\n'); | ||
|
|
||
| const indexFile = await findMainIndexFile(cwd); | ||
|
|
||
| if (!indexFile) { | ||
| console.error('❌ Could not find main index.tsx or index.jsx file.'); | ||
| console.error(' Searched in common locations like src/index.tsx, src/index.jsx\n'); | ||
| throw new Error('Main index file not found'); | ||
| } | ||
|
|
||
| console.log(`✅ Found index file: ${path.relative(cwd, indexFile)}\n`); | ||
|
|
||
| // Read the file content | ||
| const content = await fs.readFile(indexFile, 'utf-8'); | ||
|
|
||
| // Check if import already exists | ||
| if (hasImport(content)) { | ||
| console.log('ℹ️ Prototype CSS import already exists in the file.\n'); | ||
| return; | ||
| } | ||
|
|
||
| // Add the import | ||
| const updatedContent = addImport(content); | ||
|
|
||
| // Write back to the file | ||
| await fs.writeFile(indexFile, updatedContent, 'utf-8'); | ||
|
|
||
| console.log('✅ Added prototype CSS import to the file.'); | ||
| console.log(` Import statement: ${CSS_IMPORT_STATEMENT}\n`); | ||
| console.log('✨ Prototype mode enabled! All UI will now render in grayscale. ✨\n'); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| jest.mock('fs-extra', () => { | ||
| const real = jest.requireActual<typeof import('fs-extra')>('fs-extra'); | ||
| return { | ||
| __esModule: true, | ||
| default: { | ||
| pathExists: jest.fn(), | ||
| readFile: jest.fn(), | ||
| writeFile: jest.fn(), | ||
| existsSync: real.existsSync, | ||
| readFileSync: real.readFileSync, | ||
| }, | ||
| }; | ||
| }); | ||
|
|
||
| jest.mock('glob', () => ({ | ||
| __esModule: true, | ||
| glob: jest.fn(), | ||
| })); | ||
|
|
||
| import path from 'path'; | ||
| import fs from 'fs-extra'; | ||
| import { glob } from 'glob'; | ||
| import { runPrototype } from '../src/prototype.js'; | ||
|
|
||
| const mockPathExists = fs.pathExists as jest.MockedFunction<typeof fs.pathExists> & jest.Mock; | ||
| const mockReadFile = fs.readFile as jest.MockedFunction<typeof fs.readFile> & jest.Mock; | ||
| const mockWriteFile = fs.writeFile as jest.MockedFunction<typeof fs.writeFile> & jest.Mock; | ||
| const mockGlob = glob as jest.MockedFunction<typeof glob> & jest.Mock; | ||
|
|
||
| describe('runPrototype', () => { | ||
| const testCwd = '/test/project'; | ||
| const CSS_IMPORT = "import '@patternfly/patternfly-cli/prototype.css';"; | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| // Suppress console.log and console.error during tests | ||
| jest.spyOn(console, 'log').mockImplementation(); | ||
| jest.spyOn(console, 'error').mockImplementation(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| jest.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('should find and modify src/index.tsx', async () => { | ||
| const indexPath = path.join(testCwd, 'src/index.tsx'); | ||
| const originalContent = `import React from 'react';\nimport ReactDOM from 'react-dom';\n\nReactDOM.render(<App />, document.getElementById('root'));`; | ||
| const expectedContent = `import React from 'react';\nimport ReactDOM from 'react-dom';\n${CSS_IMPORT}\n\nReactDOM.render(<App />, document.getElementById('root'));`; | ||
|
|
||
| mockPathExists.mockResolvedValue(true); | ||
| mockReadFile.mockResolvedValue(originalContent); | ||
| mockWriteFile.mockResolvedValue(undefined); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockPathExists).toHaveBeenCalledWith(indexPath); | ||
| expect(mockReadFile).toHaveBeenCalledWith(indexPath, 'utf-8'); | ||
| expect(mockWriteFile).toHaveBeenCalledWith(indexPath, expectedContent, 'utf-8'); | ||
| }); | ||
|
|
||
| it('should find and modify src/index.jsx', async () => { | ||
| const tsxPath = path.join(testCwd, 'src/index.tsx'); | ||
| const jsxPath = path.join(testCwd, 'src/index.jsx'); | ||
| const originalContent = `import React from 'react';\n\nReactDOM.render(<App />, document.getElementById('root'));`; | ||
|
|
||
| mockPathExists | ||
| .mockResolvedValueOnce(false) // src/index.tsx doesn't exist | ||
| .mockResolvedValueOnce(true); // src/index.jsx exists | ||
| mockReadFile.mockResolvedValue(originalContent); | ||
| mockWriteFile.mockResolvedValue(undefined); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockPathExists).toHaveBeenCalledWith(tsxPath); | ||
| expect(mockPathExists).toHaveBeenCalledWith(jsxPath); | ||
| expect(mockReadFile).toHaveBeenCalledWith(jsxPath, 'utf-8'); | ||
| }); | ||
|
|
||
| it('should not modify file if import already exists', async () => { | ||
| const indexPath = path.join(testCwd, 'src/index.tsx'); | ||
| const contentWithImport = `import React from 'react';\n${CSS_IMPORT}\nimport ReactDOM from 'react-dom';\n\nReactDOM.render(<App />, document.getElementById('root'));`; | ||
|
|
||
| mockPathExists.mockResolvedValue(true); | ||
| mockReadFile.mockResolvedValue(contentWithImport); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockReadFile).toHaveBeenCalledWith(indexPath, 'utf-8'); | ||
| expect(mockWriteFile).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should add import at the beginning if no imports exist', async () => { | ||
| const indexPath = path.join(testCwd, 'src/index.tsx'); | ||
| const originalContent = `const app = document.getElementById('root');\napp.innerHTML = 'Hello';`; | ||
| const expectedContent = `${CSS_IMPORT}\nconst app = document.getElementById('root');\napp.innerHTML = 'Hello';`; | ||
|
|
||
| mockPathExists.mockResolvedValue(true); | ||
| mockReadFile.mockResolvedValue(originalContent); | ||
| mockWriteFile.mockResolvedValue(undefined); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockWriteFile).toHaveBeenCalledWith(indexPath, expectedContent, 'utf-8'); | ||
| }); | ||
|
|
||
| it('should throw error if no index file is found', async () => { | ||
| mockPathExists.mockResolvedValue(false); | ||
| mockGlob.mockResolvedValue([]); | ||
|
|
||
| await expect(runPrototype(testCwd)).rejects.toThrow('Main index file not found'); | ||
|
|
||
| expect(mockWriteFile).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should use glob to find index file if common locations do not exist', async () => { | ||
| const foundIndexPath = path.join(testCwd, 'app/index.tsx'); | ||
| const originalContent = `import React from 'react';\n\nfunction App() { return <div>Hello</div>; }`; | ||
|
|
||
| mockPathExists.mockResolvedValue(false); | ||
| mockGlob.mockResolvedValue([foundIndexPath]); | ||
| mockReadFile.mockResolvedValue(originalContent); | ||
| mockWriteFile.mockResolvedValue(undefined); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockGlob).toHaveBeenCalled(); | ||
| expect(mockReadFile).toHaveBeenCalledWith(foundIndexPath, 'utf-8'); | ||
| expect(mockWriteFile).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should prefer src directory when multiple index files are found', async () => { | ||
| const srcIndexPath = path.join(testCwd, 'src/index.tsx'); | ||
| const otherIndexPath = path.join(testCwd, 'other/index.tsx'); | ||
| const originalContent = `import React from 'react';`; | ||
|
|
||
| mockPathExists.mockResolvedValue(false); | ||
| mockGlob.mockResolvedValue([otherIndexPath, srcIndexPath]); | ||
| mockReadFile.mockResolvedValue(originalContent); | ||
| mockWriteFile.mockResolvedValue(undefined); | ||
|
|
||
| await runPrototype(testCwd); | ||
|
|
||
| expect(mockReadFile).toHaveBeenCalledWith(srcIndexPath, 'utf-8'); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Package exports declare .d.ts files but TypeScript config may not generate them. If tsconfig.json missing "declaration": true", build produces no type definitions → consumers get type errors.
Recommendation:
Verify build output includes .d.ts files:
npm run buildls dist/index.d.ts dist/components/protoBanner.d.tsIf missing declaration files, add to tsconfig and rebuild.
Why matters: Consumers importing @patternfly/patternfly-cli in TypeScript projects will get "Cannot find module" errors without type definitions, even though runtime works.