@rollup-extras/plugin-html 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Konstantin Shutkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # Plugin Html
2
+
3
+ Rollup plugin to inject assets names into html template.
4
+
5
+ Points:
6
+
7
+ - Inject file names with hashes
8
+ - Watch on a template file and trigger rebuild if it is changed
9
+ - Provides minimalistic template by default, so you are ready to start without configuration
10
+ - Support mutliple rollup configs (will trigger only when last output generated)
11
+ - Extensible through API so you can plug in something for html processing or generate new types of html elements
12
+
13
+ Uses [`@niceties/logger`](https://github.com/kshutkin/niceties/blob/main/logger/README.md) to log messages, can be configured through `@niceties/logger` API.
14
+
15
+ [Changlelog](./CHANGELOG.md)
16
+
17
+ ## Installation
18
+
19
+ Using npm:
20
+ ```
21
+ npm install --save-dev @rollup-extras/plugin-html
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```javascript
27
+ import html from '@rollup-extras/plugin-html';
28
+
29
+ export default {
30
+ input: 'src/index.js',
31
+
32
+ output: {
33
+ format: 'es',
34
+ dir: 'dest'
35
+ },
36
+
37
+ plugins: [html()],
38
+ }
39
+ ```
40
+
41
+ ## Providing options
42
+
43
+ Just pass options to the plugin function. Returned object is the plugin instance which can be passed to rollup.
44
+
45
+ ```javascript
46
+ html({option: value, option2: value2})
47
+ ```
48
+
49
+ For additional plugin instances (in case of multiple configs) please use: `firstInstance.api.addInstance()`
50
+
51
+ ## Options
52
+
53
+ ### pluginName
54
+
55
+ Optional, string.
56
+
57
+ For debugging purposes, so many instances of a plugin can be differenciated in debugging output.
58
+
59
+ ### outputFile
60
+
61
+ Optional, string, default: `'index.html'`.
62
+
63
+ Use to override output file name. If file name with the same name exits in pipeline it will be removed or overwritten in the process but its content by default will be used as an input for this plugin. In the following example file emitted by copy plugin will be used as an input for this plugin:
64
+
65
+ ```javascript
66
+ import copy from '@rollup-extras/plugin-copy';
67
+ import html from '@rollup-extras/plugin-html';
68
+
69
+ export default {
70
+ input: 'src/index.js',
71
+
72
+ output: {
73
+ format: 'es',
74
+ dir: 'dest'
75
+ },
76
+
77
+ plugins: [copy('src/index.html'), html()],
78
+ };
79
+ ```
80
+
81
+ ### template
82
+
83
+ Optional, string, default: `'<!DOCTYPE html><html><head></head><body></body></html>'`.
84
+
85
+ Default template string or template file name. If it is a template string it should contain `</head>` and `</body>` substrings to be useful by default template factory.
86
+
87
+ ### watch
88
+
89
+ Optional, boolean, default: true.
90
+
91
+ If plugin found a template file name this option defines if plugin need to watch it or not.
92
+
93
+ ### emitFile
94
+
95
+ Optional, boolean, default: true.
96
+
97
+ Defines if plugin should use this.emitFile or should just write it to disk. Option can be ignored in setup with mutliple rollup configs.
98
+
99
+ ### useEmittedTemplate
100
+
101
+ Optional, boolean, default: true.
102
+
103
+ Defines what plugin does if it finds a file with expected file name in bundle. By default it will be used as a template. Template provided through template option should be of higher priority to the plugin.
104
+
105
+ ### conditionalLoading
106
+
107
+ Optional, boolean, default: undefined.
108
+
109
+ Defines if plugin adds `nomodule` attribute for non modular js chunks. By default it is done only if we have `es` outputs in one of the bundles processed by plugin. Also plugin values `iife` outputs higher than `umd` and if we have both filters out `umd` ones. This can be changes only by providing custom `assetFactory`.
110
+
111
+ ### injectIntoHead
112
+
113
+ Optional, RegExp | function | boolean, default: `(fileName: string) => fileName.endsWith(cssExtention)`.
114
+
115
+ Option to customize what assets should be injected into head element of a template.
116
+
117
+ ### ignore
118
+
119
+ Optional, RegExp | function | boolean, default: false.
120
+
121
+ Option to customize what assets should be ignored in process.
122
+
123
+ ### verbose
124
+
125
+ Optional, boolean, default: false.
126
+
127
+ Option to print more debug information into console (with default appender).
128
+
129
+ ### useWriteBundle
130
+
131
+ Optional, boolean, default: false.
132
+
133
+ Option to use `writeBundle` hook instead of `generateBundle`.
134
+
135
+ ### assetsFactory
136
+
137
+ Optional, function (please check type in configuration section).
138
+
139
+ To process additional types of assets / enchance default behavior. If known asset processed by factory (it returned an object, string or promise) plugin skips default processing for this asset.
140
+
141
+ Example (adds integrity attribute to a css file):
142
+ ```javascript
143
+ import copy from '@rollup-extras/plugin-copy';
144
+ import html from '@rollup-extras/plugin-html';
145
+ import crypto from 'crypto';
146
+
147
+ export default {
148
+ input: 'src/index.js',
149
+
150
+ output: {
151
+ format: 'es',
152
+ dir: 'dest'
153
+ },
154
+
155
+ plugins: [copy('src/test.css'), html({
156
+ assetsFactory: (fileName, content) => {
157
+ if (fileName.endsWith('.css')) {
158
+ const data = crypto
159
+ .createHash('sha384')
160
+ .update(content);
161
+ return `<link rel="stylesheet" href="${fileName}" integrity="sha384-${data.digest('base64')}" type="text/css">`;
162
+ }
163
+ return undefined;
164
+ }
165
+ })],
166
+ };
167
+ ```
168
+
169
+ ### templateFactory
170
+
171
+ Optional, function (please check type in configuration section).
172
+
173
+ Use to customize template with external libraries.
174
+
175
+ Example (pretty print html):
176
+
177
+ ```javascript
178
+ import html from '@rollup-extras/plugin-html';
179
+ import sb from 'simply-beautiful';
180
+
181
+ export default {
182
+ input: 'src/index.js',
183
+
184
+ output: {
185
+ format: 'es',
186
+ dir: 'dest'
187
+ },
188
+
189
+ plugins: [html({
190
+ templateFactory: (template, assets, defaultFactory) => sb.html(defaultFactory(template, assets))
191
+ })],
192
+ };
193
+ ```
194
+
195
+ ## Configuration
196
+
197
+ [Definition of config typings in typescript](./src/types.ts)
198
+
199
+ ## Prior Art
200
+
201
+ - https://github.com/haifeng2013/rollup-plugin-bundle-html
202
+ - https://github.com/rollup/plugins/tree/master/packages/html
203
+ - https://github.com/modernweb-dev/web/tree/master/packages/rollup-plugin-html
204
+ - https://github.com/posthtml/posthtml#rollup
205
+
206
+ # License
207
+
208
+ [MIT](./LICENSE)
package/dist/index.cjs ADDED
@@ -0,0 +1,334 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs/promises');
4
+ var oldStyleFs = require('fs');
5
+ var path = require('path');
6
+ var logger = require('@niceties/logger');
7
+
8
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
+
10
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
11
+ var oldStyleFs__default = /*#__PURE__*/_interopDefaultLegacy(oldStyleFs);
12
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
13
+
14
+ const defaultTemplate = '<!DOCTYPE html><html><head></head><body></body></html>';
15
+ const headClosingElement = '</head>';
16
+ const bodyClosingElement = '</body>';
17
+ const cssExtention = '.css';
18
+ function index (options = {}) {
19
+ const { pluginName, outputFile, template, watch, emitFile, conditionalLoading, logger, useWriteBundle, logLevel, useEmittedTemplate, injectIntoHead, ignore, assetsFactory, templateFactory } = normalizeOptions(options), hasCustomTemplateFactory = 'templateFactory' in options;
20
+ let templateString = defaultTemplate, hasTemplateFile = false;
21
+ if (template) {
22
+ if (isUsableByDefaultTemplateFactory(template)) {
23
+ templateString = template;
24
+ }
25
+ else {
26
+ if (watch) {
27
+ try {
28
+ // using sync fs call because we need to return plugin object immidiately
29
+ useNewTemplate(oldStyleFs__default["default"].readFileSync(template, { encoding: 'utf8' }));
30
+ hasTemplateFile = true;
31
+ }
32
+ catch (e) {
33
+ handleTemplateReadError(e);
34
+ }
35
+ }
36
+ else {
37
+ templateString = new Promise(resolve => {
38
+ fs__default["default"].readFile(template, { encoding: 'utf8' })
39
+ .then((newTemplateString) => {
40
+ if (!isUsableByDefaultTemplateFactory(newTemplateString) && !hasCustomTemplateFactory) {
41
+ warnAboutUsingDefaultTemplate();
42
+ resolve(defaultTemplate);
43
+ }
44
+ else {
45
+ resolve(newTemplateString);
46
+ }
47
+ })
48
+ .catch(handleTemplateReadError);
49
+ });
50
+ }
51
+ }
52
+ }
53
+ let remainingOutputsCount = 0, configsCount = 0, initialDir = '', fileNameInInitialDir, assets = freshAssets();
54
+ const configs = new Set(), finalHook = useWriteBundle ? 'writeBundle' : 'generateBundle', processedFiles = new Set();
55
+ const instance = {
56
+ name: pluginName,
57
+ renderStart: (options) => {
58
+ logger('started collecting information', logLevel);
59
+ initialDir = options.dir || '';
60
+ fileNameInInitialDir = path__default["default"].join(initialDir, outputFile);
61
+ return renderStart();
62
+ },
63
+ [finalHook]: generateBundle,
64
+ api: { addInstance }
65
+ };
66
+ if (hasTemplateFile && watch) {
67
+ instance.buildStart = async function () {
68
+ try {
69
+ useNewTemplate(await fs__default["default"].readFile(template, { encoding: 'utf8' }));
70
+ }
71
+ catch (e) {
72
+ handleTemplateReadError(e);
73
+ }
74
+ await buildStart.apply(this);
75
+ };
76
+ }
77
+ return instance;
78
+ function freshAssets() {
79
+ return {
80
+ asset: [],
81
+ es: [],
82
+ iife: [],
83
+ umd: []
84
+ };
85
+ }
86
+ function useNewTemplate(newTemplateString) {
87
+ if (!isUsableByDefaultTemplateFactory(newTemplateString) && !hasCustomTemplateFactory) {
88
+ warnAboutUsingDefaultTemplate();
89
+ }
90
+ else {
91
+ templateString = newTemplateString;
92
+ }
93
+ }
94
+ function handleTemplateReadError(e) {
95
+ var _a;
96
+ if (((_a = e) === null || _a === void 0 ? void 0 : _a.code) === 'ENOENT') {
97
+ logger('template nor a file or string', 2 /* warn */, e);
98
+ }
99
+ else {
100
+ logger(`error reading template\n${e}`, 2 /* warn */, e);
101
+ }
102
+ }
103
+ function warnAboutUsingDefaultTemplate() {
104
+ logger('template is unusable by default template factory, using default one', 2 /* warn */);
105
+ }
106
+ function addInstance() {
107
+ const configId = ++configsCount;
108
+ configs.add(configId);
109
+ const instance = {
110
+ name: `${pluginName}#${configId}`,
111
+ renderStart: () => {
112
+ configs.delete(configId);
113
+ return renderStart();
114
+ },
115
+ [finalHook]: generateBundle
116
+ };
117
+ if (hasTemplateFile && watch) {
118
+ instance.buildStart = buildStart;
119
+ }
120
+ return instance;
121
+ }
122
+ async function buildStart() {
123
+ this.addWatchFile('src/index.html');
124
+ }
125
+ function renderStart() {
126
+ ++remainingOutputsCount;
127
+ }
128
+ async function generateBundle(options, bundle) {
129
+ --remainingOutputsCount;
130
+ await getAssets(options, bundle);
131
+ const statistics = Object.keys(assets)
132
+ .map(key => ({ key, count: assets[key].length }))
133
+ .filter(item => item.count)
134
+ .map(({ key, count }) => `${key}: ${count}`)
135
+ .join(', ');
136
+ logger(`assets collected: [${statistics}], remaining: ${remainingOutputsCount} outputs, ${configs.size} configs`, logLevel);
137
+ const dir = options.dir || '', fileName = path__default["default"].relative(dir, fileNameInInitialDir);
138
+ if (fileName in bundle) {
139
+ if (useEmittedTemplate) {
140
+ logger(`using exiting emitted ${fileName} as an input for out templateFactory`, logLevel);
141
+ const source = bundle[fileName].type === 'asset' ? bundle[fileName].source.toString() : bundle[fileName].code;
142
+ useNewTemplate(source);
143
+ }
144
+ else {
145
+ logger(`removing exiting emitted ${fileName}`, logLevel);
146
+ }
147
+ if (configs.size === 0 && emitFile && !useWriteBundle) {
148
+ delete bundle[fileName];
149
+ }
150
+ }
151
+ if (configs.size === 0 && remainingOutputsCount === 0) {
152
+ logger.start('generating html', logLevel);
153
+ try {
154
+ const depromisifiedTemplateString = await templateString, source = await templateFactory(depromisifiedTemplateString, assets, defaultTemplateFactory);
155
+ if (!emitFile || fileName.startsWith('..') || useWriteBundle) {
156
+ if (emitFile && emitFile !== 'auto') {
157
+ logger('cannot emitFile because it is outside of current output.dir, using writeFile instead', logLevel);
158
+ }
159
+ await fs__default["default"].mkdir(path__default["default"].dirname(fileNameInInitialDir), { recursive: true });
160
+ await fs__default["default"].writeFile(fileNameInInitialDir, source);
161
+ }
162
+ else {
163
+ this.emitFile({
164
+ type: 'asset',
165
+ fileName,
166
+ source
167
+ });
168
+ }
169
+ // reset assets and configs for next iteration
170
+ assets = freshAssets();
171
+ for (let i = configsCount; i > 0; --i) {
172
+ configs.add(i);
173
+ }
174
+ processedFiles.clear();
175
+ logger.finish('html file generated');
176
+ }
177
+ catch (e) {
178
+ logger(`error generating html file\n${e}`, 3 /* error */, e);
179
+ logger.finish('html generation failed', 3 /* error */);
180
+ }
181
+ }
182
+ }
183
+ async function getAssets(options, bundle) {
184
+ const dir = options.dir || '';
185
+ for (const fileName of Object.keys(bundle)) {
186
+ const relativeToRootAssetPath = path__default["default"].join(dir, fileName);
187
+ if (ignore(relativeToRootAssetPath) || processedFiles.has(relativeToRootAssetPath)) {
188
+ continue;
189
+ }
190
+ processedFiles.add(relativeToRootAssetPath);
191
+ if (bundle[fileName].type == 'asset') {
192
+ if (await useAssetFactory(fileName, relativeToRootAssetPath, bundle[fileName].source, 'asset')) {
193
+ continue;
194
+ }
195
+ if (fileName.endsWith(cssExtention)) {
196
+ const assetPath = path__default["default"].relative(initialDir, relativeToRootAssetPath);
197
+ assets.asset.push({
198
+ html: getLinkElement(assetPath),
199
+ head: injectIntoHead(relativeToRootAssetPath),
200
+ type: 'asset'
201
+ });
202
+ continue;
203
+ }
204
+ }
205
+ else if (bundle[fileName].type == 'chunk') {
206
+ const chunk = bundle[fileName];
207
+ if (chunk.isEntry) {
208
+ if (await useAssetFactory(fileName, relativeToRootAssetPath, chunk.code, options.format)) {
209
+ continue;
210
+ }
211
+ if (options.format === 'es' || options.format === 'iife' || options.format === 'umd') {
212
+ const assetPath = path__default["default"].relative(initialDir, relativeToRootAssetPath);
213
+ assets[options.format].push({
214
+ html: (assets) => {
215
+ let useConditionalLoading = conditionalLoading;
216
+ useConditionalLoading !== null && useConditionalLoading !== void 0 ? useConditionalLoading : (useConditionalLoading = !!((assets.iife.length
217
+ || assets.umd.length)
218
+ && assets.es.length));
219
+ return (options.format === 'iife' || options.format === 'umd') ? getNonModuleScriptElement(assetPath, useConditionalLoading) : getModuleScriptElement(assetPath);
220
+ },
221
+ head: injectIntoHead(relativeToRootAssetPath),
222
+ type: 'asset'
223
+ });
224
+ }
225
+ }
226
+ }
227
+ }
228
+ }
229
+ async function useAssetFactory(fileName, relativeToRootAssetPath, source, format) {
230
+ if (assetsFactory) {
231
+ try {
232
+ let asset = await assetsFactory(fileName, source, format);
233
+ if (asset) {
234
+ if (typeof asset === 'string') {
235
+ asset = {
236
+ html: asset,
237
+ head: injectIntoHead(relativeToRootAssetPath),
238
+ type: format
239
+ };
240
+ }
241
+ if (!assets[asset.type]) {
242
+ assets[asset.type] = [];
243
+ }
244
+ assets[asset.type].push(asset);
245
+ return true;
246
+ }
247
+ }
248
+ catch (e) {
249
+ logger(`exception in assetFactory:${e}`, 2 /* warn */, e);
250
+ }
251
+ }
252
+ return false;
253
+ }
254
+ }
255
+ function isUsableByDefaultTemplateFactory(template) {
256
+ return template.indexOf(headClosingElement) >= 0 && template.indexOf(bodyClosingElement) >= 0;
257
+ }
258
+ function getLinkElement(fileName) {
259
+ return `<link rel="stylesheet" href="${fileName}" type="text/css">`;
260
+ }
261
+ function getNonModuleScriptElement(fileName, conditionalLoading) {
262
+ return `<script src="${fileName}" type="text/javascript"${conditionalLoading ? ' nomodule' : ''}></script>`;
263
+ }
264
+ function getModuleScriptElement(fileName) {
265
+ return `<script src="${fileName}" type="module"></script>`;
266
+ }
267
+ function normalizeOptions(userOptions) {
268
+ var _a, _b, _c, _d, _e, _f, _g;
269
+ const options = {
270
+ pluginName: (_a = userOptions.pluginName) !== null && _a !== void 0 ? _a : '@rollup-extras/plugin-html',
271
+ template: userOptions.template,
272
+ outputFile: (_b = userOptions.outputFile) !== null && _b !== void 0 ? _b : 'index.html',
273
+ watch: (_c = userOptions.watch) !== null && _c !== void 0 ? _c : true,
274
+ useWriteBundle: (_d = userOptions.useWriteBundle) !== null && _d !== void 0 ? _d : false,
275
+ useEmittedTemplate: (_e = userOptions.useEmittedTemplate) !== null && _e !== void 0 ? _e : !('template' in userOptions),
276
+ emitFile: (_f = userOptions.emitFile) !== null && _f !== void 0 ? _f : 'auto',
277
+ logLevel: userOptions.verbose ? 1 /* info */ : 0 /* verbose */,
278
+ conditionalLoading: userOptions.conditionalLoading,
279
+ injectIntoHead: (fileName) => fileName.endsWith(cssExtention),
280
+ ignore: toAssetPredicate(false),
281
+ assetsFactory: userOptions.assetsFactory,
282
+ templateFactory: (_g = userOptions.templateFactory) !== null && _g !== void 0 ? _g : ((template, assets, defaultTemplateFactory) => defaultTemplateFactory(template, assets)),
283
+ }, logger$1 = options.logger = logger.createLogger(options.pluginName), optionIgnoredText = 'option ignored because it is not a function, RegExp or boolean';
284
+ if (userOptions.injectIntoHead != null) {
285
+ const predicate = toAssetPredicate(userOptions.injectIntoHead);
286
+ if (predicate) {
287
+ options.injectIntoHead = predicate;
288
+ }
289
+ else {
290
+ logger$1(`injectIntoHead ${optionIgnoredText}`, 2 /* warn */);
291
+ }
292
+ }
293
+ if (userOptions.ignore != null) {
294
+ const predicate = toAssetPredicate(userOptions.ignore);
295
+ if (predicate) {
296
+ options.ignore = predicate;
297
+ }
298
+ else {
299
+ logger$1(`ignore ${optionIgnoredText}`, 2 /* warn */);
300
+ }
301
+ }
302
+ return options;
303
+ }
304
+ function toAssetPredicate(sourceOption) {
305
+ if (typeof sourceOption === 'boolean') {
306
+ return () => sourceOption;
307
+ }
308
+ else if (typeof sourceOption === 'function') {
309
+ return sourceOption;
310
+ }
311
+ else if (sourceOption instanceof RegExp) {
312
+ return (fileName) => sourceOption.test(fileName);
313
+ }
314
+ return undefined;
315
+ }
316
+ function defaultTemplateFactory(template, assets) {
317
+ const assetsAsArray = assets.asset
318
+ .concat((assets.iife.length ? assets.iife : assets.umd))
319
+ .concat(assets.es)
320
+ .map((asset) => {
321
+ if (typeof asset.html === 'function') {
322
+ asset.html = asset.html(assets);
323
+ }
324
+ return asset;
325
+ });
326
+ const headElements = assetsAsArray.filter(asset => asset.head).map(asset => asset.html);
327
+ const bodyElements = assetsAsArray.filter(asset => !asset.head).map(asset => asset.html);
328
+ const headIndex = template.toLowerCase().indexOf(headClosingElement);
329
+ template = `${template.substring(0, headIndex)}${headElements.join('')}${template.substring(headIndex)}`;
330
+ const bodyIndex = template.toLowerCase().indexOf(bodyClosingElement);
331
+ return `${template.substring(0, bodyIndex)}${bodyElements.join('')}${template.substring(bodyIndex)}`;
332
+ }
333
+
334
+ module.exports = index;
@@ -0,0 +1,3 @@
1
+ import { PluginHooks } from 'rollup';
2
+ import { HtmlPluginOptions } from './types';
3
+ export default function (options?: HtmlPluginOptions): Partial<PluginHooks>;
package/dist/index.mjs ADDED
@@ -0,0 +1,326 @@
1
+ import fs from 'fs/promises';
2
+ import oldStyleFs from 'fs';
3
+ import path from 'path';
4
+ import { createLogger } from '@niceties/logger';
5
+
6
+ const defaultTemplate = '<!DOCTYPE html><html><head></head><body></body></html>';
7
+ const headClosingElement = '</head>';
8
+ const bodyClosingElement = '</body>';
9
+ const cssExtention = '.css';
10
+ function index (options = {}) {
11
+ const { pluginName, outputFile, template, watch, emitFile, conditionalLoading, logger, useWriteBundle, logLevel, useEmittedTemplate, injectIntoHead, ignore, assetsFactory, templateFactory } = normalizeOptions(options), hasCustomTemplateFactory = 'templateFactory' in options;
12
+ let templateString = defaultTemplate, hasTemplateFile = false;
13
+ if (template) {
14
+ if (isUsableByDefaultTemplateFactory(template)) {
15
+ templateString = template;
16
+ }
17
+ else {
18
+ if (watch) {
19
+ try {
20
+ // using sync fs call because we need to return plugin object immidiately
21
+ useNewTemplate(oldStyleFs.readFileSync(template, { encoding: 'utf8' }));
22
+ hasTemplateFile = true;
23
+ }
24
+ catch (e) {
25
+ handleTemplateReadError(e);
26
+ }
27
+ }
28
+ else {
29
+ templateString = new Promise(resolve => {
30
+ fs.readFile(template, { encoding: 'utf8' })
31
+ .then((newTemplateString) => {
32
+ if (!isUsableByDefaultTemplateFactory(newTemplateString) && !hasCustomTemplateFactory) {
33
+ warnAboutUsingDefaultTemplate();
34
+ resolve(defaultTemplate);
35
+ }
36
+ else {
37
+ resolve(newTemplateString);
38
+ }
39
+ })
40
+ .catch(handleTemplateReadError);
41
+ });
42
+ }
43
+ }
44
+ }
45
+ let remainingOutputsCount = 0, configsCount = 0, initialDir = '', fileNameInInitialDir, assets = freshAssets();
46
+ const configs = new Set(), finalHook = useWriteBundle ? 'writeBundle' : 'generateBundle', processedFiles = new Set();
47
+ const instance = {
48
+ name: pluginName,
49
+ renderStart: (options) => {
50
+ logger('started collecting information', logLevel);
51
+ initialDir = options.dir || '';
52
+ fileNameInInitialDir = path.join(initialDir, outputFile);
53
+ return renderStart();
54
+ },
55
+ [finalHook]: generateBundle,
56
+ api: { addInstance }
57
+ };
58
+ if (hasTemplateFile && watch) {
59
+ instance.buildStart = async function () {
60
+ try {
61
+ useNewTemplate(await fs.readFile(template, { encoding: 'utf8' }));
62
+ }
63
+ catch (e) {
64
+ handleTemplateReadError(e);
65
+ }
66
+ await buildStart.apply(this);
67
+ };
68
+ }
69
+ return instance;
70
+ function freshAssets() {
71
+ return {
72
+ asset: [],
73
+ es: [],
74
+ iife: [],
75
+ umd: []
76
+ };
77
+ }
78
+ function useNewTemplate(newTemplateString) {
79
+ if (!isUsableByDefaultTemplateFactory(newTemplateString) && !hasCustomTemplateFactory) {
80
+ warnAboutUsingDefaultTemplate();
81
+ }
82
+ else {
83
+ templateString = newTemplateString;
84
+ }
85
+ }
86
+ function handleTemplateReadError(e) {
87
+ var _a;
88
+ if (((_a = e) === null || _a === void 0 ? void 0 : _a.code) === 'ENOENT') {
89
+ logger('template nor a file or string', 2 /* warn */, e);
90
+ }
91
+ else {
92
+ logger(`error reading template\n${e}`, 2 /* warn */, e);
93
+ }
94
+ }
95
+ function warnAboutUsingDefaultTemplate() {
96
+ logger('template is unusable by default template factory, using default one', 2 /* warn */);
97
+ }
98
+ function addInstance() {
99
+ const configId = ++configsCount;
100
+ configs.add(configId);
101
+ const instance = {
102
+ name: `${pluginName}#${configId}`,
103
+ renderStart: () => {
104
+ configs.delete(configId);
105
+ return renderStart();
106
+ },
107
+ [finalHook]: generateBundle
108
+ };
109
+ if (hasTemplateFile && watch) {
110
+ instance.buildStart = buildStart;
111
+ }
112
+ return instance;
113
+ }
114
+ async function buildStart() {
115
+ this.addWatchFile('src/index.html');
116
+ }
117
+ function renderStart() {
118
+ ++remainingOutputsCount;
119
+ }
120
+ async function generateBundle(options, bundle) {
121
+ --remainingOutputsCount;
122
+ await getAssets(options, bundle);
123
+ const statistics = Object.keys(assets)
124
+ .map(key => ({ key, count: assets[key].length }))
125
+ .filter(item => item.count)
126
+ .map(({ key, count }) => `${key}: ${count}`)
127
+ .join(', ');
128
+ logger(`assets collected: [${statistics}], remaining: ${remainingOutputsCount} outputs, ${configs.size} configs`, logLevel);
129
+ const dir = options.dir || '', fileName = path.relative(dir, fileNameInInitialDir);
130
+ if (fileName in bundle) {
131
+ if (useEmittedTemplate) {
132
+ logger(`using exiting emitted ${fileName} as an input for out templateFactory`, logLevel);
133
+ const source = bundle[fileName].type === 'asset' ? bundle[fileName].source.toString() : bundle[fileName].code;
134
+ useNewTemplate(source);
135
+ }
136
+ else {
137
+ logger(`removing exiting emitted ${fileName}`, logLevel);
138
+ }
139
+ if (configs.size === 0 && emitFile && !useWriteBundle) {
140
+ delete bundle[fileName];
141
+ }
142
+ }
143
+ if (configs.size === 0 && remainingOutputsCount === 0) {
144
+ logger.start('generating html', logLevel);
145
+ try {
146
+ const depromisifiedTemplateString = await templateString, source = await templateFactory(depromisifiedTemplateString, assets, defaultTemplateFactory);
147
+ if (!emitFile || fileName.startsWith('..') || useWriteBundle) {
148
+ if (emitFile && emitFile !== 'auto') {
149
+ logger('cannot emitFile because it is outside of current output.dir, using writeFile instead', logLevel);
150
+ }
151
+ await fs.mkdir(path.dirname(fileNameInInitialDir), { recursive: true });
152
+ await fs.writeFile(fileNameInInitialDir, source);
153
+ }
154
+ else {
155
+ this.emitFile({
156
+ type: 'asset',
157
+ fileName,
158
+ source
159
+ });
160
+ }
161
+ // reset assets and configs for next iteration
162
+ assets = freshAssets();
163
+ for (let i = configsCount; i > 0; --i) {
164
+ configs.add(i);
165
+ }
166
+ processedFiles.clear();
167
+ logger.finish('html file generated');
168
+ }
169
+ catch (e) {
170
+ logger(`error generating html file\n${e}`, 3 /* error */, e);
171
+ logger.finish('html generation failed', 3 /* error */);
172
+ }
173
+ }
174
+ }
175
+ async function getAssets(options, bundle) {
176
+ const dir = options.dir || '';
177
+ for (const fileName of Object.keys(bundle)) {
178
+ const relativeToRootAssetPath = path.join(dir, fileName);
179
+ if (ignore(relativeToRootAssetPath) || processedFiles.has(relativeToRootAssetPath)) {
180
+ continue;
181
+ }
182
+ processedFiles.add(relativeToRootAssetPath);
183
+ if (bundle[fileName].type == 'asset') {
184
+ if (await useAssetFactory(fileName, relativeToRootAssetPath, bundle[fileName].source, 'asset')) {
185
+ continue;
186
+ }
187
+ if (fileName.endsWith(cssExtention)) {
188
+ const assetPath = path.relative(initialDir, relativeToRootAssetPath);
189
+ assets.asset.push({
190
+ html: getLinkElement(assetPath),
191
+ head: injectIntoHead(relativeToRootAssetPath),
192
+ type: 'asset'
193
+ });
194
+ continue;
195
+ }
196
+ }
197
+ else if (bundle[fileName].type == 'chunk') {
198
+ const chunk = bundle[fileName];
199
+ if (chunk.isEntry) {
200
+ if (await useAssetFactory(fileName, relativeToRootAssetPath, chunk.code, options.format)) {
201
+ continue;
202
+ }
203
+ if (options.format === 'es' || options.format === 'iife' || options.format === 'umd') {
204
+ const assetPath = path.relative(initialDir, relativeToRootAssetPath);
205
+ assets[options.format].push({
206
+ html: (assets) => {
207
+ let useConditionalLoading = conditionalLoading;
208
+ useConditionalLoading !== null && useConditionalLoading !== void 0 ? useConditionalLoading : (useConditionalLoading = !!((assets.iife.length
209
+ || assets.umd.length)
210
+ && assets.es.length));
211
+ return (options.format === 'iife' || options.format === 'umd') ? getNonModuleScriptElement(assetPath, useConditionalLoading) : getModuleScriptElement(assetPath);
212
+ },
213
+ head: injectIntoHead(relativeToRootAssetPath),
214
+ type: 'asset'
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ async function useAssetFactory(fileName, relativeToRootAssetPath, source, format) {
222
+ if (assetsFactory) {
223
+ try {
224
+ let asset = await assetsFactory(fileName, source, format);
225
+ if (asset) {
226
+ if (typeof asset === 'string') {
227
+ asset = {
228
+ html: asset,
229
+ head: injectIntoHead(relativeToRootAssetPath),
230
+ type: format
231
+ };
232
+ }
233
+ if (!assets[asset.type]) {
234
+ assets[asset.type] = [];
235
+ }
236
+ assets[asset.type].push(asset);
237
+ return true;
238
+ }
239
+ }
240
+ catch (e) {
241
+ logger(`exception in assetFactory:${e}`, 2 /* warn */, e);
242
+ }
243
+ }
244
+ return false;
245
+ }
246
+ }
247
+ function isUsableByDefaultTemplateFactory(template) {
248
+ return template.indexOf(headClosingElement) >= 0 && template.indexOf(bodyClosingElement) >= 0;
249
+ }
250
+ function getLinkElement(fileName) {
251
+ return `<link rel="stylesheet" href="${fileName}" type="text/css">`;
252
+ }
253
+ function getNonModuleScriptElement(fileName, conditionalLoading) {
254
+ return `<script src="${fileName}" type="text/javascript"${conditionalLoading ? ' nomodule' : ''}></script>`;
255
+ }
256
+ function getModuleScriptElement(fileName) {
257
+ return `<script src="${fileName}" type="module"></script>`;
258
+ }
259
+ function normalizeOptions(userOptions) {
260
+ var _a, _b, _c, _d, _e, _f, _g;
261
+ const options = {
262
+ pluginName: (_a = userOptions.pluginName) !== null && _a !== void 0 ? _a : '@rollup-extras/plugin-html',
263
+ template: userOptions.template,
264
+ outputFile: (_b = userOptions.outputFile) !== null && _b !== void 0 ? _b : 'index.html',
265
+ watch: (_c = userOptions.watch) !== null && _c !== void 0 ? _c : true,
266
+ useWriteBundle: (_d = userOptions.useWriteBundle) !== null && _d !== void 0 ? _d : false,
267
+ useEmittedTemplate: (_e = userOptions.useEmittedTemplate) !== null && _e !== void 0 ? _e : !('template' in userOptions),
268
+ emitFile: (_f = userOptions.emitFile) !== null && _f !== void 0 ? _f : 'auto',
269
+ logLevel: userOptions.verbose ? 1 /* info */ : 0 /* verbose */,
270
+ conditionalLoading: userOptions.conditionalLoading,
271
+ injectIntoHead: (fileName) => fileName.endsWith(cssExtention),
272
+ ignore: toAssetPredicate(false),
273
+ assetsFactory: userOptions.assetsFactory,
274
+ templateFactory: (_g = userOptions.templateFactory) !== null && _g !== void 0 ? _g : ((template, assets, defaultTemplateFactory) => defaultTemplateFactory(template, assets)),
275
+ }, logger = options.logger = createLogger(options.pluginName), optionIgnoredText = 'option ignored because it is not a function, RegExp or boolean';
276
+ if (userOptions.injectIntoHead != null) {
277
+ const predicate = toAssetPredicate(userOptions.injectIntoHead);
278
+ if (predicate) {
279
+ options.injectIntoHead = predicate;
280
+ }
281
+ else {
282
+ logger(`injectIntoHead ${optionIgnoredText}`, 2 /* warn */);
283
+ }
284
+ }
285
+ if (userOptions.ignore != null) {
286
+ const predicate = toAssetPredicate(userOptions.ignore);
287
+ if (predicate) {
288
+ options.ignore = predicate;
289
+ }
290
+ else {
291
+ logger(`ignore ${optionIgnoredText}`, 2 /* warn */);
292
+ }
293
+ }
294
+ return options;
295
+ }
296
+ function toAssetPredicate(sourceOption) {
297
+ if (typeof sourceOption === 'boolean') {
298
+ return () => sourceOption;
299
+ }
300
+ else if (typeof sourceOption === 'function') {
301
+ return sourceOption;
302
+ }
303
+ else if (sourceOption instanceof RegExp) {
304
+ return (fileName) => sourceOption.test(fileName);
305
+ }
306
+ return undefined;
307
+ }
308
+ function defaultTemplateFactory(template, assets) {
309
+ const assetsAsArray = assets.asset
310
+ .concat((assets.iife.length ? assets.iife : assets.umd))
311
+ .concat(assets.es)
312
+ .map((asset) => {
313
+ if (typeof asset.html === 'function') {
314
+ asset.html = asset.html(assets);
315
+ }
316
+ return asset;
317
+ });
318
+ const headElements = assetsAsArray.filter(asset => asset.head).map(asset => asset.html);
319
+ const bodyElements = assetsAsArray.filter(asset => !asset.head).map(asset => asset.html);
320
+ const headIndex = template.toLowerCase().indexOf(headClosingElement);
321
+ template = `${template.substring(0, headIndex)}${headElements.join('')}${template.substring(headIndex)}`;
322
+ const bodyIndex = template.toLowerCase().indexOf(bodyClosingElement);
323
+ return `${template.substring(0, bodyIndex)}${bodyElements.join('')}${template.substring(bodyIndex)}`;
324
+ }
325
+
326
+ export { index as default };
@@ -0,0 +1,34 @@
1
+ import { InternalModuleFormat } from 'rollup';
2
+ export declare type AssetType = 'asset' | InternalModuleFormat;
3
+ export declare type Assets = {
4
+ [key in AssetType]?: AssetDescriptor[];
5
+ };
6
+ export declare type AssetPredicate = (fileName: string) => boolean;
7
+ export declare type AssetDescriptor = {
8
+ html: string | ((assets: Assets, context?: unknown) => string | unknown);
9
+ head: boolean;
10
+ type: 'asset' | InternalModuleFormat;
11
+ };
12
+ export declare type SimpleAssetDescriptor = {
13
+ html: string;
14
+ head: boolean;
15
+ type: 'asset' | InternalModuleFormat;
16
+ };
17
+ export declare type AssetFactory = (fileName: string, content: string | Uint8Array, type: 'asset' | InternalModuleFormat) => AssetDescriptor | string | undefined | Promise<AssetDescriptor | string | undefined>;
18
+ export declare type DefaultTemplateFactory = (initialTemplate: string, assets: Assets) => string;
19
+ export declare type TemplateFactory = (initialTemplate: string, assets: Assets, defaultTemplateFactory: DefaultTemplateFactory) => string | Uint8Array | Promise<string | Uint8Array>;
20
+ export declare type HtmlPluginOptions = {
21
+ pluginName?: string;
22
+ outputFile?: string;
23
+ template?: string;
24
+ watch?: boolean;
25
+ emitFile?: boolean;
26
+ verbose?: boolean;
27
+ useWriteBundle?: boolean;
28
+ useEmittedTemplate?: boolean;
29
+ conditionalLoading?: boolean;
30
+ injectIntoHead?: boolean | AssetPredicate | RegExp;
31
+ ignore?: boolean | AssetPredicate | RegExp;
32
+ assetsFactory?: AssetFactory;
33
+ templateFactory?: TemplateFactory;
34
+ };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "license": "MIT",
4
+ "name": "@rollup-extras/plugin-html",
5
+ "author": {
6
+ "name": "Konstantin Shutkin"
7
+ },
8
+ "funding": "https://www.donationalerts.com/r/excitingcode",
9
+ "type": "module",
10
+ "exports": {
11
+ ".": {
12
+ "require": "./dist/index.cjs",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.mjs",
18
+ "typings": "./dist/index.d.ts",
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "engines": {
23
+ "node": ">=15"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/kshutkin/rollup-extras.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/kshutkin/rollup-extras/issues"
31
+ },
32
+ "homepage": "https://github.com/kshutkin/rollup-extras/plugin-clean/README.md",
33
+ "keywords": [
34
+ "rollup-plugin"
35
+ ],
36
+ "scripts": {
37
+ "build": "rimraf ./dist && rollup -c",
38
+ "dev": "rimraf ./dist && rollup -c -w",
39
+ "test": "jest --collectCoverage",
40
+ "lint": "eslint ./src",
41
+ "semantic-release": "npx semantic-release"
42
+ },
43
+ "devDependencies": {
44
+ "@rollup/plugin-commonjs": "21.0.1",
45
+ "@rollup/plugin-node-resolve": "13.0.6",
46
+ "@semantic-release/changelog": "6.0.1",
47
+ "@semantic-release/commit-analyzer": "9.0.1",
48
+ "@semantic-release/git": "10.0.1",
49
+ "@semantic-release/npm": "8.0.2",
50
+ "@semantic-release/release-notes-generator": "10.0.2",
51
+ "@types/jest": "27.0.2",
52
+ "@typescript-eslint/eslint-plugin": "5.3.0",
53
+ "@typescript-eslint/parser": "5.3.0",
54
+ "conventional-changelog-angular": "5.0.13",
55
+ "eslint": "8.1.0",
56
+ "jest": "27.3.1",
57
+ "lodash": "4.17.21",
58
+ "rimraf": "3.0.2",
59
+ "is-builtin-module": "3.1.0",
60
+ "rollup": "2.60.1",
61
+ "rollup-plugin-terser": "7.0.2",
62
+ "rollup-plugin-typescript2": "0.31.1",
63
+ "rollup-plugin-preprocess": "0.0.4",
64
+ "semantic-release": "18.0.0",
65
+ "semantic-release-monorepo": "7.0.5",
66
+ "ts-jest": "27.0.7",
67
+ "typescript": "4.4.x",
68
+ "update-monorepo-package-json": "0.1.6"
69
+ },
70
+ "peerDependencies": {
71
+ "rollup": "^2.60.2"
72
+ },
73
+ "dependencies": {
74
+ "@niceties/logger": "^1.0.4"
75
+ },
76
+ "readme": "ERROR: No README data found!"
77
+ }