diff --git a/package.json b/package.json index 899de373..b1bdac83 100644 --- a/package.json +++ b/package.json @@ -45,15 +45,9 @@ }, "dependencies": { "@nuxt/kit": "^3.20.2", - "citty": "^0.1.6", + "citty": "^0.2.0", "consola": "^3.4.2", - "destr": "^2.0.5", - "dotenv": "^17.2.3", - "git-url-parse": "^16.1.0", - "is-docker": "^4.0.0", "ofetch": "^1.5.1", - "package-manager-detector": "^1.6.0", - "pathe": "^2.0.3", "rc9": "^2.1.2", "std-env": "^3.10.0" }, @@ -62,7 +56,6 @@ "@nuxt/module-builder": "^1.0.2", "@nuxt/schema": "^3.20.2", "@nuxt/test-utils": "^3.23.0", - "@types/git-url-parse": "^16.0.2", "@vitest/coverage-v8": "^4.0.17", "changelogen": "^0.6.2", "eslint": "^9.39.2", @@ -80,6 +73,6 @@ "@nuxt/telemetry": "workspace:*" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.12.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 386e433d..79d5ad02 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,32 +15,14 @@ importers: specifier: ^3.20.2 version: 3.20.2(magicast@0.5.1) citty: - specifier: ^0.1.6 - version: 0.1.6 + specifier: ^0.2.0 + version: 0.2.0 consola: specifier: ^3.4.2 version: 3.4.2 - destr: - specifier: ^2.0.5 - version: 2.0.5 - dotenv: - specifier: ^17.2.3 - version: 17.2.3 - git-url-parse: - specifier: ^16.1.0 - version: 16.1.0 - is-docker: - specifier: ^4.0.0 - version: 4.0.0 ofetch: specifier: ^1.5.1 version: 1.5.1 - package-manager-detector: - specifier: ^1.6.0 - version: 1.6.0 - pathe: - specifier: ^2.0.3 - version: 2.0.3 rc9: specifier: ^2.1.2 version: 2.1.2 @@ -60,9 +42,6 @@ importers: '@nuxt/test-utils': specifier: ^3.23.0 version: 3.23.0(magicast@0.5.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@24.10.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) - '@types/git-url-parse': - specifier: ^16.0.2 - version: 16.0.2 '@vitest/coverage-v8': specifier: ^4.0.17 version: 4.0.18(vitest@4.0.18(@types/node@24.10.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) @@ -1571,10 +1550,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/git-url-parse@16.0.2': - resolution: {integrity: sha512-STa+QaJtPbqwtioDIncRyft1xXrsTIYW0KkS6RS6l++NiDaQxqgsIkNp2Jf9nJ1KnCGPOvWaR1iO+B7LkM8+ew==} - deprecated: This is a stub types definition. git-url-parse provides its own type definitions, so you do not need this installed. - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1584,10 +1559,6 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/parse-path@7.1.0': - resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} - deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. - '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2131,6 +2102,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -2746,12 +2720,6 @@ packages: resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} hasBin: true - git-up@8.1.1: - resolution: {integrity: sha512-FDenSF3fVqBYSaJoYy1KSc2wosx0gCvKP+c+PRBht7cAaiCeQlBtfBDX9vgnNOHmdePlSFITVcn4pFfcgNvx3g==} - - git-url-parse@16.1.0: - resolution: {integrity: sha512-cPLz4HuK86wClEW7iDdeAKcCVlWXmrLpb2L+G9goW0Z1dtpNS6BXXSOckUTlJT/LDQViE1QZKstNORzHsLnobw==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2762,6 +2730,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true global-directory@4.0.1: @@ -2921,11 +2890,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-docker@4.0.0: - resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} - engines: {node: '>=20'} - hasBin: true - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2961,9 +2925,6 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-ssh@1.4.1: - resolution: {integrity: sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -3460,16 +3421,9 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} - parse-path@7.1.0: - resolution: {integrity: sha512-EuCycjZtfPcjWk7KTksnJ5xPMvWGA/6i4zrLYhRG0hGvC3GPU/jGUj3Cy+ZR0v30duV3e23R95T1lE2+lsndSw==} - parse-statements@1.0.11: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} - parse-url@9.2.0: - resolution: {integrity: sha512-bCgsFI+GeGWPAvAiUv63ZorMeif3/U0zaXABGJbOWt5OH2KCaPHF6S+0ok4aqM9RuIPGyZdx9tR9l13PsW4AYQ==} - engines: {node: '>=14.13.0'} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3732,9 +3686,6 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - protocols@2.0.2: - resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4128,7 +4079,7 @@ packages: tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me terser@5.44.1: resolution: {integrity: sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==} @@ -6090,10 +6041,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/git-url-parse@16.0.2': - dependencies: - git-url-parse: 16.1.0 - '@types/json-schema@7.0.15': {} '@types/node@24.10.1': @@ -6102,10 +6049,6 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@types/parse-path@7.1.0': - dependencies: - parse-path: 7.1.0 - '@types/resolve@1.20.2': {} '@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': @@ -6741,6 +6684,8 @@ snapshots: dependencies: consola: 3.4.2 + citty@0.2.0: {} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -7402,15 +7347,6 @@ snapshots: nypm: 0.6.2 pathe: 2.0.3 - git-up@8.1.1: - dependencies: - is-ssh: 1.4.1 - parse-url: 9.2.0 - - git-url-parse@16.1.0: - dependencies: - git-up: 8.1.1 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7587,8 +7523,6 @@ snapshots: is-docker@3.0.0: {} - is-docker@4.0.0: {} - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -7616,10 +7550,6 @@ snapshots: dependencies: '@types/estree': 1.0.8 - is-ssh@1.4.1: - dependencies: - protocols: 2.0.2 - is-stream@2.0.1: {} is-stream@3.0.0: {} @@ -8351,17 +8281,8 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 - parse-path@7.1.0: - dependencies: - protocols: 2.0.2 - parse-statements@1.0.11: {} - parse-url@9.2.0: - dependencies: - '@types/parse-path': 7.1.0 - parse-path: 7.1.0 - parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -8589,8 +8510,6 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 - protocols@2.0.2: {} - punycode@2.3.1: {} quansync@0.2.11: {} diff --git a/src/cli.ts b/src/cli.ts index d9dcb3e0..6ee2a3be 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,26 +1,47 @@ import { existsSync, readFileSync } from 'node:fs' import { homedir } from 'node:os' +import { resolve } from 'node:path' -import { resolve } from 'pathe' -import { destr } from 'destr' import * as rc from 'rc9' import { colors as c } from 'consola/utils' import { consola } from 'consola' import { loadNuxtConfig } from '@nuxt/kit' import { isTest } from 'std-env' -import { parse as parseDotenv } from 'dotenv' import { createMain, defineCommand } from 'citty' import { version } from '../package.json' import { consentVersion } from './meta' import { ensureUserconsent } from './consent' +function isTruthy(val: unknown): boolean { + return val === true || val === 'true' || val === '1' || val === 1 +} + +function parseDotenv(src: string): Record { + const result: Record = {} + for (const line of src.split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIndex = trimmed.indexOf('=') + if (eqIndex === -1) continue + const key = trimmed.slice(0, eqIndex).trim() + let value = trimmed.slice(eqIndex + 1).trim() + // Remove surrounding quotes + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith('\'') && value.endsWith('\''))) { + value = value.slice(1, -1) + } + result[key] = value + } + return result +} + const RC_FILENAME = '.nuxtrc' const sharedArgs = { global: { type: 'boolean', alias: 'g', + default: false, description: 'Apply globally', }, dir: { @@ -103,14 +124,14 @@ async function _checkDisabled(dir: string): Promise return 'because you are running in a test environment' } - if (destr(process.env.NUXT_TELEMETRY_DISABLED)) { + if (isTruthy(process.env.NUXT_TELEMETRY_DISABLED)) { return 'by the `NUXT_TELEMETRY_DISABLED` environment variable' } const dotenvFile = resolve(dir, '.env') if (existsSync(dotenvFile)) { - const _env = parseDotenv(readFileSync(dotenvFile)) - if (destr(_env.NUXT_TELEMETRY_DISABLED)) { + const _env = parseDotenv(readFileSync(dotenvFile, 'utf8')) + if (isTruthy(_env.NUXT_TELEMETRY_DISABLED)) { return 'by the `NUXT_TELEMETRY_DISABLED` environment variable set in ' + dotenvFile } } diff --git a/src/consent.ts b/src/consent.ts index 7879627d..15e9caf6 100644 --- a/src/consent.ts +++ b/src/consent.ts @@ -1,10 +1,10 @@ import { colors as c } from 'consola/utils' import { consola } from 'consola' import { isMinimal } from 'std-env' -import isDocker from 'is-docker' import { updateUserNuxtRc } from './utils/nuxtrc' import type { TelemetryOptions } from './types' import { consentVersion } from './meta' +import { isDocker } from './utils/is-docker' export async function ensureUserconsent(options: TelemetryOptions): Promise { if (options.consent && options.consent >= consentVersion) { diff --git a/src/context.ts b/src/context.ts index afed8824..9b1ef9f8 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,25 +1,24 @@ import os from 'node:os' +import { existsSync, readFileSync } from 'node:fs' import { execSync } from 'node:child_process' -import gitUrlParse from 'git-url-parse' -import { getNuxtVersion, isNuxt3 } from '@nuxt/kit' -import isDocker from 'is-docker' +import { getNuxtVersion, isNuxtMajorVersion } from '@nuxt/kit' import { provider } from 'std-env' import type { Nuxt } from '@nuxt/schema' -import { detect } from 'package-manager-detector' import type { Context, GitData, TelemetryOptions } from './types' import { hash } from './utils/hash' +import { isDocker } from './utils/is-docker' export async function createContext(nuxt: Nuxt, options: Required): Promise { - const rootDir = nuxt.options.rootDir || process.cwd() + const rootDir = nuxt.options.workspaceDir || nuxt.options.rootDir || process.cwd() const git = await getGit(rootDir) - const packageManager = await detect({ cwd: rootDir }) + const packageManager = detectPackageManager(rootDir) const { seed } = options - const projectHash = await getProjectHash(rootDir, git, seed) + const projectHash = getProjectHash(rootDir, git, seed) const projectSession = getProjectSession(projectHash, seed) const nuxtVersion = getNuxtVersion(nuxt) - const nuxtMajorVersion = isNuxt3(nuxt) ? 3 : 2 + const nuxtMajorVersion = isNuxtMajorVersion(2, nuxt) ? 2 : nuxt.options._majorVersion const nodeVersion = process.version.replace('v', '') const isEdge = nuxtVersion.includes('edge') @@ -36,7 +35,7 @@ export async function createContext(nuxt: Nuxt, options: Required { return gitRemoteUrl } +function detectPackageManager(rootDir: string): string { + // Check lockfiles first (most reliable signal) + const lockFiles: Record = { + 'bun.lockb': 'bun', + 'bun.lock': 'bun', + 'deno.lock': 'deno', + 'pnpm-lock.yaml': 'pnpm', + 'pnpm-workspace.yaml': 'pnpm', + 'yarn.lock': 'yarn', + 'package-lock.json': 'npm', + 'npm-shrinkwrap.json': 'npm', + } + for (const [file, manager] of Object.entries(lockFiles)) { + if (existsSync(`${rootDir}/${file}`)) { + return manager + } + } + + // Fall back to packageManager field in package.json (Corepack standard) + try { + const pkgJson = JSON.parse(readFileSync(`${rootDir}/package.json`, 'utf8')) + if (typeof pkgJson.packageManager === 'string') { + const name = pkgJson.packageManager.split('@')[0] + if (name) return name + } + } + catch { + // ignore + } + + return 'unknown' +} + +function parseGitUrl(gitUrl: string): { source: string, owner: string, name: string } | null { + // Normalize SSH URLs: git@github.com:owner/repo.git -> github.com/owner/repo + const normalized = gitUrl.trim() + + // Handle SSH format: git@host:owner/repo.git + const sshMatch = normalized.match(/^[\w-]+@([^:]+):(.+?)(?:\.git)?$/) + if (sshMatch) { + const [, source, path] = sshMatch + const parts = path.split('/') + if (parts.length >= 2) { + return { source, owner: parts.slice(0, -1).join('/'), name: parts[parts.length - 1] } + } + } + + // Handle HTTPS/Git protocol: https://github.com/owner/repo.git or git://... + try { + const url = new URL(normalized) + const pathname = url.pathname.replace(/\.git$/, '').replace(/^\//, '') + const parts = pathname.split('/') + if (parts.length >= 2) { + return { source: url.hostname, owner: parts.slice(0, -1).join('/'), name: parts[parts.length - 1] } + } + } + catch { + // Not a valid URL + } + + return null +} + async function getGit(rootDir: string): Promise { const gitRemote = await getGitRemote(rootDir) @@ -110,11 +172,13 @@ async function getGit(rootDir: string): Promise { return } - const meta = gitUrlParse(gitRemote) - const url = meta.toString('https') + const meta = parseGitUrl(gitRemote) + if (!meta) { + return + } return { - url, + url: `https://${meta.source}/${meta.owner}/${meta.name}`, gitRemote, source: meta.source, owner: meta.owner, diff --git a/src/events/files.ts b/src/events/files.ts index d0f951e2..03882646 100644 --- a/src/events/files.ts +++ b/src/events/files.ts @@ -1,5 +1,5 @@ import fs from 'node:fs' -import { resolve } from 'pathe' +import { resolve } from 'node:path' import { resolvePath } from '@nuxt/kit' import type { EventFactory } from '../types' diff --git a/src/module.ts b/src/module.ts index 593d97c8..a19c18c1 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,4 +1,3 @@ -import { destr } from 'destr' import { defineNuxtModule } from '@nuxt/kit' import { updateUserNuxtRc } from './utils/nuxtrc' import { Telemetry } from './telemetry' @@ -16,7 +15,7 @@ export default defineNuxtModule({ }, defaults: { endpoint: process.env.NUXT_TELEMETRY_ENDPOINT || 'https://telemetry.nuxt.com', - debug: destr(process.env.NUXT_TELEMETRY_DEBUG), + debug: process.env.NUXT_TELEMETRY_DEBUG === '1' || process.env.NUXT_TELEMETRY_DEBUG === 'true', enabled: undefined as any, seed: undefined as any, }, diff --git a/src/types.ts b/src/types.ts index d6fdf46e..3f80e93a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,7 @@ export interface Context { projectHash: string projectSession: string nuxtVersion: string - nuxtMajorVersion: 2 | 3 + nuxtMajorVersion: number isEdge: boolean nodeVersion: string os: string diff --git a/src/utils/is-docker.ts b/src/utils/is-docker.ts new file mode 100644 index 00000000..91ead052 --- /dev/null +++ b/src/utils/is-docker.ts @@ -0,0 +1,37 @@ +// based on is-docker by Sindre Sorhus (https://github.com/sindresorhus/is-docker) +import { readFileSync, statSync } from 'node:fs' + +let isDockerCached: boolean | undefined + +function hasDockerEnv(): boolean { + try { + statSync('/.dockerenv') + return true + } + catch { + return false + } +} + +function hasDockerCGroup(): boolean { + try { + return readFileSync('/proc/self/cgroup', 'utf8').includes('docker') + } + catch { + return false + } +} + +function hasDockerMountInfo(): boolean { + try { + return readFileSync('/proc/self/mountinfo', 'utf8').includes('/docker/containers/') + } + catch { + return false + } +} + +export function isDocker(): boolean { + isDockerCached ??= hasDockerEnv() || hasDockerCGroup() || hasDockerMountInfo() + return isDockerCached +}