Hone logo
Hone
Problems

Angular Incremental Compilation Simulation

Modern Angular applications benefit greatly from fast build times, especially during development. Incremental compilation is a key optimization that recompiles only the changed parts of your codebase, rather than the entire project, significantly speeding up development cycles. This challenge aims to simulate a simplified version of this concept by processing a file system and determining which files need to be recompiled based on changes and dependencies.

Problem Description

Your task is to create a simulation of an incremental compiler for a simplified Angular project structure. You will be given an initial state of the project's files and their contents, along with a list of subsequent changes (file modifications, additions, and deletions). You need to determine which files would be flagged for recompilation based on these changes.

The core idea is that a file should be recompiled if:

  1. It has been directly modified.
  2. It depends on another file that has been modified.

For simplicity, we will define "dependency" as a TypeScript import statement within a .ts file. You will need to parse these import statements to build a dependency graph.

Key Requirements:

  • File State Representation: Represent the initial state of the project files, including their content.
  • Change Processing: Process a sequence of file operations (modify, add, delete).
  • Dependency Analysis: Extract TypeScript import statements to build a dependency map.
  • Recompilation Logic: Implement the logic to identify files that need recompilation.
  • Output: Return a list of file paths that require recompilation after a set of changes.

Expected Behavior:

Given an initial set of files and a series of operations, the system should output a sorted list of file paths that need to be recompiled.

Edge Cases to Consider:

  • Deleting a file that others depend on.
  • Adding a new file that introduces new dependencies.
  • Circular dependencies (though for this simulation, we can assume they don't cause infinite recompilation loops, just that if a file in a cycle changes, all files in that cycle might need recompilation if they depend on it).
  • Files without any imports.
  • Imports that reference non-existent files (these should be ignored for dependency tracking).

Examples

Example 1:

Input:

{
  "initialFiles": {
    "src/app/app.component.ts": "export class AppComponent { ngOnInit() { console.log('init'); } }",
    "src/app/shared/utils.ts": "export function helper() { return true; }",
    "src/main.ts": "import './app/app.component'; console.log('main');"
  },
  "changes": [
    {
      "operation": "modify",
      "path": "src/app/app.component.ts",
      "content": "export class AppComponent { ngOnInit() { console.log('updated init'); } }"
    }
  ]
}

Output:

[
  "src/app/app.component.ts",
  "src/main.ts"
]

Explanation: src/app/app.component.ts was directly modified. src/main.ts imports src/app/app.component.ts (implicitly, as it's in the same directory and likely resolved by the Angular build system), so it also needs recompilation.

Example 2:

Input:

{
  "initialFiles": {
    "src/app/data.service.ts": "export class DataService { getData() { return { id: 1 }; } }",
    "src/app/user.component.ts": "import { DataService } from './data.service'; export class UserComponent { constructor() { new DataService(); } }",
    "src/app/app.module.ts": "import { UserComponent } from './user.component'; export class AppModule { }"
  },
  "changes": [
    {
      "operation": "modify",
      "path": "src/app/data.service.ts",
      "content": "export class DataService { getData() { return { id: 2 }; } }"
    },
    {
      "operation": "modify",
      "path": "src/app/user.component.ts",
      "content": "import { DataService } from './data.service'; export class UserComponent { constructor() { console.log(new DataService().getData()); } }"
    }
  ]
}

Output:

[
  "src/app/app.module.ts",
  "src/app/data.service.ts",
  "src/app/user.component.ts"
]

Explanation: src/app/data.service.ts was modified. src/app/user.component.ts depends on data.service.ts and was also modified, so both need recompilation. src/app/app.module.ts depends on user.component.ts, which was modified, so app.module.ts also needs recompilation. The output is sorted alphabetically.

Example 3: File Deletion and Addition

Input:

{
  "initialFiles": {
    "src/app/config.ts": "export const API_URL = '/api';",
    "src/app/api.service.ts": "import { API_URL } from './config'; export class ApiService { getUrl() { return API_URL; } }"
  },
  "changes": [
    {
      "operation": "delete",
      "path": "src/app/config.ts"
    },
    {
      "operation": "add",
      "path": "src/app/new-config.ts",
      "content": "export const NEW_API_URL = '/v2/api';"
    },
    {
      "operation": "modify",
      "path": "src/app/api.service.ts",
      "content": "import { NEW_API_URL } from './new-config'; export class ApiService { getUrl() { return NEW_API_URL; } }"
    }
  ]
}

Output:

[
  "src/app/api.service.ts"
]

Explanation: src/app/config.ts was deleted. src/app/api.service.ts previously depended on config.ts. A new file src/app/new-config.ts was added. src/app/api.service.ts was then modified to import from new-config.ts. Because api.service.ts was modified, it needs recompilation. Since config.ts was deleted and api.service.ts no longer imports it, api.service.ts will be updated to reflect the removal of the dependency during recompilation. new-config.ts was added, but no other files directly import it before the modification of api.service.ts, so it doesn't trigger recompilation of other files yet. However, the problem asks for files that need recompilation. In a real scenario, api.service.ts would be recompiled, and then if other files imported it, they would be too. For this simulation, we focus on the direct and indirect impact of the provided changes.

Constraints

  • File paths will always be relative to a root project directory.
  • File content will be valid TypeScript code.
  • Import statements will follow the import ... from '...'; or import '...'; syntax.
  • Dependencies are inferred by parsing import paths (e.g., import { Something } from './dependency'; means ./dependency is a dependency of the current file). Relative paths should be resolved correctly (e.g., ./utils in app.component.ts should be treated as src/app/utils.ts if app.component.ts is src/app/app.component.ts).
  • The simulation should handle up to 1000 files and 5000 changes.
  • Performance is a consideration; the solution should be reasonably efficient.

Notes

  • You'll need to implement a way to parse TypeScript import statements. Regular expressions or a simple AST parser can be used. For this problem, a regex-based approach might be sufficient to identify import paths.
  • When resolving relative import paths, consider the directory of the file containing the import statement.
  • A graph data structure (adjacency list or matrix) can be useful for representing dependencies.
  • Think about how to manage the "dirty" state of files efficiently. A queue or a set can be used to track files that need recompilation.
  • The order of operations in the changes array matters.
  • The problem statement implies a simplified dependency resolution. In a real Angular project, module resolution is more complex (e.g., path aliases, tsconfig.json paths). For this challenge, assume direct relative path resolution is sufficient. For example, an import like import { A } from './a'; within src/app/comp.ts refers to src/app/a.ts. An import like import { B } from '../b'; within src/app/comp.ts refers to src/b.ts.
Loading editor...
typescript