CodeSampleX

샘플

dotenv 17.4.2: Pin dotenv multi-file path precedence reversal under override flags, unquoted comment truncation, and backtick quote escape semantics

검증된 샘플 — npm dotenv 17.4.2: Pin dotenv multi-file path precedence reversal under override flags, unquoted comment truncation, and backtick quote escape…

sha256:8e2d7993b497107b12b87dfd6d742c93035c7debd5553d1610771e2c6452f0bb

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 node linux x64 node node npm

검증 실행 환경

환경 컨트랙트 단계 실행일
node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-16
node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · node-typescript@1
2026-08-18

케이스

HOW
목표
Pin dotenv multi-file path precedence reversal under override flags, unquoted comment truncation, and backtick quote escape semantics
패키지
심벌
  • dotenv.configDotenv
  • dotenv.populate
  • dotenv.parse
환경
node
생성일
2026-08-16T06:22:15Z

컨트랙트

  1. assert multi-path array with override: false prioritizes the first file over subsequent files
  2. assert multi-path array with override: true silently reverses precedence causing the last file to overwrite earlier files
  3. assert unquoted # characters are silently truncated as inline comments across values and URLs
  4. assert double quotes expand escape newlines while backticks and single quotes preserve literal escape characters
  5. assert colon delimiter is parsed as a valid key-value separator alongside equal signs
  6. assert dotenv.populate returns only newly assigned keys omitting existing unmutated keys

파일

  • NOTES.md
  • csx.json
  • package-lock.json
  • package.json
  • src/loader.mjs
  • test/contract.mjs

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# CodeSampleX Notes: dotenv Multi-Path Precedence & Parsing Semantics

## 1. Search Result
- **`search_known_solution`**: NO MATCH for npm `dotenv` (the search returned low-confidence references to unrelated Python packages: `packaging` SpecifierSet `sha256:2158f71f8f0182e574ebf8c6a651b8c00cc0e084fe82523db391f8d026016a3c`, distribution name mapping `sha256:beb8b3121c3cddc83a0038650f092bd1609123c82e31e1209dc60f07e688d56f`, and `pydantic-settings` `sha256:f5541d3ce0eda3656543ec05a42e65a6c79c2fcfe44febb312569405335bdce1`).

## 2. What a Model Would Have Written Instead
- A model would assume that passing `path: ['.env.local', '.env']` with `override: true` ensures `.env.local` overrides `.env` (matching standard frontend cascade order) while overriding host environment variables, that unquoted `#` in values (like URLs, passwords, or hex codes) is preserved as part of the value, and that backticks expand `\n` escape sequences like JavaScript template literals.

## 3. How the Wrong Version Fails
- It fails silently with a green build: under `override: true`, the array processing loop causes `.env` (the last file) to silently overwrite `.env.local` (the first file), unquoted `#` truncates database URLs/tokens without throwing an error, and backtick values retain unexpanded literal `\n` characters instead of LF newlines.
csx.json
{"case":{"caseId":"case:sha256:2c9e80cef88662eac683a7b9fbc19d74e82ef1a1598a55203adccb3a74fd2ef2","contract":["assert multi-path array with override: false prioritizes the first file over subsequent files","assert multi-path array with override: true silently reverses precedence causing the last file to overwrite earlier files","assert unquoted # characters are silently truncated as inline comments across values and URLs","assert double quotes expand escape newlines while backticks and single quotes preserve literal escape characters","assert colon delimiter is parsed as a valid key-value separator alongside equal signs","assert dotenv.populate returns only newly assigned keys omitting existing unmutated keys"],"goal":"Pin dotenv multi-file path precedence reversal under override flags, unquoted comment truncation, and backtick quote escape semantics","kind":"HOW","packages":["pkg:npm/dotenv@17.4.2"],"schemaVersion":1,"symbols":["dotenv.configDotenv","dotenv.populate","dotenv.parse"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/dotenv@17.4.2"],"schemaVersion":1,"symbols":["dotenv.configDotenv","dotenv.populate","dotenv.parse"],"verifierAdapter":"node-typescript@1"}
package-lock.json
{
  "name": "npm-dotenv-version",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-dotenv-version",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "dotenv": "^17.4.2"
      }
    },
    "node_modules/dotenv": {
      "version": "17.4.2",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
      "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    }
  }
}
package.json
{
  "name": "npm-dotenv-version",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "dotenv": "17.4.2"
  }
}
src/loader.mjs
import dotenv from 'dotenv';

