@web/rollup-plugin-html 0.0.0-canary-20230316175616

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +5 -0
  2. package/dist/RollupPluginHTMLOptions.d.ts +68 -0
  3. package/dist/RollupPluginHTMLOptions.d.ts.map +1 -0
  4. package/dist/RollupPluginHTMLOptions.js +3 -0
  5. package/dist/RollupPluginHTMLOptions.js.map +1 -0
  6. package/dist/assets/utils.d.ts +7 -0
  7. package/dist/assets/utils.d.ts.map +1 -0
  8. package/dist/assets/utils.js +138 -0
  9. package/dist/assets/utils.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +7 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/input/InputData.d.ts +16 -0
  15. package/dist/input/InputData.d.ts.map +1 -0
  16. package/dist/input/InputData.js +3 -0
  17. package/dist/input/InputData.js.map +1 -0
  18. package/dist/input/addRollupInput.d.ts +4 -0
  19. package/dist/input/addRollupInput.d.ts.map +1 -0
  20. package/dist/input/addRollupInput.js +36 -0
  21. package/dist/input/addRollupInput.js.map +1 -0
  22. package/dist/input/extract/extractAssets.d.ts +11 -0
  23. package/dist/input/extract/extractAssets.d.ts.map +1 -0
  24. package/dist/input/extract/extractAssets.js +37 -0
  25. package/dist/input/extract/extractAssets.js.map +1 -0
  26. package/dist/input/extract/extractModules.d.ts +13 -0
  27. package/dist/input/extract/extractModules.d.ts.map +1 -0
  28. package/dist/input/extract/extractModules.js +68 -0
  29. package/dist/input/extract/extractModules.js.map +1 -0
  30. package/dist/input/extract/extractModulesAndAssets.d.ts +14 -0
  31. package/dist/input/extract/extractModulesAndAssets.d.ts.map +1 -0
  32. package/dist/input/extract/extractModulesAndAssets.js +30 -0
  33. package/dist/input/extract/extractModulesAndAssets.js.map +1 -0
  34. package/dist/input/getInputData.d.ts +13 -0
  35. package/dist/input/getInputData.d.ts.map +1 -0
  36. package/dist/input/getInputData.js +94 -0
  37. package/dist/input/getInputData.js.map +1 -0
  38. package/dist/input/normalizeInputOptions.d.ts +4 -0
  39. package/dist/input/normalizeInputOptions.d.ts.map +1 -0
  40. package/dist/input/normalizeInputOptions.js +38 -0
  41. package/dist/input/normalizeInputOptions.js.map +1 -0
  42. package/dist/output/createHTMLOutput.d.ts +33 -0
  43. package/dist/output/createHTMLOutput.d.ts.map +1 -0
  44. package/dist/output/createHTMLOutput.js +39 -0
  45. package/dist/output/createHTMLOutput.js.map +1 -0
  46. package/dist/output/emitAssets.d.ts +12 -0
  47. package/dist/output/emitAssets.d.ts.map +1 -0
  48. package/dist/output/emitAssets.js +69 -0
  49. package/dist/output/emitAssets.js.map +1 -0
  50. package/dist/output/getEntrypointBundles.d.ts +18 -0
  51. package/dist/output/getEntrypointBundles.d.ts.map +1 -0
  52. package/dist/output/getEntrypointBundles.js +59 -0
  53. package/dist/output/getEntrypointBundles.js.map +1 -0
  54. package/dist/output/getOutputHTML.d.ts +18 -0
  55. package/dist/output/getOutputHTML.d.ts.map +1 -0
  56. package/dist/output/getOutputHTML.js +91 -0
  57. package/dist/output/getOutputHTML.js.map +1 -0
  58. package/dist/output/hashInlineScripts.d.ts +3 -0
  59. package/dist/output/hashInlineScripts.d.ts.map +1 -0
  60. package/dist/output/hashInlineScripts.js +131 -0
  61. package/dist/output/hashInlineScripts.js.map +1 -0
  62. package/dist/output/injectAbsoluteBaseUrl.d.ts +3 -0
  63. package/dist/output/injectAbsoluteBaseUrl.d.ts.map +1 -0
  64. package/dist/output/injectAbsoluteBaseUrl.js +37 -0
  65. package/dist/output/injectAbsoluteBaseUrl.js.map +1 -0
  66. package/dist/output/injectBundles.d.ts +5 -0
  67. package/dist/output/injectBundles.d.ts.map +1 -0
  68. package/dist/output/injectBundles.js +36 -0
  69. package/dist/output/injectBundles.js.map +1 -0
  70. package/dist/output/injectServiceWorkerRegistration.d.ts +9 -0
  71. package/dist/output/injectServiceWorkerRegistration.d.ts.map +1 -0
  72. package/dist/output/injectServiceWorkerRegistration.js +37 -0
  73. package/dist/output/injectServiceWorkerRegistration.js.map +1 -0
  74. package/dist/output/injectedUpdatedAssetPaths.d.ts +14 -0
  75. package/dist/output/injectedUpdatedAssetPaths.d.ts.map +1 -0
  76. package/dist/output/injectedUpdatedAssetPaths.js +60 -0
  77. package/dist/output/injectedUpdatedAssetPaths.js.map +1 -0
  78. package/dist/output/utils.d.ts +2 -0
  79. package/dist/output/utils.d.ts.map +1 -0
  80. package/dist/output/utils.js +12 -0
  81. package/dist/output/utils.js.map +1 -0
  82. package/dist/rollupPluginHTML.d.ts +13 -0
  83. package/dist/rollupPluginHTML.d.ts.map +1 -0
  84. package/dist/rollupPluginHTML.js +183 -0
  85. package/dist/rollupPluginHTML.js.map +1 -0
  86. package/dist/utils.d.ts +4 -0
  87. package/dist/utils.d.ts.map +1 -0
  88. package/dist/utils.js +10 -0
  89. package/dist/utils.js.map +1 -0
  90. package/index.mjs +6 -0
  91. package/package.json +55 -0
  92. package/src/RollupPluginHTMLOptions.ts +92 -0
  93. package/src/assets/utils.ts +145 -0
  94. package/src/index.ts +14 -0
  95. package/src/input/InputData.ts +16 -0
  96. package/src/input/addRollupInput.ts +55 -0
  97. package/src/input/extract/extractAssets.ts +53 -0
  98. package/src/input/extract/extractModules.ts +78 -0
  99. package/src/input/extract/extractModulesAndAssets.ts +34 -0
  100. package/src/input/getInputData.ts +120 -0
  101. package/src/input/normalizeInputOptions.ts +47 -0
  102. package/src/output/createHTMLOutput.ts +87 -0
  103. package/src/output/emitAssets.ts +80 -0
  104. package/src/output/getEntrypointBundles.ts +88 -0
  105. package/src/output/getOutputHTML.ts +115 -0
  106. package/src/output/hashInlineScripts.ts +153 -0
  107. package/src/output/injectAbsoluteBaseUrl.ts +41 -0
  108. package/src/output/injectBundles.ts +44 -0
  109. package/src/output/injectServiceWorkerRegistration.ts +48 -0
  110. package/src/output/injectedUpdatedAssetPaths.ts +89 -0
  111. package/src/output/utils.ts +5 -0
  112. package/src/rollupPluginHTML.ts +225 -0
  113. package/src/utils.ts +9 -0
