Full Code of chrislewisdev/prettyplan for AI

master 9c4b6c58faac cached
22 files
45.6 KB
12.7k tokens
39 symbols
1 requests
Download .txt
Repository: chrislewisdev/prettyplan
Branch: master
Commit: 9c4b6c58faac
Files: 22
Total size: 45.6 KB

Directory structure:
gitextract_054_3svt/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── src/
│   ├── index.html
│   ├── style.css
│   └── ts/
│       ├── parse.ts
│       ├── prettyplan.ts
│       ├── releases.ts
│       ├── render.ts
│       └── ui.ts
├── tests/
│   ├── extractChangeSummary.test.ts
│   ├── extractInvididualChanges.test.ts
│   ├── parseChangeSymbol.test.ts
│   ├── parseId.test.ts
│   ├── parseNewAndOldValueDiffs.test.ts
│   ├── parseSingleValueDiffs.test.ts
│   └── parseWarnings.test.ts
├── tsconfig.json
└── webpack.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules/
dist/
package-lock.json
yarn.lock

================================================
FILE: .travis.yml
================================================
language: node_js

# Tells Travis to use the latest Node version 
# It defaults to an old one otherwise
node_js: node

# By default Travis will run "npm install" and "npm test" for node projects
# So we don't need to specify them here

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2018 Chris Lewis

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# prettyplan [![Build Status](https://travis-ci.com/chrislewisdev/prettyplan.svg?branch=master)](https://travis-ci.com/chrislewisdev/prettyplan)

Prettyplan ([available online here](https://prettyplan.chrislewisdev.com/)) is a small tool to help you view large Terraform plans with ease. By pasting in your plan output, it will be formatted for:

 - Expandable/collapsible sections to help you see your plan at a high level and in detail
 - Tabular layout for easy comparison of old/new values
 - Better display formatting of multi-line strings (such as JSON documents)
 
## Terraform Version Compatibility

Prettyplan was written to work on Terraform plans from 0.11 and earlier. In 0.12, the plan output was significantly changed, addressing many of the pain points that Prettyplan addresses; for this reason, there are no current plans to update Prettyplan to work with 0.12. In my case, Prettyplan was made unnecessary by Terraform's improvements.

Contributions are still welcome if anyone would like to upgrade the code to handle plans from 0.12 onwards.
 
## Contributing

You're welcome to submit ideas/bugs (via the Issues section) or improvements (via Pull Requests)! 

The code has all been converted from JavaScript to TypeScript and is built by webpack. To work on the project locally, you should be able to get everything up and running just by having `npm`, running `npm install` and then `npm run serve` which will open up Prettyplan in your default browser, ready for any changes you make to the source files.

You can also run `npm run build` to build the project without a dev server.
 
### Tests

Tests are being run on every commit and Pull Request via Travis, but if you want to run them locally, you'll need to have `npm` on your PC, and run `npm install` followed by `npm test` in the repository.

## Deployment

The project is deployed to Netlify; a slightly older version of the code is also served on GitHub pages which used to be the main way to access Prettyplan.

## Will this steal sensitive data from my Terraform plans?

No. All the parsing/formatting is done directly in your browser, no data is sent to or from another service.


================================================
FILE: jest.config.js
================================================
module.exports = {
    "roots": ["tests"],
    "transform": {
        "\\.ts$": "ts-jest"
    },
    "testRegex": "\\.test\\.ts$",
    "moduleFileExtensions": ["ts", "js"]
}

================================================
FILE: package.json
================================================
{
  "name": "prettyplan",
  "version": "1.0.0",
  "description": "Formatting tool for terraform plan output",
  "author": "Chris Lewis",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/chrislewisdev/prettyplan"
  },
  "scripts": {
    "test": "jest",
    "build": "webpack",
    "serve": "webpack-dev-server --open"
  },
  "devDependencies": {
    "@types/jest": "^24.0.0",
    "copy-webpack-plugin": "^4.6.0",
    "html-webpack-plugin": "^3.2.0",
    "jest": "^23.6.0",
    "ts-jest": "^23.10.5",
    "ts-loader": "^5.3.3",
    "typescript": "^3.3.3",
    "webpack": "^4.29.3",
    "webpack-cli": "^3.2.3",
    "webpack-dev-server": "^3.1.14"
  }
}


================================================
FILE: src/index.html
================================================
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>prettyplan</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="style.css" />
</head>

<body>
    <div class="stripe"></div>
    <div id="release-notification" class="hidden"></div>
    <div id="branding">
        Source on <a href="https://github.com/chrislewisdev/prettyplan">GitHub</a><br />
        By <a href="https://twitter.com/chrislewisdev">Chris Lewis</a><br />
        <button class="text-button" onclick="showReleaseNotes()">Release Notes</button><br />
        <a href="https://github.com/chrislewisdev/prettyplan-cli">CLI version</a><br />
   </div>
    <div class="container">
        <h1>prettyplan</h1>
        <p>Just paste in your output from terraform plan (or use the provided example), and hit Prettify!</p>
        <p>Prettyplan does not support plans from Terraform 0.12+. For more info, <a href="https://github.com/chrislewisdev/prettyplan">see the project's readme</a>.</p>
        <textarea id="terraform-plan">
            Refreshing Terraform state in-memory prior to plan...
            The refreshed state will be used to calculate this plan, but will not be
            persisted to local or remote state storage.
            
            aws_alb_target_group.sample_app: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...up/sample-app/d5eedf0680cc9834)
            aws_iam_role.service_role: Refreshing state... (ID: SampleApp)
            aws_cloudwatch_log_group.sample_app: Refreshing state... (ID: sample-app)
            aws_ecr_repository.sample_app: Refreshing state... (ID: sample-app)
            aws_iam_role_policy.service_role_policy: Refreshing state... (ID: SampleApp:SampleApp)
            null_resource.promote_images: Refreshing state... (ID: 1236159896537553123)
            aws_ecs_task_definition.sample_app: Refreshing state... (ID: sample-app)
            aws_alb_listener_rule.routing: Refreshing state... (ID: arn:aws:elasticloadbalancing:us-east-1:...94bc/2825bddee1920172/ec8bc47bb5409ead)
            aws_ecs_service.sample_app: Refreshing state... (ID: arn:aws:ecs:us-east-1:123123123123:service/sample-app)
            
            ------------------------------------------------------------------------
            
            An execution plan has been generated and is shown below.
            Resource actions are indicated with the following symbols:
                ~ update in-place
            -/+ destroy and then create replacement
                &lt;= read (data resources)
            
            Terraform will perform the following actions:
            
                &lt;= data.external.ecr_image_digests
                    id:                       &lt;computed&gt;
                    program.#:                "1"
                    program.0:                "extract-image-digests"
                    result.%:                 &lt;computed&gt;
            
                ~ aws_ecs_service.sample_app
                    task_definition:          "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" =&gt; "$&#123; aws_ecs_task_definition.sample_app.arn &#125;"
            
            -/+ aws_ecs_task_definition.sample_app (new resource required)
                    id:                       "sample-app" =&gt; &lt;computed&gt; (forces new resource)
                    arn:                      "arn:aws:ecs:us-east-1:123123123123:task-definition/sample-app:186" =&gt; &lt;computed&gt;
                    container_definitions:    "[&#123;\"cpu\":1,\"environment\":[],\"essential\":true,\"image\":\"123123123123.dkr.ecr.us-east-1.amazonaws.com/sample-app@sha256:18979dcf521de65f736585d30b58e8085ecc44560fa8c530ad1eb17fecad1cab\",\"logConfiguration\":&#123;\"logDriver\":\"awslogs\",\"options\":&#123;\"awslogs-group\":\"sample-app\",\"awslogs-region\":\"us-east-1\",\"awslogs-stream-prefix\":\"sample-app\"&#125;&#125;,\"memory\":256,\"mountPoints\":[],\"name\":\"sample-app\",\"portMappings\":[&#123;\"containerPort\":8443,\"hostPort\":0,\"protocol\":\"tcp\"&#125;],\"volumesFrom\":[]&#125;]" =&gt; "[\n  &#123;\n    \"name\": \"sample-app\",\n    \"image\": \"$&#123; aws_ecr_repository.sample_app.repository_url &#125;@$&#123; data.external.ecr_image_digests.result[\"sample-app\"] &#125;\",\n    \"cpu\": 1,\n    \"memory\": 256,\n    \"essential\": true,\n    \"logConfiguration\": &#123;\n      \"logDriver\": \"awslogs\",\n      \"options\": &#123;\n        \"awslogs-group\": \"$&#123; aws_cloudwatch_log_group.sample_app.name &#125;\",\n        \"awslogs-region\": \"$&#123; var.target_aws_region &#125;\",\n        \"awslogs-stream-prefix\": \"sample-app\"\n      &#125;\n    &#125;,\n    \"portMappings\": [\n      &#123;\n        \"containerPort\": 8443,\n        \"hostPort\": 0\n      &#125;\n    ]\n  &#125;\n]\n" (forces new resource)
                    family:                   "sample-app" =&gt; "sample-app"
                    network_mode:             "" =&gt; &lt;computed&gt;
                    revision:                 "186" =&gt; &lt;computed&gt;
                    task_role_arn:            "arn:aws:iam::123123123123:role/SampleApp" =&gt; "arn:aws:iam::123123123123:role/SampleApp"
            
            -/+ null_resource.promote_images (new resource required)
                    id:                       "1236159896537553123" =&gt; &lt;computed&gt; (forces new resource)
                    triggers.%:               "1" =&gt; "1"
                    triggers.deploy_job_hash: "6c37ac7175bdf35e24a2f2755addd238" =&gt; "1a0bd86fc5831ee66858f2e159efa547" (forces new resource)
            
            
            Plan: 2 to add, 1 to change, 2 to destroy.
            
            ------------------------------------------------------------------------
            
            This plan was saved to: terraform.plan
            
            To perform exactly these actions, run the following command to apply:
                terraform apply "terraform.plan"
                                        </textarea><br />
        <button onclick="runPrettyplan()">Prettify it!</button>
        <div id="parsing-error-message" class="hidden">That doesn't look like a Terraform plan. Did you copy the entire output
            (without colouring) from the plan command?</div>
        <div id="prettyplan" class="prettyplan hidden">
            <ul id="errors" class="errors"></ul>
            <ul id="warnings" class="warnings"></ul>
            <button class="expand-all" onclick="expandAll()">Expand all</button>
            <button class="collapse-all hidden" onclick="collapseAll()">Collapse all</button>
            <ul id="actions" class="actions"></ul>
        </div>
    </div>
