-
Notifications
You must be signed in to change notification settings - Fork 0
RIC-T40 Build fix #19
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import type { AppIconBadgeConfig } from 'app-icon-badge/types'; | ||
|
|
||
| const mockExecFileSync = jest.fn(); | ||
| const mockExistsSync = jest.fn(() => true); | ||
| const mockStatSync = jest.fn(() => ({ size: 100 })); | ||
|
|
||
| jest.mock('child_process', () => ({ | ||
| execFileSync: mockExecFileSync, | ||
| })); | ||
|
|
||
| jest.mock('fs', () => ({ | ||
| existsSync: mockExistsSync, | ||
| statSync: mockStatSync, | ||
| })); | ||
|
|
||
| interface TestExpoConfig { | ||
| _internal?: { | ||
| projectRoot?: string; | ||
| }; | ||
| android?: { | ||
| adaptiveIcon?: { | ||
| foregroundImage?: string; | ||
| }; | ||
| }; | ||
| icon?: string; | ||
| ios?: { | ||
| icon?: string; | ||
| }; | ||
| } | ||
|
|
||
| const withAppIconBadge = jest.requireActual('../with-app-icon-badge.js') as (config: TestExpoConfig, options?: AppIconBadgeConfig) => TestExpoConfig; | ||
|
|
||
| describe('withAppIconBadge', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('generates all configured icons before rewriting their config paths', () => { | ||
| const config: TestExpoConfig = { | ||
| _internal: { projectRoot: '/project' }, | ||
| icon: './assets/icon.png', | ||
| ios: { icon: './assets/ios-icon.png' }, | ||
| android: { adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png' } }, | ||
| }; | ||
| const options: AppIconBadgeConfig = { | ||
| enabled: true, | ||
| badges: [{ type: 'banner', text: 'development' }], | ||
| }; | ||
|
|
||
| const result = withAppIconBadge(config, options); | ||
|
|
||
| expect(mockExecFileSync).toHaveBeenCalledTimes(1); | ||
| const generatorArguments = mockExecFileSync.mock.calls[0]?.[1] as string[]; | ||
| const payload = JSON.parse(generatorArguments[1] ?? '{}') as { | ||
| jobs: { isAdaptiveIcon: boolean; outputPath: string; sourcePath: string }[]; | ||
| }; | ||
| expect(payload.jobs).toEqual([ | ||
| { | ||
| sourcePath: '/project/assets/icon.png', | ||
| outputPath: '/project/.expo/app-icon-badge/icon.png', | ||
| isAdaptiveIcon: false, | ||
| }, | ||
| { | ||
| sourcePath: '/project/assets/ios-icon.png', | ||
| outputPath: '/project/.expo/app-icon-badge/ios-icon.png', | ||
| isAdaptiveIcon: false, | ||
| }, | ||
| { | ||
| sourcePath: '/project/assets/adaptive-icon.png', | ||
| outputPath: '/project/.expo/app-icon-badge/foregroundImage.png', | ||
| isAdaptiveIcon: true, | ||
| }, | ||
| ]); | ||
| expect(result.icon).toBe('.expo/app-icon-badge/icon.png'); | ||
| expect(result.ios?.icon).toBe('.expo/app-icon-badge/ios-icon.png'); | ||
| expect(result.android?.adaptiveIcon?.foregroundImage).toBe('.expo/app-icon-badge/foregroundImage.png'); | ||
| }); | ||
|
|
||
| it('leaves icon paths untouched when badges are disabled', () => { | ||
| const config: TestExpoConfig = { | ||
| _internal: { projectRoot: '/project' }, | ||
| icon: './assets/icon.png', | ||
| android: { adaptiveIcon: { foregroundImage: './assets/adaptive-icon.png' } }, | ||
| }; | ||
|
|
||
| const result = withAppIconBadge(config, { enabled: false, badges: [] }); | ||
|
|
||
| expect(mockExecFileSync).not.toHaveBeenCalled(); | ||
| expect(result.icon).toBe('./assets/icon.png'); | ||
| expect(result.android?.adaptiveIcon?.foregroundImage).toBe('./assets/adaptive-icon.png'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| const { execFileSync } = require('child_process'); | ||
| const { existsSync, statSync } = require('fs'); | ||
| const { resolve } = require('path'); | ||
|
|
||
| const GENERATED_DIRECTORY = '.expo/app-icon-badge'; | ||
| const GENERATED_APP_ICON = `${GENERATED_DIRECTORY}/icon.png`; | ||
| const GENERATED_IOS_ICON = `${GENERATED_DIRECTORY}/ios-icon.png`; | ||
| const GENERATED_ADAPTIVE_ICON = `${GENERATED_DIRECTORY}/foregroundImage.png`; | ||
|
|
||
| function assertGeneratedImage(projectRoot, imagePath) { | ||
| const absolutePath = resolve(projectRoot, imagePath); | ||
| if (!existsSync(absolutePath) || statSync(absolutePath).size === 0) { | ||
| throw new Error(`App icon badge generator did not create a valid image at ${imagePath}`); | ||
| } | ||
| } | ||
|
|
||
| function withAppIconBadge(config, options = {}) { | ||
| if (options.enabled === false) { | ||
| return config; | ||
| } | ||
|
|
||
| const projectRoot = config._internal?.projectRoot ?? process.cwd(); | ||
| const jobs = []; | ||
|
|
||
| if (config.icon) { | ||
| jobs.push({ | ||
| sourcePath: resolve(projectRoot, config.icon), | ||
| outputPath: resolve(projectRoot, GENERATED_APP_ICON), | ||
| isAdaptiveIcon: false, | ||
| }); | ||
| } | ||
|
|
||
| if (config.ios?.icon) { | ||
| jobs.push({ | ||
| sourcePath: resolve(projectRoot, config.ios.icon), | ||
| outputPath: resolve(projectRoot, GENERATED_IOS_ICON), | ||
| isAdaptiveIcon: false, | ||
| }); | ||
| } | ||
|
|
||
| if (config.android?.adaptiveIcon?.foregroundImage) { | ||
| jobs.push({ | ||
| sourcePath: resolve(projectRoot, config.android.adaptiveIcon.foregroundImage), | ||
| outputPath: resolve(projectRoot, GENERATED_ADAPTIVE_ICON), | ||
| isAdaptiveIcon: true, | ||
| }); | ||
| } | ||
|
|
||
| if (jobs.length === 0) { | ||
| return config; | ||
| } | ||
|
|
||
| const generatorPath = resolve(__dirname, '../scripts/generate-app-icon-badges.js'); | ||
| execFileSync(process.execPath, [generatorPath, JSON.stringify({ badges: options.badges ?? [], jobs })], { | ||
| cwd: projectRoot, | ||
| stdio: 'inherit', | ||
| }); | ||
|
|
||
| if (config.icon) { | ||
| assertGeneratedImage(projectRoot, GENERATED_APP_ICON); | ||
| config.icon = GENERATED_APP_ICON; | ||
| } | ||
|
|
||
| if (config.ios?.icon) { | ||
| assertGeneratedImage(projectRoot, GENERATED_IOS_ICON); | ||
| config.ios.icon = GENERATED_IOS_ICON; | ||
| } | ||
|
|
||
| if (config.android?.adaptiveIcon?.foregroundImage) { | ||
| assertGeneratedImage(projectRoot, GENERATED_ADAPTIVE_ICON); | ||
| config.android.adaptiveIcon.foregroundImage = GENERATED_ADAPTIVE_ICON; | ||
| } | ||
|
|
||
| return config; | ||
| } | ||
|
|
||
| module.exports = withAppIconBadge; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| const { copyFileSync, mkdirSync, readFileSync, unlinkSync } = require('fs'); | ||
| const { dirname } = require('path'); | ||
|
|
||
| const { addBadge } = require('app-icon-badge'); | ||
| const Jimp = require('jimp'); | ||
|
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. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
jq -e '
.dependencies.jimp //
.devDependencies.jimp //
.optionalDependencies.jimp
' package.jsonRepository: Resgrid/IC Length of output: 153 🏁 Script executed: #!/bin/bash
set -eu
printf '--- package manager / scripts context ---\n'
jq -r '{type: .type, scripts: .scripts, dependencies, devDependencies, optionalDependencies, packageManager}' package.json 2>/dev/null || true
printf '\n--- jimp references ---\n'
rg -n "require\\(['\"]jimp['\"]\\)|from ['\"]jimp['\"]|jimp" -S --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' .
printf '\n--- lock/package files ---\n'
git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|pnpm-workspace\.yaml)$' || trueRepository: Resgrid/IC Length of output: 239 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- package manager / scripts context ---'
jq -r '{type: .type, scripts: .scripts, dependencies, devDependencies, optionalDependencies, packageManager}' package.json 2>/dev/null || true
printf '%s\n' ''
printf '%s\n' '--- jimp references ---'
rg -n "require\\(['\"]jimp['\"]\\)|from ['\"]jimp['\"]|jimp" -S --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' .
printf '%s\n' ''
printf '%s\n' '--- lock/package files ---'
git ls-files | rg '(^|/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb|pnpm-workspace\.yaml)$' || trueRepository: Resgrid/IC Length of output: 50367 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
import json
from pathlib import Path
pkg = json.loads(Path('package.json').read_text())
name = pkg.get('name')
version = pkg.get('version')
print('package:', name, version)
print('direct_jimp:', pkg.get('dependencies', {}).get('jimp') or pkg.get('devDependencies', {}).get('jimp') or pkg.get('optionalDependencies', {}).get('jimp') or '(none)')
print('direct_app_icon_badge:', pkg.get('dependencies', {}).get('app-icon-badge') or pkg.get('devDependencies', {}).get('app-icon-badge') or '(none)')
lock = Path('yarn.lock').read_text()
for pkg_name in ['"jimp@%', '"app-icon-badge@%']:
print(f'\n--- lock entry: {pkg_name.lstrip(chr(34))} ---')
idx = lock.find(pkg_name)
print(idx)
if idx != -1:
start = lock.rfind('\n', 0, idx)
end = lock.find('\n\n', idx)
print(lock[start:end] if end != -1 else lock[start:start+1200])
PYRepository: Resgrid/IC Length of output: 287 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- scripts/generate-app-icon-badges.js ---'
cat -n scripts/generate-app-icon-badges.js
printf '%s\n' ''
printf '%s\n' '--- app-icon-badge lock entry (loose) ---'
rg -n '^\s*app-icon-badge' yarn.lock -A 8
printf '%s\n' ''
printf '%s\n' '--- jimp references in package files only ---'
python3 - <<'PY'
import json, glob
for path in ['package.json'] + glob.glob('*/*/package.json'):
try:
data=json.load(open(path))
except Exception:
continue
deps={}
for k in ('dependencies','devDependencies','optionalDependencies'):
deps.update(data.get(k,{}))
print(path, {_:deps[_] for _ in deps if _ == 'jimp' or _ == 'app-icon-badge'} or 'no jimp/app-icon-badge')
PYRepository: Resgrid/IC Length of output: 50366 Declare
🤖 Prompt for AI AgentsThere 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. Missing direct dependency causes // package.json (devDependencies):
// "jimp": "<version matching app-icon-badge's resolved jimp>"
const { addBadge } = require('app-icon-badge');
const Jimp = require('jimp');Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); | ||
| const IMAGE_WRITE_TIMEOUT_MS = 30000; | ||
| const IMAGE_WRITE_POLL_INTERVAL_MS = 25; | ||
|
|
||
| function delay(duration) { | ||
| return new Promise((resolve) => { | ||
| setTimeout(resolve, duration); | ||
|
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. Uncleared timer in the Kody rule violation: Clear timers on teardown/unmount Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| }); | ||
| } | ||
|
|
||
| async function waitForValidPng(imagePath) { | ||
| const startedAt = Date.now(); | ||
|
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. Wall-clock time from Date.now() is susceptible to system clock adjustments (NTP, manual changes), making elapsed-time calculations unreliable. Use a monotonic clock such as performance.now() or process.hrtime.bigint() to record the start timestamp. Also found in scripts/generate-app-icon-badges.js:20. Kody rule violation: Avoid `DateTime.Now` for Timing Operations Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
|
|
||
| while (Date.now() - startedAt < IMAGE_WRITE_TIMEOUT_MS) { | ||
| try { | ||
| const imageBuffer = readFileSync(imagePath); | ||
| if (imageBuffer.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) { | ||
| const image = await Jimp.read(imagePath); | ||
| if (image.bitmap.width > 0 && image.bitmap.height > 0) { | ||
| return; | ||
| } | ||
| } | ||
| } catch { | ||
|
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. Silent exception swallowing in the catch block on line 29 discards every error with only a comment, violating deterministic error handling. Log at debug level with context (e.g., imagePath, err) or narrow the catch to specific expected error codes. Also found in scripts/generate-app-icon-badges.js:56. Kody rule violation: Avoid empty catch blocks Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| // app-icon-badge does not await Jimp.writeAsync, so the file may not exist yet. | ||
| } | ||
|
|
||
| await delay(IMAGE_WRITE_POLL_INTERVAL_MS); | ||
| } | ||
|
|
||
| throw new Error(`Timed out waiting for app icon badge image ${imagePath}`); | ||
| } | ||
|
|
||
| async function generateBadgeImage(job, badges) { | ||
| const temporaryPath = `${job.outputPath}.${process.pid}.tmp.png`; | ||
| mkdirSync(dirname(job.outputPath), { recursive: true }); | ||
|
|
||
| try { | ||
| await addBadge({ | ||
| icon: job.sourcePath, | ||
| dstPath: temporaryPath, | ||
| badges, | ||
| isAdaptiveIcon: job.isAdaptiveIcon, | ||
| }); | ||
| await waitForValidPng(temporaryPath); | ||
| copyFileSync(temporaryPath, job.outputPath); | ||
| await waitForValidPng(job.outputPath); | ||
| } finally { | ||
| try { | ||
| unlinkSync(temporaryPath); | ||
| } catch { | ||
| // The temporary image is absent when generation fails before Jimp starts writing. | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async function main() { | ||
| const payload = JSON.parse(process.argv[2] ?? '{}'); | ||
| if (!Array.isArray(payload.jobs) || !Array.isArray(payload.badges)) { | ||
| throw new Error('App icon badge generator requires jobs and badges arrays'); | ||
| } | ||
|
|
||
| for (const job of payload.jobs) { | ||
| await generateBadgeImage(job, payload.badges); | ||
| } | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error('Failed to generate app icon badges:', error); | ||
|
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. Unstructured error logging via Kody rule violation: Include error context in structured logs Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| process.exitCode = 1; | ||
| }); | ||
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.
Uncaught exception in the
execFileSyncsubprocess call propagates raw child-process errors without context when the generator script fails (non-zero exit, missing file, crash). Wrap the call in try/catch, attach context (projectRoot, job count, target paths), and rethrow a mapped application-level Error preserving the original via cause. Also found in scripts/generate-app-icon-badges.js:41.Kody rule violation: Add try-catch blocks for external calls
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.