@@ -0,0 +1,47 @@
1
+ import { InputOption } from 'rollup';
2
+ import { InputHTMLOptions, RollupPluginHTMLOptions } from '../RollupPluginHTMLOptions';
3
+ import { createError } from '../utils';
4
+
5
+ export function normalizeInputOptions(
6
+ pluginOptions: RollupPluginHTMLOptions,
7
+ rollupInput?: InputOption,
8
+ ): InputHTMLOptions[] {
9
+ if (pluginOptions.input == null) {
10
+ if (rollupInput == null) {
11
+ throw createError('Missing input option in rollup or in HTML plugin options.');
12
+ }
13
+
14
+ if (typeof rollupInput === 'string') {
15
+ return [{ path: rollupInput }];
16
+ }
17
+
18
+ if (Array.isArray(rollupInput)) {
19
+ return rollupInput.map(path => ({ path }));
20
+ }
21
+
22
+ if (typeof rollupInput === 'object') {
23
+ return Object.entries(rollupInput).map(([name, path]) => ({ name, path }));
24
+ }
25
+
26
+ throw createError('Unable to parse rollup input option');
27
+ }
28
+
29
+ if (Array.isArray(pluginOptions.input)) {
30
+ return pluginOptions.input.map(input => {
31
+ if (typeof input === 'string') {
32
+ return { path: input };
33
+ }
34
+ return input;
35
+ });
36
+ }
37
+
38
+ if (typeof pluginOptions.input === 'object') {
39
+ return [pluginOptions.input];
40
+ }
41
+
42
+ if (typeof pluginOptions.input === 'string') {
43
+ return [{ path: pluginOptions.input }];
44
+ }
45
+
46
+ throw createError('Unable to parse html plugin input option');
47
+ }
@@ -0,0 +1,87 @@
1
+ import { getEntrypointBundles } from './getEntrypointBundles';
2
+ import { getOutputHTML } from './getOutputHTML';
3
+ import { createError } from '../utils';
4
+ import {
5
+ GeneratedBundle,
6
+ RollupPluginHTMLOptions,
7
+ TransformHtmlFunction,
8
+ } from '../RollupPluginHTMLOptions';
9
+ import { EmittedFile } from 'rollup';
10
+ import { InputData } from '../input/InputData';
11
+ import { EmittedAssets } from './emitAssets';
12
+
13
+ export interface CreateHTMLAssetParams {
14
+ outputDir: string;
15
+ input: InputData;
16
+ emittedAssets: EmittedAssets;
17
+ generatedBundles: GeneratedBundle[];
18
+ externalTransformHtmlFns: TransformHtmlFunction[];
19
+ pluginOptions: RollupPluginHTMLOptions;
20
+ defaultInjectDisabled: boolean;
21
+ serviceWorkerPath: string;
22
+ injectServiceWorker: boolean;
23
+ absolutePathPrefix?: string;
24
+ strictCSPInlineScripts: boolean;
25
+ }
26
+
27
+ export async function createHTMLAsset(params: CreateHTMLAssetParams): Promise<EmittedFile> {
28
+ const {
29
+ outputDir,
30
+ input,
31
+ emittedAssets,
32
+ generatedBundles,
33
+ externalTransformHtmlFns,
34
+ pluginOptions,
35
+ defaultInjectDisabled,
36
+ serviceWorkerPath,
37
+ injectServiceWorker,
38
+ absolutePathPrefix,
39
+ strictCSPInlineScripts,
40
+ } = params;
41
+
42
+ if (generatedBundles.length === 0) {
43
+ throw createError('Cannot output HTML when no bundles have been generated');
44
+ }
45
+
46
+ const entrypointBundles = getEntrypointBundles({
47
+ pluginOptions,
48
+ generatedBundles,
49
+ inputModuleIds: input.moduleImports,
50
+ outputDir,
51
+ htmlFileName: input.name,
52
+ });
53
+
54
+ const outputHtml = await getOutputHTML({
55
+ pluginOptions,
56
+ entrypointBundles,
57
+ input,
58
+ outputDir,
59
+ emittedAssets,
60
+ externalTransformHtmlFns,
61
+ defaultInjectDisabled,
62
+ serviceWorkerPath,
63
+ injectServiceWorker,
64
+ absolutePathPrefix,
65
+ strictCSPInlineScripts,
66
+ });
67
+
68
+ return { fileName: input.name, name: input.name, source: outputHtml, type: 'asset' };
69
+ }
70
+
71
+ export interface CreateHTMLAssetsParams {
72
+ outputDir: string;
73
+ inputs: InputData[];
74
+ emittedAssets: EmittedAssets;
75
+ generatedBundles: GeneratedBundle[];
76
+ externalTransformHtmlFns: TransformHtmlFunction[];
77
+ pluginOptions: RollupPluginHTMLOptions;
78
+ defaultInjectDisabled: boolean;
79
+ serviceWorkerPath: string;
80
+ injectServiceWorker: boolean;
81
+ absolutePathPrefix?: string;
82
+ strictCSPInlineScripts: boolean;
83
+ }
84
+
85
+ export async function createHTMLOutput(params: CreateHTMLAssetsParams): Promise<EmittedFile[]> {
86
+ return Promise.all(params.inputs.map(input => createHTMLAsset({ ...params, input })));
87
+ }
@@ -0,0 +1,80 @@
1
+ import { PluginContext } from 'rollup';
2
+ import path from 'path';
3
+
4
+ import { InputAsset, InputData } from '../input/InputData';
5
+ import { RollupPluginHTMLOptions, TransformAssetFunction } from '../RollupPluginHTMLOptions';
6
+
7
+ export interface EmittedAssets {
8
+ static: Map<string, string>;
9
+ hashed: Map<string, string>;
10
+ }
11
+
12
+ export async function emitAssets(
13
+ this: PluginContext,
14
+ inputs: InputData[],
15
+ options: RollupPluginHTMLOptions,
16
+ ) {
17
+ const emittedStaticAssets = new Map<string, string>();
18
+ const emittedHashedAssets = new Map<string, string>();
19
+ const emittedStaticAssetNames = new Set();
20
+
21
+ const transforms: TransformAssetFunction[] = [];
22
+ if (options.transformAsset) {
23
+ if (Array.isArray(options.transformAsset)) {
24
+ transforms.push(...options.transformAsset);
25
+ } else {
26
+ transforms.push(options.transformAsset);
27
+ }
28
+ }
29
+ const staticAssets: InputAsset[] = [];
30
+ const hashedAssets: InputAsset[] = [];
31
+
32
+ for (const input of inputs) {
33
+ for (const asset of input.assets) {
34
+ if (asset.hashed) {
35
+ hashedAssets.push(asset);
36
+ } else {
37
+ staticAssets.push(asset);
38
+ }
39
+ }
40
+ }
41
+
42
+ // ensure static assets are last because of https://github.com/rollup/rollup/issues/3853
43
+ const allAssets = [...hashedAssets, ...staticAssets];
44
+
45
+ for (const asset of allAssets) {
46
+ const map = asset.hashed ? emittedHashedAssets : emittedStaticAssets;
47
+ if (!map.has(asset.filePath)) {
48
+ let source: Buffer = asset.content;
49
+
50
+ // run user's transform functions
51
+ for (const transform of transforms) {
52
+ const result = await transform(asset.content, asset.filePath);
53
+ if (result != null) {
54
+ source = typeof result === 'string' ? Buffer.from(result, 'utf-8') : result;
55
+ }
56
+ }
57
+
58
+ let ref: string;
59
+ let basename = path.basename(asset.filePath);
60
+ if (asset.hashed) {
61
+ ref = this.emitFile({ type: 'asset', name: basename, source });
62
+ } else {
63
+ // ensure the output filename is unique
64
+ let i = 1;
65
+ while (emittedStaticAssetNames.has(basename)) {
66
+ const ext = path.extname(basename);
67
+ basename = `${basename.replace(ext, '')}${i}${ext}`;
68
+ i += 1;
69
+ }
70
+ emittedStaticAssetNames.add(basename);
71
+ const fileName = `assets/${basename}`;
72
+ ref = this.emitFile({ type: 'asset', name: basename, fileName, source });
73
+ }
74
+
75
+ map.set(asset.filePath, this.getFileName(ref));
76
+ }
77
+ }
78
+
79
+ return { static: emittedStaticAssets, hashed: emittedHashedAssets };
80
+ }
@@ -0,0 +1,88 @@
1
+ import { Attribute } from 'parse5';
2
+ import path from 'path';
3
+ import { OutputChunk } from 'rollup';
4
+
5
+ import {
6
+ EntrypointBundle,
7
+ GeneratedBundle,
8
+ RollupPluginHTMLOptions,
9
+ ScriptModuleTag,
10
+ } from '../RollupPluginHTMLOptions';
11
+ import { createError, NOOP_IMPORT } from '../utils';
12
+ import { toBrowserPath } from './utils';
13
+
14
+ export interface CreateImportPathParams {
15
+ publicPath?: string;
16
+ outputDir: string;
17
+ fileOutputDir: string;
18
+ htmlFileName: string;
19
+ fileName: string;
20
+ }
21
+
22
+ export function createImportPath(params: CreateImportPathParams) {
23
+ const { publicPath, outputDir, fileOutputDir, htmlFileName, fileName } = params;
24
+ const pathFromMainToFileDir = path.relative(outputDir, fileOutputDir);
25
+ let importPath;
26
+ if (publicPath) {
27
+ importPath = toBrowserPath(path.join(publicPath, pathFromMainToFileDir, fileName));
28
+ } else {
29
+ const pathFromHtmlToOutputDir = path.relative(
30
+ path.dirname(htmlFileName),
31
+ pathFromMainToFileDir,
32
+ );
33
+ importPath = toBrowserPath(path.join(pathFromHtmlToOutputDir, fileName));
34
+ }
35
+
36
+ if (importPath.startsWith('http') || importPath.startsWith('/') || importPath.startsWith('.')) {
37
+ return importPath;
38
+ }
39
+ return `./${importPath}`;
40
+ }
41
+
42
+ export interface GetEntrypointBundlesParams {
43
+ pluginOptions: RollupPluginHTMLOptions;
44
+ generatedBundles: GeneratedBundle[];
45
+ outputDir: string;
46
+ inputModuleIds: ScriptModuleTag[];
47
+ htmlFileName: string;
48
+ }
49
+
50
+ interface Entrypoint {
51
+ importPath: string;
52
+ chunk: OutputChunk;
53
+ attributes?: Attribute[];
54
+ }
55
+
56
+ export function getEntrypointBundles(params: GetEntrypointBundlesParams) {
57
+ const { pluginOptions, generatedBundles, inputModuleIds, outputDir, htmlFileName } = params;
58
+ const entrypointBundles: Record<string, EntrypointBundle> = {};
59
+
60
+ for (const { name, options, bundle } of generatedBundles) {
61
+ if (!options.format) {
62
+ throw createError('Missing module format');
63
+ }
64
+
65
+ const entrypoints: Entrypoint[] = [];
66
+ for (const chunkOrAsset of Object.values(bundle)) {
67
+ if (chunkOrAsset.type === 'chunk') {
68
+ const chunk = chunkOrAsset;
69
+ if (chunk.isEntry && chunk.facadeModuleId !== NOOP_IMPORT.importPath) {
70
+ const found = inputModuleIds.find(mod => mod.importPath === chunk.facadeModuleId);
71
+ if (chunk.facadeModuleId && found) {
72
+ const importPath = createImportPath({
73
+ publicPath: pluginOptions.publicPath,
74
+ outputDir,
75
+ fileOutputDir: options.dir ?? '',
76
+ htmlFileName,
77
+ fileName: chunkOrAsset.fileName,
78
+ });
79
+ entrypoints.push({ importPath, chunk: chunkOrAsset, attributes: found.attributes });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ entrypointBundles[name] = { name, options, bundle, entrypoints };
85
+ }
86
+
87
+ return entrypointBundles;
88
+ }
@@ -0,0 +1,115 @@
1
+ import { injectBundles } from './injectBundles';
2
+ import { InputData } from '../input/InputData';
3
+ import {
4
+ EntrypointBundle,
5
+ RollupPluginHTMLOptions,
6
+ TransformHtmlFunction,
7
+ } from '../RollupPluginHTMLOptions';
8
+ import { parse, serialize } from 'parse5';
9
+ import { minify as minifyHTMLFunc } from 'html-minifier-terser';
10
+ import { injectedUpdatedAssetPaths } from './injectedUpdatedAssetPaths';
11
+ import { EmittedAssets } from './emitAssets';
12
+ import { injectAbsoluteBaseUrl } from './injectAbsoluteBaseUrl';
13
+ import { hashInlineScripts } from './hashInlineScripts';
14
+ import { injectServiceWorkerRegistration } from './injectServiceWorkerRegistration';
15
+
16
+ export interface GetOutputHTMLParams {
17
+ input: InputData;
18
+ outputDir: string;
19
+ emittedAssets: EmittedAssets;
20
+ pluginOptions: RollupPluginHTMLOptions;
21
+ entrypointBundles: Record<string, EntrypointBundle>;
22
+ externalTransformHtmlFns?: TransformHtmlFunction[];
23
+ defaultInjectDisabled: boolean;
24
+ serviceWorkerPath: string;
25
+ injectServiceWorker: boolean;
26
+ absolutePathPrefix?: string;
27
+ strictCSPInlineScripts: boolean;
28
+ }
29
+
30
+ export async function getOutputHTML(params: GetOutputHTMLParams) {
31
+ const {
32
+ pluginOptions,
33
+ entrypointBundles,
34
+ externalTransformHtmlFns,
35
+ input,
36
+ outputDir,
37
+ emittedAssets,
38
+ defaultInjectDisabled,
39
+ serviceWorkerPath,
40
+ injectServiceWorker,
41
+ absolutePathPrefix,
42
+ strictCSPInlineScripts,
43
+ } = params;
44
+ const { default: defaultBundle, ...multiBundles } = entrypointBundles;
45
+ const { absoluteSocialMediaUrls = true, rootDir = process.cwd() } = pluginOptions;
46
+
47
+ // inject rollup output into HTML
48
+ let document = parse(input.html);
49
+ if (pluginOptions.extractAssets !== false) {
50
+ injectedUpdatedAssetPaths({
51
+ document,
52
+ input,
53
+ outputDir,
54
+ rootDir,
55
+ emittedAssets,
56
+ absolutePathPrefix,
57
+ publicPath: pluginOptions.publicPath,
58
+ });
59
+ }
60
+ if (!defaultInjectDisabled) {
61
+ injectBundles(document, entrypointBundles);
62
+ }
63
+ if (absoluteSocialMediaUrls && pluginOptions.absoluteBaseUrl) {
64
+ injectAbsoluteBaseUrl(document, pluginOptions.absoluteBaseUrl);
65
+ }
66
+ if (injectServiceWorker && serviceWorkerPath) {
67
+ injectServiceWorkerRegistration({
68
+ document,
69
+ outputDir,
70
+ serviceWorkerPath,
71
+ htmlFileName: input.name,
72
+ });
73
+ }
74
+
75
+ let outputHtml = serialize(document);
76
+
77
+ const transforms = [...(externalTransformHtmlFns ?? [])];
78
+ if (pluginOptions.transformHtml) {
79
+ if (Array.isArray(pluginOptions.transformHtml)) {
80
+ transforms.push(...pluginOptions.transformHtml);
81
+ } else {
82
+ transforms.push(pluginOptions.transformHtml);
83
+ }
84
+ }
85
+
86
+ // run transform functions on output HTML
87
+ for (const transform of transforms) {
88
+ outputHtml = await transform(outputHtml, {
89
+ bundle: defaultBundle,
90
+ bundles: multiBundles,
91
+ htmlFileName: input.name,
92
+ });
93
+ }
94
+
95
+ if (pluginOptions.minify) {
96
+ outputHtml = await minifyHTMLFunc(outputHtml, {
97
+ collapseWhitespace: true,
98
+ removeComments: true,
99
+ removeRedundantAttributes: true,
100
+ removeScriptTypeAttributes: true,
101
+ removeStyleLinkTypeAttributes: true,
102
+ useShortDoctype: true,
103
+ minifyCSS: true,
104
+ minifyJS: true,
105
+ });
106
+ }
107
+
108
+ if (strictCSPInlineScripts) {
109
+ document = parse(outputHtml);
110
+ hashInlineScripts(document);
111
+ outputHtml = serialize(document);
112
+ }
113
+
114
+ return outputHtml;
115
+ }
@@ -0,0 +1,153 @@
1
+ import { Document, Element, ParentNode } from 'parse5';
2
+ import {
3
+ findElement,
4
+ findElements,
5
+ getTagName,
6
+ hasAttribute,
7
+ getAttribute,
8
+ getTextContent,
9
+ createElement,
10
+ findNode,
11
+ prepend,
12
+ setAttribute,
13
+ } from '@web/parse5-utils';
14
+ import crypto from 'crypto';
15
+
16
+ function isMetaCSPTag(node: Element) {
17
+ if (
18
+ getTagName(node) === 'meta' &&
19
+ getAttribute(node, 'http-equiv') === 'Content-Security-Policy'
20
+ ) {
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ function isInlineScript(node: Element) {
27
+ if (getTagName(node) === 'script' && !hasAttribute(node, 'src')) {
28
+ return true;
29
+ }
30
+ return false;
31
+ }
32
+
33
+ /**
34
+ * Parses Meta CSP Content string as an object so we can easily mutate it in JS
35
+ * E.g.:
36
+ *
37
+ * "default-src 'self'; prefetch-src 'self'; upgrade-insecure-requests; style-src 'self' 'unsafe-inline';"
38
+ *
39
+ * becomes
40
+ *
41
+ * {
42
+ * 'default-src': ["'self'"],
43
+ * 'prefetch-src': ["'self'"],
44
+ * 'upgrade-insecure-requests': [],
45
+ * 'style-src': ["'self'", "'unsafe-inline'"]
46
+ * }
47
+ *
48
+ */
49
+ function parseMetaCSPContent(content: string): { [key: string]: string[] } {
50
+ return content.split(';').reduce((acc, curr) => {
51
+ const trimmed = curr.trim();
52
+ if (!trimmed) {
53
+ return acc;
54
+ }
55
+ const splitItem = trimmed.split(' ');
56
+ const [, ...values] = splitItem;
57
+ return {
58
+ ...acc,
59
+ [splitItem[0]]: values,
60
+ };
61
+ }, {});
62
+ }
63
+
64
+ /**
65
+ * Serializes
66
+ *
67
+ * {
68
+ * 'default-src': ["'self'"],
69
+ * 'prefetch-src': ["'self'"],
70
+ * 'upgrade-insecure-requests': [],
71
+ * 'style-src': ["'self'", "'unsafe-inline'"]
72
+ * }
73
+ *
74
+ * back to
75
+ *
76
+ * "default-src 'self'; prefetch-src 'self'; upgrade-insecure-requests; style-src 'self' 'unsafe-inline';"
77
+ */
78
+ function serializeMetaCSPContent(data: { [key: string]: string[] }): string {
79
+ const dataEntries = Object.entries(data);
80
+ return dataEntries.reduce((accOuter, currOuter, indexOuter) => {
81
+ let suffixOuter = ' ';
82
+ let sep = ' ';
83
+
84
+ // If there are no items for this key
85
+ if (currOuter[1].length === 0) {
86
+ suffixOuter = '; ';
87
+ sep = '';
88
+ }
89
+
90
+ // Don't insert space suffix when it is the last item
91
+ if (indexOuter === dataEntries.length - 1) {
92
+ suffixOuter = '';
93
+ }
94
+
95
+ return `${accOuter}${currOuter[0]}${sep}${currOuter[1].reduce(
96
+ (accInner, currInner, indexInner) => {
97
+ let suffixInner = ' ';
98
+ if (indexInner === currOuter[1].length - 1) {
99
+ suffixInner = ';';
100
+ }
101
+ return `${accInner}${currInner}${suffixInner}`;
102
+ },
103
+ '',
104
+ )}${suffixOuter}`;
105
+ }, '');
106
+ }
107
+
108
+ function injectCSPScriptRules(metaCSPEl: Element, hashes: string[]) {
109
+ const content = getAttribute(metaCSPEl, 'content');
110
+ if (content) {
111
+ const data = parseMetaCSPContent(content);
112
+
113
+ if (Array.isArray(data['script-src'])) {
114
+ data['script-src'].push(...hashes);
115
+ } else {
116
+ data['script-src'] = ["'self'", ...hashes];
117
+ }
118
+
119
+ const newContent = serializeMetaCSPContent(data);
120
+ setAttribute(metaCSPEl, 'content', newContent);
121
+ }
122
+ }
123
+
124
+ function injectCSPMetaTag(document: Document, hashes: string[]) {
125
+ const metaTag = createElement('meta', {
126
+ 'http-equiv': 'Content-Security-Policy',
127
+ content: `script-src 'self' ${hashes.join(' ')};`,
128
+ });
129
+ const head = findNode(document, node => node.nodeName === 'head');
130
+ if (head) {
131
+ prepend(head as ParentNode, metaTag);
132
+ }
133
+ }
134
+
135
+ export function hashInlineScripts(document: Document) {
136
+ const metaCSPEl = findElement(document, isMetaCSPTag);
137
+ const inlineScripts = findElements(document, isInlineScript);
138
+ const hashes: string[] = [];
139
+ inlineScripts.forEach(node => {
140
+ if (node.childNodes[0]) {
141
+ const scriptContent = getTextContent(node.childNodes[0]);
142
+ const hash = crypto.createHash('sha256').update(scriptContent).digest('base64');
143
+ hashes.push(`'sha256-${hash}'`);
144
+ }
145
+ });
146
+ if (hashes.length > 0) {
147
+ if (metaCSPEl) {
148
+ injectCSPScriptRules(metaCSPEl, hashes);
149
+ } else {
150
+ injectCSPMetaTag(document, hashes);
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,41 @@
1
+ import { Document, Element } from 'parse5';
2
+ import { findElements, getTagName, getAttribute, setAttribute } from '@web/parse5-utils';
3
+
4
+ function isAbsoluteableNode(node: Element) {
5
+ const metaAttributes = ['og:url', 'og:image'];
6
+ switch (getTagName(node)) {
7
+ case 'link':
8
+ if (getAttribute(node, 'rel') === 'canonical' && getAttribute(node, 'href')) {
9
+ return true;
10
+ }
11
+ return false;
12
+ case 'meta':
13
+ if (
14
+ metaAttributes.includes(getAttribute(node, 'property')!) &&
15
+ getAttribute(node, 'content')
16
+ ) {
17
+ return true;
18
+ }
19
+ return false;
20
+ default:
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export function injectAbsoluteBaseUrl(document: Document, absoluteBaseUrl: string) {
26
+ const nodes = findElements(document, isAbsoluteableNode);
27
+ for (const node of nodes) {
28
+ switch (getTagName(node)) {
29
+ case 'link':
30
+ setAttribute(node, 'href', new URL(getAttribute(node, 'href')!, absoluteBaseUrl).href);
31
+ break;
32
+ case 'meta':
33
+ setAttribute(
34
+ node,
35
+ 'content',
36
+ new URL(getAttribute(node, 'content')!, absoluteBaseUrl).href,
37
+ );
38
+ break;
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,44 @@
1
+ import { Document, Attribute } from 'parse5';
2
+ import { createScript, findElement, getTagName, appendChild } from '@web/parse5-utils';
3
+
4
+ import { EntrypointBundle } from '../RollupPluginHTMLOptions';
5
+ import { createError } from '../utils';
6
+
7
+ export function createLoadScript(src: string, format: string, attributes?: Attribute[]) {
8
+ const attributesObject: Record<string, string> = {};
9
+ if (attributes) {
10
+ for (const attribute of attributes) {
11
+ attributesObject[attribute.name] = attribute.value;
12
+ }
13
+ }
14
+ if (['es', 'esm', 'module'].includes(format)) {
15
+ return createScript({ type: 'module', src, ...attributesObject });
16
+ }
17
+
18
+ if (['system', 'systemjs'].includes(format)) {
19
+ return createScript({}, `System.import(${JSON.stringify(src)});`);
20
+ }
21
+
22
+ return createScript({ src, defer: '' });
23
+ }
24
+
25
+ export function injectBundles(
26
+ document: Document,
27
+ entrypointBundles: Record<string, EntrypointBundle>,
28
+ ) {
29
+ const body = findElement(document, e => getTagName(e) === 'body');
30
+ if (!body) {
31
+ throw new Error('Missing body in HTML document.');
32
+ }
33
+
34
+ for (const { options, entrypoints } of Object.values(entrypointBundles)) {
35
+ if (!options.format) throw createError('Missing output format.');
36
+
37
+ for (const entrypoint of entrypoints) {
38
+ appendChild(
39
+ body,
40
+ createLoadScript(entrypoint.importPath, options.format, entrypoint.attributes),
41
+ );
42
+ }
43
+ }
44
+ }