</body>

</html>

================================================
FILE: src/style.css
================================================
body {
    font-family: Arial, Helvetica, sans-serif;
    text-rendering: optimizeLegibility;
    background: #ECF7FE;
    color: #000000c0;
    font-size: 15px;
    margin: 0;
}
@keyframes fade-in {
    0% {
        opacity: 0;
    }
    100% {
        opacity: 1;
    }
}

.stripe {
    width: 100%;
    height: 5px;
    background: #5C4CE4;
    animation-name: wipe-in;
    animation-duration: 1s;
}
@keyframes wipe-in {
    0% {
        width: 0%;
    }
    100% {
        width: 100%;
    }
}

#release-notification {
    background: #5C4CE4;
    color: white;
    font-weight: bold;
    text-align: center;
    overflow: hidden;
    padding: 10px 0 15px 0;
    height: 20px;
    animation-name: notification-pop-in;
    animation-duration: 2s;
}
#release-notification a {
    color: white;
}
#release-notification.dismissed {
    animation-name: notification-pop-out;
    animation-duration: .5s;
    height: 0;
    padding: 0;
}
@keyframes notification-pop-in {
    0% {
        height: 0;
        padding: 0;
    }
    50% {
        height: 0;
        padding: 0;
    }
}
@keyframes notification-pop-out {
    0% {
        height: 20px;
        padding: 10px 0 15px 0;
    }
    100% {
        height: 0;
        padding: 0;
    }
}

#modal-container {
    animation-name: fade-in;
    animation-duration: .2s;
}
.modal-pane {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: #ffffffe6;
    z-index: 10; 
}
.modal-content {
    position: fixed;
    width: 60%;
    height: 60%;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: #ffffff;
    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
    z-index: 20;
}
.modal-close {
    position: absolute;
    right: 0;
    padding: 10px;
}
.modal-close button.text-button {
    color: #4526AC;
    text-decoration: none;
    font-weight: normal;
}
.release-notes {
    max-width: 80%;
    margin: 0 auto 0 auto;
    overflow-y: auto;
    max-height: 100%;
}

#branding {
    float: right;
    padding-top: 10px;
    padding-right: 10px;
    font-size: 10px;
    color: #4526AC;
    text-align: right;
}
#branding a {
    color: #4526AC;
}

