Skip to content

Commit

Permalink
Bump to TypeScript 5.6 /w strictPropertyInit
Browse files Browse the repository at this point in the history
This commit migrates from TypeScript 5.5 to 5.6 with
`strictPropertyInitialization` flag enabled for stricter quality
controls.

Supporting changes:

- Move GitHubProjectDetails to application layer for better SRP
- Convert mutable fields to immutable states with getters
- Initialize test stubs with default values
- Split complex state updates into pure functions
- Improve error messages in test assertions
- Upgrade `nanoid` dependency to avoid security issues
  • Loading branch information
undergroundwires committed Dec 12, 2024
1 parent ce09243 commit 4dd8e11
Show file tree
Hide file tree
Showing 32 changed files with 391 additions and 310 deletions.
59 changes: 31 additions & 28 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"start-server-and-test": "^2.0.8",
"terser": "^5.36.0",
"tslib": "^2.8.1",
"typescript": "~5.5.4",
"typescript": "~5.6.3",
"vite": "^5.4.11",
"vitest": "^2.1.8",
"vue-tsc": "^2.1.10",
Expand Down
13 changes: 7 additions & 6 deletions src/application/Context/ApplicationContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export class ApplicationContext implements IApplicationContext {

public collection: ICategoryCollection;

public currentOs: OperatingSystem;
public get currentOs(): OperatingSystem {
return this.collection.os;
}

public get state(): ICategoryCollectionState {
return this.getState(this.collection.os);
Expand All @@ -26,7 +28,7 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication,
initialContext: OperatingSystem,
) {
this.setContext(initialContext);
this.collection = this.getCollection(initialContext);
this.states = initializeStates(app);
}

Expand All @@ -38,14 +40,13 @@ export class ApplicationContext implements IApplicationContext {
newState: this.getState(os),
oldState: this.getState(this.currentOs),
};
this.setContext(os);
this.collection = this.getCollection(os);
this.contextChanged.notify(event);
}

