Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .fallowrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "./node_modules/fallow/schema.json",
"entry": ["src/index.ts", "src/gitlab/index.ts", "src/azure/index.ts"]
"entry": ["src/index.ts", "src/gitlab/index.ts", "src/azure/index.ts"],
"ignoreDependencies": ["@e18e/eslint-plugin"]
}
48 changes: 24 additions & 24 deletions dist/azure/index.mjs

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions dist/gitlab/index.mjs

Large diffs are not rendered by default.

188 changes: 94 additions & 94 deletions dist/index.mjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"zod": "^4.0.0"
},
"devDependencies": {
"@e18e/eslint-plugin": "^0.8.1",
"@types/node": "^24.0.0",
"fallow": "3.33.1",
"typescript": "^7.0.0",
Expand Down
39 changes: 39 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import { analyzeProjectNpmrc } from "./ci/npmrc.js";
// Literal written into `.npmrc`; pnpm/npm expand it against the env at install time.
const NODE_AUTH_TOKEN_REF = "${NODE_AUTH_TOKEN}";

const URL_PROTOCOL_RE = /^\w+:/;
const LINE_BREAK_RE = /\r?\n/;

function getRunnerNpmrcPath(): string {
return resolve(process.env.RUNNER_TEMP || process.cwd(), ".npmrc");
}

function stripProtocol(url: string): string {
return url.replace(/^\w+:/, "");
return url.replace(URL_PROTOCOL_RE, "");
}

function buildAuthLine(registryUrl: string): string {
Expand Down Expand Up @@ -65,7 +68,7 @@ function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?:
const lines: string[] = [];
const existing = readNpmrc(fileLocation);
if (existing !== undefined) {
for (const line of existing.split(/\r?\n/)) {
for (const line of existing.split(LINE_BREAK_RE)) {
const lower = line.toLowerCase();
if (lower.startsWith(`${scopePrefix}registry`)) continue;
if (lower.startsWith(authPrefix) && lower.includes("_authtoken")) continue;
Expand Down
5 changes: 4 additions & 1 deletion src/azure/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { logInfo, logWarning, prependPath, setVariable } from "./commands.js";
import { installVitePlus } from "./install-viteplus.js";
import { parseAzureInputs, resolveProjectDirFromInputs } from "./inputs.js";

const UNEXPANDED_MACRO_RE = /^\$\([^)]+\)$/;

export type AzurePhase = "prepare" | "finalize";

export interface AzurePorts {
Expand Down Expand Up @@ -172,7 +174,8 @@ export async function runFinalize(
const authVariables = analyzeProjectNpmrc(npmrc).envVarRefs;
authVariables.add("NODE_AUTH_TOKEN");
for (const name of authVariables) {
if (!isReservedAuthVariable(name) && /^\$\([^)]+\)$/.test(env[name] || "")) delete env[name];
if (!isReservedAuthVariable(name) && UNEXPANDED_MACRO_RE.test(env[name] || ""))
delete env[name];
}

ports.configureAuth(
Expand Down
14 changes: 8 additions & 6 deletions src/ci/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import type { ExportVariable, RuntimeEnv } from "./types.js";

const NODE_AUTH_TOKEN_REF = "${NODE_AUTH_TOKEN}";

const LINE_BREAK_RE = /\r?\n/;
const RESERVED_ENV_NAME_RE = /^(?:PATH|HOME|USERPROFILE|TMPDIR|CI)$/i;
const RESERVED_ENV_PREFIX_RE = /^(?:SETUP_VP_|RUNNER_|GITHUB_|CI_|SYSTEM_|AGENT_|BUILD_|RELEASE_)/i;
const URL_PROTOCOL_RE = /^\w+:/;

function readUserConfigLines(env: RuntimeEnv, replacedKeys: Set<string>): string[] {
const homeDir = env.HOME || env.USERPROFILE;
const file =
Expand All @@ -14,16 +19,13 @@ function readUserConfigLines(env: RuntimeEnv, replacedKeys: Set<string>): string
(homeDir && path.join(homeDir, ".npmrc"));
const contents = file ? readNpmrc(file) : "";
return contents
.split(/\r?\n/)
.split(LINE_BREAK_RE)
.filter((line) => !replacedKeys.has(line.split("=")[0]!.trim().toLowerCase()));
}

export function isReservedAuthVariable(name: string): boolean {
if (["GITHUB_TOKEN", "CI_JOB_TOKEN", "SYSTEM_ACCESSTOKEN"].includes(name)) return false;
return (
/^(?:PATH|HOME|USERPROFILE|TMPDIR|CI)$/i.test(name) ||
/^(?:SETUP_VP_|RUNNER_|GITHUB_|CI_|SYSTEM_|AGENT_|BUILD_|RELEASE_)/i.test(name)
);
return RESERVED_ENV_NAME_RE.test(name) || RESERVED_ENV_PREFIX_RE.test(name);
}

function writeUserConfig(
Expand Down Expand Up @@ -73,7 +75,7 @@ export function configureAuth(
scopePrefix = `${scope.toLowerCase()}:`;
}

const authUrl = registryUrl.replace(/^\w+:/, "").toLowerCase();
const authUrl = registryUrl.replace(URL_PROTOCOL_RE, "").toLowerCase();
const replacedKeys = new Set([`${scopePrefix}registry`, `${authUrl}:_authtoken`]);
const lines = readUserConfigLines(targetEnv, replacedKeys);
lines.push(
Expand Down
4 changes: 3 additions & 1 deletion src/ci/env-current.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { supportsScopedEnv } from "./node-manager.js";
import type { LogFn } from "./types.js";

const PLAIN_VALUE_RE = /^[\w.+-]+$/;

export const ENV_CURRENT_ARGS = ["env", "current", "--json"];

export interface CurrentEnv {
Expand Down Expand Up @@ -52,5 +54,5 @@ function supportsEnvCurrent(versionOutput: string): boolean {

// Values are written to a GitLab dotenv report, so accept only plain names and versions.
function plainValue(value: unknown): string {
return typeof value === "string" && /^[\w.+-]+$/.test(value) ? value : "unknown";
return typeof value === "string" && PLAIN_VALUE_RE.test(value) ? value : "unknown";
}
27 changes: 17 additions & 10 deletions src/ci/lockfile-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import type { LockFileInfo } from "./types.js";
import { isWithin } from "./project.js";
import type { ResolutionContext } from "./resolution.js";

// Lockfiles always record fully-resolved versions, so a valid result starts with
// major.minor.patch; use this to sanity-check extracted values.
const VERSION_START_RE = /^\d+\.\d+\.\d+/;
const PNPM_PACKAGE_VERSION_RE = /^\s*\/?vite-plus@(\d+\.\d+\.\d+[^\s():'"]*)/m;
const PNPM_PEER_SUFFIX_RE = /\(.*$/;
const LINE_BREAK_RE = /\r?\n/;
const INDENTED_LINE_RE = /^\s/;
const YARN_PACKAGE_HEADER_RE = /(?:^|[",\s])vite-plus@/;
const YARN_VERSION_RE = /^\s+version:?\s+"?(\d+\.\d+\.\d+[^"\s]*)"?/;

export interface LockfileResolver {
tryResolveVitePlusVersionFromLockfile: (
projectDir: string,
Expand All @@ -17,9 +27,6 @@ export interface LockfileResolver {

export function createLockfileResolver(context: ResolutionContext): LockfileResolver {
const { getWorkspaceDir, info, debug } = context;
// Lockfiles always record fully-resolved versions, so a valid result starts with
// major.minor.patch; use this to sanity-check extracted values.
const VERSION_START_RE = /^\d+\.\d+\.\d+/;

/**
* Resolve the exact `vite-plus` version from the project's lockfile, as a
Expand Down Expand Up @@ -219,7 +226,7 @@ export function createLockfileResolver(context: ResolutionContext): LockfileReso
// matched.
const rootVersion = pnpmImporterVersion(root);
if (rootVersion) return rootVersion;
const match = content.match(/^\s*\/?vite-plus@(\d+\.\d+\.\d+[^\s():'"]*)/m);
const match = content.match(PNPM_PACKAGE_VERSION_RE);
return match?.[1];
}

Expand All @@ -233,7 +240,7 @@ export function createLockfileResolver(context: ResolutionContext): LockfileReso
const raw =
typeof dep === "string" ? dep : (dep as { version?: unknown } | undefined)?.version;
if (typeof raw === "string") {
const clean = raw.replace(/\(.*$/, "").trim(); // strip pnpm peer suffix
const clean = raw.replace(PNPM_PEER_SUFFIX_RE, "").trim(); // strip pnpm peer suffix
if (VERSION_START_RE.test(clean)) return clean;
}
}
Expand All @@ -248,12 +255,12 @@ export function createLockfileResolver(context: ResolutionContext): LockfileReso
// let the caller fall back rather than guess another workspace's version.
function fromYarnLock(content: string): string | undefined {
const versions = new Set<string>();
const lines = content.split(/\r?\n/);
const lines = content.split(LINE_BREAK_RE);
for (let i = 0; i < lines.length; i++) {
if (/^\s/.test(lines[i])) continue; // skip indented (in-block) lines
if (!/(?:^|[",\s])vite-plus@/.test(lines[i])) continue; // not a vite-plus entry header
for (let j = i + 1; j < lines.length && /^\s/.test(lines[j]); j++) {
const m = lines[j].match(/^\s+version:?\s+"?(\d+\.\d+\.\d+[^"\s]*)"?/);
if (INDENTED_LINE_RE.test(lines[i])) continue; // skip indented (in-block) lines
if (!YARN_PACKAGE_HEADER_RE.test(lines[i])) continue; // not a vite-plus entry header
for (let j = i + 1; j < lines.length && INDENTED_LINE_RE.test(lines[j]); j++) {
const m = lines[j].match(YARN_VERSION_RE);
if (m) {
versions.add(m[1]);
break;
Expand Down
4 changes: 3 additions & 1 deletion src/ci/node-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { parseInstalledVpVersion } from "./version.js";

const VERSION_PARTS_RE = /^(\d+)\.(\d+)\.(\d+)/;

// True and unset leave the installer default unchanged (enabled on CI).
// False switches to system Node.js after installation via vp env off node
// (vp env off before 0.3.1), without disabling package-manager management.
Expand All @@ -21,7 +23,7 @@ export function parseNodeManager(input: string | undefined): boolean | undefined
export function supportsScopedEnv(versionOutput: string): boolean {
const version = parseInstalledVpVersion(versionOutput);
if (version.startsWith("0.0.0-commit.")) return true;
const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
const match = version.match(VERSION_PARTS_RE);
if (!match)
throw new Error("Could not determine the installed Vite+ version for env configuration.");
const [, major, minor, patch] = match.map(Number);
Expand Down
7 changes: 5 additions & 2 deletions src/ci/node-version-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { basename } from "node:path";
import { resolvePath } from "./project.js";
import type { ResolutionContext } from "./resolution.js";

const VERSION_PREFIX_RE = /^v/i;
const WHITESPACE_RE = /\s+/;

export interface NodeVersionResolver {
resolveNodeVersionFile: (filePath: string, baseDir?: string) => string;
}
Expand Down Expand Up @@ -44,7 +47,7 @@ export function createNodeVersionResolver(
}

// Strip leading 'v' prefix (e.g., "v20.11.0" -> "20.11.0")
version = version.replace(/^v/i, "");
version = version.replace(VERSION_PREFIX_RE, "");

info(`Resolved Node.js version '${version}' from ${filePath}`);
return version;
Expand Down Expand Up @@ -86,7 +89,7 @@ export function createNodeVersionResolver(
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;

const [tool, ...versions] = trimmed.split(/\s+/);
const [tool, ...versions] = trimmed.split(WHITESPACE_RE);
if (tool !== "nodejs" && tool !== "node") continue;

// asdf allows multiple fallback versions; pick the first installable one
Expand Down
4 changes: 3 additions & 1 deletion src/ci/npmrc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { readFileSync } from "node:fs";

const LINE_BREAK_RE = /\r?\n/;

export function readNpmrc(file: string): string {
try {
return readFileSync(file, "utf8");
Expand All @@ -14,7 +16,7 @@ export function analyzeProjectNpmrc(content: string): {
} {
const envVarRefs = new Set<string>();

for (const rawLine of content.split(/\r?\n/)) {
for (const rawLine of content.split(LINE_BREAK_RE)) {
const line = rawLine.trim();
if (!line || line.startsWith(";") || line.startsWith("#")) continue;

Expand Down
8 changes: 6 additions & 2 deletions src/ci/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { accessSync, constants, statSync } from "node:fs";
import path from "node:path";
import { isWindows } from "./platform.js";

const PATH_SEPARATOR_RE = /[\\/:]/;
const QUOTED_PATH_RE = /^"(.*)"$/;

export function run(command: string, args: string[], options: SpawnSyncOptions = {}): void {
const result = spawnSync(command, args, { stdio: "inherit", ...options });
if (result.error) throw result.error;
Expand Down Expand Up @@ -38,7 +41,8 @@ export function commandPath(
command: string,
env: NodeJS.ProcessEnv = process.env,
): string | undefined {
if (!command || command === "." || command === ".." || /[\\/:]/.test(command)) return undefined;
if (!command || command === "." || command === ".." || PATH_SEPARATOR_RE.test(command))
return undefined;

const windows = isWindows();
const pathKey = windows
Expand All @@ -52,7 +56,7 @@ export function commandPath(
const accessMode = windows ? constants.F_OK : constants.X_OK;

for (const entry of searchPath.split(path.delimiter)) {
const directory = windows ? entry.replace(/^"(.*)"$/, "$1") : entry;
const directory = windows ? entry.replace(QUOTED_PATH_RE, "$1") : entry;
// Empty and relative entries can refer to attacker-controlled checkout files.
if (!path.isAbsolute(directory)) continue;
// On Windows, isAbsolute also accepts drive-root-relative paths such as
Expand Down
4 changes: 3 additions & 1 deletion src/ci/run-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { isWindows } from "./platform.js";
import { runWithOutput } from "./process.js";
import type { InstallCommand, RunInstallEntry, RunInstallInput } from "./types.js";

const VP_NOT_FOUND_RE = /Command 'vp' not found in PATH/;

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
Expand Down Expand Up @@ -62,7 +64,7 @@ export function parseRunInstall(value: string): RunInstallEntry[] {
export const parseYamlSubset = parseRunInstall;

export function isSfwVpNotFoundFlake(stdout: string, stderr: string): boolean {
return /Command 'vp' not found in PATH/.test(stdout + "\n" + stderr);
return VP_NOT_FOUND_RE.test(stdout + "\n" + stderr);
}

export async function runInstall(
Expand Down
Loading
Loading