.container {
    margin: 10px 10px 0 10px;
    animation-name: fade-in;
    animation-duration: 1s;
}
@media only screen and (min-width: 600px) {
    .container {
        max-width: 80%;
        margin-left: auto;
        margin-right: auto;
    }
}

h1, h2 {
    text-align: center;
    color: #4526AC;
}

#terraform-plan {
    width: 100%;
    min-height: 300px;
    border: none;
    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
    padding: 10px;
    margin-bottom: 10px;
    resize: none;
    background: #ffffffe6;
}

button {
    font-size: 18px;
    background: #5C4CE4;
    color: #fff;
    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
    border: none;
    border-radius: 2px;
    width: 170px;
    height: 40px;
}
button:hover {
    background: #6567EA;
    cursor: pointer;
}
button:active {
    background: #5037CA;
}
button.text-button {
    background: none;
    box-shadow: none;
    border-radius: 0;
    width: auto;
    height: auto;
    text-decoration: underline;
    font-size: inherit;
    font-weight: inherit;
    font-family: Arial, Helvetica, sans-serif; 
    color: inherit;
    text-align: inherit;
    padding: 0;
}

#parsing-error-message {
    background-color: #ffffff;
    padding: 10px;
    color: #000000c0;
    margin: 4px;
    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
    font-weight: bold;
    border-left: 2px solid red;
    animation-name: error;
    animation-duration: 1s;
}

@keyframes error {
    0% {
        background-color: red;
    }
    100% {
        background-color: white;
    }
}

.prettyplan ul {
    padding-left: 0;
    font-size: 13px;
}

.prettyplan li {
    list-style: none;
    background: #ffffffe6;
    padding: 10px;
    color: #000000c0;
    margin: 4px;
    box-shadow: 0 2px 6px 0 hsla(0, 0%, 0%, 0.2);
}

.prettyplan ul.warnings li {
    border-left: 3px solid #757575;
}

.prettyplan ul.actions li.update {
    border-left: 3px solid #ff8f00;
}
.prettyplan ul.actions li.create {
    border-left: 3px solid #2e7d32;
}
.prettyplan ul.actions li.destroy {
    border-left: 3px solid #b71c1c;
}
.prettyplan ul.actions li.recreate {
    border-left: 3px solid #1565c0;
}
.prettyplan ul.actions li.read {
    border-left: 3px solid #519bf0;
}

.badge {
    display: inline-block;
    text-transform: uppercase;
    margin-right: 10px;
    padding: 3px;
    font-size: 12px;
    font-weight: bold;
}
.warnings .badge {
    color: #757575;
}
li.update .badge {
    color: #ff8f00;
}
li.create .badge {
    color: #2e7d32;
}
li.destroy .badge {
    color: #b71c1c;
}
li.recreate .badge {
    color: #1565c0;
}
li.read .badge {
    color: #519bf0;
}

.id-segment:not(:last-child)::after {
    content: " > ";
}
.id-segment.name, .id-segment.type {
    font-weight: bold;
}

.change-count {
    float: right;
}

.summary {
    cursor: pointer;
}

.changes {
    margin: 5px auto 0 auto;
    padding: 5px;
}
.changes table {
    width: 100%;
    word-break: break-all;
    font-size: 13px;
}
.changes table td {
    padding: 10px;
    width: 40%;
}
pre {
    white-space: pre-wrap;
}
.changes table td.property {
    width: 20%;
    text-align: right;
    font-weight: bold;
}
.changes table tr:nth-child(even) {
    background-color: #f5f5f5;
}

.forces-new-resource {
    color: #b71c1c;
}

.collapsed, .hidden {
    display: none;
}


================================================
FILE: src/ts/parse.ts
================================================
export interface ResourceId {
    name: string;
    type: string;
    prefixes: string[];
}
export interface Warning {
    id: ResourceId;
    detail: string;
}
export enum ChangeType {
    Create = 'create',
    Read = 'read',
    Update = 'update',
    Destroy = 'destroy',
    Recreate = 'recreate',
    Unknown = 'unknown'
}
export interface Diff {
    property: string;
    old?: string;
    new: string;
    forcesNewResource?: string;
}
export interface Action {
    id: ResourceId;
    type: ChangeType;
    changes: Diff[];
}
export interface Plan {
    warnings: Warning[];
    actions: Action[];    
}

export function parse(terraformPlan: string): Plan {
    var warnings = parseWarnings(terraformPlan);

    var changeSummary = extractChangeSummary(terraformPlan);
    var changes = extractIndividualChanges(changeSummary);

    var plan = { warnings: warnings, actions: [] };
    for (var i = 0; i < changes.length; i++) {
        plan.actions.push(parseChange(changes[i]));
    }

    return plan;
}

export function parseWarnings(terraformPlan: string): Warning[] {
    let warningRegex: RegExp = new RegExp('Warning: (.*:)(.*)', 'gm');
    let warning: RegExpExecArray;
    let warnings: Warning[] = [];

    do {
        warning = warningRegex.exec(terraformPlan);
        if (warning) {
            warnings.push({ id: parseId(warning[1]), detail: warning[2] });
        }
    } while (warning);

    return warnings;
}

export function extractChangeSummary(terraformPlan: string): string {
    var beginActionRegex = new RegExp('Terraform will perform the following actions:', 'gm');
    var begin = beginActionRegex.exec(terraformPlan);

    if (begin) return terraformPlan.substring(begin.index + 45);
    else return terraformPlan;
}

export function extractIndividualChanges(changeSummary: string): string[] {
    //TODO: Fix the '-/' in '-/+' getting chopped off
    var changeRegex = new RegExp('([~+-]|-\/\+|<=) [\\S\\s]*?((?=-\/\+|[~+-] |<=|Plan:)|$)', 'g');
    var change;
    var changes = [];

    do {
        change = changeRegex.exec(changeSummary);
        if (change) changes.push(change[0]);
    } while (change);

    return changes;
}

export function parseChange(change: string): Action {
    var changeTypeAndIdRegex = new RegExp('([~+-]|-\/\+|<=) (.*)$', 'gm');
    var changeTypeAndId = changeTypeAndIdRegex.exec(change);
    var changeTypeSymbol = changeTypeAndId[1];
    var resourceId = changeTypeAndId[2];

    var type;
    type = parseChangeSymbol(changeTypeSymbol);

    //Workaround for recreations showing up as '+' changes
    if (resourceId.match('(new resource required)')) {
        type = 'recreate';
        resourceId = resourceId.replace(' (new resource required)', '');
    }

    var diffs;
    if (type === 'create' || type === 'read') {
        diffs = parseSingleValueDiffs(change);
    }
    else {
        diffs = parseNewAndOldValueDiffs(change);
    }

    return {
        id: parseId(resourceId),
        type: type,
        changes: diffs
    };
}