private setContext(os: OperatingSystem): void {
private getCollection(os: OperatingSystem): ICategoryCollection {
validateOperatingSystem(os, this.app);
this.collection = this.app.getCollection(os);
this.currentOs = os;
return this.app.getCollection(os);
}

private getState(os: OperatingSystem): ICategoryCollectionState {
Expand Down
44 changes: 34 additions & 10 deletions src/application/Context/State/Code/ApplicationCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,51 @@ import type { IApplicationCode } from './IApplicationCode';
export class ApplicationCode implements IApplicationCode {
public readonly changed = new EventSource<ICodeChangedEvent>();

public current: string;
private state: CodeGenerationResult;

private scriptPositions = new Map<SelectedScript, CodePosition>();
public get current(): string {
return this.state.code;
}

private get scriptPositions(): Map<SelectedScript, CodePosition> {
return this.state.scriptPositions;
}

constructor(
selection: ReadonlyScriptSelection,
private readonly scriptingDefinition: IScriptingDefinition,
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
) {
this.setCode(selection.selectedScripts);
selection.changed.on((scripts) => {
this.setCode(scripts);
this.state = this.generateCode(selection.selectedScripts);
selection.changed.on((newScripts) => {
const oldScripts = Array.from(this.scriptPositions.keys());
this.state = this.generateCode(newScripts);
this.notifyCodeChange(oldScripts, this.state);
});
}

private setCode(scripts: ReadonlyArray<SelectedScript>): void {
const oldScripts = Array.from(this.scriptPositions.keys());
private generateCode(scripts: readonly SelectedScript[]): CodeGenerationResult {
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
this.current = code.code;
this.scriptPositions = code.scriptPositions;
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
return {
code: code.code,
scriptPositions: code.scriptPositions,
};
}

private notifyCodeChange(
oldScripts: readonly SelectedScript[],
newCode: CodeGenerationResult,
): void {
const event = new CodeChangedEvent(
newCode.code,
oldScripts,
newCode.scriptPositions,
);
this.changed.notify(event);
}
}

interface CodeGenerationResult {
readonly code: string;
readonly scriptPositions: Map<SelectedScript, CodePosition>;
}
2 changes: 1 addition & 1 deletion src/application/Parser/ApplicationParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IApplication } from '@/domain/IApplication';
import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/Project/ProjectDetailsParser';
import { Application } from '@/domain/Application';
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,12 @@ function parseParameterWithContextualError(
}

function hasCode(data: FunctionData): data is CodeFunctionData {
return (data as CodeInstruction).code !== undefined;
const { code } = (data as CodeInstruction);
return !isNullOrUndefined(code) && code !== '';
}

function hasCall(data: FunctionData): data is CallFunctionData {
return (data as CallInstruction).call !== undefined;
return !isNullOrUndefined((data as CallInstruction).call);
}

function ensureValidFunctions(functions: readonly FunctionData[]) {
Expand Down
58 changes: 58 additions & 0 deletions src/application/Parser/Project/GitHubProjectDetailsFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { assertInRange } from '@/application/Common/Enum';
import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ProjectDetailsParameters, ProjectDetailsFactory } from './ProjectDetailsFactory';

export const createGitHubProjectDetails: ProjectDetailsFactory = (parameters) => {
validateParameters(parameters);
const githubRepositoryWebUrl = getWebUrl(parameters.repositoryUrl);
return {
name: parameters.name,
version: parameters.version,
slogan: parameters.slogan,
repositoryUrl: parameters.repositoryUrl,
homepage: parameters.homepage,
repositoryWebUrl: githubRepositoryWebUrl,
feedbackUrl: `${githubRepositoryWebUrl}/issues`,
releaseUrl: `${githubRepositoryWebUrl}/releases/tag/${parameters.version}`,
getDownloadUrl: (os) => {
const fileName = getFileName(os, parameters.version.toString());
return `${githubRepositoryWebUrl}/releases/download/${parameters.version}/${fileName}`;
},
};
};

function validateParameters(parameters: ProjectDetailsParameters) {
if (!parameters.name) {
throw new Error('name is undefined');
}
if (!parameters.slogan) {
throw new Error('undefined slogan');
}
if (!parameters.repositoryUrl) {
throw new Error('repositoryUrl is undefined');
}
if (!parameters.homepage) {
throw new Error('homepage is undefined');
}
}

function getWebUrl(gitUrl: string) {
if (gitUrl.endsWith('.git')) {
return gitUrl.substring(0, gitUrl.length - 4);
}
return gitUrl;
}

function getFileName(os: OperatingSystem, version: string): string {
assertInRange(os, OperatingSystem);
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
case OperatingSystem.macOS:
return `privacy.sexy-${version}.dmg`;
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new RangeError(`Unsupported os: ${OperatingSystem[os]}`);
}
}
14 changes: 14 additions & 0 deletions src/application/Parser/Project/ProjectDetailsFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { Version } from '@/domain/Version';

export interface ProjectDetailsParameters {
readonly name: string,
readonly version: Version,
readonly slogan: string,
readonly repositoryUrl: string,
readonly homepage: string,
}

export type ProjectDetailsFactory = (
args: ProjectDetailsParameters,
) => ProjectDetails;
30 changes: 30 additions & 0 deletions src/application/Parser/Project/ProjectDetailsParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
import { Version } from '@/domain/Version';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
import { createGitHubProjectDetails } from './GitHubProjectDetailsFactory';
import type { ProjectDetailsFactory } from './ProjectDetailsFactory';

export const parseProjectDetails: MetadataProjectDetailsParser = (
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
createProjectDetails: ProjectDetailsFactory = createGitHubProjectDetails,
) => {
return createProjectDetails({
name: metadata.name,
version: new Version(
metadata.version,
),
slogan: metadata.slogan,
repositoryUrl: metadata.repositoryUrl,
homepage: metadata.homepageUrl,
});
};

export type MetadataProjectDetailsParser = ProjectDetailsParser & ((
metadata?: IAppMetadata,
createProjectDetails?: ProjectDetailsFactory,
) => ProjectDetails);

export interface ProjectDetailsParser {
(): ProjectDetails;
}
Loading

0 comments on commit 4dd8e11

Please sign in to comment.