Skip to content

Commit

Permalink
Merge pull request #785 from adobecom/ewmobile
Browse files Browse the repository at this point in the history
MWPW-157573 Optimize EdgeWorker for Mobile Widget
  • Loading branch information
Blainegunn authored Sep 12, 2024
2 parents c9db7d4 + c502e91 commit 76ebf4f
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 59 deletions.
120 changes: 69 additions & 51 deletions edgeworkers/Acrobat_DC_web_prod/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ export async function responseProvider(request) {

// Make preliminary pass through the content to capture version metadata
const firstPassRewriter = new HtmlRewritingStream();
let version, widgetVersion;
let version, widgetVersion, mobileWidget;
const prefix = isProd ? '' : 'stg-';
firstPassRewriter.onElement(`meta[name="${prefix}dc-widget-version"]`, el => {
widgetVersion = el.getAttribute('content');
});
firstPassRewriter.onElement(`meta[name="${prefix}dc-generate-cache-version"]`, el => {
version = el.getAttribute('content');
});

firstPassRewriter.onElement(`meta[name="mobile-widget"]`, el => {
mobileWidget = el.getAttribute('content');
});
const nullWriter = new WritableStream({
write() {},
close() {},
Expand Down Expand Up @@ -74,7 +76,7 @@ export async function responseProvider(request) {
delete responseHeaders[prop];
}

return [responseStream, responseHeaders, version, widgetVersion];
return [responseStream, responseHeaders, version, widgetVersion, mobileWidget];
};

const fetchResource = async path => {
Expand All @@ -86,49 +88,46 @@ export async function responseProvider(request) {
};

const fetchFrictionlessPageAndInlineSnippet = async () => {
const [responseStream, responseHeaders, version, widgetVersion] = await fetchFrictionlessPage();
const [responseStream, responseHeaders, version, widgetVersion, mobileWidget] = await fetchFrictionlessPage();

if (!verb || !locale || !version || !widgetVersion) {
throw new Error('Missing metadata');
}

const snippet =
await fetchResource(`/dc/dc-generate-cache/dc-hosted-${version}/${verb}-${locale}.html`);
const snippetHead = snippet.substring(snippet.indexOf('<head>')+6, snippet.indexOf('</head>'));
const snippetBody = snippet.substring(snippet.indexOf('<body>')+6, snippet.indexOf('</body>'));

rewriter.onElement('head', el => {
el.append(snippetHead);
});
rewriter.onElement('div.dc-converter-widget', el => {
el.append(`<section id="edge-snippet">${snippetBody}</section>`);
});

if (!(mobileWidget && request.device.isMobile)) {
const snippet =
await fetchResource(`/dc/dc-generate-cache/dc-hosted-${version}/${verb}-${locale}.html`);
const snippetHead = snippet.substring(snippet.indexOf('<head>')+6, snippet.indexOf('</head>'));
const snippetBody = snippet.substring(snippet.indexOf('<body>')+6, snippet.indexOf('</body>'));

rewriter.onElement('head', el => {
el.append(snippetHead);
});
rewriter.onElement('div.dc-converter-widget', el => {
el.append(`<section id="edge-snippet">${snippetBody}</section>`);
});
}
const dcCoreVersion = widgetVersion.split("_")[0];

return [responseStream, responseHeaders, dcCoreVersion];
return [responseStream, responseHeaders, dcCoreVersion, mobileWidget];
};

const scriptHashes = [];

const fetchAndInlineScripts = async () => {
const [
scripts,
dcConverter,
] = await Promise.all([
fetchResource('/acrobat/scripts/scripts.js'),
fetchResource('/acrobat/blocks/dc-converter-widget/dc-converter-widget.js'),
])

const inlineScripts = async (mobileWidget, scripts, dcConverter) => {
// Inline dc-converter-widget.js and scripts.js. Remove modular definition and import.
// Change relative paths to absolute. Remove JS-driven CSP in favor of HTTP header.
const inlineScript = dcConverter
.replace('export default', 'const dcConverter = ')
.replace('import(\'../../scripts/frictionless.js\')', 'import(\'/acrobat/scripts/frictionless.js\')')
+ scripts
.replace('const { default: dcConverter } = await import(`../blocks/${blockName}/${blockName}.js`);', '')
let inlineScript = scripts
.replace('await import(\'./contentSecurityPolicy/csp.js\')', '{default:()=>{}}')
.replace('await import(\'./dcLana.js\')', 'await import(\'/acrobat/scripts/dcLana.js\')')
.replace('await import(\'./dcLana.js\')', 'await import(\'/acrobat/scripts/dcLana.js\')');

if (!(mobileWidget && request.device.isMobile)) {
inlineScript = dcConverter
.replace('export default', 'const dcConverter = ')
.replace('import(\'../../scripts/frictionless.js\')', 'import(\'/acrobat/scripts/frictionless.js\')')
+ inlineScript
.replace('const { default: dcConverter } = await import(`../blocks/${blockName}/${blockName}.js`);', '')
}

// Generate hash of inlined script and add to our CSP policy
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(inlineScript));
Expand All @@ -146,15 +145,7 @@ export async function responseProvider(request) {
});
};

const fetchAndInlineStyles = async () => {
const [
dcStyles,
miloStyles
] = await Promise.all([
fetchResource('/acrobat/styles/styles.css'),
fetchResource('/libs/styles/styles.css'),
]);

const inlineStyles = (dcStyles, miloStyles) => {
rewriter.onElement('head', el => {
el.append(`<style id="inline-milo-styles">${miloStyles}</style>`)
el.append(`<style id="inline-dc-styles">${dcStyles}</style>`)
Expand All @@ -163,30 +154,47 @@ export async function responseProvider(request) {

try {
const [
[responseStream, responseHeaders, dcCoreVersion],
[responseStream, responseHeaders, dcCoreVersion, mobileWidget],
scripts,
dcConverter,
dcStyles,
miloStyles
] = await Promise.all([
fetchFrictionlessPageAndInlineSnippet(),
fetchAndInlineScripts(),
fetchAndInlineStyles(),
fetchResource('/acrobat/scripts/scripts.js'),
fetchResource('/acrobat/blocks/dc-converter-widget/dc-converter-widget.js'),
fetchResource('/acrobat/styles/styles.css'),
fetchResource('/libs/styles/styles.css'),
]);

await inlineScripts(mobileWidget, scripts, dcConverter);
inlineStyles(dcStyles, miloStyles);

const csp = contentSecurityPolicy(isProd, scriptHashes);
const acrobat = isProd ? 'https://acrobat.adobe.com' : 'https://stage.acrobat.adobe.com';
const pdfnow = isProd ? 'https://pdfnow.adobe.io' : 'https://pdfnow-stage.adobe.io';
const adobeid = isProd ? 'https://adobeid-na1.services.adobe.com' : 'https://adobeid-na1-stg1.services.adobe.com';
const headers = {
...responseHeaders,
'Content-Security-Policy': csp,
Link: [
`<${acrobat}>;rel="preconnect"`,

let headerLink = [
`<${adobeid}>;rel="preconnect"`,
`<${pdfnow}>;rel="preconnect"`,
'<https://assets.adobedtm.com>;rel="preconnect"',
'<https://use.typekit.net>;rel="preconnect"',
`</libs/deps/imslib.min.js>;rel="preload";as="script"`,
];
if (!(mobileWidget && request.device.isMobile)) {
headerLink = [...headerLink,
`<${acrobat}>;rel="preconnect"`,
`<${pdfnow}>;rel="preconnect"`,
`<${acrobat}/dc-core/${dcCoreVersion}/dc-core.js>;rel="preload";as="script"`,
`<${acrobat}/dc-core/${dcCoreVersion}/dc-core.css>;rel="preload";as="style"`,
].join()
];
}
headerLink = headerLink.join();

const headers = {
...responseHeaders,
'Content-Security-Policy': csp,
Link: headerLink
};

return createResponse(
Expand All @@ -198,3 +206,13 @@ export async function responseProvider(request) {
return createResponse(error.status ?? 500, {}, error.body ?? error.message);
}
}

export function onClientRequest (request) {
request.setVariable('PMUSER_DEVICETYPE', 'Desktop');
if (request.device.isMobile) {
request.setVariable('PMUSER_DEVICETYPE', 'Mobile');
} else if (request.device.isTablet) {
request.setVariable('PMUSER_DEVICETYPE', 'Tablet');
}
request.cacheKey.includeVariable('PMUSER_DEVICETYPE');
}
46 changes: 43 additions & 3 deletions test/edgeworkers/Acrobat_DC_web_prod/main.jest.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import Request from "request";
import { responseProvider as replaceResponseProvider } from "../../../edgeworkers/Acrobat_DC_web_prod/main.js";
import { onClientRequest } from "../../../edgeworkers/Acrobat_DC_web_prod/main.js";
import { createResponse } from "create-response";
import { httpRequest } from "http-request";
import { HttpResponsePdf } from "response-pdf";
Expand Down Expand Up @@ -68,7 +69,8 @@ describe("EdgeWorker that consumes an HTML document and rewrites it", () => {
'https://www.adobe.com/acrobat/blocks/dc-converter-widget/dc-converter-widget.js',
'https://www.adobe.com/acrobat/styles/styles.css',
'https://www.adobe.com/libs/styles/styles.css',
'https://www.adobe.com/dc/dc-generate-cache/dc-hosted-1.0/pdf-to-ppt-en-us.html']);
'https://www.adobe.com/dc/dc-generate-cache/dc-hosted-1.0/pdf-to-ppt-en-us.html'
]);
});
});

Expand All @@ -85,10 +87,30 @@ describe("EdgeWorker that consumes an HTML document and rewrites it", () => {
'https://www.adobe.com/acrobat/blocks/dc-converter-widget/dc-converter-widget.js',
'https://www.adobe.com/acrobat/styles/styles.css',
'https://www.adobe.com/libs/styles/styles.css',
'https://www.adobe.com/dc/dc-generate-cache/dc-hosted-1.0/pdf-to-ppt-ja-jp.html']);
'https://www.adobe.com/dc/dc-generate-cache/dc-hosted-1.0/pdf-to-ppt-ja-jp.html'
]);
});
});