export function parseId(resourceId: string): ResourceId {
    var idSegments = resourceId.split('.');
    var resourceName = idSegments[idSegments.length - 1];
    var resourceType = idSegments[idSegments.length - 2] || null;
    var resourcePrefixes = idSegments.slice(0, idSegments.length - 2);

    return { name: resourceName, type: resourceType, prefixes: resourcePrefixes };
}

export function parseChangeSymbol(changeTypeSymbol): ChangeType {
    if (changeTypeSymbol === "-")
        return ChangeType.Destroy;
    else if (changeTypeSymbol === "+")
        return ChangeType.Create;
    else if (changeTypeSymbol === "~")
        return ChangeType.Update
    else if (changeTypeSymbol === "<=")
        return ChangeType.Read;
    else if (changeTypeSymbol === "-/+")
        return ChangeType.Recreate;
    else
        return ChangeType.Unknown;
}

export function parseSingleValueDiffs(change): Diff[] {
    var propertyAndValueRegex = new RegExp('\\s*(.*?): *(?:<computed>|"(|[\\S\\s]*?[^\\\\])")', 'gm');
    var diff;
    var diffs = [];

    do {
        diff = propertyAndValueRegex.exec(change);
        if (diff) {
            diffs.push({
                property: diff[1].trim(),
                new: diff[2] !== undefined ? diff[2] : "<computed>"
            });
        }
    } while (diff);

    return diffs;
}

export function parseNewAndOldValueDiffs(change): Diff[] {
    var propertyAndNewAndOldValueRegex = new RegExp('\\s*(.*?): *(?:"(|[\\S\\s]*?[^\\\\])")[\\S\\s]*?=> *(?:<computed>|"(|[\\S\\s]*?[^\\\\])")( \\(forces new resource\\))?', 'gm');
    var diff;
    var diffs = [];

    do {
        diff = propertyAndNewAndOldValueRegex.exec(change);
        if (diff) {
            diffs.push({
                property: diff[1].trim(),
                old: diff[2],
                new: diff[3] !== undefined ? diff[3] : "<computed>",
                forcesNewResource: diff[4] !== undefined
            });
        }
    } while (diff);

    return diffs;
}

================================================
FILE: src/ts/prettyplan.ts
================================================
import { getCurrentVersion, getLastUsedVersion, updateLastUsedVersion } from './releases';
import { expandAll, collapseAll, accordion, closeModal } from './ui';
import { showReleaseNotification, hideReleaseNotification, showReleaseNotes, displayParsingErrorMessage, hideParsingErrorMessage, clearExistingOutput, unHidePlan, render } from './render';
import { parse } from './parse';

window.addEventListener('load', function () {
    if (getCurrentVersion() != getLastUsedVersion()) {
        showReleaseNotification(getCurrentVersion());
        updateLastUsedVersion();
    }
}); 

(<any>window).runPrettyplan = () => {
    hideParsingErrorMessage();
    clearExistingOutput();

    var terraformPlan = (<HTMLTextAreaElement>document.getElementById("terraform-plan")).value;
    var plan = parse(terraformPlan);

    if (plan.warnings.length === 0 && plan.actions.length === 0) {
        displayParsingErrorMessage();
    }

    render(plan);
    unHidePlan();
}

(<any>window).showReleaseNotes = showReleaseNotes;
(<any>window).expandAll = expandAll;
(<any>window).collapseAll = collapseAll;
(<any>window).accordion = accordion;
(<any>window).closeModal = closeModal;
(<any>window).hideReleaseNotification = hideReleaseNotification;

================================================
FILE: src/ts/releases.ts
================================================
export interface Release {
    version: string;
    notes: string[];
}

export function getCurrentVersion(): string {
    return releases[0].version;
}

export function getLastUsedVersion(): string {
    return window.localStorage.getItem('lastUsedVersion');
}

export function updateLastUsedVersion(): void {
    window.localStorage.setItem('lastUsedVersion', getCurrentVersion());
}

export function getReleases(): Release[] {
    return releases;
}

//New releases should always go at the top of this list.
let releases: Release[] = [
    {
        version: 'v1.3',
        notes: [
            'A command-line version of Prettyplan is now available! Check it out on <a target="_blank" href="https://github.com/chrislewisdev/prettyplan-cli">GitHub</a>'
        ]
    },
    {
        version: 'v1.2',
        notes: [
            '<em>&lt;computed&gt;</em> values now display properly instead of being interpreted as HTML (<a target="_blank" href="https://github.com/chrislewisdev/prettyplan/issues/2">#2</a>)',
            'Resource changes with <em>(forces new resource)</em> now have this highlighted in the table of changes (<a target="_blank" href="https://github.com/chrislewisdev/prettyplan/issues/3">#3</a>)',
            'Italics for <em>&lt;computed&gt;</em> or <em>${variable}</em> values to help set them apart from regular values'
        ]
    },
    {
        version: 'v1.1',
        notes: [
            'Added handy release notes!',
            'Fixed parsing of large AWS IAM policy documents (<a target="_blank" href="https://github.com/chrislewisdev/prettyplan/issues/10">#10</a>)'
        ]
    },
    {
        version: 'v1.0',
        notes: [
            'See your Terraform plans transformed into a beautiful tabulated format!',
            'Support for prettyifying JSON content for easier reading',
            'Theming consistent with the Terraform colour scheme',
            'Works in Firefox, Chrome, and Edge'
        ]
    }
];

================================================
FILE: src/ts/render.ts
================================================
import { Plan, Action, Diff, Warning, ResourceId } from './parse'
import { removeChildren, addClass, removeClass, createModalContainer } from './ui';
import { Release, getReleases } from './releases';

export function clearExistingOutput(): void {
    removeChildren(document.getElementById('errors'));
    removeChildren(document.getElementById('warnings'));
    removeChildren(document.getElementById('actions'));
}

export function hideParsingErrorMessage(): void {
    addClass(document.getElementById('parsing-error-message'), 'hidden');
}

export function displayParsingErrorMessage(): void {
    removeClass(document.getElementById('parsing-error-message'), 'hidden');
}

export function unHidePlan(): void {
    removeClass(document.getElementById('prettyplan'), 'hidden');
}