/**
 * Loads configuration from multiple env file paths into a target environment object.
 *
 * @param {Object} params
 * @param {string[]} params.paths - Array of file paths to parse in order.
 * @param {Object} [params.targetEnv] - Destination object (defaults to an isolated object).
 * @param {boolean} [params.override=false] - Whether to override existing keys during population.
 * @param {boolean} [params.quiet=true] - Whether to suppress runtime injection logs.
 * @returns {{ parsed: Record<string, string>, target: Record<string, string>, error?: Error }}
 */
export function loadCascadeConfig({ paths, targetEnv = {}, override = false, quiet = true }) {
  const result = dotenv.configDotenv({
    path: paths,
    processEnv: targetEnv,
    override,
    quiet
  });

  return {
    parsed: result.parsed || {},
    target: targetEnv,
    error: result.error
  };
}

/**
 * Parses raw .env file string contents.
 *
 * @param {string} src - Raw env file content.
 * @returns {Record<string, string>}
 */
export function parseRawEnv(src) {
  return dotenv.parse(src);
}

/**
 * Populates a target object with parsed key-value pairs according to override settings.
 *
 * @param {Record<string, string>} target - Destination object to mutate.
 * @param {Record<string, string>} parsed - Key-value pairs to apply.
 * @param {Object} [options]
 * @param {boolean} [options.override=false] - Whether to overwrite existing keys on target.
 * @param {boolean} [options.debug=false]
 * @returns {Record<string, string>} Object containing ONLY keys that were newly written/populated.
 */
export function populateEnv(target, parsed, options = {}) {
  return dotenv.populate(target, parsed, options);
}
test/contract.mjs
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { loadCascadeConfig, parseRawEnv, populateEnv } from '../src/loader.mjs';

