mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
feat: Add release and lint scripts to node CLI (#18935)
This commit is contained in:
@@ -1,13 +1,297 @@
|
||||
# @n8n/create-node
|
||||
|
||||
Scaffold a new community n8n node
|
||||
A powerful scaffolding tool to quickly create custom n8n community nodes with best practices built-in.
|
||||
|
||||
## Usage
|
||||
## 🚀 Quick Start
|
||||
|
||||
Create a new n8n node in seconds:
|
||||
|
||||
```bash
|
||||
npm create @n8n/node
|
||||
# or
|
||||
pnpm create @n8n/node
|
||||
# or
|
||||
yarn create @n8n/node
|
||||
```
|
||||
|
||||
Follow the interactive prompts to configure your node, or specify options directly:
|
||||
|
||||
```bash
|
||||
pnpm create @n8n/node my-awesome-node --template declarative/custom
|
||||
```
|
||||
|
||||
## 📋 Command Line Options
|
||||
|
||||
```bash
|
||||
pnpm create @n8n/node [NAME] [OPTIONS]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-f, --force` | Overwrite destination folder if it already exists |
|
||||
| `--skip-install` | Skip automatic dependency installation |
|
||||
| `--template <template>` | Specify which template to use |
|
||||
|
||||
### Available Templates
|
||||
|
||||
- **`declarative/custom`** - Start with a minimal declarative node structure
|
||||
- **`declarative/github-issues`** - GitHub Issues integration example
|
||||
- **`programmatic/example`** - Full programmatic node with advanced features
|
||||
|
||||
## 🎯 Interactive Setup
|
||||
|
||||
The CLI will guide you through setting up your node:
|
||||
|
||||
```
|
||||
$ pnpm create @n8n/node
|
||||
┌ @n8n/create-node
|
||||
│
|
||||
◇ What is your node called?
|
||||
│ my-awesome-api-node
|
||||
│
|
||||
◇ What kind of node are you building?
|
||||
│ HTTP API
|
||||
│
|
||||
◇ What template do you want to use?
|
||||
│ Start from scratch
|
||||
│
|
||||
◇ What's the base URL of the API?
|
||||
│ https://api.example.com/v1
|
||||
│
|
||||
◇ What type of authentication does your API use?
|
||||
│ API Key
|
||||
│
|
||||
◇ Files copied ✓
|
||||
│
|
||||
◇ Dependencies installed ✓
|
||||
│
|
||||
◇ Next Steps ─────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ cd ./my-awesome-api-node && pnpm run dev │
|
||||
│ │
|
||||
│ 📚 Documentation: https://docs.n8n.io/integrations/creating-nodes/ │
|
||||
│ 💬 Community: https://community.n8n.io │
|
||||
│ │
|
||||
├──────────────────────────────────────────────────────────────────────────────────╯
|
||||
│
|
||||
└ Created ./my-awesome-api-node ✨
|
||||
```
|
||||
|
||||
## 🛠️ Development Workflow
|
||||
|
||||
### 1. Navigate to your project
|
||||
|
||||
```bash
|
||||
cd ./my-awesome-api-node
|
||||
```
|
||||
|
||||
### 2. Start development server
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
This command:
|
||||
- Starts n8n in development mode on `http://localhost:5678`
|
||||
- Enables hot reload for your node changes
|
||||
- Automatically includes your node in the n8n instance
|
||||
- Links your node to `~/.n8n-node-cli/.n8n/custom` for development
|
||||
- Watches for file changes and rebuilds automatically
|
||||
|
||||
### 3. Test your node
|
||||
|
||||
- Open n8n at `http://localhost:5678`
|
||||
- Create a new workflow
|
||||
- Find your node in the node panel
|
||||
- Test parameters and functionality in real-time
|
||||
|
||||
## 📦 Generated Project Commands
|
||||
|
||||
Your generated project comes with these convenient npm scripts:
|
||||
|
||||
### Development
|
||||
```bash
|
||||
pnpm dev
|
||||
# Runs: n8n-node dev
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
pnpm build
|
||||
# Runs: n8n-node build
|
||||
```
|
||||
|
||||
### Linting
|
||||
```bash
|
||||
pnpm lint
|
||||
# Runs: n8n-node lint
|
||||
|
||||
pnpm lint:fix
|
||||
# Runs: n8n-node lint --fix
|
||||
```
|
||||
|
||||
### Publishing
|
||||
```bash
|
||||
pnpm run release
|
||||
# Runs: n8n-node release
|
||||
```
|
||||
|
||||
## 📦 Build & Deploy
|
||||
|
||||
### Build for production
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Generates:
|
||||
- Compiled TypeScript code
|
||||
- Bundled node package
|
||||
- Optimized assets and icons
|
||||
- Ready-to-publish package
|
||||
|
||||
### Quality checks
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
Validates:
|
||||
- Code style and formatting
|
||||
- n8n node conventions
|
||||
- Common integration issues
|
||||
- Cloud publication readiness
|
||||
|
||||
Fix issues automatically:
|
||||
|
||||
```bash
|
||||
pnpm lint:fix
|
||||
```
|
||||
|
||||
### Publish your node
|
||||
|
||||
```bash
|
||||
pnpm run release
|
||||
```
|
||||
|
||||
Runs [release-it](https://github.com/release-it/release-it) to handle the complete release process:
|
||||
- Ensures working directory is clean
|
||||
- Verifies you're on the main git branch
|
||||
- Increments your package version
|
||||
- Runs build and lint checks
|
||||
- Updates changelog
|
||||
- Creates git tag with version bump
|
||||
- Creates GitHub release with changelog
|
||||
- Publishes to npm
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
Your generated project includes:
|
||||
|
||||
```
|
||||
my-awesome-api-node/
|
||||
├── src/
|
||||
│ ├── nodes/
|
||||
│ │ └── MyAwesomeApi/
|
||||
│ │ ├── MyAwesomeApi.node.ts # Main node logic
|
||||
│ │ └── MyAwesomeApi.node.json # Node metadata
|
||||
│ └── credentials/
|
||||
│ └── MyAwesomeApiAuth.credentials.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
The CLI expects your project to follow this structure for proper building and development.
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
The CLI reads configuration from your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "n8n-nodes-my-awesome-node",
|
||||
"n8n": {
|
||||
"n8nNodesApiVersion": 1,
|
||||
"nodes": [
|
||||
"dist/nodes/MyAwesomeApi/MyAwesomeApi.node.js"
|
||||
],
|
||||
"credentials": [
|
||||
"dist/credentials/MyAwesomeApiAuth.credentials.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Node Types
|
||||
|
||||
Choose the right template for your use case:
|
||||
|
||||
| Template | Best For | Features |
|
||||
|----------|----------|----------|
|
||||
| **Declarative** | REST APIs, simple integrations | JSON-based configuration, automatic UI generation |
|
||||
| **Programmatic** | Complex logic, custom operations | Full TypeScript control, advanced error handling |
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Node not appearing in n8n:**
|
||||
```bash
|
||||
# Clear n8n node cli cache and restart
|
||||
rm -rf ~/.n8n-node-cli/.n8n/custom
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
**TypeScript errors:**
|
||||
```bash
|
||||
# Reinstall dependencies
|
||||
rm -rf node_modules pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
**Build failures:**
|
||||
```bash
|
||||
# Check for linting issues first
|
||||
pnpm lint --fix
|
||||
pnpm build
|
||||
```
|
||||
|
||||
**Development server issues:**
|
||||
```bash
|
||||
# Clear cache and restart development server
|
||||
rm -rf ~/.n8n-node-cli/.n8n/custom
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 🔧 Advanced Usage
|
||||
|
||||
### Using External n8n Instance
|
||||
|
||||
If you prefer to use your own n8n installation:
|
||||
|
||||
```bash
|
||||
pnpm dev --external-n8n
|
||||
```
|
||||
|
||||
### Custom User Folder
|
||||
|
||||
Specify a custom location for n8n user data:
|
||||
|
||||
```bash
|
||||
pnpm dev --custom-user-folder /path/to/custom/folder
|
||||
```
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **[Node Development Guide](https://docs.n8n.io/integrations/creating-nodes/)** - Complete documentation
|
||||
- **[API Reference](https://docs.n8n.io/integrations/creating-nodes/build/reference/)** - Technical specifications
|
||||
- **[Community Forum](https://community.n8n.io)** - Get help and share your nodes
|
||||
- **[Node Examples](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes)** - Official node implementations
|
||||
- **[@n8n/node-cli](https://www.npmjs.com/package/@n8n/node-cli)** - The underlying CLI tool
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Found a bug or want to contribute? Check out the [n8n repository](https://github.com/n8n-io/n8n) and join our community!
|
||||
|
||||
---
|
||||
|
||||
**Happy node building! 🎉**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@n8n/create-node",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Official CLI to create new community nodes for n8n",
|
||||
"bin": {
|
||||
"create-n8n-node": "bin/create.js"
|
||||
|
||||
@@ -1,47 +1,272 @@
|
||||
# @n8n/node-cli
|
||||
|
||||
Official CLI for developing community nodes for [n8n](https://n8n.io).
|
||||
Official CLI for developing community nodes for n8n.
|
||||
|
||||
## Features
|
||||
## 🚀 Getting Started
|
||||
|
||||
- 🔧 Scaffold new n8n nodes
|
||||
- 💻 Develop n8n nodes with live preview
|
||||
|
||||
## Installation
|
||||
|
||||
Run directly via `npx`:
|
||||
**To create a new node**, run:
|
||||
|
||||
```bash
|
||||
npx n8n-node new
|
||||
pnpm create @n8n/node
|
||||
```
|
||||
|
||||
Or install globally:
|
||||
This will generate a project with `pnpm` scripts that use this CLI under the hood.
|
||||
|
||||
## 📦 Generated Project Commands
|
||||
|
||||
After creating your node with `pnpm create @n8n/node`, you'll use these commands in your project:
|
||||
|
||||
### Development
|
||||
```bash
|
||||
pnpm dev
|
||||
# Runs: n8n-node dev
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
pnpm build
|
||||
# Runs: n8n-node build
|
||||
```
|
||||
|
||||
### Linting
|
||||
```bash
|
||||
pnpm lint
|
||||
# Runs: n8n-node lint
|
||||
|
||||
pnpm lint:fix
|
||||
# Runs: n8n-node lint --fix
|
||||
```
|
||||
|
||||
### Publishing
|
||||
```bash
|
||||
pnpm run release
|
||||
# Runs: n8n-node release
|
||||
```
|
||||
|
||||
## 🛠️ CLI Reference
|
||||
|
||||
> **Note:** These commands are typically wrapped by `pnpm` scripts in generated projects.
|
||||
|
||||
```bash
|
||||
npm install -g @n8n/node-cli
|
||||
n8n-node [COMMAND] [OPTIONS]
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### `n8n-node new`
|
||||
|
||||
Create a new node project.
|
||||
|
||||
```bash
|
||||
n8n-node new [NAME] [OPTIONS]
|
||||
```
|
||||
|
||||
**Flags:**
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-f, --force` | Overwrite destination folder if it already exists |
|
||||
| `--skip-install` | Skip installing dependencies |
|
||||
| `--template <template>` | Choose template: `declarative/custom`, `declarative/github-issues`, `programmatic/example` |
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
n8n-node new
|
||||
n8n-node new n8n-nodes-my-app --skip-install
|
||||
n8n-node new n8n-nodes-my-app --force
|
||||
n8n-node new n8n-nodes-my-app --template declarative/custom
|
||||
```
|
||||
|
||||
## Commands
|
||||
> **Note:** This command is used internally by `pnpm create @n8n/node` to provide the interactive scaffolding experience.
|
||||
|
||||
## Create a node
|
||||
#### `n8n-node dev`
|
||||
|
||||
Run n8n with your node in development mode with hot reload.
|
||||
|
||||
```bash
|
||||
n8n-node new # Scaffold a new node
|
||||
n8n-node dev [--external-n8n] [--custom-user-folder <value>]
|
||||
```
|
||||
|
||||
## Build a node
|
||||
**Flags:**
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--external-n8n` | Run n8n externally instead of in a subprocess |
|
||||
| `--custom-user-folder <path>` | Folder to use to store user-specific n8n data (default: `~/.n8n-node-cli`) |
|
||||
|
||||
This command:
|
||||
- Starts n8n on `http://localhost:5678` (unless using `--external-n8n`)
|
||||
- Links your node to n8n's custom nodes directory (`~/.n8n-node-cli/.n8n/custom`)
|
||||
- Rebuilds on file changes for live preview
|
||||
- Watches for changes in your `src/` directory
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Standard development with built-in n8n
|
||||
n8n-node dev
|
||||
|
||||
# Use external n8n instance
|
||||
n8n-node dev --external-n8n
|
||||
|
||||
# Custom n8n extensions directory
|
||||
n8n-node dev --custom-user-folder /home/user
|
||||
```
|
||||
|
||||
#### `n8n-node build`
|
||||
|
||||
Compile your node and prepare it for distribution.
|
||||
|
||||
```bash
|
||||
n8n-node build # Build your node; should be ran in the root of your custom node
|
||||
n8n-node build
|
||||
```
|
||||
|
||||
## Develop a node
|
||||
**Flags:** None
|
||||
|
||||
Generates:
|
||||
- Compiled TypeScript code
|
||||
- Bundled node package
|
||||
- Optimized assets and icons
|
||||
- Ready-to-publish package in `dist/`
|
||||
|
||||
#### `n8n-node lint`
|
||||
|
||||
Lint the node in the current directory.
|
||||
|
||||
```bash
|
||||
n8n-node dev # Develop your node with hot reloading; should be ran in the root of your custom node
|
||||
n8n-node lint [--fix]
|
||||
```
|
||||
|
||||
## Related
|
||||
**Flags:**
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--fix` | Automatically fix problems |
|
||||
|
||||
`@n8n/create-node`: Lightweight wrapper to support `npm create @n8n/node`
|
||||
**Examples:**
|
||||
```bash
|
||||
# Check for linting issues
|
||||
n8n-node lint
|
||||
|
||||
# Automatically fix fixable issues
|
||||
n8n-node lint --fix
|
||||
```
|
||||
|
||||
#### `n8n-node release`
|
||||
|
||||
Publish your community node package to npm.
|
||||
|
||||
```bash
|
||||
n8n-node release
|
||||
```
|
||||
|
||||
**Flags:** None
|
||||
|
||||
This command handles the complete release process using [release-it](https://github.com/release-it/release-it):
|
||||
- Builds the node
|
||||
- Runs linting checks
|
||||
- Updates changelog
|
||||
- Creates git tags
|
||||
- Creates GitHub releases
|
||||
- Publishes to npm
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
The recommended workflow using the scaffolding tool:
|
||||
|
||||
1. **Create your node**:
|
||||
```bash
|
||||
pnpm create @n8n/node my-awesome-node
|
||||
cd my-awesome-node
|
||||
```
|
||||
|
||||
2. **Start development**:
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
- Starts n8n on `http://localhost:5678`
|
||||
- Links your node automatically
|
||||
- Rebuilds on file changes
|
||||
|
||||
3. **Test your node** at `http://localhost:5678`
|
||||
|
||||
4. **Lint your code**:
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
5. **Build for production**:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
6. **Publish**:
|
||||
```bash
|
||||
pnpm run release
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
The CLI expects your project to follow this structure:
|
||||
|
||||
```
|
||||
my-node/
|
||||
├── src/
|
||||
│ ├── nodes/
|
||||
│ │ └── MyNode/
|
||||
│ │ ├── MyNode.node.ts
|
||||
│ │ └── MyNode.node.json
|
||||
│ └── credentials/
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
The CLI reads configuration from your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "n8n-nodes-my-awesome-node",
|
||||
"n8n": {
|
||||
"n8nNodesApiVersion": 1,
|
||||
"nodes": [
|
||||
"dist/nodes/MyNode/MyNode.node.js"
|
||||
],
|
||||
"credentials": [
|
||||
"dist/credentials/MyNodeAuth.credentials.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Development server issues
|
||||
```bash
|
||||
# Clear n8n custom nodes cache
|
||||
rm -rf ~/.n8n-node-cli/.n8n/custom
|
||||
|
||||
# Restart development server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Build failures
|
||||
```bash
|
||||
# Run linting first
|
||||
pnpm lint
|
||||
|
||||
# Clean build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **[Creating Nodes Guide](https://docs.n8n.io/integrations/creating-nodes/)** - Complete documentation
|
||||
- **[Node Development Reference](https://docs.n8n.io/integrations/creating-nodes/build/reference/)** - API specifications
|
||||
- **[Community Forum](https://community.n8n.io)** - Get help and showcase your nodes
|
||||
- **[@n8n/create-node](https://www.npmjs.com/package/@n8n/create-node)** - Recommended scaffolding tool
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Found an issue? Contribute to the [n8n repository](https://github.com/n8n-io/n8n) on GitHub.
|
||||
|
||||
---
|
||||
|
||||
**Happy node development! 🎉**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@n8n/node-cli",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"description": "Official CLI for developing community nodes for n8n",
|
||||
"bin": {
|
||||
"n8n-node": "bin/n8n-node.mjs"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { cancel, intro, log, outro, spinner } from '@clack/prompts';
|
||||
import { Command } from '@oclif/core';
|
||||
import { spawn } from 'child_process';
|
||||
import glob from 'fast-glob';
|
||||
import { cp, mkdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import picocolors from 'picocolors';
|
||||
import { rimraf } from 'rimraf';
|
||||
|
||||
import { runCommand } from '../utils/child-process';
|
||||
import { ensureN8nPackage } from '../utils/prompts';
|
||||
|
||||
export default class Build extends Command {
|
||||
@@ -44,42 +44,17 @@ export default class Build extends Command {
|
||||
}
|
||||
|
||||
async function runTscBuild(): Promise<void> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn('tsc', [], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
let stdout = '';
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer) => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (chunk: Buffer) => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
log.error(`${stdout.trim()}\n${stderr.trim()}`);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
log.error(`${stdout.trim()}\n${stderr.trim()}`);
|
||||
reject(new Error(`tsc exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
return await runCommand('tsc', [], {
|
||||
context: 'local',
|
||||
printOutput: ({ stdout, stderr }) => {
|
||||
log.error(stdout.concat(stderr).toString());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function copyStaticFiles() {
|
||||
const staticFiles = glob.sync(['**/*.{png,svg}', '**/__schema__/**/*.json'], {
|
||||
ignore: ['dist'],
|
||||
ignore: ['dist', 'node_modules'],
|
||||
});
|
||||
|
||||
return await Promise.all(
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { intro, outro, spinner } from '@clack/prompts';
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import picocolors from 'picocolors';
|
||||
import { rimraf } from 'rimraf';
|
||||
|
||||
import { ensureFolder } from '../../utils/filesystem';
|
||||
import { detectPackageManager } from '../../utils/package-manager';
|
||||
import { copyStaticFiles } from '../build';
|
||||
import { commands, readPackageName } from './utils';
|
||||
import { ensureN8nPackage, onCancel } from '../../utils/prompts';
|
||||
import { validateNodeName } from '../../utils/validation';
|
||||
import { copyStaticFiles } from '../build';
|
||||
import { commands, readPackageName } from './utils';
|
||||
import { runCommand } from '../../utils/child-process';
|
||||
|
||||
export default class Dev extends Command {
|
||||
static override description = 'Run n8n with the node and rebuild on changes for live preview';
|
||||
@@ -21,58 +24,70 @@ export default class Dev extends Command {
|
||||
'external-n8n': Flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'By default n8n-node dev will run n8n in a sub process. Enable this option if you would like to run n8n elsewhere.',
|
||||
'By default n8n-node dev will run n8n in a sub process. Enable this option if you would like to run n8n elsewhere. Make sure to set N8N_DEV_RELOAD to true in that case.',
|
||||
}),
|
||||
'custom-nodes-dir': Flags.directory({
|
||||
default: path.join(os.homedir(), '.n8n/custom'),
|
||||
'custom-user-folder': Flags.directory({
|
||||
default: path.join(os.homedir(), '.n8n-node-cli'),
|
||||
description:
|
||||
'Where to link your custom node. By default it will link to ~/.n8n/custom. You probably want to enable this option if you run n8n with a custom N8N_CUSTOM_EXTENSIONS env variable.',
|
||||
'Folder to use to store user-specific n8n data. By default it will use ~/.n8n-node-cli. You probably want to enable this option if you run n8n with a custom N8N_CUSTOM_EXTENSIONS env variable.',
|
||||
}),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(Dev);
|
||||
|
||||
const packageManager = detectPackageManager() ?? 'npm';
|
||||
const { isN8nInstalled, runCommand, runPersistentCommand } = commands();
|
||||
const packageManager = (await detectPackageManager()) ?? 'npm';
|
||||
const { runPersistentCommand } = commands();
|
||||
|
||||
intro(picocolors.inverse(' n8n-node dev '));
|
||||
|
||||
await ensureN8nPackage('n8n-node dev');
|
||||
|
||||
const installed = await isN8nInstalled();
|
||||
if (!installed && !flags['external-n8n']) {
|
||||
console.error(
|
||||
'❌ n8n is not installed or not in PATH. Learn how to install n8n here: https://docs.n8n.io/hosting/installation/npm',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await copyStaticFiles();
|
||||
|
||||
const linkingSpinner = spinner();
|
||||
linkingSpinner.start('Linking custom node to n8n');
|
||||
await runCommand(packageManager, ['link']);
|
||||
|
||||
const customPath = flags['custom-nodes-dir'];
|
||||
const n8nUserFolder = flags['custom-user-folder'];
|
||||
const customNodesFolder = path.join(n8nUserFolder, '.n8n', 'custom');
|
||||
|
||||
await ensureFolder(customPath);
|
||||
await ensureFolder(customNodesFolder);
|
||||
|
||||
const packageName = await readPackageName();
|
||||
const invalidNodeNameError = validateNodeName(packageName);
|
||||
|
||||
if (invalidNodeNameError) return onCancel(invalidNodeNameError);
|
||||
|
||||
await runCommand(packageManager, ['link', packageName], { cwd: customPath });
|
||||
// Remove existing package.json to avoid conflicts
|
||||
await rimraf(path.join(customNodesFolder, 'package.json'));
|
||||
await runCommand(packageManager, ['link', packageName], {
|
||||
cwd: customNodesFolder,
|
||||
});
|
||||
|
||||
linkingSpinner.stop('Linked custom node to n8n');
|
||||
|
||||
outro('✓ Setup complete');
|
||||
|
||||
if (!flags['external-n8n']) {
|
||||
// Run n8n with hot reload enabled
|
||||
runPersistentCommand('n8n', [], {
|
||||
cwd: customPath,
|
||||
env: { N8N_DEV_RELOAD: 'true' },
|
||||
runPersistentCommand('npx', ['-y', '--quiet', 'n8n'], {
|
||||
cwd: n8nUserFolder,
|
||||
env: {
|
||||
...process.env,
|
||||
N8N_DEV_RELOAD: 'true',
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
DB_SQLITE_POOL_SIZE: '10',
|
||||
N8N_USER_FOLDER: n8nUserFolder,
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'false',
|
||||
},
|
||||
name: 'n8n',
|
||||
color: picocolors.green,
|
||||
});
|
||||
}
|
||||
|
||||
// Run `tsc --watch` in background
|
||||
runPersistentCommand('tsc', ['--watch'], {
|
||||
runPersistentCommand(packageManager, ['exec', '--', 'tsc', '--watch'], {
|
||||
name: 'build',
|
||||
color: picocolors.cyan,
|
||||
});
|
||||
|
||||
@@ -22,34 +22,6 @@ export function commands() {
|
||||
process.on('SIGINT', () => cleanup('SIGINT'));
|
||||
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
||||
|
||||
async function runCommand(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
opts: {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, args, {
|
||||
cwd: opts.cwd,
|
||||
env: { ...process.env, ...opts.env },
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${cmd} exited with code ${code}`));
|
||||
});
|
||||
|
||||
registerChild(child);
|
||||
});
|
||||
}
|
||||
|
||||
function runPersistentCommand(
|
||||
cmd: string,
|
||||
args: string[],
|
||||
@@ -101,18 +73,7 @@ export function commands() {
|
||||
});
|
||||
}
|
||||
|
||||
async function isN8nInstalled(): Promise<boolean> {
|
||||
try {
|
||||
await runCommand('n8n', ['--version'], {});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isN8nInstalled,
|
||||
runCommand,
|
||||
runPersistentCommand,
|
||||
};
|
||||
}
|
||||
|
||||
34
packages/@n8n/node-cli/src/commands/lint.ts
Normal file
34
packages/@n8n/node-cli/src/commands/lint.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Command, Flags } from '@oclif/core';
|
||||
|
||||
import { ChildProcessError, runCommand } from '../utils/child-process';
|
||||
|
||||
export default class Lint extends Command {
|
||||
static override description = 'Lint the node in the current directory. Includes auto-fixing.';
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>'];
|
||||
static override flags = {
|
||||
fix: Flags.boolean({ description: 'Automatically fix problems', default: false }),
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
const { flags } = await this.parse(Lint);
|
||||
|
||||
const args = ['.'];
|
||||
|
||||
if (flags.fix) {
|
||||
args.push('--fix');
|
||||
}
|
||||
|
||||
try {
|
||||
await runCommand('eslint', args, { context: 'local', stdio: 'inherit' });
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ChildProcessError) {
|
||||
if (error.signal) {
|
||||
process.kill(process.pid, error.signal);
|
||||
} else {
|
||||
process.exit(error.code ?? 0);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { confirm, intro, isCancel, note, outro, spinner } from '@clack/prompts';
|
||||
import { confirm, intro, isCancel, log, note, outro, spinner } from '@clack/prompts';
|
||||
import { Args, Command, Flags } from '@oclif/core';
|
||||
import { camelCase } from 'change-case';
|
||||
import fs from 'node:fs/promises';
|
||||
@@ -9,9 +9,10 @@ import { declarativeTemplatePrompt, nodeNamePrompt, nodeTypePrompt } from './pro
|
||||
import { createIntro } from './utils';
|
||||
import type { TemplateData, TemplateWithRun } from '../../template/core';
|
||||
import { getTemplate, isTemplateName, isTemplateType, templates } from '../../template/templates';
|
||||
import { ChildProcessError, runCommand } from '../../utils/child-process';
|
||||
import { delayAtLeast, folderExists } from '../../utils/filesystem';
|
||||
import { tryReadGitUser } from '../../utils/git';
|
||||
import { detectPackageManager, installDependencies } from '../../utils/package-manager';
|
||||
import { initGit, tryReadGitUser } from '../../utils/git';
|
||||
import { detectPackageManager } from '../../utils/package-manager';
|
||||
import { onCancel } from '../../utils/prompts';
|
||||
import { validateNodeName } from '../../utils/validation';
|
||||
|
||||
@@ -80,7 +81,7 @@ export default class New extends Command {
|
||||
}
|
||||
|
||||
const config = (await template.prompts?.()) ?? {};
|
||||
const packageManager = detectPackageManager() ?? 'npm';
|
||||
const packageManager = (await detectPackageManager()) ?? 'npm';
|
||||
const templateData: TemplateData = {
|
||||
destinationPath: destination,
|
||||
nodePackageName: nodeName,
|
||||
@@ -99,22 +100,59 @@ export default class New extends Command {
|
||||
await delayAtLeast(template.run(templateData), 1000);
|
||||
copyingSpinner.stop('Files copied');
|
||||
|
||||
const gitSpinner = spinner();
|
||||
gitSpinner.start('Initializing git repository');
|
||||
|
||||
try {
|
||||
await initGit(destination);
|
||||
|
||||
gitSpinner.stop('Git repository initialized');
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof ChildProcessError) {
|
||||
gitSpinner.stop(
|
||||
`Could not initialize git repository: ${error.message}`,
|
||||
error.code ?? undefined,
|
||||
);
|
||||
process.exit(error.code ?? 1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!flags['skip-install']) {
|
||||
const installingSpinner = spinner();
|
||||
installingSpinner.start('Installing dependencies');
|
||||
|
||||
try {
|
||||
await delayAtLeast(installDependencies({ dir: destination, packageManager }), 1000);
|
||||
await delayAtLeast(
|
||||
runCommand(packageManager, ['install'], {
|
||||
cwd: destination,
|
||||
printOutput: ({ stdout, stderr }) => {
|
||||
log.error(stdout.concat(stderr).toString());
|
||||
},
|
||||
}),
|
||||
1000,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
installingSpinner.stop('Could not install dependencies', 1);
|
||||
return process.exit(1);
|
||||
if (error instanceof ChildProcessError) {
|
||||
installingSpinner.stop(
|
||||
`Could not install dependencies: ${error.message}`,
|
||||
error.code ?? undefined,
|
||||
);
|
||||
process.exit(error.code ?? 1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
installingSpinner.stop('Dependencies installed');
|
||||
}
|
||||
|
||||
note(
|
||||
`Need help? Check out the docs: https://docs.n8n.io/integrations/creating-nodes/build/${type}-style-node/`,
|
||||
`cd ./${nodeName} && ${packageManager} run dev
|
||||
|
||||
📚 Documentation: https://docs.n8n.io/integrations/creating-nodes/build/${type}-style-node/
|
||||
💬 Community: https://community.n8n.io`,
|
||||
'Next Steps',
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { detectPackageManager } from '../../utils/package-manager';
|
||||
import { detectPackageManagerFromUserAgent } from '../../utils/package-manager';
|
||||
|
||||
export const createIntro = () => {
|
||||
const maybePackageManager = detectPackageManager();
|
||||
const maybePackageManager = detectPackageManagerFromUserAgent();
|
||||
const packageManager = maybePackageManager ?? 'npm';
|
||||
return maybePackageManager ? ` ${packageManager} create @n8n/node ` : ' n8n-node new ';
|
||||
};
|
||||
|
||||
22
packages/@n8n/node-cli/src/commands/prerelease.ts
Normal file
22
packages/@n8n/node-cli/src/commands/prerelease.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Command } from '@oclif/core';
|
||||
|
||||
import { detectPackageManager } from '../utils/package-manager';
|
||||
|
||||
export default class Prerelease extends Command {
|
||||
static override description =
|
||||
'Only for internal use. Prevent npm publish, instead require npm run release';
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>'];
|
||||
static override flags = {};
|
||||
static override hidden = true;
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.parse(Prerelease);
|
||||
|
||||
const packageManager = (await detectPackageManager()) ?? 'npm';
|
||||
|
||||
if (!process.env.RELEASE_MODE) {
|
||||
console.log(`Run \`${packageManager} run release\` to publish the package`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
packages/@n8n/node-cli/src/commands/release.ts
Normal file
52
packages/@n8n/node-cli/src/commands/release.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Command } from '@oclif/core';
|
||||
|
||||
import { ChildProcessError, runCommand } from '../utils/child-process';
|
||||
import { detectPackageManager } from '../utils/package-manager';
|
||||
|
||||
export default class Release extends Command {
|
||||
static override description = 'Publish your community node package to npm';
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>'];
|
||||
static override flags = {};
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.parse(Release);
|
||||
|
||||
const pm = (await detectPackageManager()) ?? 'npm';
|
||||
|
||||
try {
|
||||
await runCommand(
|
||||
'release-it',
|
||||
[
|
||||
'-n',
|
||||
'--git.requireBranch main',
|
||||
'--git.requireCleanWorkingDir',
|
||||
'--git.requireUpstream',
|
||||
'--git.requireCommits',
|
||||
'--git.commit',
|
||||
'--git.tag',
|
||||
'--git.push',
|
||||
'--git.changelog="npx auto-changelog --stdout --unreleased --commit-limit false -u --hide-credit"',
|
||||
'--github.release',
|
||||
`--hooks.before:init="${pm} run lint && ${pm} run build"`,
|
||||
'--hooks.after:bump="npx auto-changelog -p"',
|
||||
],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
context: 'local',
|
||||
env: {
|
||||
RELEASE_MODE: 'true',
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ChildProcessError) {
|
||||
if (error.signal) {
|
||||
process.kill(process.pid, error.signal);
|
||||
} else {
|
||||
process.exit(error.code ?? 0);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
import Build from './commands/build';
|
||||
import Dev from './commands/dev';
|
||||
import Lint from './commands/lint';
|
||||
import New from './commands/new';
|
||||
import Prerelease from './commands/prerelease';
|
||||
import Release from './commands/release';
|
||||
|
||||
export const commands = {
|
||||
new: New,
|
||||
build: Build,
|
||||
dev: Dev,
|
||||
prerelease: Prerelease,
|
||||
release: Release,
|
||||
lint: Lint,
|
||||
};
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "eslint .",
|
||||
"release": "release-it"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "n8n-node lint",
|
||||
"lint:fix": "n8n-node lint --fix",
|
||||
"release": "n8n-node release",
|
||||
"prepublishOnly": "n8n-node prerelease"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -32,11 +34,6 @@
|
||||
"dist/nodes/Example/Example.node.js"
|
||||
]
|
||||
},
|
||||
"release-it": {
|
||||
"hooks": {
|
||||
"before:init": "{{packageManager.name}} run lint && {{packageManager.name}} run build"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/node-cli": "*",
|
||||
"eslint": "9.32.0",
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "eslint .",
|
||||
"release": "release-it"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "n8n-node lint",
|
||||
"lint:fix": "n8n-node lint --fix",
|
||||
"release": "n8n-node release",
|
||||
"prepublishOnly": "n8n-node prerelease"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -35,11 +37,6 @@
|
||||
"dist/nodes/GithubIssues/GithubIssues.node.js"
|
||||
]
|
||||
},
|
||||
"release-it": {
|
||||
"hooks": {
|
||||
"before:init": "{{packageManager.name}} run lint && {{packageManager.name}} run build"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/node-cli": "*",
|
||||
"eslint": "9.32.0",
|
||||
|
||||
@@ -15,13 +15,15 @@
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "eslint .",
|
||||
"release": "release-it"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "n8n-node build",
|
||||
"build:watch": "tsc --watch",
|
||||
"dev": "n8n-node dev",
|
||||
"lint": "n8n-node lint",
|
||||
"lint:fix": "n8n-node lint --fix",
|
||||
"release": "n8n-node release",
|
||||
"prepublishOnly": "n8n-node prerelease"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
@@ -32,11 +34,6 @@
|
||||
"dist/nodes/Example/Example.node.js"
|
||||
]
|
||||
},
|
||||
"release-it": {
|
||||
"hooks": {
|
||||
"before:init": "{{packageManager.name}} run lint && {{packageManager.name}} run build"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/node-cli": "*",
|
||||
"eslint": "9.32.0",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
|
||||
82
packages/@n8n/node-cli/src/utils/child-process.ts
Normal file
82
packages/@n8n/node-cli/src/utils/child-process.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { spawn, type SpawnOptions, type StdioOptions } from 'node:child_process';
|
||||
|
||||
import { detectPackageManager } from './package-manager';
|
||||
|
||||
export class ChildProcessError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: number | null,
|
||||
public signal: NodeJS.Signals | null,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCommand(
|
||||
cmd: string,
|
||||
args: string[] = [],
|
||||
opts: {
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
stdio?: StdioOptions;
|
||||
context?: 'local' | 'global';
|
||||
printOutput?: (options: { stdout: Buffer[]; stderr: Buffer[] }) => void;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const packageManager = (await detectPackageManager()) ?? 'npm';
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const options: SpawnOptions = {
|
||||
cwd: opts.cwd,
|
||||
env: { ...process.env, ...opts.env },
|
||||
stdio: opts.stdio ?? ['ignore', 'pipe', 'pipe'],
|
||||
};
|
||||
const child =
|
||||
opts.context === 'local'
|
||||
? spawn(packageManager, ['exec', '--', cmd, ...args], options)
|
||||
: spawn(cmd, args, options);
|
||||
|
||||
const stdoutBuffers: Buffer[] = [];
|
||||
const stderrBuffers: Buffer[] = [];
|
||||
|
||||
child.stdout?.on('data', (data: Buffer) => {
|
||||
stdoutBuffers.push(data);
|
||||
});
|
||||
child.stderr?.on('data', (data: Buffer) => {
|
||||
stderrBuffers.push(data);
|
||||
});
|
||||
|
||||
function printOutput() {
|
||||
if (opts.printOutput) {
|
||||
opts.printOutput({ stdout: stdoutBuffers, stderr: stderrBuffers });
|
||||
return;
|
||||
}
|
||||
for (const buffer of stdoutBuffers) {
|
||||
process.stdout.write(buffer);
|
||||
}
|
||||
for (const buffer of stderrBuffers) {
|
||||
process.stderr.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
child.on('error', (error) => {
|
||||
printOutput();
|
||||
reject(new ChildProcessError(error.message, null, null));
|
||||
});
|
||||
|
||||
child.on('close', (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
printOutput();
|
||||
reject(
|
||||
new ChildProcessError(
|
||||
`${cmd} exited with code ${code}${signal ? ` (signal: ${signal})` : ''}`,
|
||||
code,
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
import { runCommand } from './child-process';
|
||||
|
||||
type GitUser = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
@@ -32,3 +34,7 @@ export function tryReadGitUser(): GitUser {
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function initGit(dir: string): Promise<void> {
|
||||
await runCommand('git', ['init', '-b', 'main'], { cwd: dir });
|
||||
}
|
||||
|
||||
196
packages/@n8n/node-cli/src/utils/package-manager.test.ts
Normal file
196
packages/@n8n/node-cli/src/utils/package-manager.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { Stats } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
import { detectPackageManager, detectPackageManagerFromUserAgent } from './package-manager';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('node:child_process');
|
||||
vi.mock('node:fs/promises');
|
||||
vi.mock('@clack/prompts');
|
||||
|
||||
describe('package manager utils', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('detectPackageManagerFromUserAgent', () => {
|
||||
it('returns pnpm when user agent contains pnpm', () => {
|
||||
process.env.npm_config_user_agent = 'pnpm/8.6.0 npm/? node/v18.16.0 darwin x64';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe('pnpm');
|
||||
});
|
||||
|
||||
it('returns yarn when user agent contains yarn', () => {
|
||||
process.env.npm_config_user_agent = 'yarn/1.22.19 npm/? node/v18.16.0 darwin x64';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe('yarn');
|
||||
});
|
||||
|
||||
it('returns npm when user agent contains npm', () => {
|
||||
process.env.npm_config_user_agent = 'npm/9.5.1 node/v18.16.0 darwin x64 workspaces/false';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe('npm');
|
||||
});
|
||||
|
||||
it('prioritizes pnpm over yarn and npm when multiple are present', () => {
|
||||
process.env.npm_config_user_agent = 'pnpm/8.6.0 yarn/1.22.19 npm/9.5.1 node/v18.16.0';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe('pnpm');
|
||||
});
|
||||
|
||||
it('prioritizes yarn over npm when both are present but no pnpm', () => {
|
||||
process.env.npm_config_user_agent = 'yarn/1.22.19 npm/9.5.1 node/v18.16.0';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe('yarn');
|
||||
});
|
||||
|
||||
it('returns null when npm_config_user_agent is not set', () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null when user agent does not contain any package manager', () => {
|
||||
process.env.npm_config_user_agent = 'node/v18.16.0 darwin x64';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('returns null when user agent is empty string', () => {
|
||||
process.env.npm_config_user_agent = '';
|
||||
|
||||
const result = detectPackageManagerFromUserAgent();
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detectPackageManager', () => {
|
||||
it('returns package manager from user agent when available', async () => {
|
||||
process.env.npm_config_user_agent = 'pnpm/8.6.0 npm/? node/v18.16.0';
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe('pnpm');
|
||||
expect(vi.mocked(fs).stat).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('detects npm from package-lock.json when user agent is not available', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
vi.mocked(fs).stat.mockImplementation(async (path) => {
|
||||
if (path === 'package-lock.json') {
|
||||
const stats = mock<Stats>();
|
||||
stats.isFile.mockReturnValue(true);
|
||||
return await Promise.resolve(stats);
|
||||
}
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe('npm');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('package-lock.json');
|
||||
});
|
||||
|
||||
it('detects yarn from yarn.lock when user agent is not available', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
vi.mocked(fs).stat.mockImplementation(async (path) => {
|
||||
if (path === 'yarn.lock') {
|
||||
const stats = mock<Stats>();
|
||||
stats.isFile.mockReturnValue(true);
|
||||
return await Promise.resolve(stats);
|
||||
}
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe('yarn');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('package-lock.json');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('yarn.lock');
|
||||
});
|
||||
|
||||
it('detects pnpm from pnpm-lock.yaml when user agent is not available', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
vi.mocked(fs).stat.mockImplementation(async (path) => {
|
||||
if (path === 'pnpm-lock.yaml') {
|
||||
const stats = mock<Stats>();
|
||||
stats.isFile.mockReturnValue(true);
|
||||
return await Promise.resolve(stats);
|
||||
}
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe('pnpm');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('package-lock.json');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('yarn.lock');
|
||||
expect(vi.mocked(fs).stat).toHaveBeenCalledWith('pnpm-lock.yaml');
|
||||
});
|
||||
|
||||
it('prioritizes npm lock file when multiple lock files exist', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
const stats = mock<Stats>();
|
||||
stats.isFile.mockReturnValue(true);
|
||||
vi.mocked(fs).stat.mockResolvedValue(stats);
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe('npm');
|
||||
});
|
||||
|
||||
it('returns null when no user agent and no lock files exist', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
vi.mocked(fs).stat.mockRejectedValue(new Error('File not found'));
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('ignores directories that match lock file names', async () => {
|
||||
delete process.env.npm_config_user_agent;
|
||||
|
||||
vi.mocked(fs).stat.mockImplementation(async (path) => {
|
||||
if (path === 'package-lock.json') {
|
||||
const stats = mock<Stats>();
|
||||
stats.isFile.mockReturnValue(false);
|
||||
return await Promise.resolve(stats);
|
||||
}
|
||||
throw new Error('File not found');
|
||||
});
|
||||
|
||||
const result = await detectPackageManager();
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,45 +1,45 @@
|
||||
import { log } from '@clack/prompts';
|
||||
import { spawn } from 'node:child_process';
|
||||
import pico from 'picocolors';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
type PackageManager = 'npm' | 'yarn' | 'pnpm';
|
||||
|
||||
export function detectPackageManager(): PackageManager | null {
|
||||
export function detectPackageManagerFromUserAgent(): PackageManager | null {
|
||||
if ('npm_config_user_agent' in process.env) {
|
||||
const ua = process.env['npm_config_user_agent'] ?? '';
|
||||
if (ua.includes('pnpm')) return 'pnpm';
|
||||
if (ua.includes('yarn')) return 'yarn';
|
||||
if (ua.includes('npm')) return 'npm';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function installDependencies({
|
||||
dir,
|
||||
packageManager,
|
||||
}: { dir: string; packageManager: PackageManager }): Promise<void> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(packageManager, ['install'], {
|
||||
cwd: dir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
});
|
||||
async function detectPackageManagerFromLockFiles(): Promise<PackageManager | null> {
|
||||
const lockFiles: Record<PackageManager, string> = {
|
||||
npm: 'package-lock.json',
|
||||
yarn: 'yarn.lock',
|
||||
pnpm: 'pnpm-lock.yaml',
|
||||
};
|
||||
|
||||
const output: Buffer[] = [];
|
||||
|
||||
child.stdout.on('data', (chunk: Buffer) => output.push(chunk));
|
||||
child.stderr.on('data', (chunk: Buffer) => output.push(chunk));
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
const error = new Error(`${packageManager} install exited with code ${code}`);
|
||||
log.error(`${pico.bold(pico.red(error.message))}
|
||||
${output.map((item) => item.toString()).join('\n')}`);
|
||||
reject(error);
|
||||
for (const [pm, lockFile] of Object.entries(lockFiles)) {
|
||||
try {
|
||||
const stats = await fs.stat(lockFile);
|
||||
if (stats.isFile()) {
|
||||
return pm as PackageManager;
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
// File does not exist
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function detectPackageManager(): Promise<PackageManager | null> {
|
||||
// When used via package.json scripts or `npm/yarn/pnpm create`, we can detect the package manager via the user agent
|
||||
const fromUserAgent = detectPackageManagerFromUserAgent();
|
||||
if (fromUserAgent) return fromUserAgent;
|
||||
|
||||
// When used directly via `n8n-node` CLI, we can try to detect the package manager via the lock files
|
||||
const fromLockFiles = await detectPackageManagerFromLockFiles();
|
||||
if (fromLockFiles) return fromLockFiles;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"@redocly/cli": "^1.28.5",
|
||||
"@types/aws4": "^1.5.1",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
@@ -106,6 +105,7 @@
|
||||
"@n8n_io/ai-assistant-sdk": "catalog:",
|
||||
"@n8n_io/license-sdk": "2.23.0",
|
||||
"@rudderstack/rudder-sdk-node": "2.1.4",
|
||||
"@parcel/watcher": "^2.5.1",
|
||||
"@sentry/node": "catalog:",
|
||||
"aws4": "1.11.0",
|
||||
"axios": "catalog:",
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -1504,6 +1504,9 @@ importers:
|
||||
'@n8n_io/license-sdk':
|
||||
specifier: 2.23.0
|
||||
version: 2.23.0
|
||||
'@parcel/watcher':
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
'@rudderstack/rudder-sdk-node':
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4(tslib@2.8.1)
|
||||
@@ -1736,9 +1739,6 @@ importers:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
version: link:../@n8n/typescript-config
|
||||
'@parcel/watcher':
|
||||
specifier: ^2.5.1
|
||||
version: 2.5.1
|
||||
'@redocly/cli':
|
||||
specifier: ^1.28.5
|
||||
version: 1.28.5(encoding@0.1.13)
|
||||
|
||||
Reference in New Issue
Block a user