export function showReleaseNotification(version: string): void {
    const notificationElement = document.getElementById('release-notification');
    notificationElement.innerHTML = components.releaseNotification(version);
    removeClass(notificationElement, 'hidden');
}

export function hideReleaseNotification(): void {
    addClass(document.getElementById('release-notification'), 'dismissed');
}

export function showReleaseNotes(): void {
    createModalContainer().innerHTML = components.modal(components.releaseNotes(getReleases()));
}

export function render(plan: Plan): void {
    if (plan.warnings) {
        const warningList = document.getElementById('warnings');
        warningList.innerHTML = plan.warnings.map(components.warning).join('');
    }

    if (plan.actions) {
        const actionList = document.getElementById('actions');
        actionList.innerHTML = plan.actions.map(components.action).join('');
    }
}

const components = {
    badge: (label: string): string => `
        <span class="badge">${label}</span>
    `,

    id: (id: ResourceId): string => `
        <span class="id">
            ${id.prefixes.map(prefix => 
                `<span class="id-segment prefix">${prefix}</span>`
            ).join('')}
            <span class="id-segment type">${id.type}</span>
            <span class="id-segment name">${id.name}</span>
        </span>
    `,

    warning: (warning: Warning): string => `
        <li>
            ${components.badge('warning')}
            ${components.id(warning.id)}
            <span>${warning.detail}</span>
        </li>
    `,

    changeCount: (count: number): string => `
        <span class="change-count">
            ${count + ' change' + (count > 1 ? 's' : '')}
        </span>
    `,

    change: (change: Diff): string => `
        <tr>
            <td class="property">
                ${change.property}
                ${change.forcesNewResource ? `<br /><span class="forces-new-resource">(forces new resource)</span>` : ''}
            </td>
            <td class="old-value">${change.old ? prettify(change.old) : ''}</td>
            <td class="new-value">${prettify(change.new)}</td>
        </tr>
    `,

    action: (action: Action): string => `
        <li class="${action.type}">
            <div class="summary" onclick="accordion(this)">
                ${components.badge(action.type)}
                ${components.id(action.id)}
                ${action.changes ? components.changeCount(action.changes.length) : ''}
            </div>
            <div class="changes collapsed">
                <table>
                    ${action.changes.map(components.change).join('')}
                </table>
            </div>
        </li>
    `,

    modal: (content: string): string => `
        <div class="modal-pane" onclick="closeModal()"></div>
        <div class="modal-content">
            <div class="modal-close"><button class="text-button" onclick="closeModal()">close</button></div>
            ${content}
        </div>
    `,

    releaseNotes: (releases: Release[]): string => `
        <div class="release-notes">
            <h1>Release Notes</h1>
            ${releases.map(components.release).join('')}
        </div>
    `,

    release: (release: Release): string => `
        <h2>${release.version}</h2>
        <ul>
            ${release.notes.map((note) => `<li>${note}</li>`).join('')}
        </ul>
    `,

    releaseNotification: (version: string): string => `
        Welcome to ${version}!
        <button class="text-button" onclick="showReleaseNotes(); hideReleaseNotification()">View release notes?</button>
        (or <button class="text-button" onclick="hideReleaseNotification()">ignore</button>)
    `
};

function prettify(value: string): string {
    if (value === '<computed>')
    {
        return `<em>&lt;computed&gt;</em>`;
    }
    else if (value.startsWith('${') && value.endsWith('}'))
    {
        return `<em>${value}</em>`;
    }
    else if (value.indexOf('\\n') >= 0 || value.indexOf('\\"') >= 0) {
        var sanitisedValue = value.replace(new RegExp('\\\\n', 'g'), '\n')
                                  .replace(new RegExp('\\\\"', 'g'), '"');
        
        return `<pre>${prettifyJson(sanitisedValue)}</pre>`;
    }
    else {
        return value;
    }
}

function prettifyJson(maybeJson: string): string {
    try {
        return JSON.stringify(JSON.parse(maybeJson), null, 2);
    }
    catch (e) {
        return maybeJson;
    }
}

================================================
FILE: src/ts/ui.ts
================================================
export function accordion(element: Element): void {
    const changes = element.parentElement.getElementsByClassName('changes');
    for (var i = 0; i < changes.length; i++) {
        toggleClass(changes[i], 'collapsed');
    }
}

export function toggleClass(element: Element, className: string): void {
    if (!element.className.match(className)) {
        element.className += ' ' + className;
    }
    else {
        element.className = element.className.replace(className, '');
    }
}

export function addClass(element: Element, className: string): void {
    if (!element.className.match(className)) element.className += ' ' + className;
}

export function removeClass(element: Element, className: string): void {
    element.className = element.className.replace(className, '');
}

export function expandAll(): void {
    const sections = document.querySelectorAll('.changes.collapsed');

    for (var i = 0; i < sections.length; i++) {
        toggleClass(sections[i], 'collapsed');
    }

    toggleClass(document.querySelector('.expand-all'), 'hidden');
    toggleClass(document.querySelector('.collapse-all'), 'hidden');
}

export function collapseAll(): void {
    const sections = document.querySelectorAll('.changes:not(.collapsed)');

    for (var i = 0; i < sections.length; i++) {
        toggleClass(sections[i], 'collapsed');
    }

    toggleClass(document.querySelector('.expand-all'), 'hidden');
    toggleClass(document.querySelector('.collapse-all'), 'hidden');
}

export function removeChildren(element: Element): void {
    while (element.lastChild) {
        element.removeChild(element.lastChild);
    }
}

export function createModalContainer(): HTMLElement {
    const modalElement = document.createElement('div');
    modalElement.id = 'modal-container';

    document.body.appendChild(modalElement);

    return modalElement;
}

export function closeModal(): void {
    const modalElement = document.getElementById('modal-container');
    document.body.removeChild(modalElement);
}

================================================
FILE: tests/extractChangeSummary.test.ts
================================================
import { extractChangeSummary } from '../src/ts/parse'

test('extract change summary - single line', function() {
    const extractedSummary = extractChangeSummary('Terraform will perform the following actions:<summary>');

    expect(extractedSummary).toBe('<summary>');
});

test('extract change summary - multi line', function() {
    const extractedSummary = extractChangeSummary(`
        Text preceding the terraform plan

        Terraform will perform the following actions:

        <summary>`
    );

    expect(extractedSummary).toBe('\n\n        <summary>');
});

test('extract change summary - without any Terraform summary prefix', function() {
    const extractedSummary = extractChangeSummary('<summary>');

    expect(extractedSummary).toBe('<summary>');
});

