fix(pwa): avoid CSP-blocked stylesheet deferral

This commit is contained in:
4gray committed 2026-05-24 14:59:08 +03:00
1 parent 697eab6e73
commit bb4e386c6c
6 files changed
+113 -5

No files matched your search

+15 -4
View File
@@ -2,8 +2,19 @@ import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
const isStaticPwaE2E = process.env['IPTVNATOR_E2E_STATIC_PWA'] === '1';
const staticPwaPort = process.env['IPTVNATOR_E2E_STATIC_PORT'] ?? '4300';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
const baseURL =
process.env['BASE_URL'] ||
(isStaticPwaE2E
? `http://localhost:${staticPwaPort}`
: 'http://localhost:4200');
const webServerCommand =
isStaticPwaE2E
? `pnpm nx run web:serve-static --port=${staticPwaPort}`
: 'pnpm nx run web:serve';
const reuseExistingWebServer = isStaticPwaE2E ? false : !process.env['CI'];
/**
* Read environment variables from file.
@@ -29,9 +40,9 @@ export default defineConfig({
*/
webServer: [
{
command: 'pnpm nx run web:serve',
url: 'http://localhost:4200',
reuseExistingServer: !process.env['CI'],
command: webServerCommand,
url: baseURL,
reuseExistingServer: reuseExistingWebServer,
cwd: workspaceRoot,
},
{
+69
View File
@@ -0,0 +1,69 @@
import { expect, test } from '@playwright/test';
test('@pwa-static PWA build applies the full stylesheet under CSP', async ({
page,
}) => {
const cspConsoleErrors: string[] = [];
page.on('console', (message) => {
if (
message.type() === 'error' &&
/content security policy|inline event handler|onload/i.test(
message.text()
)
) {
cspConsoleErrors.push(message.text());
}
});
await page.goto('/');
const stylesheetLinks = page.locator('link[rel="stylesheet"]');
await expect(stylesheetLinks.first()).toBeAttached();
const stylesheetMedia = await stylesheetLinks.evaluateAll((links) =>
links.map((link) => ({
href: link.getAttribute('href'),
media: link.getAttribute('media'),
}))
);
const appStylesheet = stylesheetMedia.find((link) =>
/^styles-.*\.css$/.test(link.href ?? '')
);
expect(appStylesheet).toBeDefined();
expect(appStylesheet?.media).not.toBe('print');
const materialIcon = page.locator('mat-icon.material-icons').first();
await expect(materialIcon).toBeVisible();
await expect
.poll(() =>
materialIcon.evaluate((icon) =>
getComputedStyle(icon).fontFamily.toLowerCase()
)
)
.toContain('material icons');
await expect
.poll(() =>
page.evaluate(() =>
document.fonts.check('24px "Material Icons"')
)
)
.toBe(true);
const ngswResponse = await page.request.get('/ngsw.json');
await expect(ngswResponse).toBeOK();
const ngswManifest = (await ngswResponse.json()) as {
assetGroups?: Array<{ urls?: string[] }>;
};
const cachedUrls = ngswManifest.assetGroups?.flatMap(
(assetGroup) => assetGroup.urls ?? []
);
expect(cachedUrls).toEqual(
expect.arrayContaining([expect.stringMatching(/^(?:\.\/|\/)media\//)])
);
expect(cspConsoleErrors).toEqual([]);
});
+18 -1
View File
@@ -66,6 +66,15 @@
"maximumError": "15kb"
}
],
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false,
"removeSpecialComments": true
},
"fonts": true
},
"outputHashing": "all",
"fileReplacements": [
{
@@ -89,7 +98,15 @@
}
],
"outputHashing": "all",
"optimization": true,
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false,
"removeSpecialComments": true
},
"fonts": true
},
"extractLicenses": true,
"sourceMap": false,
"fileReplacements": [
+6
View File
@@ -47,6 +47,12 @@ sources through `media-src` and `connect-src` for `http:`, `https:`, `blob:`,
and `data:`. The policy keeps `script-src` self-hosted and currently keeps
`unsafe-inline` for existing inline styles.
Angular production builds must not rely on inline event handlers for stylesheet
activation. Keep `web:build:production` and `web:build:pwa` configured without
critical CSS stylesheet deferral (`optimization.styles.inlineCritical: false`)
unless the CSP is intentionally changed and runtime-validated in both Electron
and the self-hosted PWA.
Before tightening either value, validate both Electron development startup and
the PWA/electron build configurations. Playback-heavy changes should also check
that HLS, MPEG-TS, thumbnails, and local file playback are still allowed by the
+4
View File
@@ -40,6 +40,10 @@ The build must emit these files in `dist/apps/web`:
- `safety-worker.js`
- `worker-basic.min.js`
Angular also emits hashed font and media assets under `dist/apps/web/media/`.
Keep `/media/**` in `ngsw-config.json` so the PWA service worker can cache
bundled fonts, including Material Icons.
`web:serve-static` serves `dist/apps/web` and builds with `web:build:pwa`, so it
exercises the same output layout as Docker. If Nx daemon state returns stale
service worker outputs while changing build options, run:
+1
View File
@@ -23,6 +23,7 @@
"files": [
"/assets/**",
"!/assets/app-config.js",
"/media/**",
"/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)"
]
}