function runContract() {
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dotenv-csx-'));

  try {
    const baseEnvPath = path.join(tmpDir, '.env');
    const localEnvPath = path.join(tmpDir, '.env.local');

    fs.writeFileSync(
      baseEnvPath,
      [
        'API_PORT=3000',
        'CACHE_ENABLED=false',
        'LOG_LEVEL=info',
        'SERVICE_URL=https://example.com/api',
      ].join('\n')
    );

    fs.writeFileSync(
      localEnvPath,
      [
        'API_PORT=8080',
        'LOG_LEVEL=debug',
      ].join('\n')
    );

    // =========================================================================
    // 1. Multi-path cascade ordering: override=false vs override=true
    // =========================================================================

    // Case 1A: With override: false (default), the FIRST file in array sets keys;
    // subsequent files cannot overwrite earlier files in parsedAll.
    const initialTargetA = { PRESET_SYSTEM_VAR: 'from_host' };
    const resA = loadCascadeConfig({
      paths: [localEnvPath, baseEnvPath],
      targetEnv: initialTargetA,
      override: false,
    });

    assert.equal(resA.target.API_PORT, '8080', 'First file (.env.local) must win when override: false');
    assert.equal(resA.target.LOG_LEVEL, 'debug', '.env.local LOG_LEVEL must win over .env base');
    assert.equal(resA.target.CACHE_ENABLED, 'false', 'Unset keys in .env.local must fall back to .env');

    // Case 1B: If a key is already preset in targetEnv, override: false protects host env.
    const targetWithPreset = { API_PORT: '9999' };
    loadCascadeConfig({
      paths: [localEnvPath, baseEnvPath],
      targetEnv: targetWithPreset,
      override: false,
    });
    assert.equal(targetWithPreset.API_PORT, '9999', 'Existing host env must NOT be overwritten when override: false');

    // Case 1C: TRAP: When a naive caller sets override: true with [localEnv, baseEnv]
    // expecting .env.local to override .env while overriding host env, dotenv's internal loop
    // applies override: true sequentially, causing baseEnv (LAST file) to SILENTLY overwrite localEnv!
    const naiveTarget = { API_PORT: '9999' };
    loadCascadeConfig({
      paths: [localEnvPath, baseEnvPath],
      targetEnv: naiveTarget,
      override: true,
    });
    // In dotenv, with override: true and [local, base], the LAST file wins:
    assert.equal(naiveTarget.API_PORT, '3000', 'With override: true, the LAST file (.env base) silently overwrites .env.local');

    // Case 1D: The correct way to make .env.local override .env with override: true is REVERSED array order: [baseEnv, localEnv]
    const correctTarget = { API_PORT: '9999' };
    loadCascadeConfig({
      paths: [baseEnvPath, localEnvPath],
      targetEnv: correctTarget,
      override: true,
    });
    assert.equal(correctTarget.API_PORT, '8080', 'With override: true and [base, local], .env.local correctly wins and overrides host env');

    // =========================================================================
    // 2. Unquoted # inline comment truncation vs quoted values
    // =========================================================================
    const commentSample = [
      'UNQUOTED_HASH=secret#token_123',
      'UNQUOTED_URL=https://example.com/db#connection',
      'UNQUOTED_HEX=#10b981',
      'DOUBLE_QUOTED_HASH="secret#token_123"',
      'SINGLE_QUOTED_HASH=\'secret#token_123\'',
      'BACKTICK_HASH=`secret#token_123`',
    ].join('\n');

    const parsedComments = parseRawEnv(commentSample);
    // Unquoted # values are silently truncated as comments
    assert.equal(parsedComments.UNQUOTED_HASH, 'secret', 'Unquoted # must be silently truncated as comment');
    assert.equal(parsedComments.UNQUOTED_URL, 'https://example.com/db', 'Unquoted URL fragment # must be stripped');
    assert.equal(parsedComments.UNQUOTED_HEX, '', 'Unquoted hex starting with # becomes empty string');

    // Quoted values retain # intact
    assert.equal(parsedComments.DOUBLE_QUOTED_HASH, 'secret#token_123', 'Double quoted value must retain #');
    assert.equal(parsedComments.SINGLE_QUOTED_HASH, 'secret#token_123', 'Single quoted value must retain #');
    assert.equal(parsedComments.BACKTICK_HASH, 'secret#token_123', 'Backtick quoted value must retain #');

    // =========================================================================
    // 3. Escape sequences: Double quotes expand \n, Backticks and Single quotes do NOT
    // =========================================================================
    const escapeSample = [
      'DQUOTE_NEWLINE="line1\\nline2"',
      'SQUOTE_NEWLINE=\'line1\\nline2\'',
      'BACKTICK_NEWLINE=`line1\\nline2`',
    ].join('\n');

    const parsedEscapes = parseRawEnv(escapeSample);
    assert.equal(parsedEscapes.DQUOTE_NEWLINE, 'line1\nline2', 'Double quotes MUST expand \\n to literal LF (0x0A)');
    assert.equal(parsedEscapes.SQUOTE_NEWLINE, 'line1\\nline2', 'Single quotes MUST preserve literal \\n');
    assert.equal(parsedEscapes.BACKTICK_NEWLINE, 'line1\\nline2', 'Backticks MUST strip backticks but preserve literal \\n (no expansion)');

    // =========================================================================
    // 4. Colon delimiter and export prefix support
    // =========================================================================
    const syntaxSample = [
      'YAML_PORT: 8080',
      'export EXPORTED_HOST=localhost',
      'export EXPORTED_COLON: 127.0.0.1',
    ].join('\n');

    const parsedSyntax = parseRawEnv(syntaxSample);
    assert.equal(parsedSyntax.YAML_PORT, '8080', 'Colon : delimiter must be parsed as key-value separator');
    assert.equal(parsedSyntax.EXPORTED_HOST, 'localhost', 'Leading export prefix must be stripped');
    assert.equal(parsedSyntax.EXPORTED_COLON, '127.0.0.1', 'Leading export prefix with colon delimiter must work');

    // =========================================================================
    // 5. populate() return value vs target mutation semantics
    // =========================================================================
    const mutationTarget = { EXISTING_KEY: 'original' };
    const populatedReturn = populateEnv(
      mutationTarget,
      { EXISTING_KEY: 'will_be_ignored', FRESH_KEY: 'applied' },
      { override: false }
    );

    assert.equal(mutationTarget.EXISTING_KEY, 'original', 'target.EXISTING_KEY must remain unchanged');
    assert.equal(mutationTarget.FRESH_KEY, 'applied', 'target.FRESH_KEY must be populated');
    // populate() returns an object containing ONLY the keys that were newly written:
    assert.deepEqual(populatedReturn, { FRESH_KEY: 'applied' }, 'populate() must return only freshly written keys, omitting skipped existing keys');

    console.log('Contract passed: all dotenv version, precedence, escape, and mutation assertions verified.');
  } finally {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  }
}

runContract();

오리진 시더

csx-seed