================================================
FILE: tests/extractInvididualChanges.test.ts
================================================
import { extractIndividualChanges } from '../src/ts/parse';

test('extract individual changes - with plan summary at end', function() {
    const changes = extractIndividualChanges(`
      + module.alb.aws_alb_listener.default_https
          ssl_policy:                                             "old" => "new"
    
      ~ module.api.aws_alb_listener_rule.default
          condition.2636223071.field:                             "path-pattern" => ""
          condition.2636223071.field:                             "path-pattern" => ""
          condition.2636223071.field:                             "path-pattern" => ""
    
      - module.api.aws_alb_target_group.default
          health_check.0.path:                                    "/healthcheck/old" => "/healthcheck/new"
    
      -/+ module.service_a.aws_ecs_service.default
          task_definition:                                        "service-a:185" => "service-a:179"
    
      <= module.service_b.aws_ecs_service.default
          task_definition:                                        "service-b:171" => "service-b:165"
      Plan: 2 to add, 1 to change, 2 to destroy.
    `);

    expect(changes).toHaveLength(5);
});

test('extract individual changes - without plan summary at end', function() {
    const changes = extractIndividualChanges(`
      + module.alb.aws_alb_listener.default_https
          ssl_policy:                                             "old" => "new"
    
      ~ module.api.aws_alb_listener_rule.default
          condition.2636223071.field:                             "path-pattern" => ""
    `);

    expect(changes).toHaveLength(2);
});

test('extract individual changes - with extra text at the start', function() {
    const changes = extractIndividualChanges(`
      this text here should not be detected part of the change
      neither should this
      -------------------------------------------

      + module.alb.aws_alb_listener.default_https
          ssl_policy:                                             "old" => "new"
    
      ~ module.api.aws_alb_listener_rule.default
          condition.2636223071.field:                             "path-pattern" => ""
    `);

    expect(changes).toHaveLength(2);
});

================================================
FILE: tests/parseChangeSymbol.test.ts
================================================
import { parseChangeSymbol } from '../src/ts/parse';

test('parse change symbol', function() {
    expect(parseChangeSymbol('+')).toBe('create');
    expect(parseChangeSymbol('-')).toBe('destroy');
    expect(parseChangeSymbol('~')).toBe('update');
    expect(parseChangeSymbol('<=')).toBe('read');
    expect(parseChangeSymbol('-/+')).toBe('recreate');
    expect(parseChangeSymbol('gibberish')).toBe('unknown');
});

================================================
FILE: tests/parseId.test.ts
================================================
import { parseId } from '../src/ts/parse'

test('parse id - no prefixes', function() {
    const id = parseId('aws_route53_record.domain_name');

    expect(id.name).toBe('domain_name');
    expect(id.type).toBe('aws_route53_record');
    expect(id.prefixes).toEqual([]);
});
test('parse id - with prefixes', function() {
    const id = parseId('module.api.aws_ecs_service.api_service');

    expect(id.name).toBe('api_service');
    expect(id.type).toBe('aws_ecs_service');
    expect(id.prefixes).toEqual(['module', 'api']);
});
test('parse id - name only', function() {
    const id = parseId('api_service');

    expect(id.name).toBe('api_service');
    expect(id.type).toBeNull();
    expect(id.prefixes).toEqual([]);
});

================================================
FILE: tests/parseNewAndOldValueDiffs.test.ts
================================================
import { parseNewAndOldValueDiffs } from '../src/ts/parse';

test('new and old value diffs - quote formatting', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "old_value" => "new_value"');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].old).toBe('old_value');
    expect(diffs[0].new).toBe('new_value');
});
test('new and old value diffs - empty quotes', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "" => "new_value"');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].old).toBe('');
    expect(diffs[0].new).toBe('new_value');
});
test('new and old value diffs - computed values', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "old_value" => <computed>');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].old).toBe('old_value');
    expect(diffs[0].new).toBe('<computed>');
});
test('new and old value diffs - whitespace handling', function() {
    const diffs = parseNewAndOldValueDiffs('   property_name   : " old_value " => "new_value "');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].old).toBe(' old_value ');
    expect(diffs[0].new).toBe('new_value ');
});
test('new and old value diffs - multi line', function() {
    const diffs = parseNewAndOldValueDiffs('property1: "old1" => "new1"\n property2: "old2" => "new2"');

    expect(diffs).toHaveLength(2);
    expect(diffs[0].property).toBe('property1');
    expect(diffs[0].old).toBe('old1');
    expect(diffs[0].new).toBe('new1');
    expect(diffs[1].property).toBe('property2');
    expect(diffs[1].old).toBe('old2');
    expect(diffs[1].new).toBe('new2');
});
test('new and old value diffs - IAM policy document', function() {
    const diffs = parseNewAndOldValueDiffs(`
    policy:                                     "{\\r\\n    \\"Version\\": \\"2012-10-17\\",\\r\\n    \\"Statement\\": [\\r\\n        {\\r\\n            \\"Action\\": [\\r\\n                \\"sqs:*\\",\\r\\n                \\"sns:*\\"\\r\\n\\r\\n            ],\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\",\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n                \\"logs:CreateLogGroup\\",\\r\\n                \\"logs:CreateLogStream\\",\\r\\n                \\"logs:PutLogEvents\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n
        \\"s3:PutObject\\",\\r\\n                \\"s3:GetObject\\",\\r\\n                \\"s3:GetObjectVersion\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Action\\": [\\r\\n                \\"cloudformation:*\\"\\r\\n            ],\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n"
    => "{\\r\\n    \\"Version\\": \\"2012-10-17\\",\\r\\n    \\"Statement\\": [\\r\\n        {\\r\\n            \\"Action\\": [\\r\\n                \\"sqs:*\\",\\r\\n                \\"sns:*\\"\\r\\n\\r\\n            ],\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\",\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n                \\"logs:CreateLogGroup\\",\\r\\n                \\"logs:CreateLogStream\\",\\r\\n                \\"logs:PutLogEvents\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n
        \\"s3:PutObject\\",\\r\\n                \\"s3:GetObject\\",\\r\\n                \\"s3:GetObjectVersion\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Action\\": [\\r\\n                \\"cloudformation:*\\"\\r\\n            ],\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n"
    `);

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('policy');
});
test('force new resource - false for normal diffs', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "old_value" => "new_value"');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].forcesNewResource).toBe(false);
});
test('force new resource - true when included', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "old_value" => "new_value" (forces new resource)');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].forcesNewResource).toBe(true);
});
test('force new resource - works for <computed> values', function() {
    const diffs = parseNewAndOldValueDiffs('property_name: "old_value" => <computed> (forces new resource)');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].forcesNewResource).toBe(true);
});

