const fs = require("fs"); const { TemplatePath } = require("@11ty/eleventy-utils"); const EleventyExtensionMap = require("./EleventyExtensionMap"); const TemplateData = require("./TemplateData"); const TemplateGlob = require("./TemplateGlob"); const TemplatePassthroughManager = require("./TemplatePassthroughManager"); const EleventyBaseError = require("./EleventyBaseError"); const checkPassthroughCopyBehavior = require("./Util/PassthroughCopyBehaviorCheck"); class EleventyFilesError extends EleventyBaseError {} const debug = require("debug")("Eleventy:EleventyFiles"); // const debugDev = require("debug")("Dev:Eleventy:EleventyFiles"); class EleventyFiles { constructor(input, outputDir, formats, eleventyConfig) { if (!eleventyConfig) { throw new EleventyFilesError("Missing `eleventyConfig`` argument."); } this.eleventyConfig = eleventyConfig; this.config = eleventyConfig.getConfig(); this.aggregateBench = this.config.benchmarkManager.get("Aggregate"); this.input = input; this.inputDir = TemplatePath.getDir(this.input); this.outputDir = outputDir; this.initConfig(); this.formats = formats; this.eleventyIgnoreContent = false; // init has not yet been called() this.alreadyInit = false; } setFileSystemSearch(fileSystemSearch) { this.fileSystemSearch = fileSystemSearch; } /* Overrides this.input and this.inputDir, * Useful when input is a file and inputDir is not its direct parent */ setInput(inputDir, input) { this.inputDir = inputDir; this.input = input; this.initConfig(); if (this.alreadyInit) { this.init(); } } initConfig() { this.includesDir = TemplatePath.join(this.inputDir, this.config.dir.includes); if ("layouts" in this.config.dir) { this.layoutsDir = TemplatePath.join(this.inputDir, this.config.dir.layouts); } } init() { this.alreadyInit = true; // Input is a directory if (this.input === this.inputDir) { this.templateGlobs = this.extensionMap.getGlobs(this.inputDir); } else { // input is not a directory this.templateGlobs = TemplateGlob.map([this.input]); } this.initPassthroughManager(); this.setupGlobs(); } get validTemplateGlobs() { if (!this._validTemplateGlobs) { let globs; // Input is a directory if (this.input === this.inputDir) { globs = this.extensionMap.getValidGlobs(this.inputDir); } else { globs = this.templateGlobs; } this._validTemplateGlobs = globs; } return this._validTemplateGlobs; } get passthroughGlobs() { let paths = new Set(); // stuff added in addPassthroughCopy() for (let path of this.passthroughManager.getConfigPathGlobs()) { paths.add(path); } // non-template language extensions for (let path of this.extensionMap.getPassthroughCopyGlobs(this.inputDir)) { paths.add(path); } return Array.from(paths); } restart() { this.passthroughManager.reset(); this.setupGlobs(); this._glob = null; } /* For testing */ _setConfig(config) { if (!config.ignores) { config.ignores = new Set(); config.ignores.add("**/node_modules/**"); } this.config = config; this.initConfig(); } /* Set command root for local project paths */ // This is only used by tests _setLocalPathRoot(dir) { this.localPathRoot = dir; } set extensionMap(extensionMap) { this._extensionMap = extensionMap; } get extensionMap() { // for tests if (!this._extensionMap) { this._extensionMap = new EleventyExtensionMap(this.formats, this.eleventyConfig); this._extensionMap.config = this.config; } return this._extensionMap; } setRunMode(runMode) { this.runMode = runMode; } initPassthroughManager() { let mgr = new TemplatePassthroughManager(this.eleventyConfig); mgr.setInputDir(this.inputDir); mgr.setOutputDir(this.outputDir); mgr.setRunMode(this.runMode); mgr.extensionMap = this.extensionMap; mgr.setFileSystemSearch(this.fileSystemSearch); this.passthroughManager = mgr; } getPassthroughManager() { return this.passthroughManager; } setPassthroughManager(mgr) { mgr.extensionMap = this.extensionMap; this.passthroughManager = mgr; } set templateData(templateData) { this._templateData = templateData; } get templateData() { if (!this._templateData) { this._templateData = new TemplateData(this.inputDir, this.eleventyConfig); } return this._templateData; } getDataDir() { let data = this.templateData; return data.getDataDir(); } setupGlobs() { this.fileIgnores = this.getIgnores(); this.extraIgnores = this._getIncludesAndDataDirs(); this.uniqueIgnores = this.getIgnoreGlobs(); // Conditional added for tests that don’t have a config if (this.config && this.config.events) { this.config.events.emit("eleventy.ignores", this.uniqueIgnores); } this.normalizedTemplateGlobs = this.templateGlobs; } getIgnoreGlobs() { let uniqueIgnores = new Set(); for (let ignore of this.fileIgnores) { uniqueIgnores.add(ignore); } for (let ignore of this.extraIgnores) { uniqueIgnores.add(ignore); } // Placing the config ignores last here is important to the tests for (let ignore of this.config.ignores) { uniqueIgnores.add(TemplateGlob.normalizePath(this.localPathRoot || ".", ignore)); } return Array.from(uniqueIgnores); } static getFileIgnores(ignoreFiles) { if (!Array.isArray(ignoreFiles)) { ignoreFiles = [ignoreFiles]; } let ignores = []; for (let ignorePath of ignoreFiles) { ignorePath = TemplatePath.normalize(ignorePath); let dir = TemplatePath.getDirFromFilePath(ignorePath); if (fs.existsSync(ignorePath) && fs.statSync(ignorePath).size > 0) { let ignoreContent = fs.readFileSync(ignorePath, "utf8"); ignores = ignores.concat(EleventyFiles.normalizeIgnoreContent(dir, ignoreContent)); } } ignores.forEach((path) => debug(`${ignoreFiles} ignoring: ${path}`)); return ignores; } static normalizeIgnoreContent(dir, ignoreContent) { let ignores = []; if (ignoreContent) { ignores = ignoreContent .split("\n") .map((line) => { return line.trim(); }) .filter((line) => { if (line.charAt(0) === "!") { debug( ">>> When processing .gitignore/.eleventyignore, Eleventy does not currently support negative patterns but encountered one:" ); debug(">>>", line); debug("Follow along at https://github.com/11ty/eleventy/issues/693 to track support."); } // empty lines or comments get filtered out return line.length > 0 && line.charAt(0) !== "#" && line.charAt(0) !== "!"; }) .map((line) => { let path = TemplateGlob.normalizePath(dir, "/", line); path = TemplatePath.addLeadingDotSlash(TemplatePath.relativePath(path)); try { // Note these folders must exist to get /** suffix let stat = fs.statSync(path); if (stat.isDirectory()) { return path + "/**"; } return path; } catch (e) { return path; } }); } return ignores; } setEleventyIgnoreContent(content) { this.eleventyIgnoreContent = content; } getIgnores() { let files = new Set(); for (let ignore of EleventyFiles.getFileIgnores(this.getIgnoreFiles())) { files.add(ignore); } // testing API if (this.eleventyIgnoreContent !== false) { files.add(this.eleventyIgnoreContent); } // ignore output dir unless that would exclude all input if (!TemplatePath.startsWithSubPath(this.inputDir, this.outputDir)) { files.add(TemplateGlob.map(this.outputDir + "/**")); } return Array.from(files); } getIgnoreFiles() { let ignoreFiles = []; let rootDirectory = this.localPathRoot || "."; if (this.config.useGitIgnore) { ignoreFiles.push(TemplatePath.join(rootDirectory, ".gitignore")); } if (this.eleventyIgnoreContent === false) { let absoluteInputDir = TemplatePath.absolutePath(this.inputDir); ignoreFiles.push(TemplatePath.join(rootDirectory, ".eleventyignore")); if (rootDirectory !== absoluteInputDir) { ignoreFiles.push(TemplatePath.join(this.inputDir, ".eleventyignore")); } } return ignoreFiles; } getIncludesDir() { return this.includesDir; } getLayoutsDir() { return this.layoutsDir; } getFileGlobs() { return this.normalizedTemplateGlobs; } getRawFiles() { return this.templateGlobs; } getWatchPathCache() { // Issue #1325: make sure passthrough copy files are not included here if (!this.pathCache) { throw new Error("Watching requires `.getFiles()` to be called first in EleventyFiles"); } // Filter out the passthrough copy paths. return this.pathCache.filter((path) => { return ( this.extensionMap.isFullTemplateFilePath(path) && this.extensionMap.shouldSpiderJavaScriptDependencies(path) ); }); } _globSearch() { let globs = this.getFileGlobs(); // returns a promise debug("Searching for: %o", globs); return this.fileSystemSearch.search("templates", globs, { ignore: this.uniqueIgnores, }); } async getFiles() { let bench = this.aggregateBench.get("Searching the file system (templates)"); bench.before(); let globResults = await this._globSearch(); let paths = TemplatePath.addLeadingDotSlashArray(globResults); bench.after(); // Note 2.0.0-canary.19 removed a `filter` option for custom template syntax here that was unpublished and unused. this.pathCache = paths; return paths; } // Assumption here that filePath is not a passthrough copy file isFullTemplateFile(paths, filePath) { if (!filePath) { return false; } for (let path of paths) { if (path === filePath) { return true; } } return false; } /* For `eleventy --watch` */ getGlobWatcherFiles() { // TODO improvement: tie the includes and data to specific file extensions (currently using `**`) let directoryGlobs = this._getIncludesAndDataDirs(); if (checkPassthroughCopyBehavior(this.config, this.runMode)) { return this.validTemplateGlobs.concat(directoryGlobs); } // Revert to old passthroughcopy copy files behavior return this.validTemplateGlobs.concat(this.passthroughGlobs).concat(directoryGlobs); } /* For `eleventy --watch` */ getGlobWatcherFilesForPassthroughCopy() { return this.passthroughGlobs; } /* For `eleventy --watch` */ async getGlobWatcherTemplateDataFiles() { let templateData = this.templateData; return await templateData.getTemplateDataFileGlob(); } /* For `eleventy --watch` */ // TODO this isn’t great but reduces complexity avoiding using TemplateData:getLocalDataPaths for each template in the cache async getWatcherTemplateJavaScriptDataFiles() { let globs = await this.templateData.getTemplateJavaScriptDataFileGlob(); let bench = this.aggregateBench.get("Searching the file system (watching)"); bench.before(); let results = TemplatePath.addLeadingDotSlashArray( await this.fileSystemSearch.search("js-dependencies", globs, { ignore: ["**/node_modules/**"], }) ); bench.after(); return results; } /* Ignored by `eleventy --watch` */ getGlobWatcherIgnores() { // convert to format without ! since they are passed in as a separate argument to glob watcher let entries = new Set( this.fileIgnores.map((ignore) => TemplatePath.stripLeadingDotSlash(ignore)) ); for (let ignore of this.config.watchIgnores) { entries.add(TemplateGlob.normalizePath(this.localPathRoot || ".", ignore)); } // de-duplicated return Array.from(entries); } _getIncludesAndDataDirs() { let files = []; // we want this to fail on "" because we don’t want to ignore the // entire input directory when using "" if (this.config.dir.includes) { files = files.concat(TemplateGlob.map(this.includesDir + "/**")); } // we want this to fail on "" because we don’t want to ignore the // entire input directory when using "" if (this.config.dir.layouts) { files = files.concat(TemplateGlob.map(this.layoutsDir + "/**")); } if (this.config.dir.data && this.config.dir.data !== ".") { let dataDir = this.getDataDir(); files = files.concat(TemplateGlob.map(dataDir + "/**")); } return files; } } module.exports = EleventyFiles;