@mieweb/wikigdrive
Version:
Google Drive to MarkDown synchronization
174 lines (173 loc) • 6.79 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import debug from 'debug';
import { build } from 'esbuild';
import fg from 'fast-glob';
import ts from 'typescript';
const log = debug('vite-plugin-watch-workspace');
const FILE_TYPES = ['ts', 'tsx'];
const RELATIVE_PATH_REGEX = /(\.+\/)*/;
const getTsConfigFollowExtends = (filename, rootDir) => {
// biome-ignore lint/suspicious/noExplicitAny: TODO replace any
let extendedConfig = {};
const config = ts.readConfigFile(filename, ts.sys.readFile).config;
if (config.extends) {
const importPath = path.resolve(rootDir || '', config.extends);
const newRootDir = path.dirname(importPath);
extendedConfig = getTsConfigFollowExtends(importPath, newRootDir);
}
return {
...extendedConfig,
...config,
compilerOptions: {
...extendedConfig.compilerOptions,
...config.compilerOptions,
},
};
};
const getFilesAndTsConfigs = async (workspacePath, currentPackage, packageDir, fileTypes, ignorePaths) => {
const packagePath = path.resolve(workspacePath, packageDir);
const tsConfigPath = path.resolve(packagePath, 'tsconfig.json');
// check whether the user has passed a glob
const currentPackageGlob = currentPackage.includes('*')
? currentPackage
: `${currentPackage}/**/*`;
const tsconfig = getTsConfigFollowExtends(tsConfigPath);
const rootDir = tsconfig.compilerOptions.rootDir || './';
const files = await fg(path.resolve(packagePath, `${rootDir}/**/*.(${fileTypes.join('|')})`), {
ignore: [
'**/node_modules/**',
currentPackageGlob,
...(ignorePaths || []),
],
});
// keep the tsconfig path beside each file to avoid looking for file ids in arrays later
return files.map((file) => [file, tsConfigPath]);
};
const getExternalFileLists = async (workspaceRoot, currentPackage, fileTypes, ignorePaths) => {
const workspaceDenoJson = path.resolve(workspaceRoot, 'deno.json');
const workspaces = JSON.parse(fs.readFileSync(workspaceDenoJson, 'utf8')).workspace;
log(workspaces);
const externalFiles = {};
const filesConfigs = (await Promise.all(workspaces.map(async (workspace) => {
// get directories in each workspace
const workspacePath = path.resolve(workspaceRoot, workspace.replaceAll('*', ''));
log(workspacePath);
// get directories in workSpacePath
const packages = fs
.readdirSync(workspacePath)
.filter((dir) => fs.lstatSync(path.join(workspacePath, dir)).isDirectory());
log('packages', packages);
// get files and tsconfigs in each package
return await Promise.all(packages.map(async (packageDir) => await getFilesAndTsConfigs(workspacePath, currentPackage, packageDir, fileTypes, ignorePaths)));
}))).flatMap((filesConfigs) => filesConfigs.flat());
for (const [file, tsconfig] of filesConfigs) {
externalFiles[file] = tsconfig;
}
return externalFiles;
};
const getLoader = (fileExtension) => {
switch (fileExtension) {
case '.ts':
return 'ts';
case '.tsx':
return 'tsx';
case '.js':
return 'js';
case '.jsx':
return 'jsx';
case '.css':
return 'css';
case '.json':
return 'json';
default:
return 'ts';
}
};
const getOutExtension = (fileExtension) => {
switch (fileExtension) {
case '.ts':
return '.js';
case '.tsx':
return '.js';
case '.js':
return '.js';
case '.jsx':
return '.js';
case '.css':
return '.css';
case '.json':
return '.json';
default:
return '.js';
}
};
// biome-ignore lint/suspicious/noExplicitAny: TODO replace any
const getOutDir = (file, tsconfig) => {
const rootFolder = tsconfig.compilerOptions.rootDir?.replace(RELATIVE_PATH_REGEX, '');
const outFolder = tsconfig.compilerOptions.outDir?.replace(RELATIVE_PATH_REGEX, '');
return path.dirname(file).replace(rootFolder, outFolder);
};
const getOutFile = (outdir, file, fileExtension) => {
const outExtension = getOutExtension(fileExtension);
return path.resolve(outdir, path.basename(file).replace(fileExtension, outExtension));
};
/**
* Plugin to watch a workspace for changes and rebuild when detected using esbuild
* @param config
* The config contains the following parameters
* - workspaceRoot: path to the root of the workspace
* - currentPackage: path to the current package or glob. Will be transformed to a glob if a path is passed.
* - format: esm | cjs
* - fileTypes: ts | tsx | js | jsx | ... (optional)
* - ignorePaths: paths or globs to ignore (optional)
* @constructor
*/
export const VitePluginWatchWorkspace = async (config) => {
const externalFiles = await getExternalFileLists(config.workspaceRoot, config.currentPackage, config.fileTypes || FILE_TYPES, config.ignorePaths);
return {
name: 'vite-plugin-watch-workspace',
buildStart() {
Object.keys(externalFiles).map((file) => {
this.addWatchFile(file);
});
},
async handleHotUpdate({ file, server }) {
log(`File', ${file}`);
const tsconfigPath = externalFiles[file];
if (!tsconfigPath) {
log(`tsconfigPath not found for file ${file}`);
return;
}
const tsconfig = getTsConfigFollowExtends(tsconfigPath);
const fileExtension = path.extname(file);
const loader = getLoader(fileExtension);
const outdir = getOutDir(file, tsconfig);
const outfile = getOutFile(outdir, file, fileExtension);
log(`Outfile ${outfile}, loader ${loader}`);
const buildResult = await build({
tsconfig: tsconfigPath,
stdin: {
contents: fs.readFileSync(file, 'utf8'),
loader,
resolveDir: path.dirname(file),
},
outfile,
platform: config.format === 'cjs' ? 'node' : 'neutral',
format: config.format || 'esm',
});
log(`buildResult', ${JSON.stringify(buildResult)}`);
server.ws.send({
type: 'update',
updates: [
{
acceptedPath: file,
type: 'js-update',
path: file,
timestamp: Date.now(),
},
],
});
},
};
};