================================================
FILE: tests/parseSingleValueDiffs.test.ts
================================================
import { parseSingleValueDiffs } from '../src/ts/parse';

test('single value diffs - quote formatting', function() {
    const diffs = parseSingleValueDiffs('property_name: "new_value"');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].new).toBe('new_value');
});
test('single value diffs - empty quotes', function() {
    const diffs = parseSingleValueDiffs('property_name: ""');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].new).toBe('');
});
test('single value diffs - computed values', function() {
    const diffs = parseSingleValueDiffs('property_name: <computed>');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].new).toBe('<computed>');
});
test('single value diffs - whitespace handling', function() {
    const diffs = parseSingleValueDiffs('     property_name :    " value "   ');

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('property_name');
    expect(diffs[0].new).toBe(' value ');
});
test('single value diffs - multi line', function() {
    const diffs = parseSingleValueDiffs('property1: "value1"\n property2: "value2"');

    expect(diffs).toHaveLength(2);
    expect(diffs[0].property).toBe('property1');
    expect(diffs[0].new).toBe('value1');
    expect(diffs[1].property).toBe('property2');
    expect(diffs[1].new).toBe('value2');
});
test('single value diffs - IAM policy document', function() {
    const diffs = parseSingleValueDiffs(`
    policy:                                     "{\\r\\n    \\"Version\\": \\"2012-10-17\\",\\r\\n    \\"Statement\\": [\\r\\n        {\\r\\n            \\"Action\\": [\\r\\n                \\"sqs:*\\",\\r\\n                \\"sns:*\\"\\r\\n\\r\\n            ],\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\",\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n                \\"logs:CreateLogGroup\\",\\r\\n                \\"logs:CreateLogStream\\",\\r\\n                \\"logs:PutLogEvents\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Resource\\": [\\r\\n                \\"*\\"\\r\\n            ],\\r\\n            \\"Action\\": [\\r\\n
        \\"s3:PutObject\\",\\r\\n                \\"s3:GetObject\\",\\r\\n                \\"s3:GetObjectVersion\\"\\r\\n            ]\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n            \\"Action\\": [\\r\\n                \\"cloudformation:*\\"\\r\\n            ],\\r\\n            \\"Resource\\": \\"*\\"\\r\\n        },\\r\\n        {\\r\\n            \\"Effect\\": \\"Allow\\",\\r\\n"
    `);

    expect(diffs).toHaveLength(1);
    expect(diffs[0].property).toBe('policy');
});

================================================
FILE: tests/parseWarnings.test.ts
================================================
import { parseWarnings } from '../src/ts/parse';

test('parse warnings - single warning', function() {
    const warnings = parseWarnings('Warning: resource_name: <warning detail>');

    expect(warnings).toHaveLength(1);
    expect(warnings[0].id.name).toBe('resource_name:');
    expect(warnings[0].detail).toBe(' <warning detail>');
});

test('parse warnings - multiple warning', function() {
    const warnings = parseWarnings('Warning: r1: w1\nWarning: r2: w2\nWarning: r3: w3');

    expect(warnings).toHaveLength(3);

    expect(warnings[0].id.name).toBe('r1:');
    expect(warnings[0].detail).toBe(' w1');

    expect(warnings[1].id.name).toBe('r2:');
    expect(warnings[1].detail).toBe(' w2');

    expect(warnings[2].id.name).toBe('r3:');
    expect(warnings[2].detail).toBe(' w3');
});

test('parse warnings - no warnings', function() {
    const warnings = parseWarnings('Here are some things that are NOT warnings');

    expect(warnings).toHaveLength(0);
});

================================================
FILE: tsconfig.json
================================================
{
    "compilerOptions": {
        "outDir": "./dist/",
        "module": "es6",
        "target": "es6"
    }
}

