Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/apple/ascClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import type {
WinBackOfferResource,
} from '../core/types/appleCatalog.js';
import { ACCESSIBILITY_SUPPORT_KEYS } from '../core/types/appleCatalog.js';
import type { MutableDeep } from '../core/types/mutable.js';
/** Scheme + host of the App Store Connect API; most resources hang off `/v1`, a few newer ones off `/v2`. */
const API_ORIGIN = 'https://api.appstoreconnect.apple.com';
const BASE_URL = `${API_ORIGIN}/v1`;
Expand Down Expand Up @@ -321,7 +322,7 @@ const pickListingFields = (
const pickAccessibilitySupport = (
attributes: Partial<AccessibilitySupport>,
): AccessibilitySupport => {
const support: AccessibilitySupport = {};
const support: MutableDeep<AccessibilitySupport> = {};
for (const key of ACCESSIBILITY_SUPPORT_KEYS) {
const fieldValue = attributes[key];
if (typeof fieldValue === 'boolean') support[key] = fieldValue;
Expand Down Expand Up @@ -1042,7 +1043,7 @@ export class AppStoreConnectClient {
return appleResources.map((localization) => {
let locale = localization.attributes.locale;
if (locale === undefined) locale = '';
const localizedExperience: AppClipLocalizationResource = {
const localizedExperience: MutableDeep<AppClipLocalizationResource> = {
id: localization.id,
locale,
};
Expand Down Expand Up @@ -1330,7 +1331,7 @@ export class AppStoreConnectClient {
name: string,
bundleIdResourceId: string,
certificateId: string,
deviceIds: string[],
deviceIds: readonly string[],
profileType: ProvisioningProfileType = AD_HOC_PROFILE_TYPE,
): Promise<ProfileResource> {
const appleResources = await this.createResource<{
Expand Down Expand Up @@ -1457,7 +1458,7 @@ export class AppStoreConnectClient {
});
}
/** Clear the StoreKit purchase history for one or more sandbox testers (a single batched request). */
async clearSandboxTesterPurchaseHistory(testerIds: string[]): Promise<void> {
async clearSandboxTesterPurchaseHistory(testerIds: readonly string[]): Promise<void> {
await this.createResource(this.v2('/sandboxTestersClearPurchaseHistoryRequest'), {
type: 'sandboxTestersClearPurchaseHistoryRequest',
relationships: {
Expand Down Expand Up @@ -1743,7 +1744,7 @@ export class AppStoreConnectClient {
type: 'subscriptionOfferCodes',
attributes: {
name: input.name,
customerEligibilities: input.customerEligibilities,
customerEligibilities: [...input.customerEligibilities],
offerEligibility: input.offerEligibility,
duration: input.duration,
offerMode: input.offerMode,
Expand Down Expand Up @@ -2066,7 +2067,7 @@ export class AppStoreConnectClient {
return { id: appleResources.id, inAppPurchaseId, subscriptionId, enabled, visibleForAllUsers };
}
/** Replace the app's promoted-purchase ordering with `orderedIds` (the product-page display order). */
async reorderPromotedPurchases(appId: string, orderedIds: string[]): Promise<void> {
async reorderPromotedPurchases(appId: string, orderedIds: readonly string[]): Promise<void> {
const requestDocument: components['schemas']['AppPromotedPurchasesLinkagesRequest'] = {
data: orderedIds.map((id) => ({ type: 'promotedPurchases', id })),
};
Expand Down Expand Up @@ -2244,13 +2245,13 @@ export class AppStoreConnectClient {
return betaTester;
}
/** Add existing testers to a beta group in one relationship call (invites external testers). */
async addTestersToGroup(groupId: string, testerIds: string[]): Promise<void> {
async addTestersToGroup(groupId: string, testerIds: readonly string[]): Promise<void> {
await this.request<unknown>('POST', `/betaGroups/${groupId}/relationships/betaTesters`, {
data: testerIds.map((id) => ({ type: 'betaTesters', id })),
});
}
/** Remove testers from a beta group; they keep app access through any other group they're in. */
async removeTestersFromGroup(groupId: string, testerIds: string[]): Promise<void> {
async removeTestersFromGroup(groupId: string, testerIds: readonly string[]): Promise<void> {
await this.request<unknown>('DELETE', `/betaGroups/${groupId}/relationships/betaTesters`, {
data: testerIds.map((id) => ({ type: 'betaTesters', id })),
});
Expand Down Expand Up @@ -2562,7 +2563,7 @@ export class AppStoreConnectClient {
*/
async updateAppAvailabilityTerritories(
availabilityId: string,
territories: string[],
territories: readonly string[],
): Promise<void> {
const desired = new Set(territories);
const territoryAvailabilities = await this.listAppTerritoryAvailabilities(availabilityId);
Expand Down Expand Up @@ -3274,15 +3275,15 @@ export class AppStoreConnectClient {
email: invite.email,
firstName: invite.firstName,
lastName: invite.lastName,
roles: invite.roles,
roles: [...invite.roles],
allAppsVisible: invite.allAppsVisible,
provisioningAllowed: invite.provisioningAllowed,
},
});
let email = appleResources.attributes.email;
if (email === undefined) email = invite.email;
let roles = appleResources.attributes.roles;
if (roles === undefined) roles = invite.roles;
if (roles === undefined) roles = [...invite.roles];
const pendingInvitation: UserInvitationResource = { id: appleResources.id, email, roles };
if (appleResources.attributes.firstName !== undefined)
Object.assign(pendingInvitation, { firstName: appleResources.attributes.firstName });
Expand Down Expand Up @@ -4474,7 +4475,10 @@ export class AppStoreConnectClient {
return { id: reservation.data.id, operations };
}
/** PUT a reserved asset's bytes to Apple's CDN, one operation (chunk) at a time, with transient-retry. */
private async putAssetBytes(operations: UploadOperation[], bytes: Buffer): Promise<void> {
private async putAssetBytes(
operations: readonly UploadOperation[],
bytes: Buffer,
): Promise<void> {
for (const operation of operations) {
let offset = operation.offset;
if (offset === undefined) offset = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/apple/generated/specPatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function generatedHeader(spec: AscSpecMeta): string {
* Pick the real spec file from a zip's entry list, accepting Apple's occasional download suffix and
* skipping the macOS resource-fork sibling. Returns the matching entry or null when absent.
*/
export function pickSpecEntry(entries: string[]): string | null {
export function pickSpecEntry(entries: readonly string[]): string | null {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. pickspecentry uses function declaration 📘 Rule violation ⚙ Maintainability

pickSpecEntry is a module-level exported function declaration instead of a const arrow
function. This violates the code-style rule requiring module-level functions to be declared as
const arrow functions before first use.
Agent Prompt
## Issue description
The module-level exported function `pickSpecEntry` is declared using `export function ...`, but the style rule requires module-level functions to be declared as `export const ... = (...) => {}` (const arrow) before first use.

## Issue Context
This is in `src/apple/generated/specPatch.ts`, and the PR modified the `pickSpecEntry` signature.

## Fix Focus Areas
- src/apple/generated/specPatch.ts[56-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

const specEntry = entries.find((entry) => {
const fileName = entry.split('/').at(-1);
return (
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const registerCompletionCommand = (program: Command): void => {
completion
.command(`${COMPLETE_SUBCOMMAND} [words...]`, { hidden: true })
.description('internal: emit completion candidates for the words typed so far')
.action((words: string[]) => {
.action((words: readonly string[]) => {
return runCliProgram(
completionCommandProgram({ operation: 'complete', words, commandTree: program }),
);
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/testflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const registerTestflightCommand = (program: Command): void => {
.option('--csv <path>', 'import testers from a CSV (email,firstName,lastName per line)')
.option('--dry-run', 'report what would change without inviting anyone', false)
.option('-y, --yes', 'skip the confirmation prompt', false)
.action((emails: string[], commandOptions: AddTesterOptions) =>
.action((emails: readonly string[], commandOptions: AddTesterOptions) =>
runCliProgram(
testflightCommandProgram({
operation: 'add',
Expand All @@ -118,7 +118,7 @@ export const registerTestflightCommand = (program: Command): void => {
.option('-g, --group <name>', "beta group to remove from (auto-selected if there's only one)")
.option('--dry-run', 'report what would change without removing anyone', false)
.option('-y, --yes', 'skip the confirmation prompt', false)
.action((emails: string[], commandOptions: TesterMutationOptions) =>
.action((emails: readonly string[], commandOptions: TesterMutationOptions) =>
runCliProgram(
testflightCommandProgram({
operation: 'remove',
Expand Down
2 changes: 1 addition & 1 deletion src/cli/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type EnvFlags = {
printEnv: boolean;
};
/** Commander reducer: collect a repeatable string option into an array. */
const collectEnv = (environmentFlag: string, previousFlags: string[]): string[] => {
const collectEnv = (environmentFlag: string, previousFlags: readonly string[]): string[] => {
return [...previousFlags, environmentFlag];
};
/**
Expand Down
16 changes: 9 additions & 7 deletions src/core/adopt/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ export type PlannedEntitlement = {
};

export type CapabilityPlanInput = {
enabledTypes: string[];
settingsByType: Record<string, CapabilitySetting[]>;
enabledTypes: readonly string[];
settingsByType: Record<string, readonly CapabilitySetting[]>;
profileEntitlements: Record<string, EntitlementValue> | null;
existing: Record<string, unknown>;
};

/** Render capability settings as concise key/value advice. */
const describeSettings = (settings: CapabilitySetting[] | undefined): string | undefined => {
const describeSettings = (
settings: readonly CapabilitySetting[] | undefined,
): string | undefined => {
if (settings === undefined) return undefined;
if (settings.length === 0) return undefined;
const settingDescriptions = settings.map((setting) => {
Expand Down Expand Up @@ -73,7 +75,7 @@ export const planCapabilityEntitlements = (input: CapabilityPlanInput): PlannedE

/** Choose the best profile content for entitlement recovery. */
const chooseProfileContent = (
profiles: { name: string; profileContent: string }[],
profiles: readonly { readonly name: string; readonly profileContent: string }[],
): string | null => {
if (profiles.length === 0) return null;
const appStoreProfile = profiles.find((profile) => /app\s*store/i.test(profile.name));
Expand Down Expand Up @@ -102,7 +104,7 @@ export const capabilitiesAdopter: Adopter<ProfileEntitlementRequirements> = {
let profileEntitlements: Record<string, EntitlementValue> | null = null;
if (profileContent !== null)
profileEntitlements = yield* extractProfileEntitlements(profileContent);
const settingsByType: Record<string, CapabilitySetting[]> = {};
const settingsByType: Record<string, readonly CapabilitySetting[]> = {};
for (const capability of capabilities) {
if (capability.settings !== undefined)
settingsByType[capability.capabilityType] = capability.settings;
Expand All @@ -129,8 +131,8 @@ export const capabilitiesAdopter: Adopter<ProfileEntitlementRequirements> = {
value: entitlement.value,
},
};
if (entitlement.note !== undefined) plannedWrite.note = entitlement.note;
return plannedWrite;
if (entitlement.note === undefined) return plannedWrite;
return { ...plannedWrite, note: entitlement.note };
});
}),
};
10 changes: 5 additions & 5 deletions src/core/adopt/certs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export type LocalSigningView = {
};

export type CertPlanInput = {
certificates: CertificateResource[];
profiles: ProfileResource[];
certificates: readonly CertificateResource[];
profiles: readonly ProfileResource[];
local: LocalSigningView;
bundleId: string;
};
Expand All @@ -29,8 +29,8 @@ const signingReport = (description: string, note?: string): PlannedWrite => {
fidelity: 'detect',
change: { home: 'keychain' },
};
if (note !== undefined) plannedWrite.note = note;
return plannedWrite;
if (note === undefined) return plannedWrite;
return { ...plannedWrite, note };
};

/** Compare live signing assets against the locally cached private-key view. */
Expand Down Expand Up @@ -78,7 +78,7 @@ export const certsAdopter: Adopter<CertAdopterRequirements> = {
Effect.gen(function* () {
const bundleResource = yield* appleCatalog.findBundleId(target.bundleId);
const certificates = yield* appleCatalog.listDistributionCertificates();
let profiles: ProfileResource[] = [];
let profiles: readonly ProfileResource[] = [];
if (bundleResource !== null)
profiles = yield* appleCatalog.listProfilesForBundleId(bundleResource.id);
const storedCredentials = yield* describeStoredCredentials(target.keyId);
Expand Down
10 changes: 5 additions & 5 deletions src/core/adopt/configWriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ import type {
SubscriptionGroupConfig,
} from '../types/catalog.js';
/** Fold one bundle's imported product pieces into a single {@link AppProducts}, dropping empty arms. */
export const aggregateProductPieces = (pieces: ProductPiece[]): AppProducts => {
export const aggregateProductPieces = (pieces: readonly ProductPiece[]): AppProducts => {
const inAppPurchases: InAppPurchaseConfig[] = [];
const subscriptionGroups: SubscriptionGroupConfig[] = [];
for (const piece of pieces) {
if (piece.type === 'iap') inAppPurchases.push(piece.iap);
else subscriptionGroups.push(piece.group);
}
const products: AppProducts = {};
if (inAppPurchases.length > 0) products.inAppPurchases = inAppPurchases;
if (subscriptionGroups.length > 0) products.subscriptionGroups = subscriptionGroups;
return products;
if (inAppPurchases.length === 0 && subscriptionGroups.length === 0) return {};
if (inAppPurchases.length === 0) return { subscriptionGroups };
if (subscriptionGroups.length === 0) return { inAppPurchases };
return { inAppPurchases, subscriptionGroups };
};
/** Serialize a `products` block (keyed by bundle id) as an indented, paste-ready TypeScript section. */
export const serializeProductsSection = (
Expand Down
5 changes: 3 additions & 2 deletions src/core/adopt/orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import type { AdoptCatalogApi, AdoptTarget, Adopter, PlannedWrite } from '../types/adopt.js';
import type { AppDescriptor } from '../types/app.js';
import type { InAppPurchaseConfig } from '../types/catalog.js';
import type { MutableDeep } from '../types/mutable.js';
const makeApi = (overrides: Partial<AdoptCatalogApi> = {}): AdoptCatalogApi => {
const base: AdoptCatalogApi = {
getAppId: () => Effect.succeed('app1'),
Expand All @@ -40,12 +41,12 @@ const app = (
bundleId?: string,
configPath = `/repo/${name}/app.json`,
): AppDescriptor => {
const appDescriptor: AppDescriptor = { name, dir: `/repo/${name}`, configPath };
const appDescriptor: MutableDeep<AppDescriptor> = { name, dir: `/repo/${name}`, configPath };
if (bundleId) appDescriptor.bundleId = bundleId;
return appDescriptor;
};
/** Run local adopt writes with Effect Platform's Node filesystem and path services. */
const runApplyAdopt = (plans: TargetPlan[], applyContext: ApplyContext) =>
const runApplyAdopt = (plans: readonly TargetPlan[], applyContext: ApplyContext) =>
Effect.runPromise(applyAdopt(plans, applyContext).pipe(Effect.provide(NodeContext.layer)));
describe('detectTargets', () => {
it('separates apps with a live record from those skipped, with a confirming signal', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/core/adopt/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const detectTargets = (
export const planTargets = <Requirements>(
appleCatalog: AdoptCatalogApi,
detection: Detection,
adopters: Adopter<Requirements>[],
adopters: readonly Adopter<Requirements>[],
): Effect.Effect<TargetPlan[], never, Requirements> =>
Effect.forEach(
detection.detected,
Expand Down Expand Up @@ -194,7 +194,7 @@ export type AdoptApplyResult = {
};

/** Collect imported product pieces into a bundle-keyed catalog. */
const collectProducts = (plans: TargetPlan[]): Record<string, AppProducts> => {
const collectProducts = (plans: readonly TargetPlan[]): Record<string, AppProducts> => {
const productsByBundleId: Record<string, AppProducts> = {};
for (const targetPlan of plans) {
const productPieces: ProductPiece[] = [];
Expand All @@ -212,7 +212,7 @@ const collectProducts = (plans: TargetPlan[]): Record<string, AppProducts> => {

/** Apply a confirmed adoption plan to local configuration and delegated listing pulls. */
export const applyAdopt = <Requirements>(
plans: TargetPlan[],
plans: readonly TargetPlan[],
applyContext: ApplyContext<Requirements>,
): Effect.Effect<AdoptApplyResult, unknown, FileSystem.FileSystem | Path.Path | Requirements> =>
Effect.gen(function* () {
Expand Down
30 changes: 17 additions & 13 deletions src/core/adopt/products.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Effect } from 'effect';
import type { AdoptCatalogApi, Adopter, PlannedWrite } from '../types/adopt.js';
import type { MutableDeep } from '../types/mutable.js';
import type {
InAppPurchaseResource,
LocalizationResource,
Expand Down Expand Up @@ -44,9 +45,11 @@ const toSubscriptionPeriod = (
};

/** Convert App Store localization resources into config localization entries. */
const toProductLocalizations = (localizations: LocalizationResource[]): ProductLocalization[] =>
const toProductLocalizations = (
localizations: readonly LocalizationResource[],
): ProductLocalization[] =>
localizations.map((localization) => {
const productLocalization: ProductLocalization = {
const productLocalization: MutableDeep<ProductLocalization> = {
locale: localization.locale,
name: localization.name,
};
Expand Down Expand Up @@ -75,7 +78,7 @@ const importInAppPurchase = (
productId: purchase.productId,
referenceName: purchase.name,
type: purchaseType,
localizations: toProductLocalizations(localizations),
localizations: [...toProductLocalizations(localizations)],
};
const plannedWrite: PlannedWrite = {
description: `products: import in-app purchase ${purchase.productId} (${purchaseType})`,
Expand All @@ -86,11 +89,11 @@ const importInAppPurchase = (
piece: { type: 'iap', iap: purchaseConfig },
},
};
if (hasPrice) {
plannedWrite.note =
'priced on App Store Connect - add `price` in config or keep managing it in the UI';
}
return plannedWrite;
if (!hasPrice) return plannedWrite;
return {
...plannedWrite,
note: 'priced on App Store Connect - add `price` in config or keep managing it in the UI',
};
});

type ImportedSubscription = Readonly<{
Expand Down Expand Up @@ -118,7 +121,7 @@ const importSubscription = (
productId: subscription.productId,
referenceName: subscription.name,
subscriptionPeriod,
localizations: toProductLocalizations(localizations),
localizations: [...toProductLocalizations(localizations)],
},
pricedUnimported: hasPrice,
};
Expand Down Expand Up @@ -169,10 +172,11 @@ const importSubscriptionGroup = (
piece: { type: 'subscriptionGroup', group: groupConfig },
},
};
if (unimportedPriceIds.length > 0) {
plannedWrite.note = `priced on App Store Connect, not imported - set \`price\` for: ${unimportedPriceIds.join(', ')}`;
}
return plannedWrite;
if (unimportedPriceIds.length === 0) return plannedWrite;
return {
...plannedWrite,
note: `priced on App Store Connect, not imported - set \`price\` for: ${unimportedPriceIds.join(', ')}`,
};
});

/** Read products and plan their launch.config imports. */
Expand Down
Loading
Loading