Skip to content

Commit

Permalink
feat: add primer preview (#205)
Browse files Browse the repository at this point in the history
* finish fixing localPreview

* add more tests and docs for primer preview

* improve primer http test

* chore(deps): update dependency husky to v4.2.5 (#151)

Co-authored-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: Michael Clayton <mclayton@redhat.com>

* fix(deps): update dependency inquirer to v8 (#180)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* chore(deps): update dependency jasmine-spec-reporter to v7 (#190)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* fix(deps): update dependency http-proxy to v1.18.1 [security] (#178)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* chore(deps): update dependency doctoc to v2 (#175)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* chore(deps): update dependency prettier to v2.3.1 (#172)

Co-authored-by: Renovate Bot <bot@renovateapp.com>

* chore(deps): update dependency braces to 2.3.1 [security]

* fix: print correct target url for remote requests

* finish fixing localPreview

* add more tests and docs for primer preview

* update package-lock

* fix package-lock registry references

* update browser-sync to 2.27.4

* update package-lock

* add legacy flag to chrome caching

* remove 'request' and downgrade browser-sync

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Renovate Bot <bot@renovateapp.com>
  • Loading branch information
3 people committed Jul 26, 2021
1 parent 5c17e73 commit 2c07cce
Show file tree
Hide file tree
Showing 26 changed files with 2,378 additions and 952 deletions.
59 changes: 45 additions & 14 deletions RH.md
Expand Up @@ -16,37 +16,45 @@ Then, answer a few questions to create a `spandx.config.js`.
npx spandx init cp
```

If you need to, add local DNS entries for `*.foo.redhat.com`. If you aren't sure if you nee this, it's safe to go ahead and run it. Note: you'll probably need to `sudo` this command, but if you aren't comfortable doing that, run it without sudo and it will print out what you need to paste into `/etc/hosts`.
If you need to, add local DNS entries for `*.foo.redhat.com`. If you aren't sure if you nee this, it's safe to go ahead and run it. Note: you'll probably need to `sudo` this command, but if you aren't comfortable doing that, run it without sudo and it will print out what you need to paste into `/etc/hosts`.

```
npx spandx init cp addhosts
```


## Portal Chrome settings

This section describes the Portal Chrome-specific settings in more detail. The `spandx init cp` command sets good defaults for Customer Portal work, but if you need to customize the settings, read on.
This section describes the Portal Chrome-specific settings in more detail. The `spandx init cp` command sets good defaults for Customer Portal work, but if you need to customize the settings, read on.

Portal Chrome settings live in your spandx config file under the `portalChrome` property.

### Resolving SPA comments, such as `<!-- SPA_HEAD -->`

spandx can replace these comments with their corresponding Portal Chrome parts:

Portal Chrome settings live in your spandx config file under the `portalChrome` property. It currently only supports one property, `resolveSPAComments`, but more settings may arise in the future.
- `<!-- SPA_HEAD -->` &larr; `/services/chrome/head`
- `<!-- SPA_HEADER -->` &larr; `/services/chrome/header`
- `<!-- SPA_FOOTER -->` &larr; `/services/chrome/footer`

This can be enabled in your spandx.config.js file with the following setting.

```js
module.exports = {
portalChrome: {
resolveSPAComments: true
}
resolveSPAComments: true,
},
};
```

If `resolveSPAComments` is true, spandx will inject Portal Chrome into any `text/html` request that passes through. It looks for the following placeholder comments and will replace them with the corresponding chrome part.

If `resolveSPAComments` is true, spandx will inject Portal Chrome into any `text/html` request that passes through. It looks for the following placeholder comments and will replace them with the corresponding chrome part.

comment | location | description
---|---|---
`<!-- SPA_HEAD -->` | within your `<head>` tag | will be replaced with the contents of `/services/chrome/head`
`<!-- SPA_HEADER -->` | just after `<body>` | will be replaced with the contents of `/services/chrome/header`
`<!-- SPA_FOOTER -->` | just before `</body>` | will be replaced with the contents of `/services/chrome/footer`
| comment | location | description |
| --------------------- | ------------------------ | --------------------------------------------------------------- |
| `<!-- SPA_HEAD -->` | within your `<head>` tag | will be replaced with the contents of `/services/chrome/head` |
| `<!-- SPA_HEADER -->` | just after `<body>` | will be replaced with the contents of `/services/chrome/header` |
| `<!-- SPA_FOOTER -->` | just before `</body>` | will be replaced with the contents of `/services/chrome/footer` |

This setting involves making requests to `/services/chrome/*`, which brings up the question of which host to fetch the chroming from. To make this as easy as possible, this feature taps into your existing spandx routes. In short, if you can hit `/services/chrome/head` from your spandx host, you'll be fine.
This setting involves making requests to `/services/chrome/*`, which brings up the question of which host to fetch the chroming from. To make this as easy as possible, this feature taps into your existing spandx routes. In short, if you can hit `/services/chrome/head` from your spandx host, you'll be fine.

For example, if you're routing `/app` to your local application and everything else to the Customer Portal, spandx will be able to resolve `/services/chrome/*` just fine.

Expand All @@ -57,4 +65,27 @@ routes: {
}
```

## Primer settings

Primer is an upcoming re-imagining of Portal Chrome.

### Preview Primer head/header/footer

This feature allows grafting HTML template changes into an existing page, even on a remote server, effectively previewing what the changes will look like when they are deployed to that server.

With `primer.preview` set to `true`, spandx detects the head, header, and footer (the "parts") in Customer Portal pages you visit and replaces them with fresh Primer parts fetched from `/services/primer`. You can control the routing of this request in your `routes` object.
```js
module.exports = {
primer: {
preview: true,
},
};
```

Primer's head, header, and footer parts are wrapped with comments which are used as tokens to identify the location of each part. These tokens facilitate the removal of old parts and injection of fresh parts.

The wrapper comment tokens look like this:

- `<!-- CP_PRIMER_HEAD --> ... <!-- /CP_PRIMER_HEAD -->`
- `<!-- CP_PRIMER_HEADER --> ... <!-- /CP_PRIMER_HEADER -->`
- `<!-- CP_PRIMER_FOOTER --> ... <!-- /CP_PRIMER_FOOTER -->`
59 changes: 30 additions & 29 deletions app/chromeCache.js
@@ -1,4 +1,4 @@
const request = require("request");
const got = require("got");

const DEFAULT_CHROME_HOST = "https://access.redhat.com";
const DEFAULT_CHROME_PATH = "/services/chrome/";
Expand All @@ -9,53 +9,54 @@ let chromePath;

async function getParts({
host = DEFAULT_CHROME_HOST,
path = DEFAULT_CHROME_PATH
path = DEFAULT_CHROME_PATH,
useCached = true,
legacy = false,
} = {}) {
if (cache[host + path]) {
if (useCached && cache[host + path]) {
return cache[host + path];
}

const headReq = fetchChromePart({ host, part: "head", path });
const headerReq = fetchChromePart({ host, part: "header", path });
const footerReq = fetchChromePart({ host, part: "footer", path });
const headReq = fetchChromePart({ host, part: "head", path, legacy });
const headerReq = fetchChromePart({ host, part: "header", path, legacy });
const footerReq = fetchChromePart({ host, part: "footer", path, legacy });

const head = await headReq;
const header = await headerReq;
const footer = await footerReq;

cache[host + path] = { head, header, footer };
const parts = { head, header, footer };

chromeHost = host;
chromePath = path;
if (useCached) {
cache[host + path] = parts;
chromeHost = host;
chromePath = path;
}

return cache[host + path];
return parts;
}

function fetchChromePart({
async function fetchChromePart({
host = DEFAULT_CHROME_HOST,
path = DEFAULT_CHROME_PATH,
part
part,
legacy = false,
} = {}) {
return new Promise((resolve, reject) => {
const url = `${host}${path}${part}?legacy=false`;
console.log(`fetching chrome from ${url}`);
request(
{
url,
strictSSL: false
},
function(err, response, body) {
if (err) {
reject(err);
}
resolve(body);
}
);
});
const url = `${host}${path}${part}${legacy ? "?legacy=false" : ""}`;
console.log(`fetching chrome from ${url}`);

try {
const res = await got(url);
return res.body;
} catch (e) {
console.error("GOT BAD HAPPEN");
console.error(e);
}
}

module.exports = {
chromeHost,
chromePath,
getParts
getParts,
fetchChromePart,
};
78 changes: 72 additions & 6 deletions app/chromeMiddleware.js
@@ -1,14 +1,15 @@
const chromeCache = require("./chromeCache");
const { createTokenSlicer } = require("token-slice");

function SPACommentResolver(conf) {
return async function SPACommentResolverMiddleware(data, req, res) {
const isHTML = (res.getHeader("content-type") || "").includes("html");
if (isHTML) {
const origin = req.headers["x-spandx-origin"];
const host = `http${conf.bs.https ? "s" : ""}://${origin}:${
conf.port
}`;
const chromeParts = await chromeCache.getParts({ host });
const host = req.headers["x-spandx-origin"];
const chromeParts = await chromeCache.getParts({
host,
legacy: true,
});
return data
.toString()
.replace(/<!--\s+SPA_HEAD\s+-->/, chromeParts.head)
Expand All @@ -20,4 +21,69 @@ function SPACommentResolver(conf) {
};
}

module.exports = { SPACommentResolver };
function chromeSwapper(conf) {
return async function ChromeSwapperMiddleware(data, req, res) {
const isHTML = (res.getHeader("content-type") || "").includes("html");
const isPrimerAlready = req.url.startsWith("/services/primer");
if (isHTML && !isPrimerAlready) {
const origin = req.headers["x-spandx-origin"];
console.log({ origin });
const chromeParts = await chromeCache.getParts({
host: origin,
path: "/services/primer/",
useCached: false,
});

return data
.toString()
.replace(
/<!--\s*CP_PRIMER_HEAD\s*-->.*<!--\s*\/CP_PRIMER_HEAD\s*-->/s,
`${chromeParts.head}`
)
.replace(
/<!--\s*CP_PRIMER_HEADER\s*-->.*<!--\s*\/CP_PRIMER_HEADER\s*-->/s,
`${chromeParts.header}`
)
.replace(
/<!--\s*CP_PRIMER_FOOTER\s*-->.*<!--\s*\/CP_PRIMER_FOOTER\s*-->/s,
`${chromeParts.footer}`
);

// if any tokens were found, swap them
// if (tokens.result.length) {
// tokens.result.sort(
// (a, b) => a.outer.startIndex - b.outer.startIndex
// );

// // TODO make this work when only 1 or 2 of the token pairs were found
// const ret =
// // body pre-head
// body.slice(0, tokens.result[0].outer.startIndex) +
// // head
// chromeParts.head +
// // body post-head
// body.slice(
// tokens.result[0].outer.endIndex,
// tokens.result[1].outer.startIndex
// ) +
// // header
// chromeParts.header +
// // body post-header
// body.slice(
// tokens.result[1].outer.endIndex,
// tokens.result[2].outer.startIndex
// ) +
// // footer
// chromeParts.footer +
// // body post-footer
// body.slice(tokens.result[2].outer.endIndex, body.length);

// return "HELLO";
// }
}

return data;
};
}

module.exports = { SPACommentResolver, chromeSwapper };
11 changes: 5 additions & 6 deletions app/config.js
Expand Up @@ -57,6 +57,7 @@ async function create(incomingConfig = {}, configDir = __dirname) {

// choose a port for the internal proxy, avoiding the external port just chosen
incomingConfig.internalPort = await porty.find({
min: incomingConfig.port,
avoids: [incomingConfig.port]
});

Expand All @@ -82,14 +83,12 @@ async function fromFile(filePath = `${process.cwd()}/spandx.config.js`) {
} catch (e) {
if (e.toString().indexOf("Error: Cannot find module") === 0) {
throw new ConfigOpenError(
`Tried to open spandx config file ${c.fg.l.cyan}${filePath}${
c.end
`Tried to open spandx config file ${c.fg.l.cyan}${filePath}${c.end
} but couldn't find it, or couldn't access it.`
);
} else {
throw new ConfigProcessError(
`Tried to process spandx config file ${c.fg.l.cyan}${filePath}${
c.end
`Tried to process spandx config file ${c.fg.l.cyan}${filePath}${c.end
} ` + `but. Got an exception loading the config: ${e}`
);
}
Expand Down Expand Up @@ -185,8 +184,8 @@ function processConf(conf, configDir = __dirname) {
});
} else {
// single host mode
conf.host = { default: conf.host };
webRouteHosts.forEach(r => (r.host = { default: r.host })); // convert host string to object
conf.host = {default: conf.host};
webRouteHosts.forEach(r => (r.host = {default: r.host})); // convert host string to object
}

// build a list of file paths to watch for auto-reload, by combining the
Expand Down
11 changes: 8 additions & 3 deletions app/router.js
Expand Up @@ -7,7 +7,7 @@ const { flow, includes, get } = require("lodash/fp");
const finalhandler = require("finalhandler");
const serveStatic = require("serve-static");
const resolveHome = require("./resolveHome");
const HttpsProxyAgent = require("https-proxy-agent");
const ProxyAgent = require("proxy-agent");
const priv = {};

priv.tryPlugin = (plugin, req, res, target, cb) => {
Expand Down Expand Up @@ -38,7 +38,7 @@ priv.doProxy = (proxy, req, res, target, confProxy = null) => {
// pattern provided in the proxy.pattern property,
// add a new HttpsProxyAgent
if (regex.test(target)) {
options.agent = new HttpsProxyAgent(confProxy.host);
options.agent = new ProxyAgent(confProxy.host);
}
}

Expand Down Expand Up @@ -108,6 +108,7 @@ module.exports = (conf, proxy) => {
new RegExp(`^${routeKey}/?`),
"/"
); // remove route path (will be replaced with disk path)

const absoluteFilePath = path.resolve(
conf.configDir,
resolveHome(route),
Expand Down Expand Up @@ -140,7 +141,11 @@ module.exports = (conf, proxy) => {

if (conf.verbose) {
console.log(
`GET ${c.fg.l.green}${req.url}${c.end} from ${c.fg.l.blue}${target}${c.end}`
`GET ${c.fg.l.green}${req.url}${c.end} from ${
c.fg.l.blue
}${target.replace(new RegExp(`${req.url}$`), "")}${c.end}${
c.fg.l.green
}${req.url}${c.end}`
);
}

Expand Down

0 comments on commit 2c07cce

Please sign in to comment.