UNPKG

snowdev

Version:

Zero configuration, unbundled, opinionated, development and prototyping server for simple ES modules development: types generation, format and linting, dev server and TypeScript support.

348 lines (301 loc) 9.83 kB
import { promises as fs } from "node:fs"; import { join, extname } from "node:path"; import { fileURLToPath } from "node:url"; import console from "console-ansi"; import ts from "typescript"; import * as prettier from "prettier"; import sortPackageJson from "sort-package-json"; import { ESLint } from "eslint"; import jsdoc from "jsdoc-api"; import jsdoc2md from "jsdoc-to-markdown"; import * as TypeDoc from "typedoc"; import concatMd from "concat-md"; import { glob } from "glob"; import { RF_OPTIONS, escapeRegExp, sortPaths } from "./utils.js"; const __dirname = fileURLToPath(new URL(".", import.meta.url)); const lint = async (cwd, files, options) => { console.time(lint.description); try { const babelEslint = options.eslint?.find( ({ languageOptions }) => languageOptions?.parser?.meta?.name === "@babel/eslint-parser", ); if (babelEslint) { babelEslint.languageOptions.parserOptions.babelOptions = { cwd, ...(options.babel || {}), ...(babelEslint.languageOptions.parserOptions.babelOptions || {}), }; } const eslint = new ESLint({ cwd, baseConfig: options.eslint, overrideConfigFile: true, }); const lintResults = await eslint.lintFiles(files); const results = (await eslint.loadFormatter("stylish")).format(lintResults); if (results) console.log(lint.description, results); console.timeEnd(lint.description); return results; } catch (error) { console.error(error); } console.timeEnd(lint.description); }; lint.description = `build: lint sources`; const format = async (cwd, files, options) => { console.time(format.description); for (const file of files) { try { await fs.writeFile( file, await prettier.format(await fs.readFile(file, "utf-8"), { parser: options.ts ? "typescript" : "babel", ...((await prettier.resolveConfig(file)) || {}), ...(options.prettier || {}), }), "utf-8", ); } catch (error) { console.error(error); } } try { const packageJsonFile = join(cwd, "package.json"); await fs.writeFile( packageJsonFile, sortPackageJson(await fs.readFile(packageJsonFile, "utf-8")), "utf-8", ); } catch (error) { console.error(error); } console.timeEnd(format.description); }; format.description = `build: format sources`; const docs = async (cwd, files, options) => { console.time(docs.description); const isFile = !!extname(options.docs); const docsFolder = isFile ? ".temp_docs" : options.docs; const isMarkdown = options.docsFormat === "md"; let inlinedDocs; if (options.ts) { try { await fs.rm(join(cwd, docsFolder), RF_OPTIONS); await fs.mkdir(join(cwd, docsFolder), { recursive: true }); const app = await TypeDoc.Application.bootstrapWithPlugins( { entryPoints: files, exclude: options.ignore, logLevel: "Info", ...(isMarkdown ? { readme: "none", plugin: ["typedoc-plugin-markdown"], hideBreadcrumbs: true, } : { plugin: ["typedoc-material-theme"], }), ...(options.typedoc || {}), }, [ new TypeDoc.TSConfigReader(), new TypeDoc.PackageJsonReader(), new TypeDoc.TypeDocReader(), ], ); app.logger.info = console.info; const configPath = ts.findConfigFile( cwd, ts.sys.fileExists, "tsconfig.json", ); if (!configPath) { app.options.setCompilerOptions( files.flat(), options.tsconfig.compilerOptions, ); } // Ignore errors in dependencies app.options._compilerOptions.skipLibCheck = true; const project = await app.convert(); if (project) await app.generateDocs(project, docsFolder); await fs.writeFile(join(cwd, docsFolder, ".nojekyll"), "", "utf-8"); if (isMarkdown && isFile) { inlinedDocs = await concatMd.default(join(cwd, docsFolder)); } } catch (error) { console.error(error); } } else { if (isMarkdown) { inlinedDocs = await jsdoc2md.render({ files, configure: options.jsdoc || join(__dirname, "jsdoc.json"), ...options.jsdoc2md, }); // TODO: remove if jsdoc ever properly works with ESM and classes inlinedDocs = inlinedDocs.replaceAll("new exports.", "new "); if (!isFile) { await fs.mkdir(join(cwd, docsFolder), { recursive: true }); await fs.writeFile( join(cwd, docsFolder, "README.md"), inlinedDocs, "utf-8", ); return; } } else { await jsdoc.render({ files, destination: join(cwd, docsFolder) }); if (isFile) { console.error("Output html to a file not supported."); return; } } } if (isFile) { await fs.rm(join(cwd, docsFolder), RF_OPTIONS); const filePath = join(cwd, options.docs); const formattedDocs = ( await prettier.format(inlinedDocs, { parser: isMarkdown ? "markdown" : "html", ...((await prettier.resolveConfig(filePath)) || {}), }) ) .split("\n") .map((line) => line.trimEnd()) .join("\n"); if (options.docsStart && options.docsEnd) { await fs.writeFile( filePath, (await fs.readFile(filePath, "utf-8")).replace( new RegExp( `${escapeRegExp(options.docsStart)}([\\s\\S]*?)${escapeRegExp( options.docsEnd, )}`, ), `${options.docsStart}\n\n${formattedDocs}\n${options.docsEnd}`, ), "utf-8", ); } else { await fs.writeFile(filePath, formattedDocs, "utf-8"); } } console.timeEnd(docs.description); }; docs.description = `build: update docs`; const types = async (cwd, files, options, watch) => { console.time(types.description); try { const configPath = ts.findConfigFile( cwd, ts.sys.fileExists, "tsconfig.json", ); const formatHost = { getCanonicalFileName: (path) => path, getCurrentDirectory: ts.sys.getCurrentDirectory, getNewLine: () => ts.sys.newLine, }; if (watch) { ts.createWatchProgram( ts.createWatchCompilerHost( configPath, {}, // config.compilerOptions, ts.sys, ts.createEmitAndSemanticDiagnosticsBuilderProgram, function (diagnostic) { console.info(diagnostic.file.path); const results = `Error ${ diagnostic.code } : ${ts.flattenDiagnosticMessageText( diagnostic.messageText, formatHost.getNewLine(), )}`; console.error(results); if (typeof watch === "function") { watch(`${diagnostic.file.path}\n${results}`); } }, function (diagnostic) { console.info(ts.formatDiagnostic(diagnostic, formatHost)); }, ), ); } else { const config = configPath ? ts.readConfigFile(configPath, ts.sys.readFile).config : options.tsconfig; if (!configPath) config.include = files; if (config.compilerOptions.declarationDir) { await fs.rm( join(cwd, config.compilerOptions.declarationDir), RF_OPTIONS, ); } if (config.compilerOptions.outDir) { await fs.rm(join(cwd, config.compilerOptions.outDir), RF_OPTIONS); } const parsedCommandLine = ts.parseJsonConfigFileContent( config, ts.sys, cwd, ); const program = ts.createProgram({ options: parsedCommandLine.options, rootNames: parsedCommandLine.fileNames, configFileParsingDiagnostics: parsedCommandLine.errors, }); const { diagnostics, emitSkipped } = program.emit(); const allDiagnostics = ts .getPreEmitDiagnostics(program) .concat(diagnostics, parsedCommandLine.errors); const diagnosticToConsoleMethod = { [ts.DiagnosticCategory["Message"]]: "log", [ts.DiagnosticCategory["Suggestion"]]: "info", [ts.DiagnosticCategory["Warning"]]: "warn", [ts.DiagnosticCategory["Error"]]: "error", }; allDiagnostics.forEach((diagnostic) => { const { line, character } = diagnostic.file ? ts.getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start) : { line: 0, character: 0 }; console[diagnosticToConsoleMethod[diagnostic.category] || "log"]( `TypeScript\n${diagnostic.file?.fileName} (${line + 1}, ${ character + 1 }): ${ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`, ); }); if (emitSkipped) console.error("Emit skipped."); } } catch (error) { console.error(error); } console.timeEnd(types.description); }; types.description = `build: run TypeScript (generate types, watch or compile)`; const build = async (options) => { const cwd = options.cwd; const files = sortPaths( await glob(options.files, { cwd, ignore: options.ignore, absolute: true, }), ); console.log(`build files:\n- ${files.join("\n- ")}`); if (options.format) await format(cwd, files, options); await Promise.all( [ options.types ? types(cwd, files, options) : 0, options.docs ? docs(cwd, files, options) : 0, ].filter(Boolean), ); if (options.lint) await lint(cwd, files, options); }; build.description = `Lint and Format sources, run TypeScript, update README API.`; export { types, lint }; export default build;