larry babby and threejs for glsl

This commit is contained in:
Sam
2024-06-24 21:24:00 +12:00
parent 87d5dc634d
commit 907ebae4c0
6474 changed files with 1279596 additions and 8 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-present Devon Govett
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,22 @@
import type {FilePath} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
import type {PackageInstaller, PackageManager} from './lib/types';
export * from './lib/types';
export const Npm: {
new (): PackageInstaller;
};
export const Pnpm: {
new (): PackageInstaller;
};
export const Yarn: {
new (): PackageInstaller;
};
export const MockPackageInstaller: {
new (): PackageInstaller;
};
export const NodePackageManager: {
new (fs: FileSystem, projectRoot: FilePath, installer?: PackageInstaller): PackageManager;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
import type { FilePath, FileCreateInvalidation, SemverRange, DependencySpecifier, PackageJSON } from "@parcel/types";
import type { FileSystem } from "@parcel/fs";
export type ResolveResult = {
resolved: FilePath | DependencySpecifier;
pkg?: PackageJSON | null | undefined;
invalidateOnFileCreate: Array<FileCreateInvalidation>;
invalidateOnFileChange: Set<FilePath>;
type: number;
};
export type InstallOptions = {
installPeers?: boolean;
saveDev?: boolean;
packageInstaller?: PackageInstaller | null | undefined;
};
export type InstallerOptions = {
modules: Array<ModuleRequest>;
fs: FileSystem;
cwd: FilePath;
packagePath?: FilePath | null | undefined;
saveDev?: boolean;
};
export interface PackageInstaller {
install(opts: InstallerOptions): Promise<void>;
}
export type Invalidations = {
invalidateOnFileCreate: Array<FileCreateInvalidation>;
invalidateOnFileChange: Set<FilePath>;
invalidateOnStartup: boolean;
};
export interface PackageManager {
require(id: DependencySpecifier, from: FilePath, arg2: {
range?: SemverRange | null | undefined;
shouldAutoInstall?: boolean;
saveDev?: boolean;
} | null | undefined): Promise<any>;
resolve(id: DependencySpecifier, from: FilePath, arg2: {
range?: SemverRange | null | undefined;
shouldAutoInstall?: boolean;
saveDev?: boolean;
} | null | undefined): Promise<ResolveResult>;
getInvalidations(id: DependencySpecifier, from: FilePath): Invalidations;
invalidate(id: DependencySpecifier, from: FilePath): void;
}
export type ModuleRequest = {
readonly name: string;
readonly range: SemverRange | null | undefined;
};

View File

@@ -0,0 +1,72 @@
{
"name": "@parcel/package-manager",
"version": "2.12.0",
"description": "Blazing fast, zero configuration web application bundler",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"repository": {
"type": "git",
"url": "https://github.com/parcel-bundler/parcel.git"
},
"main": "lib/index.js",
"source": "src/index.js",
"types": "index.d.ts",
"engines": {
"node": ">= 12.0.0"
},
"scripts": {
"build-ts": "mkdir -p lib && flow-to-ts src/types.js > lib/types.d.ts",
"check-ts": "tsc --noEmit index.d.ts",
"test": "mocha test"
},
"targets": {
"types": false,
"main": {
"includeNodeModules": {
"@parcel/core": false,
"@parcel/diagnostic": false,
"@parcel/fs": false,
"@parcel/logger": false,
"@parcel/node-resolver-core": false,
"@parcel/types": false,
"@parcel/utils": false,
"@parcel/workers": false,
"@swc/core": false,
"semver": false
}
}
},
"dependencies": {
"@parcel/diagnostic": "2.12.0",
"@parcel/fs": "2.12.0",
"@parcel/logger": "2.12.0",
"@parcel/node-resolver-core": "3.3.0",
"@parcel/types": "2.12.0",
"@parcel/utils": "2.12.0",
"@parcel/workers": "2.12.0",
"@swc/core": "^1.3.36",
"semver": "^7.5.2"
},
"devDependencies": {
"command-exists": "^1.2.6",
"cross-spawn": "^6.0.4",
"nullthrows": "^1.1.1",
"split2": "^3.1.1"
},
"peerDependencies": {
"@parcel/core": "^2.12.0"
},
"browser": {
"./src/NodePackageManager.js": false,
"./src/Npm.js": false,
"./src/Pnpm.js": false,
"./src/Yarn.js": false
},
"gitHead": "2059029ee91e5f03a273b0954d3e629d7375f986"
}

View File

@@ -0,0 +1,39 @@
// @flow strict-local
import type {JSONObject} from '@parcel/types';
import logger from '@parcel/logger';
import {Transform} from 'stream';
// Transforms chunks of json strings to parsed objects.
// Pair with split2 to parse stream of newline-delimited text.
export default class JSONParseStream extends Transform {
constructor(options: mixed) {
super({...options, objectMode: true});
}
// $FlowFixMe We are in object mode, so we emit objects, not strings
_transform(
chunk: Buffer | string,
encoding: string,
callback: (err: ?Error, parsed: ?JSONObject) => mixed,
) {
try {
let parsed;
try {
parsed = JSON.parse(chunk.toString());
} catch (e) {
// Be permissive and ignoreJSON parse errors in case there was
// a non-JSON line in the package manager's stdout.
logger.verbose({
message: 'Ignored invalid JSON message: ' + chunk.toString(),
origin: '@parcel/package-manager',
});
return;
}
callback(null, parsed);
} catch (err) {
callback(err);
}
}
}

View File

@@ -0,0 +1,90 @@
// @flow
import type {ModuleRequest, PackageInstaller, InstallerOptions} from './types';
import type {FileSystem} from '@parcel/fs';
import type {FilePath} from '@parcel/types';
import path from 'path';
import {ncp} from '@parcel/fs';
import {registerSerializableClass} from '@parcel/core';
import pkg from '../package.json';
import {moduleRequestsFromDependencyMap} from './utils';
type Package = {|
fs: FileSystem,
packagePath: FilePath,
|};
// This PackageInstaller implementation simply copies files from one filesystem to another.
// Mostly useful for testing purposes.
export class MockPackageInstaller implements PackageInstaller {
packages: Map<string, Package> = new Map<string, Package>();
register(packageName: string, fs: FileSystem, packagePath: FilePath) {
this.packages.set(packageName, {fs, packagePath});
}
async install({
modules,
fs,
cwd,
packagePath,
saveDev = true,
}: InstallerOptions): Promise<void> {
if (packagePath == null) {
packagePath = path.join(cwd, 'package.json');
await fs.writeFile(packagePath, '{}');
}
let pkg = JSON.parse(await fs.readFile(packagePath, 'utf8'));
let key = saveDev ? 'devDependencies' : 'dependencies';
if (!pkg[key]) {
pkg[key] = {};
}
for (let module of modules) {
pkg[key][module.name] =
'^' + (await this.installPackage(module, fs, packagePath));
}
await fs.writeFile(packagePath, JSON.stringify(pkg));
}
async installPackage(
moduleRequest: ModuleRequest,
fs: FileSystem,
packagePath: FilePath,
): Promise<any> {
let pkg = this.packages.get(moduleRequest.name);
if (!pkg) {
throw new Error('Unknown package ' + moduleRequest.name);
}
let dest = path.join(
path.dirname(packagePath),
'node_modules',
moduleRequest.name,
);
await ncp(pkg.fs, pkg.packagePath, fs, dest);
let packageJSON = JSON.parse(
await fs.readFile(path.join(dest, 'package.json'), 'utf8'),
);
if (packageJSON.dependencies != null) {
for (let dep of moduleRequestsFromDependencyMap(
packageJSON.dependencies,
)) {
await this.installPackage(dep, fs, packagePath);
}
}
return packageJSON.version;
}
}
registerSerializableClass(
`${pkg.version}:MockPackageInstaller`,
MockPackageInstaller,
);

View File

@@ -0,0 +1,629 @@
// @flow
import type {FilePath, DependencySpecifier, SemverRange} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
import type {
ModuleRequest,
PackageManager,
PackageInstaller,
InstallOptions,
Invalidations,
} from './types';
import type {ResolveResult} from './types';
import {registerSerializableClass} from '@parcel/core';
import ThrowableDiagnostic, {
encodeJSONKeyComponent,
escapeMarkdown,
generateJSONCodeHighlights,
md,
} from '@parcel/diagnostic';
import {NodeFS} from '@parcel/fs';
import nativeFS from 'fs';
import Module from 'module';
import path from 'path';
import semver from 'semver';
import logger from '@parcel/logger';
import nullthrows from 'nullthrows';
import {getModuleParts} from '@parcel/utils';
import {getConflictingLocalDependencies} from './utils';
import {installPackage} from './installPackage';
import pkg from '../package.json';
import {ResolverBase} from '@parcel/node-resolver-core';
import {pathToFileURL} from 'url';
import {transformSync} from '@swc/core';
// Package.json fields. Must match package_json.rs.
const MAIN = 1 << 0;
const SOURCE = 1 << 2;
const ENTRIES =
MAIN |
(process.env.PARCEL_BUILD_ENV !== 'production' ||
process.env.PARCEL_SELF_BUILD
? SOURCE
: 0);
const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
// There can be more than one instance of NodePackageManager, but node has only a single module cache.
// Therefore, the resolution cache and the map of parent to child modules should also be global.
const cache = new Map<DependencySpecifier, ResolveResult>();
const children = new Map<FilePath, Set<DependencySpecifier>>();
const invalidationsCache = new Map<string, Invalidations>();
// This implements a package manager for Node by monkey patching the Node require
// algorithm so that it uses the specified FileSystem instead of the native one.
// It also handles installing packages when they are required if not already installed.
// See https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js
// for reference to Node internals.
export class NodePackageManager implements PackageManager {
fs: FileSystem;
projectRoot: FilePath;
installer: ?PackageInstaller;
resolver: ResolverBase;
currentExtensions: Array<string>;
constructor(
fs: FileSystem,
projectRoot: FilePath,
installer?: ?PackageInstaller,
) {
this.fs = fs;
this.projectRoot = projectRoot;
this.installer = installer;
// $FlowFixMe - no type for _extensions
this.currentExtensions = Object.keys(Module._extensions).map(e =>
e.substring(1),
);
}
_createResolver(): ResolverBase {
return new ResolverBase(this.projectRoot, {
fs:
this.fs instanceof NodeFS && process.versions.pnp == null
? undefined
: {
canonicalize: path => this.fs.realpathSync(path),
read: path => this.fs.readFileSync(path),
isFile: path => this.fs.statSync(path).isFile(),
isDir: path => this.fs.statSync(path).isDirectory(),
},
mode: 2,
entries: ENTRIES,
packageExports: true,
moduleDirResolver:
process.versions.pnp != null
? (module, from) => {
// $FlowFixMe[prop-missing]
let pnp = Module.findPnpApi(path.dirname(from));
return pnp.resolveToUnqualified(
// append slash to force loading builtins from npm
module + '/',
from,
);
}
: undefined,
extensions: this.currentExtensions,
typescript: true,
});
}
static deserialize(opts: any): NodePackageManager {
return new NodePackageManager(opts.fs, opts.projectRoot, opts.installer);
}
serialize(): {|
$$raw: boolean,
fs: FileSystem,
projectRoot: FilePath,
installer: ?PackageInstaller,
|} {
return {
$$raw: false,
fs: this.fs,
projectRoot: this.projectRoot,
installer: this.installer,
};
}
async require(
name: DependencySpecifier,
from: FilePath,
opts: ?{|
range?: ?SemverRange,
shouldAutoInstall?: boolean,
saveDev?: boolean,
|},
): Promise<any> {
let {resolved, type} = await this.resolve(name, from, opts);
if (type === 2) {
logger.warn({
message: 'ES module dependencies are experimental.',
origin: '@parcel/package-manager',
codeFrames: [
{
filePath: resolved,
codeHighlights: [],
},
],
});
// On Windows, Node requires absolute paths to be file URLs.
if (process.platform === 'win32' && path.isAbsolute(resolved)) {
resolved = pathToFileURL(resolved);
}
// $FlowFixMe
return import(resolved);
}
return this.load(resolved, from);
}
requireSync(name: DependencySpecifier, from: FilePath): any {
let {resolved} = this.resolveSync(name, from);
return this.load(resolved, from);
}
load(filePath: FilePath, from: FilePath): any {
if (!path.isAbsolute(filePath)) {
// Node builtin module
// $FlowFixMe
return require(filePath);
}
// $FlowFixMe[prop-missing]
const cachedModule = Module._cache[filePath];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// $FlowFixMe
let m = new Module(filePath, Module._cache[from] || module.parent);
// $FlowFixMe _extensions not in type
const extensions = Object.keys(Module._extensions);
// This handles supported extensions changing due to, for example, esbuild/register being used
// We assume that the extension list will change in size - as these tools usually add support for
// additional extensions.
if (extensions.length !== this.currentExtensions.length) {
this.currentExtensions = extensions.map(e => e.substring(1));
this.resolver = this._createResolver();
}
// $FlowFixMe[prop-missing]
Module._cache[filePath] = m;
// Patch require within this module so it goes through our require
m.require = id => {
return this.requireSync(id, filePath);
};
// Patch `fs.readFileSync` temporarily so that it goes through our file system
let {readFileSync, statSync} = nativeFS;
// $FlowFixMe
nativeFS.readFileSync = (filename, encoding) => {
return this.fs.readFileSync(filename, encoding);
};
// $FlowFixMe
nativeFS.statSync = filename => {
return this.fs.statSync(filename);
};
if (!filePath.includes(NODE_MODULES)) {
let extname = path.extname(filePath);
if (
(extname === '.ts' || extname === '.tsx') &&
// $FlowFixMe
!Module._extensions[extname]
) {
let compile = m._compile;
m._compile = (code, filename) => {
let out = transformSync(code, {filename, module: {type: 'commonjs'}});
compile.call(m, out.code, filename);
};
// $FlowFixMe
Module._extensions[extname] = (m, filename) => {
// $FlowFixMe
delete Module._extensions[extname];
// $FlowFixMe
Module._extensions['.js'](m, filename);
};
}
}
try {
m.load(filePath);
} catch (err) {
// $FlowFixMe[prop-missing]
delete Module._cache[filePath];
throw err;
} finally {
// $FlowFixMe
nativeFS.readFileSync = readFileSync;
// $FlowFixMe
nativeFS.statSync = statSync;
}
return m.exports;
}
async resolve(
id: DependencySpecifier,
from: FilePath,
options?: ?{|
range?: ?SemverRange,
shouldAutoInstall?: boolean,
saveDev?: boolean,
|},
): Promise<ResolveResult> {
let basedir = path.dirname(from);
let key = basedir + ':' + id;
let resolved = cache.get(key);
if (!resolved) {
let [name] = getModuleParts(id);
try {
resolved = this.resolveInternal(id, from);
} catch (e) {
if (
e.code !== 'MODULE_NOT_FOUND' ||
options?.shouldAutoInstall !== true ||
id.startsWith('.') // a local file, don't autoinstall
) {
if (
e.code === 'MODULE_NOT_FOUND' &&
options?.shouldAutoInstall !== true
) {
let err = new ThrowableDiagnostic({
diagnostic: {
message: escapeMarkdown(e.message),
hints: [
'Autoinstall is disabled, please install this package manually and restart Parcel.',
],
},
});
// $FlowFixMe - needed for loadParcelPlugin
err.code = 'MODULE_NOT_FOUND';
throw err;
} else {
throw e;
}
}
let conflicts = await getConflictingLocalDependencies(
this.fs,
name,
from,
this.projectRoot,
);
if (conflicts == null) {
this.invalidate(id, from);
await this.install([{name, range: options?.range}], from, {
saveDev: options?.saveDev ?? true,
});
return this.resolve(id, from, {
...options,
shouldAutoInstall: false,
});
}
throw new ThrowableDiagnostic({
diagnostic: conflicts.fields.map(field => ({
message: md`Could not find module "${name}", but it was listed in package.json. Run your package manager first.`,
origin: '@parcel/package-manager',
codeFrames: [
{
filePath: conflicts.filePath,
language: 'json',
code: conflicts.json,
codeHighlights: generateJSONCodeHighlights(conflicts.json, [
{
key: `/${field}/${encodeJSONKeyComponent(name)}`,
type: 'key',
message: 'Defined here, but not installed',
},
]),
},
],
})),
});
}
let range = options?.range;
if (range != null) {
let pkg = resolved.pkg;
if (pkg == null || !semver.satisfies(pkg.version, range)) {
let conflicts = await getConflictingLocalDependencies(
this.fs,
name,
from,
this.projectRoot,
);
if (conflicts == null && options?.shouldAutoInstall === true) {
this.invalidate(id, from);
await this.install([{name, range}], from);
return this.resolve(id, from, {
...options,
shouldAutoInstall: false,
});
} else if (conflicts != null) {
throw new ThrowableDiagnostic({
diagnostic: {
message: md`Could not find module "${name}" satisfying ${range}.`,
origin: '@parcel/package-manager',
codeFrames: [
{
filePath: conflicts.filePath,
language: 'json',
code: conflicts.json,
codeHighlights: generateJSONCodeHighlights(
conflicts.json,
conflicts.fields.map(field => ({
key: `/${field}/${encodeJSONKeyComponent(name)}`,
type: 'key',
message: 'Found this conflicting local requirement.',
})),
),
},
],
},
});
}
let version = pkg?.version;
let message = md`Could not resolve package "${name}" that satisfies ${range}.`;
if (version != null) {
message += md` Found ${version}.`;
}
throw new ThrowableDiagnostic({
diagnostic: {
message,
hints: [
'Looks like the incompatible version was installed transitively. Add this package as a direct dependency with a compatible version range.',
],
},
});
}
}
cache.set(key, resolved);
invalidationsCache.clear();
// Add the specifier as a child to the parent module.
// Don't do this if the specifier was an absolute path, as this was likely a dynamically resolved path
// (e.g. babel uses require() to load .babelrc.js configs and we don't want them to be added as children of babel itself).
if (!path.isAbsolute(name)) {
let moduleChildren = children.get(from);
if (!moduleChildren) {
moduleChildren = new Set();
children.set(from, moduleChildren);
}
moduleChildren.add(name);
}
}
return resolved;
}
resolveSync(name: DependencySpecifier, from: FilePath): ResolveResult {
let basedir = path.dirname(from);
let key = basedir + ':' + name;
let resolved = cache.get(key);
if (!resolved) {
resolved = this.resolveInternal(name, from);
cache.set(key, resolved);
invalidationsCache.clear();
if (!path.isAbsolute(name)) {
let moduleChildren = children.get(from);
if (!moduleChildren) {
moduleChildren = new Set();
children.set(from, moduleChildren);
}
moduleChildren.add(name);
}
}
return resolved;
}
async install(
modules: Array<ModuleRequest>,
from: FilePath,
opts?: InstallOptions,
) {
await installPackage(this.fs, this, modules, from, this.projectRoot, {
packageInstaller: this.installer,
...opts,
});
}
getInvalidations(name: DependencySpecifier, from: FilePath): Invalidations {
let basedir = path.dirname(from);
let cacheKey = basedir + ':' + name;
let resolved = cache.get(cacheKey);
if (resolved && path.isAbsolute(resolved.resolved)) {
let cached = invalidationsCache.get(resolved.resolved);
if (cached != null) {
return cached;
}
let res = {
invalidateOnFileCreate: [],
invalidateOnFileChange: new Set(),
invalidateOnStartup: false,
};
let seen = new Set();
let addKey = (name, from) => {
let basedir = path.dirname(from);
let key = basedir + ':' + name;
if (seen.has(key)) {
return;
}
seen.add(key);
let resolved = cache.get(key);
if (!resolved || !path.isAbsolute(resolved.resolved)) {
return;
}
res.invalidateOnFileCreate.push(...resolved.invalidateOnFileCreate);
res.invalidateOnFileChange.add(resolved.resolved);
for (let file of resolved.invalidateOnFileChange) {
res.invalidateOnFileChange.add(file);
}
let moduleChildren = children.get(resolved.resolved);
if (moduleChildren) {
for (let specifier of moduleChildren) {
addKey(specifier, resolved.resolved);
}
}
};
addKey(name, from);
// If this is an ES module, we won't have any of the dependencies because import statements
// cannot be intercepted. Instead, ask the resolver to parse the file and recursively analyze the deps.
if (resolved.type === 2) {
let invalidations = this.resolver.getInvalidations(resolved.resolved);
invalidations.invalidateOnFileChange.forEach(i =>
res.invalidateOnFileChange.add(i),
);
invalidations.invalidateOnFileCreate.forEach(i =>
res.invalidateOnFileCreate.push(i),
);
res.invalidateOnStartup ||= invalidations.invalidateOnStartup;
if (res.invalidateOnStartup) {
logger.warn({
message: md`${path.relative(
this.projectRoot,
resolved.resolved,
)} contains non-statically analyzable dependencies in its module graph. This causes Parcel to invalidate the cache on startup.`,
origin: '@parcel/package-manager',
});
}
}
invalidationsCache.set(resolved.resolved, res);
return res;
}
return {
invalidateOnFileCreate: [],
invalidateOnFileChange: new Set(),
invalidateOnStartup: false,
};
}
invalidate(name: DependencySpecifier, from: FilePath) {
let seen = new Set();
let invalidate = (name, from) => {
let basedir = path.dirname(from);
let key = basedir + ':' + name;
if (seen.has(key)) {
return;
}
seen.add(key);
let resolved = cache.get(key);
if (!resolved || !path.isAbsolute(resolved.resolved)) {
return;
}
invalidationsCache.delete(resolved.resolved);
// $FlowFixMe
let module = Module._cache[resolved.resolved];
if (module) {
// $FlowFixMe
delete Module._cache[resolved.resolved];
}
let moduleChildren = children.get(resolved.resolved);
if (moduleChildren) {
for (let specifier of moduleChildren) {
invalidate(specifier, resolved.resolved);
}
}
children.delete(resolved.resolved);
cache.delete(key);
};
invalidate(name, from);
this.resolver = this._createResolver();
}
resolveInternal(name: string, from: string): ResolveResult {
if (this.resolver == null) {
this.resolver = this._createResolver();
}
let res = this.resolver.resolve({
filename: name,
specifierType: 'commonjs',
parent: from,
});
// Invalidate whenever the .pnp.js file changes.
// TODO: only when we actually resolve a node_modules package?
if (process.versions.pnp != null && res.invalidateOnFileChange) {
// $FlowFixMe[prop-missing]
let pnp = Module.findPnpApi(path.dirname(from));
res.invalidateOnFileChange.push(pnp.resolveToUnqualified('pnpapi', null));
}
if (res.error) {
let e = new Error(`Could not resolve module "${name}" from "${from}"`);
// $FlowFixMe
e.code = 'MODULE_NOT_FOUND';
throw e;
}
let getPkg;
switch (res.resolution.type) {
case 'Path':
getPkg = () => {
let pkgPath = this.fs.findAncestorFile(
['package.json'],
nullthrows(res.resolution.value),
this.projectRoot,
);
return pkgPath
? JSON.parse(this.fs.readFileSync(pkgPath, 'utf8'))
: null;
};
// fallthrough
case 'Builtin':
return {
resolved: res.resolution.value,
invalidateOnFileChange: new Set(res.invalidateOnFileChange),
invalidateOnFileCreate: res.invalidateOnFileCreate,
type: res.moduleType,
get pkg() {
return getPkg();
},
};
default:
throw new Error('Unknown resolution type');
}
}
}
registerSerializableClass(
`${pkg.version}:NodePackageManager`,
NodePackageManager,
);

View File

@@ -0,0 +1,94 @@
// @flow strict-local
import type {PackageInstaller, InstallerOptions} from './types';
import path from 'path';
import spawn from 'cross-spawn';
import logger from '@parcel/logger';
import promiseFromProcess from './promiseFromProcess';
import {registerSerializableClass} from '@parcel/core';
import {npmSpecifierFromModuleRequest} from './utils';
// $FlowFixMe
import pkg from '../package.json';
const NPM_CMD = 'npm';
export class Npm implements PackageInstaller {
async install({
modules,
cwd,
fs,
packagePath,
saveDev = true,
}: InstallerOptions): Promise<void> {
// npm doesn't auto-create a package.json when installing,
// so create an empty one if needed.
if (packagePath == null) {
await fs.writeFile(path.join(cwd, 'package.json'), '{}');
}
let args = ['install', '--json', saveDev ? '--save-dev' : '--save'].concat(
modules.map(npmSpecifierFromModuleRequest),
);
// When Parcel is run by npm (e.g. via package.json scripts), several environment variables are
// added. When parcel in turn calls npm again, these can cause npm to behave stragely, so we
// filter them out when installing packages.
let env = {};
for (let key in process.env) {
if (!key.startsWith('npm_') && key !== 'INIT_CWD' && key !== 'NODE_ENV') {
env[key] = process.env[key];
}
}
let installProcess = spawn(NPM_CMD, args, {cwd, env});
let stdout = '';
installProcess.stdout.on('data', (buf: Buffer) => {
stdout += buf.toString();
});
let stderr = [];
installProcess.stderr.on('data', (buf: Buffer) => {
stderr.push(buf.toString().trim());
});
try {
await promiseFromProcess(installProcess);
let results: NPMResults = JSON.parse(stdout);
let addedCount = results.added.length;
if (addedCount > 0) {
logger.log({
origin: '@parcel/package-manager',
message: `Added ${addedCount} packages via npm`,
});
}
// Since we succeeded, stderr might have useful information not included
// in the json written to stdout. It's also not necessary to log these as
// errors as they often aren't.
for (let message of stderr) {
if (message.length > 0) {
logger.log({
origin: '@parcel/package-manager',
message,
});
}
}
} catch (e) {
throw new Error(
'npm failed to install modules: ' +
e.message +
' - ' +
stderr.join('\n'),
);
}
}
}
type NPMResults = {|
added: Array<{name: string, ...}>,
|};
registerSerializableClass(`${pkg.version}:Npm`, Npm);

View File

@@ -0,0 +1,194 @@
// @flow strict-local
import type {PackageInstaller, InstallerOptions} from './types';
import path from 'path';
import fs from 'fs';
import commandExists from 'command-exists';
import spawn from 'cross-spawn';
import logger from '@parcel/logger';
import split from 'split2';
import JSONParseStream from './JSONParseStream';
import promiseFromProcess from './promiseFromProcess';
import {registerSerializableClass} from '@parcel/core';
import {exec, npmSpecifierFromModuleRequest} from './utils';
// $FlowFixMe
import pkg from '../package.json';
const PNPM_CMD = 'pnpm';
type LogLevel = 'error' | 'warn' | 'info' | 'debug';
type ErrorLog = {|
err: {|
message: string,
code: string,
stack: string,
|},
|};
type PNPMLog =
| {|
+name: 'pnpm:progress',
packageId: string,
status: 'fetched' | 'found_in_store' | 'resolved',
|}
| {|
+name: 'pnpm:root',
added?: {|
id?: string,
name: string,
realName: string,
version?: string,
dependencyType?: 'prod' | 'dev' | 'optional',
latest?: string,
linkedFrom?: string,
|},
removed?: {|
name: string,
version?: string,
dependencyType?: 'prod' | 'dev' | 'optional',
|},
|}
| {|+name: 'pnpm:importing', from: string, method: string, to: string|}
| {|+name: 'pnpm:link', target: string, link: string|}
| {|+name: 'pnpm:stats', prefix: string, removed?: number, added?: number|};
type PNPMResults = {|
level: LogLevel,
prefix?: string,
message?: string,
...ErrorLog,
...PNPMLog,
|};
let hasPnpm: ?boolean;
let pnpmVersion: ?number;
export class Pnpm implements PackageInstaller {
static async exists(): Promise<boolean> {
if (hasPnpm != null) {
return hasPnpm;
}
try {
hasPnpm = Boolean(await commandExists('pnpm'));
} catch (err) {
hasPnpm = false;
}
return hasPnpm;
}
async install({
modules,
cwd,
saveDev = true,
}: InstallerOptions): Promise<void> {
if (pnpmVersion == null) {
let version = await exec('pnpm --version');
pnpmVersion = parseInt(version.stdout, 10);
}
let args = ['add', '--reporter', 'ndjson'];
if (saveDev) {
args.push('-D');
}
if (pnpmVersion >= 7) {
if (fs.existsSync(path.join(cwd, 'pnpm-workspace.yaml'))) {
// installs in workspace root (regardless of cwd)
args.push('-w');
}
} else {
// ignores workspace root check
args.push('-W');
}
args = args.concat(modules.map(npmSpecifierFromModuleRequest));
let env = {};
for (let key in process.env) {
if (!key.startsWith('npm_') && key !== 'INIT_CWD' && key !== 'NODE_ENV') {
env[key] = process.env[key];
}
}
let addedCount = 0,
removedCount = 0;
let installProcess = spawn(PNPM_CMD, args, {
cwd,
env,
});
installProcess.stdout
.pipe(split())
.pipe(new JSONParseStream())
.on('error', e => {
logger.warn({
origin: '@parcel/package-manager',
message: e.chunk,
stack: e.stack,
});
})
.on('data', (json: PNPMResults) => {
if (json.level === 'error') {
logger.error({
origin: '@parcel/package-manager',
message: json.err.message,
stack: json.err.stack,
});
} else if (json.level === 'info' && typeof json.message === 'string') {
logger.info({
origin: '@parcel/package-manager',
message: prefix(json.message),
});
} else if (json.name === 'pnpm:stats') {
addedCount += json.added ?? 0;
removedCount += json.removed ?? 0;
}
});
let stderr = [];
installProcess.stderr
.on('data', str => {
stderr.push(str.toString());
})
.on('error', e => {
logger.warn({
origin: '@parcel/package-manager',
message: e.message,
});
});
try {
await promiseFromProcess(installProcess);
if (addedCount > 0 || removedCount > 0) {
logger.log({
origin: '@parcel/package-manager',
message: `Added ${addedCount} ${
removedCount > 0 ? `and removed ${removedCount} ` : ''
}packages via pnpm`,
});
}
// Since we succeeded, stderr might have useful information not included
// in the json written to stdout. It's also not necessary to log these as
// errors as they often aren't.
for (let message of stderr) {
logger.log({
origin: '@parcel/package-manager',
message,
});
}
} catch (e) {
throw new Error('pnpm failed to install modules');
}
}
}
function prefix(message: string): string {
return 'pnpm: ' + message;
}
registerSerializableClass(`${pkg.version}:Pnpm`, Pnpm);

View File

@@ -0,0 +1,157 @@
// @flow strict-local
import type {PackageInstaller, InstallerOptions} from './types';
import commandExists from 'command-exists';
import spawn from 'cross-spawn';
import logger from '@parcel/logger';
import split from 'split2';
import JSONParseStream from './JSONParseStream';
import promiseFromProcess from './promiseFromProcess';
import {registerSerializableClass} from '@parcel/core';
import {exec, npmSpecifierFromModuleRequest} from './utils';
// $FlowFixMe
import pkg from '../package.json';
const YARN_CMD = 'yarn';
type YarnStdOutMessage =
| {|
+type: 'step',
data: {|
message: string,
current: number,
total: number,
|},
|}
| {|+type: 'success', data: string|}
| {|+type: 'info', data: string|}
| {|+type: 'tree' | 'progressStart' | 'progressTick'|};
type YarnStdErrMessage = {|
+type: 'error' | 'warning',
data: string,
|};
let hasYarn: ?boolean;
let yarnVersion: ?number;
export class Yarn implements PackageInstaller {
static async exists(): Promise<boolean> {
if (hasYarn != null) {
return hasYarn;
}
try {
hasYarn = Boolean(await commandExists('yarn'));
} catch (err) {
hasYarn = false;
}
return hasYarn;
}
async install({
modules,
cwd,
saveDev = true,
}: InstallerOptions): Promise<void> {
if (yarnVersion == null) {
let version = await exec('yarn --version');
yarnVersion = parseInt(version.stdout, 10);
}
let args = ['add', '--json'].concat(
modules.map(npmSpecifierFromModuleRequest),
);
if (saveDev) {
args.push('-D');
if (yarnVersion < 2) {
args.push('-W');
}
}
// When Parcel is run by Yarn (e.g. via package.json scripts), several environment variables are
// added. When parcel in turn calls Yarn again, these can cause Yarn to behave stragely, so we
// filter them out when installing packages.
let env = {};
for (let key in process.env) {
if (
!key.startsWith('npm_') &&
key !== 'YARN_WRAP_OUTPUT' &&
key !== 'INIT_CWD' &&
key !== 'NODE_ENV'
) {
env[key] = process.env[key];
}
}
let installProcess = spawn(YARN_CMD, args, {cwd, env});
installProcess.stdout
// Invoking yarn with --json provides streaming, newline-delimited JSON output.
.pipe(split())
.pipe(new JSONParseStream())
.on('error', e => {
logger.error(e, '@parcel/package-manager');
})
.on('data', (message: YarnStdOutMessage) => {
switch (message.type) {
case 'step':
logger.progress(
prefix(
`[${message.data.current}/${message.data.total}] ${message.data.message}`,
),
);
return;
case 'success':
case 'info':
logger.info({
origin: '@parcel/package-manager',
message: prefix(message.data),
});
return;
default:
// ignore
}
});
installProcess.stderr
.pipe(split())
.pipe(new JSONParseStream())
.on('error', e => {
logger.error(e, '@parcel/package-manager');
})
.on('data', (message: YarnStdErrMessage) => {
switch (message.type) {
case 'warning':
logger.warn({
origin: '@parcel/package-manager',
message: prefix(message.data),
});
return;
case 'error':
logger.error({
origin: '@parcel/package-manager',
message: prefix(message.data),
});
return;
default:
// ignore
}
});
try {
return await promiseFromProcess(installProcess);
} catch (e) {
throw new Error('Yarn failed to install modules:' + e.message);
}
}
}
function prefix(message: string): string {
return 'yarn: ' + message;
}
registerSerializableClass(`${pkg.version}:Yarn`, Yarn);

View File

@@ -0,0 +1,17 @@
// @flow
export default function getCurrentPackageManager(
userAgent: ?string = process.env.npm_config_user_agent,
): ?{|name: string, version: string|} {
if (!userAgent) {
return undefined;
}
const pmSpec = userAgent.split(' ')[0];
const separatorPos = pmSpec.lastIndexOf('/');
const name = pmSpec.substring(0, separatorPos);
return {
name: name,
version: pmSpec.substring(separatorPos + 1),
};
}

View File

@@ -0,0 +1,8 @@
// @flow
export type * from './types';
export * from './Npm';
export * from './Pnpm';
export * from './Yarn';
export * from './MockPackageInstaller';
export * from './NodePackageManager';
export {_addToInstallQueue} from './installPackage';

View File

@@ -0,0 +1,284 @@
// @flow
import type {FilePath, PackageJSON} from '@parcel/types';
import type {
ModuleRequest,
PackageManager,
PackageInstaller,
InstallOptions,
} from './types';
import type {FileSystem} from '@parcel/fs';
import invariant from 'assert';
import path from 'path';
import nullthrows from 'nullthrows';
import semver from 'semver';
import ThrowableDiagnostic, {
generateJSONCodeHighlights,
encodeJSONKeyComponent,
md,
} from '@parcel/diagnostic';
import logger from '@parcel/logger';
import {loadConfig, PromiseQueue, resolveConfig} from '@parcel/utils';
import WorkerFarm from '@parcel/workers';
import {Npm} from './Npm';
import {Yarn} from './Yarn';
import {Pnpm} from './Pnpm.js';
import {getConflictingLocalDependencies} from './utils';
import getCurrentPackageManager from './getCurrentPackageManager';
import validateModuleSpecifier from './validateModuleSpecifier';
async function install(
fs: FileSystem,
packageManager: PackageManager,
modules: Array<ModuleRequest>,
from: FilePath,
projectRoot: FilePath,
options: InstallOptions = {},
): Promise<void> {
let {installPeers = true, saveDev = true, packageInstaller} = options;
let moduleNames = modules.map(m => m.name).join(', ');
logger.progress(`Installing ${moduleNames}...`);
let fromPkgPath = await resolveConfig(
fs,
from,
['package.json'],
projectRoot,
);
let cwd = fromPkgPath ? path.dirname(fromPkgPath) : fs.cwd();
if (!packageInstaller) {
packageInstaller = await determinePackageInstaller(fs, from, projectRoot);
}
try {
await packageInstaller.install({
modules,
saveDev,
cwd,
packagePath: fromPkgPath,
fs,
});
} catch (err) {
throw new Error(`Failed to install ${moduleNames}: ${err.message}`);
}
if (installPeers) {
await Promise.all(
modules.map(m =>
installPeerDependencies(
fs,
packageManager,
m,
from,
projectRoot,
options,
),
),
);
}
}
async function installPeerDependencies(
fs: FileSystem,
packageManager: PackageManager,
module: ModuleRequest,
from: FilePath,
projectRoot: FilePath,
options,
) {
const {resolved} = await packageManager.resolve(module.name, from);
const modulePkg: PackageJSON = nullthrows(
await loadConfig(fs, resolved, ['package.json'], projectRoot),
).config;
const peers = modulePkg.peerDependencies || {};
let modules: Array<ModuleRequest> = [];
for (let [name, range] of Object.entries(peers)) {
invariant(typeof range === 'string');
let conflicts = await getConflictingLocalDependencies(
fs,
name,
from,
projectRoot,
);
if (conflicts) {
let {pkg} = await packageManager.resolve(name, from);
invariant(pkg);
if (!semver.satisfies(pkg.version, range)) {
throw new ThrowableDiagnostic({
diagnostic: {
message: md`Could not install the peer dependency "${name}" for "${module.name}", installed version ${pkg.version} is incompatible with ${range}`,
origin: '@parcel/package-manager',
codeFrames: [
{
filePath: conflicts.filePath,
language: 'json',
code: conflicts.json,
codeHighlights: generateJSONCodeHighlights(
conflicts.json,
conflicts.fields.map(field => ({
key: `/${field}/${encodeJSONKeyComponent(name)}`,
type: 'key',
message: 'Found this conflicting local requirement.',
})),
),
},
],
},
});
}
continue;
}
modules.push({name, range});
}
if (modules.length) {
await install(
fs,
packageManager,
modules,
from,
projectRoot,
Object.assign({}, options, {installPeers: false}),
);
}
}
async function determinePackageInstaller(
fs: FileSystem,
filepath: FilePath,
projectRoot: FilePath,
): Promise<PackageInstaller> {
let configFile = await resolveConfig(
fs,
filepath,
['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock'],
projectRoot,
);
let configName = configFile && path.basename(configFile);
// Always use the package manager that seems to be used in the project,
// falling back to a different one wouldn't update the existing lockfile.
if (configName === 'package-lock.json') {
return new Npm();
} else if (configName === 'pnpm-lock.yaml') {
return new Pnpm();
} else if (configName === 'yarn.lock') {
return new Yarn();
}
let currentPackageManager = getCurrentPackageManager()?.name;
if (currentPackageManager === 'npm') {
return new Npm();
} else if (currentPackageManager === 'yarn') {
return new Yarn();
} else if (currentPackageManager === 'pnpm') {
return new Pnpm();
}
if (await Yarn.exists()) {
return new Yarn();
} else if (await Pnpm.exists()) {
return new Pnpm();
} else {
return new Npm();
}
}
let queue = new PromiseQueue({maxConcurrent: 1});
let modulesInstalling: Set<string> = new Set();
// Exported so that it may be invoked from the worker api below.
// Do not call this directly! This can result in concurrent package installations
// across multiple instances of the package manager.
export function _addToInstallQueue(
fs: FileSystem,
packageManager: PackageManager,
modules: Array<ModuleRequest>,
filePath: FilePath,
projectRoot: FilePath,
options?: InstallOptions,
): Promise<mixed> {
modules = modules.map(request => ({
name: validateModuleSpecifier(request.name),
range: request.range,
}));
// Wrap PromiseQueue and track modules that are currently installing.
// If a request comes in for a module that is currently installing, don't bother
// enqueuing it.
let modulesToInstall = modules.filter(
m => !modulesInstalling.has(getModuleRequestKey(m)),
);
if (modulesToInstall.length) {
for (let m of modulesToInstall) {
modulesInstalling.add(getModuleRequestKey(m));
}
queue
.add(() =>
install(
fs,
packageManager,
modulesToInstall,
filePath,
projectRoot,
options,
).then(() => {
for (let m of modulesToInstall) {
modulesInstalling.delete(getModuleRequestKey(m));
}
}),
)
.then(
() => {},
() => {},
);
}
return queue.run();
}
export function installPackage(
fs: FileSystem,
packageManager: PackageManager,
modules: Array<ModuleRequest>,
filePath: FilePath,
projectRoot: FilePath,
options?: InstallOptions,
): Promise<mixed> {
if (WorkerFarm.isWorker()) {
let workerApi = WorkerFarm.getWorkerApi();
// TODO this should really be `__filename` but without the rewriting.
let bundlePath =
process.env.PARCEL_BUILD_ENV === 'production' &&
!process.env.PARCEL_SELF_BUILD
? path.join(__dirname, '..', 'lib/index.js')
: __filename;
return workerApi.callMaster({
location: bundlePath,
args: [fs, packageManager, modules, filePath, projectRoot, options],
method: '_addToInstallQueue',
});
}
return _addToInstallQueue(
fs,
packageManager,
modules,
filePath,
projectRoot,
options,
);
}
function getModuleRequestKey(moduleRequest: ModuleRequest): string {
return [moduleRequest.name, moduleRequest.range].join('@');
}

View File

@@ -0,0 +1,19 @@
// @flow strict-local
import type {ChildProcess} from 'child_process';
export default function promiseFromProcess(
childProcess: ChildProcess,
): Promise<void> {
return new Promise((resolve, reject) => {
childProcess.on('error', reject);
childProcess.on('close', code => {
if (code !== 0) {
reject(new Error('Child process failed'));
return;
}
resolve();
});
});
}

View File

@@ -0,0 +1,63 @@
// @flow
import type {
FilePath,
FileCreateInvalidation,
SemverRange,
DependencySpecifier,
PackageJSON,
} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
export type ResolveResult = {|
resolved: FilePath | DependencySpecifier,
pkg?: ?PackageJSON,
invalidateOnFileCreate: Array<FileCreateInvalidation>,
invalidateOnFileChange: Set<FilePath>,
type: number,
|};
export type InstallOptions = {
installPeers?: boolean,
saveDev?: boolean,
packageInstaller?: ?PackageInstaller,
...
};
export type InstallerOptions = {|
modules: Array<ModuleRequest>,
fs: FileSystem,
cwd: FilePath,
packagePath?: ?FilePath,
saveDev?: boolean,
|};
export interface PackageInstaller {
install(opts: InstallerOptions): Promise<void>;
}
export type Invalidations = {|
invalidateOnFileCreate: Array<FileCreateInvalidation>,
invalidateOnFileChange: Set<FilePath>,
invalidateOnStartup: boolean,
|};
export interface PackageManager {
require(
id: DependencySpecifier,
from: FilePath,
?{|range?: ?SemverRange, shouldAutoInstall?: boolean, saveDev?: boolean|},
): Promise<any>;
resolve(
id: DependencySpecifier,
from: FilePath,
?{|range?: ?SemverRange, shouldAutoInstall?: boolean, saveDev?: boolean|},
): Promise<ResolveResult>;
getInvalidations(id: DependencySpecifier, from: FilePath): Invalidations;
invalidate(id: DependencySpecifier, from: FilePath): void;
}
export type ModuleRequest = {|
+name: string,
+range: ?SemverRange,
|};

View File

@@ -0,0 +1,94 @@
// @flow strict-local
import type {ModuleRequest} from './types';
import type {FilePath} from '@parcel/types';
import type {FileSystem} from '@parcel/fs';
import invariant from 'assert';
import ThrowableDiagnostic from '@parcel/diagnostic';
import {resolveConfig} from '@parcel/utils';
import {exec as _exec} from 'child_process';
import {promisify} from 'util';
export const exec: (
command: string,
options?: child_process$execOpts,
) => Promise<{|stdout: string | Buffer, stderr: string | Buffer|}> = _exec
? promisify(_exec)
: // _exec is undefined in browser builds
_exec;
export function npmSpecifierFromModuleRequest(
moduleRequest: ModuleRequest,
): string {
return moduleRequest.range != null
? [moduleRequest.name, moduleRequest.range].join('@')
: moduleRequest.name;
}
export function moduleRequestsFromDependencyMap(dependencyMap: {|
[string]: string,
|}): Array<ModuleRequest> {
return Object.entries(dependencyMap).map(([name, range]) => {
invariant(typeof range === 'string');
return {
name,
range,
};
});
}
export async function getConflictingLocalDependencies(
fs: FileSystem,
name: string,
local: FilePath,
projectRoot: FilePath,
): Promise<?{|json: string, filePath: FilePath, fields: Array<string>|}> {
let pkgPath = await resolveConfig(fs, local, ['package.json'], projectRoot);
if (pkgPath == null) {
return;
}
let pkgStr = await fs.readFile(pkgPath, 'utf8');
let pkg;
try {
pkg = JSON.parse(pkgStr);
} catch (e) {
// TODO: codeframe
throw new ThrowableDiagnostic({
diagnostic: {
message: 'Failed to parse package.json',
origin: '@parcel/package-manager',
},
});
}
if (typeof pkg !== 'object' || pkg == null) {
// TODO: codeframe
throw new ThrowableDiagnostic({
diagnostic: {
message: 'Expected package.json contents to be an object.',
origin: '@parcel/package-manager',
},
});
}
let fields = [];
for (let field of ['dependencies', 'devDependencies', 'peerDependencies']) {
if (
typeof pkg[field] === 'object' &&
pkg[field] != null &&
pkg[field][name] != null
) {
fields.push(field);
}
}
if (fields.length > 0) {
return {
filePath: pkgPath,
json: pkgStr,
fields,
};
}
}

View File

@@ -0,0 +1,12 @@
// @flow
const MODULE_REGEX = /^((@[^/\s]+\/){0,1}([^/\s.~]+[^/\s]*)){1}(@[^/\s]+){0,1}/;
export default function validateModuleSpecifier(moduleName: string): string {
let matches = MODULE_REGEX.exec(moduleName);
if (matches) {
return matches[0];
}
return '';
}

View File

@@ -0,0 +1,384 @@
// @flow strict-local
import {MemoryFS, NodeFS, OverlayFS} from '@parcel/fs';
import assert from 'assert';
import invariant from 'assert';
import path from 'path';
import sinon from 'sinon';
import ThrowableDiagnostic from '@parcel/diagnostic';
import {loadConfig} from '@parcel/utils';
import WorkerFarm from '@parcel/workers';
import {MockPackageInstaller, NodePackageManager} from '../src';
const FIXTURES_DIR = path.join(__dirname, 'fixtures');
function normalize(res) {
return {
...res,
invalidateOnFileCreate:
res?.invalidateOnFileCreate?.sort((a, b) => {
let ax =
a.filePath ??
a.glob ??
(a.aboveFilePath != null && a.fileName != null
? a.aboveFilePath + a.fileName
: '');
let bx =
b.filePath ??
b.glob ??
(b.aboveFilePath != null && b.fileName != null
? b.aboveFilePath + b.fileName
: '');
return ax < bx ? -1 : 1;
}) ?? [],
};
}
function check(resolved, expected) {
assert.deepEqual(normalize(resolved), normalize(expected));
}
describe('NodePackageManager', function () {
let fs;
let packageManager;
let packageInstaller;
let workerFarm;
// These can sometimes take a lil while
this.timeout(20000);
beforeEach(() => {
workerFarm = new WorkerFarm({
workerPath: require.resolve('@parcel/core/src/worker.js'),
});
fs = new OverlayFS(new MemoryFS(workerFarm), new NodeFS());
packageInstaller = new MockPackageInstaller();
packageManager = new NodePackageManager(fs, '/', packageInstaller);
});
afterEach(async () => {
loadConfig.clear();
await workerFarm.end();
});
it('resolves packages that exist', async () => {
check(
await packageManager.resolve(
'foo',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
),
{
pkg: {
version: '1.1.0',
},
resolved: path.join(FIXTURES_DIR, 'has-foo/node_modules/foo/index.js'),
type: 1,
invalidateOnFileChange: new Set([
path.join(FIXTURES_DIR, 'has-foo/node_modules/foo/package.json'),
]),
invalidateOnFileCreate: [
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/node_modules/foo/index.ts',
),
},
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/node_modules/foo/index.tsx',
),
},
{
fileName: 'node_modules/foo',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'),
},
{
fileName: 'tsconfig.json',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'),
},
],
},
);
});
it('requires packages that exist', async () => {
assert.deepEqual(
await packageManager.require(
'foo',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
),
'foobar',
);
});
it("autoinstalls packages that don't exist", async () => {
packageInstaller.register('a', fs, path.join(FIXTURES_DIR, 'packages/a'));
check(
await packageManager.resolve(
'a',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
{shouldAutoInstall: true},
),
{
pkg: {
name: 'a',
},
resolved: path.join(FIXTURES_DIR, 'has-foo/node_modules/a/index.js'),
type: 1,
invalidateOnFileChange: new Set([
path.join(FIXTURES_DIR, 'has-foo/node_modules/a/package.json'),
]),
invalidateOnFileCreate: [
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/node_modules/a/index.ts',
),
},
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/node_modules/a/index.tsx',
),
},
{
fileName: 'node_modules/a',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'),
},
{
fileName: 'tsconfig.json',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo'),
},
],
},
);
});
it('does not autoinstall packages that are already listed in package.json', async () => {
packageInstaller.register('a', fs, path.join(FIXTURES_DIR, 'packages/a'));
// $FlowFixMe assert.rejects is Node 10+
await assert.rejects(
() =>
packageManager.resolve(
'a',
path.join(FIXTURES_DIR, 'has-a-not-yet-installed/index.js'),
{shouldAutoInstall: true},
),
err => {
invariant(err instanceof ThrowableDiagnostic);
assert(err.message.includes('Run your package manager'));
return true;
},
);
});
it('does not autoinstall peer dependencies that are already listed in package.json', async () => {
packageInstaller.register(
'peers',
fs,
path.join(FIXTURES_DIR, 'packages/peers'),
);
let spy = sinon.spy(packageInstaller, 'install');
await packageManager.resolve(
'peers',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
{shouldAutoInstall: true},
);
assert.deepEqual(spy.args, [
[
{
cwd: path.join(FIXTURES_DIR, 'has-foo'),
packagePath: path.join(FIXTURES_DIR, 'has-foo/package.json'),
fs,
saveDev: true,
modules: [{name: 'peers', range: undefined}],
},
],
]);
});
it('autoinstalls peer dependencies that are not listed in package.json', async () => {
packageInstaller.register(
'foo',
fs,
path.join(FIXTURES_DIR, 'packages/foo-2.0'),
);
packageInstaller.register(
'peers',
fs,
path.join(FIXTURES_DIR, 'packages/peers-2.0'),
);
let spy = sinon.spy(packageInstaller, 'install');
await packageManager.resolve(
'peers',
path.join(FIXTURES_DIR, 'empty/index.js'),
{shouldAutoInstall: true},
);
assert.deepEqual(spy.args, [
[
{
cwd: path.join(FIXTURES_DIR, 'empty'),
packagePath: path.join(FIXTURES_DIR, 'empty/package.json'),
fs,
saveDev: true,
modules: [{name: 'peers', range: undefined}],
},
],
[
{
cwd: path.join(FIXTURES_DIR, 'empty'),
packagePath: path.join(FIXTURES_DIR, 'empty/package.json'),
fs,
saveDev: true,
modules: [{name: 'foo', range: '^2.0.0'}],
},
],
]);
});
describe('range mismatch', () => {
it("cannot autoinstall if there's a local requirement", async () => {
packageManager.invalidate(
'foo',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
);
// $FlowFixMe assert.rejects is Node 10+
await assert.rejects(
() =>
packageManager.resolve(
'foo',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
{
range: '^2.0.0',
},
),
err => {
invariant(err instanceof ThrowableDiagnostic);
assert.equal(
err.message,
'Could not find module "foo" satisfying ^2.0.0.',
);
return true;
},
);
});
it("can autoinstall into local package if there isn't a local requirement", async () => {
packageInstaller.register(
'foo',
fs,
path.join(FIXTURES_DIR, 'packages/foo-2.0'),
);
let spy = sinon.spy(packageInstaller, 'install');
check(
await packageManager.resolve(
'foo',
path.join(FIXTURES_DIR, 'has-foo/subpackage/index.js'),
{
range: '^2.0.0',
shouldAutoInstall: true,
},
),
{
pkg: {
name: 'foo',
version: '2.0.0',
},
resolved: path.join(
FIXTURES_DIR,
'has-foo/subpackage/node_modules/foo/index.js',
),
type: 1,
invalidateOnFileChange: new Set([
path.join(
FIXTURES_DIR,
'has-foo/subpackage/node_modules/foo/package.json',
),
]),
invalidateOnFileCreate: [
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/subpackage/node_modules/foo/index.ts',
),
},
{
filePath: path.join(
FIXTURES_DIR,
'has-foo/subpackage/node_modules/foo/index.tsx',
),
},
{
fileName: 'node_modules/foo',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo/subpackage'),
},
{
fileName: 'tsconfig.json',
aboveFilePath: path.join(FIXTURES_DIR, 'has-foo/subpackage'),
},
],
},
);
assert.deepEqual(spy.args, [
[
{
cwd: path.join(FIXTURES_DIR, 'has-foo/subpackage'),
packagePath: path.join(
FIXTURES_DIR,
'has-foo/subpackage/package.json',
),
fs,
saveDev: true,
modules: [{name: 'foo', range: '^2.0.0'}],
},
],
]);
});
it("cannot autoinstall peer dependencies if there's an incompatible local requirement", async () => {
packageManager.invalidate(
'peers',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
);
packageInstaller.register(
'foo',
fs,
path.join(FIXTURES_DIR, 'packages/foo-2.0'),
);
packageInstaller.register(
'peers',
fs,
path.join(FIXTURES_DIR, 'packages/peers-2.0'),
);
// $FlowFixMe assert.rejects is Node 10+
await assert.rejects(
() =>
packageManager.resolve(
'peers',
path.join(FIXTURES_DIR, 'has-foo/index.js'),
{
range: '^2.0.0',
shouldAutoInstall: true,
},
),
err => {
assert(err instanceof ThrowableDiagnostic);
assert.equal(
err.message,
'Could not install the peer dependency "foo" for "peers", installed version 1.1.0 is incompatible with ^2.0.0',
);
return true;
},
);
});
});
});

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"a": "1.1.0"
}
}

