jsx-email
Version:
Render JSX email components to HTML email
164 lines • 7.87 kB
JavaScript
import { access, readFile, unlink } from 'node:fs/promises';
import { dirname, extname, relative, resolve } from 'node:path';
// Note: Keep the star here. There are environments (ahem, Stackblitz) which
// can't seem to handle the psuedo default export
import * as watcher from '@parcel/watcher';
import chalk from 'chalk';
import { log } from '../log.js';
import { getTempPath } from './commands/build.mjs';
import { buildForPreview, originalCwd, writePreviewDataFiles } from './helpers.mjs';
const exists = (path) => access(path).then(() => true, () => false);
// eslint-disable-next-line no-console
const newline = () => console.log('');
const removeChildPaths = (paths) => paths.filter((p1) => !paths.some((p2) => p1 !== p2 && relative(p2, p1) && !relative(p2, p1).startsWith('..')));
const getEntrypoints = async (files) => {
const entrypoints = new Set();
const promises = files.map(async ({ metaPath }) => {
log.debug({ exists: await exists(metaPath ?? ''), metaPath });
if (!metaPath || !(await exists(metaPath)))
return null;
const contents = await readFile(metaPath, 'utf-8');
const metafile = JSON.parse(contents);
Object.entries(metafile.outputs).forEach(([_, { entryPoint }]) => {
entrypoints.add(resolve(originalCwd, entryPoint));
});
return null;
});
await Promise.all(promises);
log.debug({ entrypoints });
return Array.from(entrypoints).filter(Boolean);
};
const getWatchDirectories = async (files, depPaths) => {
const entrypoints = await getEntrypoints(files);
const paths = [
...entrypoints.map((path) => dirname(path)),
...depPaths.map((path) => dirname(path))
];
const uniquePaths = Array.from(new Set(paths));
const watchPaths = removeChildPaths(uniquePaths);
log.debug({ watchPaths });
return { entrypoints, watchPaths };
};
const mapDeps = async (files) => {
const depPaths = [];
const metaReads = files.map(async ({ metaPath }) => {
log.debug({ exists: await exists(metaPath ?? ''), metaPath });
if (!metaPath || !(await exists(metaPath)))
return null;
const contents = await readFile(metaPath, 'utf-8');
const metafile = JSON.parse(contents);
const { outputs } = metafile;
const result = new Map();
Object.entries(outputs).forEach(([_, meat]) => {
const { entryPoint, inputs } = meat;
const resolvedEntry = resolve(originalCwd, entryPoint);
depPaths.push(resolvedEntry);
for (const dep of Object.keys(inputs)) {
const resolvedDepPath = resolve(originalCwd, dep);
const set = result.get(resolvedDepPath) ?? new Set();
depPaths.push(resolvedDepPath);
set.add(resolvedEntry);
result.set(resolvedDepPath, set);
}
});
return result;
});
const deps = (await Promise.all(metaReads)).filter(Boolean);
return { depPaths, deps };
};
export const watch = async (args) => {
newline();
log.info(chalk `{blue Starting watcher...}\n`);
const { common, files, server } = args;
const { argv } = common;
const extensions = ['.css', '.js', '.jsx', '.ts', '.tsx'];
const { depPaths, deps: metaDeps } = await mapDeps(files);
const dependencyPaths = depPaths.filter((path) => !path.includes('/node_modules/'));
const { entrypoints, watchPaths: watchDirectories } = await getWatchDirectories(files, dependencyPaths);
const templateDeps = new Map();
const validFiles = Array.from(new Set([...entrypoints, ...dependencyPaths]));
for (const map of metaDeps) {
map.forEach((value, key) => templateDeps.set(key, value));
}
log.info({ validFiles });
const handler = async (_, incoming) => {
// Note: We perform this filter in case someone has a dependency of a template,
// or has templates, at a path that includes node_modules. We also don't any
// non-template files having builds attempted on them, so check to make sure
// the event path is in the set of files we want to watch, unless it's a create
// event
const events = incoming.filter((event) => {
if (event.path.includes('/node_modules/'))
return false;
if (event.type !== 'create')
return validFiles.includes(event.path);
return true;
});
const changedFiles = events
.filter((event) => event.type !== 'create' && event.type !== 'delete')
.map((e) => e.path)
.filter((path) => extensions.includes(extname(path)));
const changedTemplates = changedFiles
.flatMap((file) => [...(templateDeps.get(file) || [])])
.filter(Boolean);
const createdFiles = events
.filter((event) => event.type === 'create')
.map((e) => e.path)
.filter((path) => extensions.includes(extname(path)));
const deletedFiles = events
.filter(({ type }) => type === 'delete')
.map((e) => e.path)
.filter((path) => extensions.includes(extname(path)));
const buildPath = await getTempPath('preview');
const { exclude } = argv;
log.debug('Changed Files:', changedFiles);
log.debug('Changed Templates:', changedTemplates);
log.debug('Created Files:', createdFiles);
log.debug('Deleted Files:', deletedFiles);
if (deletedFiles.length) {
log.info(chalk `{cyan Removed}`, deletedFiles.length, `file${deletedFiles.length === 1 ? '' : 's'}:\n `, deletedFiles.join('\n '), '\n');
deletedFiles.forEach((path) => {
let index = files.findIndex(({ fileName }) => path === fileName);
if (index === -1)
return;
const file = files[index];
files.splice(index, 1);
// Note: Don't await either, we don't need to
unlink(file.compiledPath);
unlink(`${file.writePathBase}.js`);
index = validFiles.find((fileName) => path === fileName);
if (index > -1)
validFiles.splice(index, 1);
});
}
if (createdFiles.length) {
log.info(chalk `{cyan Building}`, createdFiles.length, `file${createdFiles.length === 1 ? '' : 's'}:\n `, createdFiles.join('\n '), '\n');
await Promise.all(createdFiles.map(async (path) => {
const results = await buildForPreview({
buildPath,
exclude,
quiet: true,
targetPath: path
});
const mappedDeps = await mapDeps(results);
files.push(...results);
validFiles.push(...[path, ...mappedDeps.depPaths.filter((p) => !p.includes('/node_modules/'))]);
await writePreviewDataFiles(results);
}));
}
if (!changedTemplates.length)
return;
log.info(chalk `{cyan Rebuilding}`, changedTemplates.length, `file${changedTemplates.length === 1 ? '' : 's'}:\n `, changedTemplates.join('\n '), '\n');
changedTemplates.forEach(async (path) => {
const results = await buildForPreview({ buildPath, exclude, quiet: true, targetPath: path });
await writePreviewDataFiles(results);
});
};
log.debug('Watching Paths:', watchDirectories.sort());
const subPromises = watchDirectories.map((path) => watcher.subscribe(path, handler));
const subscriptions = await Promise.all(subPromises);
server.httpServer.on('close', () => {
subscriptions.forEach((sub) => sub.unsubscribe());
});
};
//# sourceMappingURL=watcher.mjs.map