Mirror: A frag-canvas custom element to apply Shadertoy fragment shaders to a canvas or image/video element
1import fs from 'node:fs/promises';
2import path from 'node:path/posix';
3import { fileURLToPath } from 'node:url';
4import { readFileSync } from 'node:fs';
5import { createRequire, isBuiltin } from 'node:module';
6
7import * as prettier from 'prettier';
8import commonjs from '@rollup/plugin-commonjs';
9import resolve from '@rollup/plugin-node-resolve';
10import babel from '@rollup/plugin-babel';
11import terser from '@rollup/plugin-terser';
12import cjsCheck from 'rollup-plugin-cjs-check';
13import dts from 'rollup-plugin-dts';
14
15const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
17const normalize = name => []
18 .concat(name)
19 .join(' ')
20 .replace(/[@\s/.]+/g, ' ')
21 .trim()
22 .replace(/\s+/, '-')
23 .toLowerCase();
24
25const extension = name => {
26 if (/\.d.ts$/.test(name)) {
27 return '.d.ts';
28 } else {
29 return path.extname(name);
30 }
31};
32
33const meta = JSON.parse(readFileSync('package.json'));
34const name = normalize(meta.name);
35
36const externalModules = [
37 ...Object.keys(meta.dependencies || {}),
38 ...Object.keys(meta.peerDependencies || {}),
39];
40
41const moduleRe = /^(?!node:|[.{1,2}\/])(@[\w.-]+\/)?[\w.-]+/;
42const externalRe = new RegExp(`^(${externalModules.join('|')})($|/)`);
43
44const exports = {};
45for (const key in meta.exports) {
46 const entry = meta.exports[key];
47 if (typeof entry === 'object' && !!entry.source) {
48 const entryPath = normalize(key);
49 const entryName = normalize([name, entryPath]);
50 exports[entryName] = {
51 path: entryPath,
52 ...entry,
53 };
54 }
55}
56
57const externals = new Set();
58
59const commonConfig = {
60 input: Object.entries(exports).reduce((input, [exportName, entry]) => {
61 input[exportName] = entry.source;
62 return input;
63 }, {}),
64 onwarn: () => {},
65 external(id) {
66 const isExternal = isBuiltin(id) || (externalModules.length && externalRe.test(id));
67 if (!isExternal && moduleRe.test(id))
68 externals.add(id);
69 return isExternal;
70 },
71 treeshake: {
72 unknownGlobalSideEffects: false,
73 tryCatchDeoptimization: false,
74 moduleSideEffects: false,
75 },
76};
77
78const commonPlugins = [
79 resolve({
80 extensions: ['.mjs', '.js', '.ts'],
81 mainFields: ['module', 'jsnext', 'main'],
82 preferBuiltins: false,
83 browser: true,
84 }),
85
86 commonjs({
87 ignoreGlobal: true,
88 include: /\/node_modules\//,
89 }),
90];
91
92const commonOutput = {
93 dir: './',
94 exports: 'auto',
95 sourcemap: true,
96 sourcemapExcludeSources: false,
97 hoistTransitiveImports: false,
98 indent: false,
99 freeze: false,
100 strict: false,
101 generatedCode: {
102 preset: 'es5',
103 reservedNamesAsProps: false,
104 objectShorthand: false,
105 constBindings: false,
106 },
107};
108
109const outputPlugins = [
110 {
111 name: 'outputPackageJsons',
112 async writeBundle() {
113 for (const key in exports) {
114 const entry = exports[key];
115 if (entry.path) {
116 const output = path.relative(entry.path, process.cwd());
117 const json = JSON.stringify({
118 name: key,
119 private: true,
120 version: '0.0.0',
121 main: path.join(output, entry.require),
122 module: path.join(output, entry.import),
123 types: path.join(output, entry.types),
124 source: path.join(output, entry.source),
125 exports: {
126 '.': {
127 types: path.join(output, entry.types),
128 import: path.join(output, entry.import),
129 require: path.join(output, entry.require),
130 source: path.join(output, entry.source),
131 },
132 },
133 }, null, 2);
134
135 await fs.mkdir(entry.path, { recursive: true });
136 await fs.writeFile(path.join(entry.path, 'package.json'), json);
137 }
138 }
139 },
140 },
141
142 {
143 name: 'outputBundledLicenses',
144 async writeBundle() {
145 const require = createRequire(import.meta.url);
146 const rootLicense = path.join(__dirname, '../LICENSE.md');
147 const outputLicense = path.resolve('LICENSE.md');
148 if (rootLicense === outputLicense) return;
149 const licenses = new Map();
150 for (const packageName of [...externals].sort()) {
151 let license;
152 let metaPath;
153 let meta;
154 try {
155 metaPath = require.resolve(path.join(packageName, '/package.json'));
156 meta = require(metaPath);
157 } catch (_error) {
158 continue;
159 }
160 const packagePath = path.dirname(metaPath);
161 let licenseName = (await fs.readdir(packagePath).catch(() => []))
162 .find((name) => /^licen[sc]e/i.test(name));
163 if (!licenseName) {
164 const match = /^SEE LICENSE IN (.*)/i.exec(meta.license || '');
165 licenseName = match ? match[1] : meta.license;
166 }
167 try {
168 license = await fs.readFile(path.join(packagePath, licenseName), 'utf8');
169 } catch (_error) {
170 license = meta.author
171 ? `${licenseName}, Copyright (c) ${meta.author.name || meta.author}`
172 : `${licenseName}, See license at: ${meta.repository.url || meta.repository}`;
173 }
174 licenses.set(packageName, license);
175 }
176 let output = (await fs.readFile(rootLicense, 'utf8')).trim();
177 for (const [packageName, licenseText] of licenses)
178 output += `\n\n## ${packageName}\n\n${licenseText.trim()}`;
179 await fs.writeFile(outputLicense, output);
180 },
181 },
182
183 cjsCheck(),
184
185 terser({
186 warnings: true,
187 ecma: 2015,
188 keep_fnames: true,
189 ie8: false,
190 compress: {
191 pure_getters: true,
192 toplevel: true,
193 booleans_as_integers: false,
194 keep_fnames: true,
195 keep_fargs: true,
196 if_return: false,
197 ie8: false,
198 sequences: false,
199 loops: false,
200 conditionals: false,
201 join_vars: false,
202 },
203 mangle: {
204 module: true,
205 keep_fnames: true,
206 },
207 output: {
208 beautify: true,
209 braces: true,
210 indent_level: 2,
211 },
212 }),
213];
214
215export default [
216 {
217 ...commonConfig,
218 plugins: [
219 ...commonPlugins,
220 babel({
221 babelrc: false,
222 babelHelpers: 'bundled',
223 extensions: ['mjs', 'js', 'jsx', 'ts', 'tsx'],
224 exclude: 'node_modules/**',
225 presets: [],
226 plugins: [
227 '@babel/plugin-transform-typescript',
228 '@babel/plugin-transform-block-scoping',
229 ],
230 }),
231 ],
232 output: [
233 {
234 ...commonOutput,
235 format: 'esm',
236 chunkFileNames(chunk) {
237 return `dist/chunks/[name]-chunk${extension(chunk.name) || '.mjs'}`;
238 },
239 entryFileNames(chunk) {
240 return chunk.isEntry
241 ? path.normalize(exports[chunk.name].import)
242 : `dist/[name].mjs`;
243 },
244 plugins: outputPlugins,
245 },
246 {
247 ...commonOutput,
248 format: 'cjs',
249 esModule: true,
250 externalLiveBindings: true,
251 chunkFileNames(chunk) {
252 return `dist/chunks/[name]-chunk${extension(chunk.name) || '.js'}`;
253 },
254 entryFileNames(chunk) {
255 return chunk.isEntry
256 ? path.normalize(exports[chunk.name].require)
257 : `dist/[name].js`;
258 },
259 plugins: outputPlugins,
260 },
261 ],
262 },
263
264 {
265 ...commonConfig,
266 plugins: [
267 ...commonPlugins,
268 dts(),
269 ],
270 output: {
271 ...commonOutput,
272 sourcemap: false,
273 format: 'dts',
274 chunkFileNames(chunk) {
275 return `dist/chunks/[name]-chunk${extension(chunk.name) || '.d.ts'}`;
276 },
277 entryFileNames(chunk) {
278 return chunk.isEntry
279 ? path.normalize(exports[chunk.name].types)
280 : `dist/[name].d.ts`;
281 },
282 plugins: [
283 {
284 renderChunk(code, chunk) {
285 if (chunk.fileName.endsWith('d.ts')) {
286 return prettier.format(code, {
287 filepath: chunk.fileName,
288 parser: 'typescript',
289 singleQuote: true,
290 tabWidth: 2,
291 printWidth: 100,
292 trailingComma: 'es5',
293 });
294 }
295 },
296 },
297 ],
298 },
299 },
300];