================================================
FILE: webpack.config.js
================================================
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/ts/prettyplan.ts',
    devServer: {
        contentBase: './dist'
    },
    plugins: [
        new HtmlWebpackPlugin({ template: 'src/index.html' }),
        new CopyWebpackPlugin(['src/style.css'])
    ],
    output: {
        filename: 'app.js',
        path: path.resolve(__dirname, 'dist')
    },
    resolve: { extensions: ['.ts', '.js'] },
    module: {
        rules: [
            {
                test: /\.ts$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    }
};
Download .txt
gitextract_054_3svt/

├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── jest.config.js
├── package.json
├── src/
│   ├── index.html
│   ├── style.css
│   └── ts/
│       ├── parse.ts
│       ├── prettyplan.ts
│       ├── releases.ts
│       ├── render.ts
│       └── ui.ts
├── tests/
│   ├── extractChangeSummary.test.ts
│   ├── extractInvididualChanges.test.ts
│   ├── parseChangeSymbol.test.ts
│   ├── parseId.test.ts
│   ├── parseNewAndOldValueDiffs.test.ts
│   ├── parseSingleValueDiffs.test.ts
│   └── parseWarnings.test.ts
├── tsconfig.json
└── webpack.config.js
Download .txt
SYMBOL INDEX (39 symbols across 4 files)

FILE: src/ts/parse.ts
  type ResourceId (line 1) | interface ResourceId {
  type Warning (line 6) | interface Warning {
  type ChangeType (line 10) | enum ChangeType {
  type Diff (line 18) | interface Diff {
  type Action (line 24) | interface Action {
  type Plan (line 29) | interface Plan {
  function parse (line 34) | function parse(terraformPlan: string): Plan {
  function parseWarnings (line 48) | function parseWarnings(terraformPlan: string): Warning[] {
  function extractChangeSummary (line 63) | function extractChangeSummary(terraformPlan: string): string {
  function extractIndividualChanges (line 71) | function extractIndividualChanges(changeSummary: string): string[] {
  function parseChange (line 85) | function parseChange(change: string): Action {
  function parseId (line 115) | function parseId(resourceId: string): ResourceId {
  function parseChangeSymbol (line 124) | function parseChangeSymbol(changeTypeSymbol): ChangeType {
  function parseSingleValueDiffs (line 139) | function parseSingleValueDiffs(change): Diff[] {
  function parseNewAndOldValueDiffs (line 157) | function parseNewAndOldValueDiffs(change): Diff[] {

FILE: src/ts/releases.ts
  type Release (line 1) | interface Release {
  function getCurrentVersion (line 6) | function getCurrentVersion(): string {
  function getLastUsedVersion (line 10) | function getLastUsedVersion(): string {
  function updateLastUsedVersion (line 14) | function updateLastUsedVersion(): void {
  function getReleases (line 18) | function getReleases(): Release[] {

FILE: src/ts/render.ts
  function clearExistingOutput (line 5) | function clearExistingOutput(): void {
  function hideParsingErrorMessage (line 11) | function hideParsingErrorMessage(): void {
  function displayParsingErrorMessage (line 15) | function displayParsingErrorMessage(): void {
  function unHidePlan (line 19) | function unHidePlan(): void {
  function showReleaseNotification (line 23) | function showReleaseNotification(version: string): void {
  function hideReleaseNotification (line 29) | function hideReleaseNotification(): void {
  function showReleaseNotes (line 33) | function showReleaseNotes(): void {
  function render (line 37) | function render(plan: Plan): void {
  function prettify (line 133) | function prettify(value: string): string {
  function prettifyJson (line 153) | function prettifyJson(maybeJson: string): string {

FILE: src/ts/ui.ts
  function accordion (line 1) | function accordion(element: Element): void {
  function toggleClass (line 8) | function toggleClass(element: Element, className: string): void {
  function addClass (line 17) | function addClass(element: Element, className: string): void {
  function removeClass (line 21) | function removeClass(element: Element, className: string): void {
  function expandAll (line 25) | function expandAll(): void {
  function collapseAll (line 36) | function collapseAll(): void {
  function removeChildren (line 47) | function removeChildren(element: Element): void {
  function createModalContainer (line 53) | function createModalContainer(): HTMLElement {
  function closeModal (line 62) | function closeModal(): void {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (51K chars).
[
  {
    "path": ".gitignore",
    "chars": 47,
    "preview": "node_modules/\ndist/\npackage-lock.json\nyarn.lock"
  },
  {
    "path": ".travis.yml",
    "chars": 234,
    "preview": "language: node_js\n\n# Tells Travis to use the latest Node version \n# It defaults to an old one otherwise\nnode_js: node\n\n#"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2018 Chris Lewis\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 2163,
    "preview": "# prettyplan [![Build Status](https://travis-ci.com/chrislewisdev/prettyplan.svg?branch=master)](https://travis-ci.com/c"
  },
  {
    "path": "jest.config.js",
    "chars": 173,
    "preview": "module.exports = {\n    \"roots\": [\"tests\"],\n    \"transform\": {\n        \"\\\\.ts$\": \"ts-jest\"\n    },\n    \"testRegex\": \"\\\\.te"
  },
  {
    "path": "package.json",
    "chars": 690,
    "preview": "{\n  \"name\": \"prettyplan\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Formatting tool for terraform plan output\",\n  \"author\""
  },
  {
    "path": "src/index.html",
    "chars": 6797,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta charset=\"utf-8\" />\n    <title>prettyplan</title>\n    <meta name=\"viewport\" cont"
  },
  {
    "path": "src/style.css",
    "chars": 5473,
    "preview": "body {\n    font-family: Arial, Helvetica, sans-serif;\n    text-rendering: optimizeLegibility;\n    background: #ECF7FE;\n "
  },
  {
    "path": "src/ts/parse.ts",
    "chars": 5012,
    "preview": "export interface ResourceId {\n    name: string;\n    type: string;\n    prefixes: string[];\n}\nexport interface Warning {\n "
  },
  {
    "path": "src/ts/prettyplan.ts",
    "chars": 1235,
    "preview": "import { getCurrentVersion, getLastUsedVersion, updateLastUsedVersion } from './releases';\nimport { expandAll, collapseA"
  },
  {
    "path": "src/ts/releases.ts",
    "chars": 1964,
    "preview": "export interface Release {\n    version: string;\n    notes: string[];\n}\n\nexport function getCurrentVersion(): string {\n  "
  },
  {
    "path": "src/ts/render.ts",
    "chars": 5272,
    "preview": "import { Plan, Action, Diff, Warning, ResourceId } from './parse'\nimport { removeChildren, addClass, removeClass, create"
  },
  {
    "path": "src/ts/ui.ts",
    "chars": 2015,
    "preview": "export function accordion(element: Element): void {\n    const changes = element.parentElement.getElementsByClassName('ch"
  },
  {
    "path": "tests/extractChangeSummary.test.ts",
    "chars": 775,
    "preview": "import { extractChangeSummary } from '../src/ts/parse'\n\ntest('extract change summary - single line', function() {\n    co"
  },
  {
    "path": "tests/extractInvididualChanges.test.ts",
    "chars": 2237,
    "preview": "import { extractIndividualChanges } from '../src/ts/parse';\n\ntest('extract individual changes - with plan summary at end"
  },
  {
    "path": "tests/parseChangeSymbol.test.ts",
    "chars": 417,
    "preview": "import { parseChangeSymbol } from '../src/ts/parse';\n\ntest('parse change symbol', function() {\n    expect(parseChangeSym"
  },
  {
    "path": "tests/parseId.test.ts",
    "chars": 726,
    "preview": "import { parseId } from '../src/ts/parse'\n\ntest('parse id - no prefixes', function() {\n    const id = parseId('aws_route"
  },
  {
    "path": "tests/parseNewAndOldValueDiffs.test.ts",
    "chars": 5506,
    "preview": "import { parseNewAndOldValueDiffs } from '../src/ts/parse';\n\ntest('new and old value diffs - quote formatting', function"
  },
  {
    "path": "tests/parseSingleValueDiffs.test.ts",
    "chars": 3028,
    "preview": "import { parseSingleValueDiffs } from '../src/ts/parse';\n\ntest('single value diffs - quote formatting', function() {\n   "
  },
  {
    "path": "tests/parseWarnings.test.ts",
    "chars": 973,
    "preview": "import { parseWarnings } from '../src/ts/parse';\n\ntest('parse warnings - single warning', function() {\n    const warning"
  },
  {
    "path": "tsconfig.json",
    "chars": 112,
    "preview": "{\n    \"compilerOptions\": {\n        \"outDir\": \"./dist/\",\n        \"module\": \"es6\",\n        \"target\": \"es6\"\n    }\n}"
  },
  {
    "path": "webpack.config.js",
    "chars": 738,
    "preview": "const path = require('path');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst CopyWebpackPlugin = requir"
  }
]

About this extraction

This page contains the full source code of the chrislewisdev/prettyplan GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (45.6 KB), approximately 12.7k tokens, and a symbol index with 39 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!