View File

@@ -0,0 +1 @@
module.exports = 'foobar';

View File

@@ -0,0 +1,3 @@
{
"version": "1.1.0"
}

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"foo": "1.1.0"
}
}

View File

@@ -0,0 +1 @@
module.exports = 'a';

View File

@@ -0,0 +1,3 @@
{
"name": "a"
}

View File

@@ -0,0 +1 @@
module.exports = 'foo';

View File

@@ -0,0 +1,4 @@
{
"name": "foo",
"version": "2.0.0"
}

View File

@@ -0,0 +1 @@
module.exports = 'foo';

View File

@@ -0,0 +1,7 @@
{
"name": "peers",
"version": "2.0.0",
"peerDependencies": {
"foo": "^2.0.0"
}
}

View File

@@ -0,0 +1 @@
module.exports = 'foo';

View File

@@ -0,0 +1,7 @@
{
"name": "peers",
"version": "1.0.0",
"peerDependencies": {
"foo": "^1.0.0"
}
}

View File

@@ -0,0 +1,28 @@
// @flow
import assert from 'assert';
import getCurrentPackageManager from '../src/getCurrentPackageManager';
describe('getCurrentPackageManager', () => {
it('yarn', () => {
const npm_config_user_agent = 'yarn/1.22.21 npm/? node/v21.1.0 darwin x64';
const currentPackageManager = getCurrentPackageManager(
npm_config_user_agent,
);
assert(currentPackageManager?.name, 'yarn');
});
it('npm', () => {
const npm_config_user_agent =
'npm/10.2.0 node/v21.1.0 darwin x64 workspaces/true';
const currentPackageManager = getCurrentPackageManager(
npm_config_user_agent,
);
assert(currentPackageManager?.name, 'npm');
});
it('pnpm', () => {
const npm_config_user_agent = 'pnpm/8.14.2 npm/? node/v18.17.1 darwin x64';
const currentPackageManager = getCurrentPackageManager(
npm_config_user_agent,
);
assert(currentPackageManager?.name, 'pnpm');
});
});

View File

@@ -0,0 +1,38 @@
// @flow
import assert from 'assert';
import validateModuleSpecifier from '../src/validateModuleSpecifier';
describe('Validate Module Specifiers', () => {
it('Validate Module Specifiers', () => {
let modules = [
'@parcel/transformer-posthtml/package.json',
'@some-org/package@v1.0.0',
'@org/some-package@v1.0.0-alpha.1',
'something.js/something/index.js',
'@some.org/something.js/index.js',
'lodash/something/index.js',
];
assert.deepEqual(
modules.map(module => validateModuleSpecifier(module)),
[
'@parcel/transformer-posthtml',
'@some-org/package@v1.0.0',
'@org/some-package@v1.0.0-alpha.1',
'something.js',
'@some.org/something.js',
'lodash',
],
);
});
it('Return empty on invalid modules', () => {
let modules = ['./somewhere.js', './hello/world.js', '~/hello/world.js'];
assert.deepEqual(
modules.map(module => validateModuleSpecifier(module)),
['', '', ''],
);
});
});