it("responseProvider Mobile", async () => {
let requestMock = new Request({path: '/acrobat/online/pdf-to-ppt', device: 'Mobile'});

const responsePromise = replaceResponseProvider(requestMock);
responsePromise.then(response => {
expect(response.status).toEqual(200);
expect(response.headers['header-to-keep']).toEqual('keep');
expect(response.headers).not.toHaveProperty('accept-encoding');
expect(response.headers).not.toHaveProperty('vary');
expect(fetches).toEqual([
'https://www.adobe.com/acrobat/online/pdf-to-ppt.html',
'https://www.adobe.com/acrobat/scripts/scripts.js',
'https://www.adobe.com/acrobat/blocks/dc-converter-widget/dc-converter-widget.js',
'https://www.adobe.com/acrobat/styles/styles.css',
'https://www.adobe.com/libs/styles/styles.css'
]);
});
});

it("404 exception", async () => {
let requestMock = new Request({path: '/404/online/pdf-to-ppt'});

Expand Down Expand Up @@ -122,5 +144,23 @@ describe("EdgeWorker that consumes an HTML document and rewrites it", () => {
expect(response.status).toEqual(500);
expect(response.body).toContain('Missing metadata');
});
});
});

it("onClientReqest", async () => {
let requestMock = new Request({path: '/acrobat/online/pdf-to-ppt'});

onClientRequest(requestMock);
expect(requestMock.setVariable).toBeCalledWith('PMUSER_DEVICETYPE', 'Desktop');
expect(requestMock.cacheKey.includeVariable).toBeCalledWith('PMUSER_DEVICETYPE');

requestMock = new Request({path: '/acrobat/online/pdf-to-ppt', device: 'Mobile'});

onClientRequest(requestMock);
expect(requestMock.setVariable).toBeCalledWith('PMUSER_DEVICETYPE', 'Mobile');

requestMock = new Request({path: '/acrobat/online/pdf-to-ppt', device: 'Tablet'});

onClientRequest(requestMock);
expect(requestMock.setVariable).toBeCalledWith('PMUSER_DEVICETYPE', 'Tablet');
});
});
6 changes: 3 additions & 3 deletions test/edgeworkers/__mocks__/device.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const Device = jest.fn().mockImplementation(() => {
const Device = jest.fn().mockImplementation((device) => {
return {
brandName:"Chrome",
modelName:"90",
marketingName:"Chrome 90",
isWireless:false,
isTablet:false,
isTablet: device === 'Tablet',
os:"Mac OS X",
osVersion:"10.15",
mobileBrowser:"Chrome",
Expand All @@ -18,7 +18,7 @@ const Device = jest.fn().mockImplementation(() => {
hasFlashSupport:false,
acceptsThirdPartyCookie:true,
xhtmlSupportLevel:4,
isMobile:false
isMobile: device === 'Mobile'
};
});

Expand Down
4 changes: 2 additions & 2 deletions test/edgeworkers/__mocks__/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const mockJson = jest.fn();
export const mockText = jest.fn();
export const mockArrayBuffer = jest.fn();

const Request = jest.fn().mockImplementation(({path}) => {
const Request = jest.fn().mockImplementation(({path, device}) => {
return {
host: "www.adobe.com",
method: "GET",
Expand All @@ -26,7 +26,7 @@ const Request = jest.fn().mockImplementation(({path}) => {
query: "param1=value1&param2=value2",
url: `${path}?param1=value1&param2=value2`,
userLocation: new UserLocation(),
device: new Device(),
device: new Device(device),
cpCode: 1191398,
cacheKey: new CacheKey(),
respondWith: mockRespondWith,
Expand Down

0 comments on commit 76ebf4f

Please sign in to comment.