vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
259 lines (258 loc) • 11.4 kB
JavaScript
import '../../assertEnvVite.js';
export { crawlPlusFilePaths };
export { isPlusFile };
export { getPlusFileValueConfigName };
import { assert, assertUsage, assertWarning } from '../../../../utils/assert.js';
import { assertIsNotProductionRuntime } from '../../../../utils/assertSetup.js';
import { isVersionMatch } from '../../../../utils/assertVersion.js';
import { createDebug } from '../../../../utils/debug.js';
import { deepEqual } from '../../../../utils/deepEqual.js';
import { getGlobalObject } from '../../../../utils/getGlobalObject.js';
import { hasProp } from '../../../../utils/hasProp.js';
import { assertFilePathAbsoluteFilesystem } from '../../../../utils/isFilePathAbsoluteFilesystem.js';
import { isNotNullish } from '../../../../utils/isNullish.js';
import { scriptFileExtensionPattern, isScriptFile, scriptFileExtensionList } from '../../../../utils/isScriptFile.js';
import { assertPosixPath } from '../../../../utils/path.js';
import path from 'node:path';
import { glob } from 'tinyglobby';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { isTemporaryBuildFile } from './transpileAndExecuteFile.js';
import { getEnvVarObject } from '../getEnvVarObject.js';
import pc from '@brillout/picocolors';
import picomatch from 'picomatch';
import { ignorePatternsBuiltIn } from './crawlPlusFilePaths/ignorePatternsBuiltIn.js';
const execA = promisify(exec);
const debug = createDebug('vike:crawl');
assertIsNotProductionRuntime();
const globalObject = getGlobalObject('getVikeConfig/crawlPlusFilePaths.ts', {
gitIsNotUsable: false,
});
async function crawlPlusFilePaths(userRootDir) {
assertPosixPath(userRootDir);
assertFilePathAbsoluteFilesystem(userRootDir);
const userSettings = getUserSettings();
const { ignorePatterns, ignoreMatchers } = getIgnore(userSettings);
// Crawl
const filesGit = userSettings.git !== false && (await gitLsFiles(userRootDir, ignorePatterns, ignoreMatchers));
const filesGitNothingFound = !filesGit || filesGit.length === 0;
const filesGlob = (filesGitNothingFound || debug.isActivated) && (await tinyglobby(userRootDir, ignorePatterns));
let files = !filesGitNothingFound
? filesGit
: // Fallback to tinyglobby for users that dynamically generate plus files. (Assuming that no plus file is found because of the user's .gitignore list.)
filesGlob;
assert(files);
if (debug.isActivated && filesGit && filesGlob) {
assertWarning(deepEqual(filesGlob.slice().sort(), filesGit.slice().sort()), "Git and glob results aren't matching.", { onlyOnce: false });
}
// Filter build files
files = files.filter((filePath) => !isTemporaryBuildFile(filePath));
// Normalize
const plusFiles = files.map((filePath) => {
// Both `$ git-ls files` and tinyglobby return posix paths
assertPosixPath(filePath);
assert(!filePath.startsWith(userRootDir));
const filePathAbsoluteUserRootDir = path.posix.join('/', filePath);
assert(isPlusFile(filePathAbsoluteUserRootDir));
return { filePathAbsoluteUserRootDir };
});
return plusFiles;
}
// Same as tinyglobby() but using `$ git ls-files`
async function gitLsFiles(userRootDir, ignorePatterns, ignoreMatchers) {
if (globalObject.gitIsNotUsable)
return null;
// Preserve UTF-8 file paths.
// https://github.com/vikejs/vike/issues/1658
// https://stackoverflow.com/questions/22827239/how-to-make-git-properly-display-utf-8-encoded-pathnames-in-the-console-window/22828826#22828826
// https://stackoverflow.com/questions/15884180/how-do-i-override-git-configuration-options-by-command-line-parameters/15884261#15884261
const preserveUTF8 = '-c core.quotepath=off';
const cmd = [
'git',
preserveUTF8,
'ls-files',
// Performance gain seems negligible: https://github.com/vikejs/vike/pull/1688#issuecomment-2166206648
...scriptFileExtensionList.map((ext) => `"**/+*.${ext}" "+*.${ext}"`),
// Performance gain is non-negligible.
// - https://github.com/vikejs/vike/pull/1688#issuecomment-2166206648
// - When node_modules/ is untracked the performance gain could be significant?
...ignorePatterns.map((pattern) => `--exclude="${pattern}"`),
// --others --exclude-standard => list untracked files (--others) while using .gitignore (--exclude-standard)
// --cached => list tracked files
'--others --exclude-standard --cached',
].join(' ');
let filesAll;
let filesDeleted;
try {
;
[filesAll, filesDeleted] = await Promise.all([
// Main command
runCmd1(cmd, userRootDir),
// Get tracked but deleted files
runCmd1('git ls-files --deleted', userRootDir),
]);
}
catch (err) {
if (await isGitNotUsable(userRootDir)) {
globalObject.gitIsNotUsable = true;
return null;
}
throw err;
}
if (debug.isActivated) {
debug('[git] userRootDir:', userRootDir);
debug('[git] cmd:', cmd);
debug('[git] result:', filesAll);
debug('[git] filesDeleted:', filesDeleted);
}
const files = [];
for (const filePath of filesAll) {
// + file?
if (!path.posix.basename(filePath).startsWith('+'))
continue;
// We have to repeat the same exclusion logic here because the option --exclude of `$ git ls-files` only applies to untracked files. (We use --exclude only to speed up the `$ git ls-files` command.)
if (ignoreMatchers.some((m) => m(filePath)))
continue;
// JavaScript file?
if (!isScriptFile(filePath))
continue;
// Deleted?
if (filesDeleted.includes(filePath))
continue;
files.push(filePath);
}
return files;
}
// Same as gitLsFiles() but using tinyglobby
async function tinyglobby(userRootDir, ignorePatterns) {
const pattern = `**/+*.${scriptFileExtensionPattern}`;
const options = {
ignore: ignorePatterns,
cwd: userRootDir,
dot: false,
};
const files = await glob(pattern, options);
// Make build deterministic, in order to get a stable generated hash for dist/client/assets/entries/entry-client-routing.${hash}.js
// https://github.com/vikejs/vike/pull/1750
files.sort();
if (debug.isActivated) {
debug('[glob] pattern:', pattern);
debug('[glob] options:', options);
debug('[glob] result:', files);
}
return files;
}
// Whether Git is installed and whether we can use it
async function isGitNotUsable(userRootDir) {
// Check Git version
{
const res = await runCmd2('git --version', userRootDir);
if ('err' in res)
return true;
let { stdout, stderr } = res;
assert(stderr === '');
const prefix = 'git version ';
assert(stdout.startsWith(prefix));
const gitVersion = stdout.slice(prefix.length);
// - Works with Git 2.43.1 but also (most certainly) with earlier versions.
// - We didn't bother test which is the earliest version that works.
// - Git 2.32.0 doesn't seem to work: https://github.com/vikejs/vike/discussions/1549
// - Maybe it's because of StackBlitz: looking at the release notes, Git 2.32.0 should be working.
if (!isVersionMatch(gitVersion, ['2.43.1']))
return true;
}
// Is userRootDir inside a Git repository?
{
const res = await runCmd2('git rev-parse --is-inside-work-tree', userRootDir);
if ('err' in res)
return true;
let { stdout, stderr } = res;
assert(stderr === '');
assert(stdout === 'true');
return false;
}
}
async function runCmd1(cmd, cwd) {
const { stdout } = await execA(cmd, {
cwd,
// https://github.com/vikejs/vike/issues/1982
maxBuffer: Infinity,
});
/* Not always true: https://github.com/vikejs/vike/issues/1440#issuecomment-1892831303
assert(res.stderr === '')
*/
return stdout.toString().split('\n').filter(Boolean);
}
async function runCmd2(cmd, cwd) {
let res;
try {
res = await execA(cmd, { cwd });
}
catch (err) {
return { err };
}
let { stdout, stderr } = res;
stdout = stdout.toString().trim();
stderr = stderr.toString().trim();
return { stdout, stderr };
}
function getUserSettings() {
const userSettings = getEnvVarObject('VIKE_CRAWL') ?? {};
const wrongUsage = (settingName, settingType) => `Setting ${pc.cyan(settingName)} in VIKE_CRAWL should be a ${pc.cyan(settingType)}`;
assertUsage(hasProp(userSettings, 'git', 'boolean') || hasProp(userSettings, 'git', 'undefined'), wrongUsage('git', 'boolean'));
assertUsage(hasProp(userSettings, 'ignore', 'string[]') ||
hasProp(userSettings, 'ignore', 'string') ||
hasProp(userSettings, 'ignore', 'undefined'), wrongUsage('git', 'string or an array of strings'));
assertUsage(hasProp(userSettings, 'ignoreBuiltIn', 'boolean') || hasProp(userSettings, 'ignoreBuiltIn', 'undefined'), wrongUsage('ignoreBuiltIn', 'boolean'));
const settingNames = ['git', 'ignore', 'ignoreBuiltIn'];
Object.keys(userSettings).forEach((name) => {
assertUsage(settingNames.includes(name), `Unknown setting ${pc.bold(pc.red(name))} in VIKE_CRAWL`);
});
return userSettings;
}
function isPlusFile(filePath) {
assertPosixPath(filePath);
if (isTemporaryBuildFile(filePath))
return false;
const fileName = filePath.split('/').pop();
return fileName.startsWith('+');
}
function getPlusFileValueConfigName(filePath) {
if (!isPlusFile(filePath))
return null;
const fileName = path.posix.basename(filePath);
// assertNoUnexpectedPlusSign(filePath, fileName)
const basename = fileName.split('.')[0];
assert(basename.startsWith('+'));
const configName = basename.slice(1);
assertUsage(configName !== '', `${filePath} Invalid filename ${fileName}`);
return configName;
}
/* https://github.com/vikejs/vike/issues/1407
function assertNoUnexpectedPlusSign(filePath: string, fileName: string) {
const dirs = path.posix.dirname(filePath).split('/')
dirs.forEach((dir, i) => {
const dirPath = dirs.slice(0, i + 1).join('/')
assertUsage(
!dir.includes('+'),
`Character '+' is a reserved character: remove '+' from the directory name ${dirPath}/`
)
})
assertUsage(
!fileName.slice(1).includes('+'),
`Character '+' is only allowed at the beginning of filenames: make sure ${filePath} doesn't contain any '+' in its filename other than its first letter`
)
}
*/
function getIgnore(userSettings) {
const ignorePatternsSetByUser = [userSettings.ignore].flat().filter(isNotNullish);
const { ignoreBuiltIn } = userSettings;
const ignorePatterns = [...(ignoreBuiltIn === false ? [] : ignorePatternsBuiltIn), ...ignorePatternsSetByUser];
const ignoreMatchers = ignorePatterns.map((p) => picomatch(p, {
// We must pass the same settings than tinyglobby
// https://github.com/SuperchupuDev/tinyglobby/blob/fcfb08a36c3b4d48d5488c21000c95a956d9797c/src/index.ts#L191-L194
dot: false,
nocase: false,
}));
return { ignorePatterns, ignoreMatchers };
}