first commit

This commit is contained in:
2025-10-26 23:10:15 +08:00
commit 8f0345b7be
14961 changed files with 2356381 additions and 0 deletions

29
node_modules/@n8n/node-cli/dist/template/core.d.ts generated vendored Normal file
View File

@@ -0,0 +1,29 @@
export type TemplateData<Config extends object = object> = {
destinationPath: string;
nodePackageName: string;
user?: Partial<{
name: string;
email: string;
}>;
packageManager: {
name: 'npm' | 'yarn' | 'pnpm';
installCommand: string;
};
config: Config;
};
type Require<T, K extends keyof T> = T & {
[P in K]-?: T[P];
};
export type Template<Config extends object = object> = {
name: string;
description: string;
path: string;
prompts?: () => Promise<Config>;
run?: (data: TemplateData<Config>) => Promise<void>;
};
export type TemplateWithRun<Config extends object = object> = Require<Template<Config>, 'run'>;
export declare function copyTemplateFilesToDestination<Config extends object>(template: Template<Config>, data: TemplateData): Promise<void>;
export declare function copyDefaultTemplateFilesToDestination(data: TemplateData): Promise<void>;
export declare function templateStaticFiles(data: TemplateData): Promise<void>;
export declare function createTemplate<Config extends object>(template: Template<Config>): TemplateWithRun<Config>;
export {};

55
node_modules/@n8n/node-cli/dist/template/core.js generated vendored Normal file
View File

@@ -0,0 +1,55 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.copyTemplateFilesToDestination = copyTemplateFilesToDestination;
exports.copyDefaultTemplateFilesToDestination = copyDefaultTemplateFilesToDestination;
exports.templateStaticFiles = templateStaticFiles;
exports.createTemplate = createTemplate;
const fast_glob_1 = __importDefault(require("fast-glob"));
const handlebars_1 = __importDefault(require("handlebars"));
const promises_1 = __importDefault(require("node:fs/promises"));
const node_path_1 = __importDefault(require("node:path"));
const filesystem_1 = require("../utils/filesystem");
async function copyTemplateFilesToDestination(template, data) {
await (0, filesystem_1.copyFolder)({
source: template.path,
destination: data.destinationPath,
ignore: ['dist', 'node_modules'],
});
}
async function copyDefaultTemplateFilesToDestination(data) {
await (0, filesystem_1.copyFolder)({
source: node_path_1.default.resolve(__dirname, 'templates/shared/default'),
destination: data.destinationPath,
ignore: ['dist', 'node_modules'],
});
}
async function templateStaticFiles(data) {
const files = await (0, fast_glob_1.default)('**/*.{md,json,yml}', {
ignore: ['tsconfig.json', 'tsconfig.build.json'],
cwd: data.destinationPath,
absolute: true,
dot: true,
});
await Promise.all(files.map(async (file) => {
const content = await promises_1.default.readFile(file, 'utf-8');
const newContent = handlebars_1.default.compile(content, { noEscape: true })(data);
if (newContent !== content) {
await promises_1.default.writeFile(file, newContent);
}
}));
}
function createTemplate(template) {
return {
...template,
run: async (data) => {
await copyDefaultTemplateFilesToDestination(data);
await copyTemplateFilesToDestination(template, data);
await templateStaticFiles(data);
await template.run?.(data);
},
};
}
//# sourceMappingURL=core.js.map

1
node_modules/@n8n/node-cli/dist/template/core.js.map generated vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/template/core.ts"],"names":[],"mappings":";;;;;AAgCA,wEASC;AAED,sFAMC;AAED,kDAkBC;AAED,wCAYC;AAnFD,0DAA6B;AAC7B,4DAAoC;AACpC,gEAAkC;AAClC,0DAA6B;AAE7B,oDAAiD;AA2B1C,KAAK,UAAU,8BAA8B,CACnD,QAA0B,EAC1B,IAAkB;IAElB,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,QAAQ,CAAC,IAAI;QACrB,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,qCAAqC,CAAC,IAAkB;IAC7E,MAAM,IAAA,uBAAU,EAAC;QAChB,MAAM,EAAE,mBAAI,CAAC,OAAO,CAAC,SAAS,EAAE,0BAA0B,CAAC;QAC3D,WAAW,EAAE,IAAI,CAAC,eAAe;QACjC,MAAM,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC;KAChC,CAAC,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,mBAAmB,CAAC,IAAkB;IAC3D,MAAM,KAAK,GAAG,MAAM,IAAA,mBAAI,EAAC,oBAAoB,EAAE;QAC9C,MAAM,EAAE,CAAC,eAAe,EAAE,qBAAqB,CAAC;QAChD,GAAG,EAAE,IAAI,CAAC,eAAe;QACzB,QAAQ,EAAE,IAAI;QACd,GAAG,EAAE,IAAI;KACT,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,GAAG,CAChB,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,OAAO,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,oBAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAEzE,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;YAC5B,MAAM,kBAAE,CAAC,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACtC,CAAC;IACF,CAAC,CAAC,CACF,CAAC;AACH,CAAC;AAED,SAAgB,cAAc,CAC7B,QAA0B;IAE1B,OAAO;QACN,GAAG,QAAQ;QACX,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACnB,MAAM,qCAAqC,CAAC,IAAI,CAAC,CAAC;YAClD,MAAM,8BAA8B,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACrD,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;YAChC,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;KACD,CAAC;AACH,CAAC"}

View File

@@ -0,0 +1,17 @@
export declare function updateNodeAst({ nodePath, className, baseUrl, }: {
nodePath: string;
className: string;
baseUrl: string;
}): import("ts-morph").SourceFile;
export declare function updateCredentialAst({ repoName, baseUrl, credentialPath, credentialName, credentialDisplayName, credentialClassName, }: {
repoName: string;
credentialPath: string;
credentialName: string;
credentialDisplayName: string;
credentialClassName: string;
baseUrl: string;
}): import("ts-morph").SourceFile;
export declare function addCredentialToNode({ nodePath, credentialName, }: {
nodePath: string;
credentialName: string;
}): import("ts-morph").SourceFile;

View File

@@ -0,0 +1,110 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.updateNodeAst = updateNodeAst;
exports.updateCredentialAst = updateCredentialAst;
exports.addCredentialToNode = addCredentialToNode;
const change_case_1 = require("change-case");
const ts_morph_1 = require("ts-morph");
const ast_1 = require("../../../../utils/ast");
function updateNodeAst({ nodePath, className, baseUrl, }) {
const sourceFile = (0, ast_1.loadSingleSourceFile)(nodePath);
const classDecl = sourceFile.getClasses()[0];
classDecl.rename(className);
const nodeDescriptionObj = classDecl
.getPropertyOrThrow('description')
.getInitializerIfKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
(0, ast_1.updateStringProperty)({
obj: nodeDescriptionObj,
key: 'displayName',
value: (0, change_case_1.capitalCase)(className),
});
(0, ast_1.updateStringProperty)({
obj: nodeDescriptionObj,
key: 'name',
value: (0, change_case_1.camelCase)(className),
});
(0, ast_1.updateStringProperty)({
obj: nodeDescriptionObj,
key: 'description',
value: `Interact with the ${(0, change_case_1.capitalCase)(className)} API`,
});
const icon = (0, ast_1.getChildObjectLiteral)({ obj: nodeDescriptionObj, key: 'icon' });
(0, ast_1.updateStringProperty)({
obj: icon,
key: 'light',
value: `file:${(0, change_case_1.camelCase)(className)}.svg`,
});
(0, ast_1.updateStringProperty)({
obj: icon,
key: 'dark',
value: `file:${(0, change_case_1.camelCase)(className)}.dark.svg`,
});
const requestDefaults = (0, ast_1.getChildObjectLiteral)({
obj: nodeDescriptionObj,
key: 'requestDefaults',
});
(0, ast_1.updateStringProperty)({
obj: requestDefaults,
key: 'baseURL',
value: baseUrl,
});
const defaults = (0, ast_1.getChildObjectLiteral)({
obj: nodeDescriptionObj,
key: 'defaults',
});
(0, ast_1.updateStringProperty)({ obj: defaults, key: 'name', value: (0, change_case_1.capitalCase)(className) });
return sourceFile;
}
function updateCredentialAst({ repoName, baseUrl, credentialPath, credentialName, credentialDisplayName, credentialClassName, }) {
const sourceFile = (0, ast_1.loadSingleSourceFile)(credentialPath);
const classDecl = sourceFile.getClasses()[0];
classDecl.rename(credentialClassName);
(0, ast_1.updateStringProperty)({
obj: classDecl,
key: 'displayName',
value: credentialDisplayName,
});
(0, ast_1.updateStringProperty)({
obj: classDecl,
key: 'name',
value: credentialName,
});
const docUrlProp = classDecl.getProperty('documentationUrl');
if (docUrlProp) {
const initializer = docUrlProp.getInitializerIfKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral);
const newUrl = initializer.getLiteralText().replace('/repo', `/${repoName}`);
initializer.setLiteralValue(newUrl);
}
const testProperty = classDecl.getProperty('test');
if (testProperty) {
const testRequest = testProperty
.getInitializerIfKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression)
.getPropertyOrThrow('request')
.asKindOrThrow(ts_morph_1.SyntaxKind.PropertyAssignment)
.getInitializerIfKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
(0, ast_1.updateStringProperty)({
obj: testRequest,
key: 'baseURL',
value: baseUrl,
});
}
return sourceFile;
}
function addCredentialToNode({ nodePath, credentialName, }) {
const sourceFile = (0, ast_1.loadSingleSourceFile)(nodePath);
const classDecl = sourceFile.getClasses()[0];
const descriptionProp = classDecl
.getPropertyOrThrow('description')
.getInitializerIfKindOrThrow(ts_morph_1.SyntaxKind.ObjectLiteralExpression);
const credentialsProp = descriptionProp.getPropertyOrThrow('credentials');
if (credentialsProp.getKind() === ts_morph_1.SyntaxKind.PropertyAssignment) {
const initializer = credentialsProp.getFirstDescendantByKindOrThrow(ts_morph_1.SyntaxKind.ArrayLiteralExpression);
const credentialObject = ts_morph_1.ts.factory.createObjectLiteralExpression([
ts_morph_1.ts.factory.createPropertyAssignment(ts_morph_1.ts.factory.createIdentifier('name'), ts_morph_1.ts.factory.createStringLiteral(credentialName, true)),
ts_morph_1.ts.factory.createPropertyAssignment(ts_morph_1.ts.factory.createIdentifier('required'), ts_morph_1.ts.factory.createTrue()),
]);
initializer.addElement((0, ts_morph_1.printNode)(credentialObject));
}
return sourceFile;
}
//# sourceMappingURL=ast.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../../../../../src/template/templates/declarative/custom/ast.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,161 @@
import { camelCase, capitalCase } from 'change-case';
import { ts, SyntaxKind, printNode } from 'ts-morph';
import {
getChildObjectLiteral,
loadSingleSourceFile,
updateStringProperty,
} from '../../../../utils/ast';
export function updateNodeAst({
nodePath,
className,
baseUrl,
}: { nodePath: string; className: string; baseUrl: string }) {
const sourceFile = loadSingleSourceFile(nodePath);
const classDecl = sourceFile.getClasses()[0];
classDecl.rename(className);
const nodeDescriptionObj = classDecl
.getPropertyOrThrow('description')
.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
updateStringProperty({
obj: nodeDescriptionObj,
key: 'displayName',
value: capitalCase(className),
});
updateStringProperty({
obj: nodeDescriptionObj,
key: 'name',
value: camelCase(className),
});
updateStringProperty({
obj: nodeDescriptionObj,
key: 'description',
value: `Interact with the ${capitalCase(className)} API`,
});
const icon = getChildObjectLiteral({ obj: nodeDescriptionObj, key: 'icon' });
updateStringProperty({
obj: icon,
key: 'light',
value: `file:${camelCase(className)}.svg`,
});
updateStringProperty({
obj: icon,
key: 'dark',
value: `file:${camelCase(className)}.dark.svg`,
});
const requestDefaults = getChildObjectLiteral({
obj: nodeDescriptionObj,
key: 'requestDefaults',
});
updateStringProperty({
obj: requestDefaults,
key: 'baseURL',
value: baseUrl,
});
const defaults = getChildObjectLiteral({
obj: nodeDescriptionObj,
key: 'defaults',
});
updateStringProperty({ obj: defaults, key: 'name', value: capitalCase(className) });
return sourceFile;
}
export function updateCredentialAst({
repoName,
baseUrl,
credentialPath,
credentialName,
credentialDisplayName,
credentialClassName,
}: {
repoName: string;
credentialPath: string;
credentialName: string;
credentialDisplayName: string;
credentialClassName: string;
baseUrl: string;
}) {
const sourceFile = loadSingleSourceFile(credentialPath);
const classDecl = sourceFile.getClasses()[0];
classDecl.rename(credentialClassName);
updateStringProperty({
obj: classDecl,
key: 'displayName',
value: credentialDisplayName,
});
updateStringProperty({
obj: classDecl,
key: 'name',
value: credentialName,
});
const docUrlProp = classDecl.getProperty('documentationUrl');
if (docUrlProp) {
const initializer = docUrlProp.getInitializerIfKindOrThrow(SyntaxKind.StringLiteral);
const newUrl = initializer.getLiteralText().replace('/repo', `/${repoName}`);
initializer.setLiteralValue(newUrl);
}
const testProperty = classDecl.getProperty('test');
if (testProperty) {
const testRequest = testProperty
.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression)
.getPropertyOrThrow('request')
.asKindOrThrow(SyntaxKind.PropertyAssignment)
.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
updateStringProperty({
obj: testRequest,
key: 'baseURL',
value: baseUrl,
});
}
return sourceFile;
}
export function addCredentialToNode({
nodePath,
credentialName,
}: { nodePath: string; credentialName: string }) {
const sourceFile = loadSingleSourceFile(nodePath);
const classDecl = sourceFile.getClasses()[0];
const descriptionProp = classDecl
.getPropertyOrThrow('description')
.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
const credentialsProp = descriptionProp.getPropertyOrThrow('credentials');
if (credentialsProp.getKind() === SyntaxKind.PropertyAssignment) {
const initializer = credentialsProp.getFirstDescendantByKindOrThrow(
SyntaxKind.ArrayLiteralExpression,
);
const credentialObject = ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('name'),
ts.factory.createStringLiteral(credentialName, true),
),
ts.factory.createPropertyAssignment(
ts.factory.createIdentifier('required'),
ts.factory.createTrue(),
),
]);
initializer.addElement(printNode(credentialObject));
}
return sourceFile;
}

View File

@@ -0,0 +1,3 @@
export declare const credentialTypePrompt: () => Promise<"custom" | "apiKey" | "bearer" | "basicAuth" | "none" | "oauth2">;
export declare const baseUrlPrompt: () => Promise<string>;
export declare const oauthFlowPrompt: () => Promise<"clientCredentials" | "authorizationCode">;

View File

@@ -0,0 +1,80 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.oauthFlowPrompt = exports.baseUrlPrompt = exports.credentialTypePrompt = void 0;
const prompts_1 = require("@clack/prompts");
const prompts_2 = require("../../../../utils/prompts");
const credentialTypePrompt = async () => await (0, prompts_2.withCancelHandler)((0, prompts_1.select)({
message: 'What type of authentication does your API use?',
options: [
{
label: 'API Key',
value: 'apiKey',
hint: 'Send a secret key via headers, query, or body',
},
{
label: 'Bearer Token',
value: 'bearer',
hint: 'Send a token via Authorization header (Authorization: Bearer <token>)',
},
{
label: 'OAuth2',
value: 'oauth2',
hint: 'Use an OAuth 2.0 flow to obtain access tokens on behalf of a user or app',
},
{
label: 'Basic Auth',
value: 'basicAuth',
hint: 'Send username and password encoded in base64 via the Authorization header',
},
{
label: 'Custom',
value: 'custom',
hint: 'Create your own credential logic; an empty credential class will be scaffolded for you',
},
{
label: 'None',
value: 'none',
hint: 'No authentication; no credential class will be generated',
},
],
initialValue: 'apiKey',
}));
exports.credentialTypePrompt = credentialTypePrompt;
const baseUrlPrompt = async () => await (0, prompts_2.withCancelHandler)((0, prompts_1.text)({
message: "What's the base URL of the API?",
placeholder: 'https://api.example.com/v2',
defaultValue: 'https://api.example.com/v2',
validate: (value) => {
if (!value)
return;
if (!value.startsWith('https://') && !value.startsWith('http://')) {
return 'Base URL must start with http(s)://';
}
try {
new URL(value);
}
catch (error) {
return 'Must be a valid URL';
}
return;
},
}));
exports.baseUrlPrompt = baseUrlPrompt;
const oauthFlowPrompt = async () => await (0, prompts_2.withCancelHandler)((0, prompts_1.select)({
message: 'What OAuth2 flow does your API use?',
options: [
{
label: 'Authorization code',
value: 'authorizationCode',
hint: 'Users log in and approve access (use this if unsure)',
},
{
label: 'Client credentials',
value: 'clientCredentials',
hint: 'Server-to-server auth without user interaction',
},
],
initialValue: 'authorizationCode',
}));
exports.oauthFlowPrompt = oauthFlowPrompt;
//# sourceMappingURL=prompts.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../../../src/template/templates/declarative/custom/prompts.ts"],"names":[],"mappings":";;;AAAA,4CAA8C;AAG9C,uDAA8D;AAEvD,MAAM,oBAAoB,GAAG,KAAK,IAAI,EAAE,CAC9C,MAAM,IAAA,2BAAiB,EACtB,IAAA,gBAAM,EAAiB;IACtB,OAAO,EAAE,gDAAgD;IACzD,OAAO,EAAE;QACR;YACC,KAAK,EAAE,SAAS;YAChB,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,+CAA+C;SACrD;QACD;YACC,KAAK,EAAE,cAAc;YACrB,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,uEAAuE;SAC7E;QACD;YACC,KAAK,EAAE,QAAQ;YACf,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,0EAA0E;SAChF;QACD;YACC,KAAK,EAAE,YAAY;YACnB,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,2EAA2E;SACjF;QACD;YACC,KAAK,EAAE,QAAQ;YACf,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,wFAAwF;SAC9F;QACD;YACC,KAAK,EAAE,MAAM;YACb,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,0DAA0D;SAChE;KACD;IACD,YAAY,EAAE,QAAQ;CACtB,CAAC,CACF,CAAC;AAtCU,QAAA,oBAAoB,wBAsC9B;AAEI,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE,CACvC,MAAM,IAAA,2BAAiB,EACtB,IAAA,cAAI,EAAC;IACJ,OAAO,EAAE,iCAAiC;IAC1C,WAAW,EAAE,4BAA4B;IACzC,YAAY,EAAE,4BAA4B;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QACnB,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YACnE,OAAO,qCAAqC,CAAC;QAC9C,CAAC;QAED,IAAI,CAAC;YACJ,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,qBAAqB,CAAC;QAC9B,CAAC;QACD,OAAO;IACR,CAAC;CACD,CAAC,CACF,CAAC;AArBU,QAAA,aAAa,iBAqBvB;AAEI,MAAM,eAAe,GAAG,KAAK,IAAI,EAAE,CACzC,MAAM,IAAA,2BAAiB,EACtB,IAAA,gBAAM,EAA4C;IACjD,OAAO,EAAE,qCAAqC;IAC9C,OAAO,EAAE;QACR;YACC,KAAK,EAAE,oBAAoB;YAC3B,KAAK,EAAE,mBAAmB;YAC1B,IAAI,EAAE,sDAAsD;SAC5D;QACD;YACC,KAAK,EAAE,oBAAoB;YAC3B,KAAK,EAAE,mBAAmB;YAC1B,IAAI,EAAE,gDAAgD;SACtD;KACD;IACD,YAAY,EAAE,mBAAmB;CACjC,CAAC,CACF,CAAC;AAlBU,QAAA,eAAe,mBAkBzB"}

View File

@@ -0,0 +1,87 @@
import { select, text } from '@clack/prompts';
import type { CredentialType } from './types';
import { withCancelHandler } from '../../../../utils/prompts';
export const credentialTypePrompt = async () =>
await withCancelHandler(
select<CredentialType>({
message: 'What type of authentication does your API use?',
options: [
{
label: 'API Key',
value: 'apiKey',
hint: 'Send a secret key via headers, query, or body',
},
{
label: 'Bearer Token',
value: 'bearer',
hint: 'Send a token via Authorization header (Authorization: Bearer <token>)',
},
{
label: 'OAuth2',
value: 'oauth2',
hint: 'Use an OAuth 2.0 flow to obtain access tokens on behalf of a user or app',
},
{
label: 'Basic Auth',
value: 'basicAuth',
hint: 'Send username and password encoded in base64 via the Authorization header',
},
{
label: 'Custom',
value: 'custom',
hint: 'Create your own credential logic; an empty credential class will be scaffolded for you',
},
{
label: 'None',
value: 'none',
hint: 'No authentication; no credential class will be generated',
},
],
initialValue: 'apiKey',
}),
);
export const baseUrlPrompt = async () =>
await withCancelHandler(
text({
message: "What's the base URL of the API?",
placeholder: 'https://api.example.com/v2',
defaultValue: 'https://api.example.com/v2',
validate: (value) => {
if (!value) return;
if (!value.startsWith('https://') && !value.startsWith('http://')) {
return 'Base URL must start with http(s)://';
}
try {
new URL(value);
} catch (error) {
return 'Must be a valid URL';
}
return;
},
}),
);
export const oauthFlowPrompt = async () =>
await withCancelHandler(
select<'clientCredentials' | 'authorizationCode'>({
message: 'What OAuth2 flow does your API use?',
options: [
{
label: 'Authorization code',
value: 'authorizationCode',
hint: 'Users log in and approve access (use this if unsure)',
},
{
label: 'Client credentials',
value: 'clientCredentials',
hint: 'Server-to-server auth without user interaction',
},
],
initialValue: 'authorizationCode',
}),
);

View File

@@ -0,0 +1,2 @@
import type { CustomTemplateConfig } from './types';
export declare const customTemplate: import("../../../core").TemplateWithRun<CustomTemplateConfig>;

View File

@@ -0,0 +1,83 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.customTemplate = void 0;
const change_case_1 = require("change-case");
const node_path_1 = __importDefault(require("node:path"));
const ast_1 = require("./ast");
const prompts_1 = require("./prompts");
const filesystem_1 = require("../../../../utils/filesystem");
const package_1 = require("../../../../utils/package");
const core_1 = require("../../../core");
exports.customTemplate = (0, core_1.createTemplate)({
name: 'Start from scratch',
description: 'Blank template with guided setup',
path: node_path_1.default.join(__dirname, 'template'),
prompts: async () => {
const baseUrl = await (0, prompts_1.baseUrlPrompt)();
const credentialType = await (0, prompts_1.credentialTypePrompt)();
if (credentialType === 'oauth2') {
const flow = await (0, prompts_1.oauthFlowPrompt)();
return { credentialType, baseUrl, flow };
}
return { credentialType, baseUrl };
},
run: async (data) => {
await renameNode(data, 'Example');
await addCredential(data);
},
});
async function renameNode(data, oldNodeName) {
const { config, nodePackageName: nodeName, destinationPath } = data;
const newClassName = (0, change_case_1.pascalCase)(nodeName.replace('n8n-nodes-', ''));
const oldNodeDir = node_path_1.default.resolve(destinationPath, `nodes/${oldNodeName}`);
await (0, filesystem_1.renameFilesInDirectory)(oldNodeDir, oldNodeName, newClassName);
const newNodeDir = await (0, filesystem_1.renameDirectory)(oldNodeDir, newClassName);
const newNodePath = node_path_1.default.resolve(newNodeDir, `${newClassName}.node.ts`);
const newNodeAst = (0, ast_1.updateNodeAst)({
nodePath: newNodePath,
baseUrl: config.baseUrl,
className: newClassName,
});
await (0, filesystem_1.writeFileSafe)(newNodePath, newNodeAst.getFullText());
const nodes = [`dist/nodes/${newClassName}/${newClassName}.node.js`];
await (0, package_1.setNodesPackageJson)(destinationPath, nodes);
}
async function addCredential(data) {
const { config, destinationPath, nodePackageName } = data;
if (config.credentialType === 'none')
return;
const credentialTemplateName = config.credentialType === 'oauth2'
? config.credentialType + (0, change_case_1.pascalCase)(config.flow)
: config.credentialType;
const credentialTemplatePath = node_path_1.default.resolve(__dirname, `../../shared/credentials/${credentialTemplateName}.credentials.ts`);
const nodeName = nodePackageName.replace('n8n-nodes', '');
const repoName = nodeName;
const { baseUrl, credentialType } = config;
const credentialClassName = config.credentialType === 'oauth2'
? (0, change_case_1.pascalCase)(`${nodeName}-OAuth2-api`)
: (0, change_case_1.pascalCase)(`${nodeName}-api`);
const credentialName = (0, change_case_1.camelCase)(`${nodeName}${credentialType === 'oauth2' ? 'OAuth2Api' : 'Api'}`);
const credentialDisplayName = `${(0, change_case_1.capitalCase)(nodeName)} ${credentialType === 'oauth2' ? 'OAuth2 API' : 'API'}`;
const updatedCredentialAst = (0, ast_1.updateCredentialAst)({
repoName,
baseUrl,
credentialName,
credentialDisplayName,
credentialClassName,
credentialPath: credentialTemplatePath,
});
await (0, filesystem_1.writeFileSafe)(node_path_1.default.resolve(destinationPath, `credentials/${credentialClassName}.credentials.ts`), updatedCredentialAst.getFullText());
await (0, package_1.addCredentialPackageJson)(destinationPath, `dist/credentials/${credentialClassName}.credentials.js`);
for (const nodePath of await (0, package_1.getPackageJsonNodes)(destinationPath)) {
const srcNodePath = node_path_1.default.resolve(destinationPath, nodePath.replace(/.js$/, '.ts').replace(/^dist\//, ''));
const updatedNodeAst = (0, ast_1.addCredentialToNode)({
nodePath: srcNodePath,
credentialName,
});
await (0, filesystem_1.writeFileSafe)(srcNodePath, updatedNodeAst.getFullText());
}
}
//# sourceMappingURL=template.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../../../../src/template/templates/declarative/custom/template.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,121 @@
import { camelCase, capitalCase, pascalCase } from 'change-case';
import path from 'node:path';
import { addCredentialToNode, updateCredentialAst, updateNodeAst } from './ast';
import { baseUrlPrompt, credentialTypePrompt, oauthFlowPrompt } from './prompts';
import type { CustomTemplateConfig } from './types';
import {
renameDirectory,
renameFilesInDirectory,
writeFileSafe,
} from '../../../../utils/filesystem';
import {
setNodesPackageJson,
addCredentialPackageJson,
getPackageJsonNodes,
} from '../../../../utils/package';
import { createTemplate, type TemplateData } from '../../../core';
export const customTemplate = createTemplate({
name: 'Start from scratch',
description: 'Blank template with guided setup',
path: path.join(__dirname, 'template'),
prompts: async (): Promise<CustomTemplateConfig> => {
const baseUrl = await baseUrlPrompt();
const credentialType = await credentialTypePrompt();
if (credentialType === 'oauth2') {
const flow = await oauthFlowPrompt();
return { credentialType, baseUrl, flow };
}
return { credentialType, baseUrl };
},
run: async (data) => {
await renameNode(data, 'Example');
await addCredential(data);
},
});
async function renameNode(data: TemplateData<CustomTemplateConfig>, oldNodeName: string) {
const { config, nodePackageName: nodeName, destinationPath } = data;
const newClassName = pascalCase(nodeName.replace('n8n-nodes-', ''));
const oldNodeDir = path.resolve(destinationPath, `nodes/${oldNodeName}`);
await renameFilesInDirectory(oldNodeDir, oldNodeName, newClassName);
const newNodeDir = await renameDirectory(oldNodeDir, newClassName);
const newNodePath = path.resolve(newNodeDir, `${newClassName}.node.ts`);
const newNodeAst = updateNodeAst({
nodePath: newNodePath,
baseUrl: config.baseUrl,
className: newClassName,
});
await writeFileSafe(newNodePath, newNodeAst.getFullText());
const nodes = [`dist/nodes/${newClassName}/${newClassName}.node.js`];
await setNodesPackageJson(destinationPath, nodes);
}
async function addCredential(data: TemplateData<CustomTemplateConfig>) {
const { config, destinationPath, nodePackageName } = data;
if (config.credentialType === 'none') return;
const credentialTemplateName =
config.credentialType === 'oauth2'
? config.credentialType + pascalCase(config.flow)
: config.credentialType;
const credentialTemplatePath = path.resolve(
__dirname,
`../../shared/credentials/${credentialTemplateName}.credentials.ts`,
);
const nodeName = nodePackageName.replace('n8n-nodes', '');
const repoName = nodeName;
const { baseUrl, credentialType } = config;
const credentialClassName =
config.credentialType === 'oauth2'
? pascalCase(`${nodeName}-OAuth2-api`)
: pascalCase(`${nodeName}-api`);
const credentialName = camelCase(
`${nodeName}${credentialType === 'oauth2' ? 'OAuth2Api' : 'Api'}`,
);
const credentialDisplayName = `${capitalCase(nodeName)} ${
credentialType === 'oauth2' ? 'OAuth2 API' : 'API'
}`;
const updatedCredentialAst = updateCredentialAst({
repoName,
baseUrl,
credentialName,
credentialDisplayName,
credentialClassName,
credentialPath: credentialTemplatePath,
});
await writeFileSafe(
path.resolve(destinationPath, `credentials/${credentialClassName}.credentials.ts`),
updatedCredentialAst.getFullText(),
);
await addCredentialPackageJson(
destinationPath,
`dist/credentials/${credentialClassName}.credentials.js`,
);
for (const nodePath of await getPackageJsonNodes(destinationPath)) {
const srcNodePath = path.resolve(
destinationPath,
nodePath.replace(/.js$/, '.ts').replace(/^dist\//, ''),
);
const updatedNodeAst = addCredentialToNode({
nodePath: srcNodePath,
credentialName,
});
await writeFileSafe(srcNodePath, updatedNodeAst.getFullText());
}
}

View File

@@ -0,0 +1,46 @@
# {{nodePackageName}}
This is an n8n community node. It lets you use _app/service name_ in your n8n workflows.
_App/service name_ is _one or two sentences describing the service this node integrates with_.
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/sustainable-use-license/) workflow automation platform.
[Installation](#installation)
[Operations](#operations)
[Credentials](#credentials)
[Compatibility](#compatibility)
[Usage](#usage)
[Resources](#resources)
[Version history](#version-history)
## Installation
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
## Operations
_List the operations supported by your node._
## Credentials
_If users need to authenticate with the app/service, provide details here. You should include prerequisites (such as signing up with the service), available authentication methods, and how to set them up._
## Compatibility
_State the minimum n8n version, as well as which versions you test against. You can also include any known version incompatibility issues._
## Usage
_This is an optional section. Use it to help users with any difficult or confusing aspects of the node._
_By the time users are looking for community nodes, they probably already know n8n basics. But if you expect new users, you can link to the [Try it out](https://docs.n8n.io/try-it-out/) documentation to help them get started._
## Resources
* [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
* _Link to app/service documentation._
## Version history
_This is another optional section. If your node has multiple versions, include a short description of available versions and what changed, as well as any compatibility impact._

View File

@@ -0,0 +1,18 @@
{
"node": "{{nodePackageName}}",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file#credentials"
}
],
"primaryDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file"
}
]
}
}

View File

@@ -0,0 +1,50 @@
import { NodeConnectionType, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
import { userDescription } from './resources/user';
import { companyDescription } from './resources/company';
export class Example implements INodeType {
description: INodeTypeDescription = {
displayName: 'Example',
name: 'example',
icon: { light: 'file:example.svg', dark: 'file:example.dark.svg' },
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Interact with the Example API',
defaults: {
name: 'Example',
},
usableAsTool: true,
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [],
requestDefaults: {
baseURL: 'https://api.example.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'User',
value: 'user',
},
{
name: 'Company',
value: 'company',
},
],
default: 'user',
},
...userDescription,
...companyDescription,
],
};
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="aquamarine"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="darkblue"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 696 B

View File

@@ -0,0 +1,61 @@
import type { INodeProperties } from 'n8n-workflow';
const showOnlyForCompanyGetMany = {
operation: ['getAll'],
resource: ['company'],
};
export const companyGetManyDescription: INodeProperties[] = [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
...showOnlyForCompanyGetMany,
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
routing: {
send: {
type: 'query',
property: 'limit',
},
output: {
maxResults: '={{$value}}',
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: showOnlyForCompanyGetMany,
},
default: false,
description: 'Whether to return all results or only up to a given limit',
routing: {
send: {
paginate: '={{ $value }}',
},
operations: {
pagination: {
type: 'offset',
properties: {
limitParameter: 'limit',
offsetParameter: 'offset',
pageSize: 100,
type: 'query',
},
},
},
},
},
];

View File

@@ -0,0 +1,34 @@
import type { INodeProperties } from 'n8n-workflow';
import { companyGetManyDescription } from './getAll';
const showOnlyForCompanies = {
resource: ['company'],
};
export const companyDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForCompanies,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get companies',
description: 'Get companies',
routing: {
request: {
method: 'GET',
url: '/companies',
},
},
},
],
default: 'getAll',
},
...companyGetManyDescription,
];

View File

@@ -0,0 +1,26 @@
import type { INodeProperties } from 'n8n-workflow';
const showOnlyForUserCreate = {
operation: ['create'],
resource: ['user'],
};
export const userCreateDescription: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: showOnlyForUserCreate,
},
description: 'The name of the user',
routing: {
send: {
type: 'body',
property: 'name',
},
},
},
];

View File

@@ -0,0 +1,17 @@
import type { INodeProperties } from 'n8n-workflow';
const showOnlyForUserGet = {
operation: ['get'],
resource: ['user'],
};
export const userGetDescription: INodeProperties[] = [
{
displayName: 'User ID',
name: 'userId',
type: 'string',
displayOptions: { show: showOnlyForUserGet },
default: '',
description: "The user's ID to retrieve",
},
];

View File

@@ -0,0 +1,60 @@
import type { INodeProperties } from 'n8n-workflow';
import { userCreateDescription } from './create';
import { userGetDescription } from './get';
const showOnlyForUsers = {
resource: ['user'],
};
export const userDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForUsers,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get users',
description: 'Get many users',
routing: {
request: {
method: 'GET',
url: '/users',
},
},
},
{
name: 'Get',
value: 'get',
action: 'Get a user',
description: 'Get the data of a single user',
routing: {
request: {
method: 'GET',
url: '=/users/{{$parameter.userId}}',
},
},
},
{
name: 'Create',
value: 'create',
action: 'Create a new user',
description: 'Create a new user',
routing: {
request: {
method: 'POST',
url: '/users',
},
},
},
],
default: 'getAll',
},
...userGetDescription,
...userCreateDescription,
];

View File

@@ -0,0 +1,48 @@
{
"name": "{{nodePackageName}}",
"version": "0.1.0",
"description": "",
"license": "MIT",
"homepage": "",
"keywords": [
"n8n-community-node-package"
],
"author": {
"name": "{{user.name}}",
"email": "{{user.email}}"
},
"repository": {
"type": "git",
"url": "https://github.com/<...>/n8n-nodes-<...>.git"
},
"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"
],
"n8n": {
"n8nNodesApiVersion": 1,
"strict": true,
"credentials": [],
"nodes": [
"dist/nodes/Example/Example.node.js"
]
},
"devDependencies": {
"@n8n/node-cli": "*",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"n8n-workflow": "*"
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"outDir": "./dist/"
},
"include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
}

View File

@@ -0,0 +1,9 @@
export type CustomTemplateConfig = {
credentialType: 'apiKey' | 'bearer' | 'basicAuth' | 'custom' | 'none';
baseUrl: string;
} | {
credentialType: 'oauth2';
baseUrl: string;
flow: string;
};
export type CredentialType = CustomTemplateConfig['credentialType'];

View File

@@ -0,0 +1,3 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../src/template/templates/declarative/custom/types.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,8 @@
export type CustomTemplateConfig =
| {
credentialType: 'apiKey' | 'bearer' | 'basicAuth' | 'custom' | 'none';
baseUrl: string;
}
| { credentialType: 'oauth2'; baseUrl: string; flow: string };
export type CredentialType = CustomTemplateConfig['credentialType'];

View File

@@ -0,0 +1 @@
export declare const githubIssuesTemplate: import("../../../core").TemplateWithRun<object>;

View File

@@ -0,0 +1,14 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.githubIssuesTemplate = void 0;
const node_path_1 = __importDefault(require("node:path"));
const core_1 = require("../../../core");
exports.githubIssuesTemplate = (0, core_1.createTemplate)({
name: 'GitHub Issues API',
description: 'Demo node with multiple operations and credentials',
path: node_path_1.default.join(__dirname, 'template'),
});
//# sourceMappingURL=template.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../../../../src/template/templates/declarative/github-issues/template.ts"],"names":[],"mappings":";;;;;;AAAA,0DAA6B;AAE7B,wCAA+C;AAElC,QAAA,oBAAoB,GAAG,IAAA,qBAAc,EAAC;IAClD,IAAI,EAAE,mBAAmB;IACzB,WAAW,EAAE,oDAAoD;IACjE,IAAI,EAAE,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;CACtC,CAAC,CAAC"}

View File

@@ -0,0 +1,9 @@
import path from 'node:path';
import { createTemplate } from '../../../core';
export const githubIssuesTemplate = createTemplate({
name: 'GitHub Issues API',
description: 'Demo node with multiple operations and credentials',
path: path.join(__dirname, 'template'),
});

View File

@@ -0,0 +1,73 @@
# {{nodePackageName}}
This is an n8n community node. It lets you use GitHub Issues in your n8n workflows.
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/sustainable-use-license/) workflow automation platform.
[Installation](#installation)
[Operations](#operations)
[Credentials](#credentials)
[Compatibility](#compatibility)
[Usage](#usage)
[Resources](#resources)
## Installation
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
## Operations
- Issues
- Get an issue
- Get many issues in a repository
- Create a new issue
- Issue Comments
- Get many issue comments
## Credentials
You can use either access token or OAuth2 to use this node.
### Access token
1. Open your GitHub profile [Settings](https://github.com/settings/profile).
2. In the left navigation, select [Developer settings](https://github.com/settings/apps).
3. In the left navigation, under Personal access tokens, select Tokens (classic).
4. Select Generate new token > Generate new token (classic).
5. Enter a descriptive name for your token in the Note field, like n8n integration.
6. Select the Expiration you'd like for the token, or select No expiration.
7. Select Scopes for your token. For most of the n8n GitHub nodes, add the `repo` scope.
- A token without assigned scopes can only access public information.
8. Select Generate token.
9. Copy the token.
Refer to [Creating a personal access token (classic)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic) for more information. Refer to Scopes for OAuth apps for more information on GitHub scopes.
![Generated Access token in GitHub](https://docs.github.com/assets/cb-17251/mw-1440/images/help/settings/personal-access-tokens.webp)
### OAuth2
If you're self-hosting n8n, create a new GitHub [OAuth app](https://docs.github.com/en/apps/oauth-apps):
1. Open your GitHub profile [Settings](https://github.com/settings/profile).
2. In the left navigation, select [Developer settings](https://github.com/settings/apps).
3. In the left navigation, select OAuth apps.
4. Select New OAuth App.
- If you haven't created an app before, you may see Register a new application instead. Select it.
5. Enter an Application name, like n8n integration.
6. Enter the Homepage URL for your app's website.
7. If you'd like, add the optional Application description, which GitHub displays to end-users.
8. From n8n, copy the OAuth Redirect URL and paste it into the GitHub Authorization callback URL.
9. Select Register application.
10. Copy the Client ID and Client Secret this generates and add them to your n8n credential.
Refer to the [GitHub Authorizing OAuth apps documentation](https://docs.github.com/en/apps/oauth-apps/using-oauth-apps/authorizing-oauth-apps) for more information on the authorization process.
## Compatibility
Compatible with n8n@1.60.0 or later
## Resources
* [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
* [GitHub API docs](https://docs.github.com/en/rest/issues)

View File

@@ -0,0 +1,45 @@
import type {
IAuthenticateGeneric,
Icon,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class GithubIssuesApi implements ICredentialType {
name = 'githubIssuesApi';
displayName = 'GitHub Issues API';
icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' };
documentationUrl =
'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#deleting-a-personal-access-token';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { password: true },
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=token {{$credentials?.accessToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.github.com',
url: '/user',
method: 'GET',
},
};
}

View File

@@ -0,0 +1,54 @@
import type { Icon, ICredentialType, INodeProperties } from 'n8n-workflow';
export class GithubIssuesOAuth2Api implements ICredentialType {
name = 'githubIssuesOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'GitHub Issues OAuth2 API';
icon: Icon = { light: 'file:../icons/github.svg', dark: 'file:../icons/github.dark.svg' };
documentationUrl = 'https://docs.github.com/en/apps/oauth-apps';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://github.com/login/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://github.com/login/oauth/access_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'repo',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'header',
},
];
}

View File

@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="#24292F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,18 @@
{
"node": "{{nodePackageName}}",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file#credentials"
}
],
"primaryDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file"
}
]
}
}

View File

@@ -0,0 +1,96 @@
import { NodeConnectionType, type INodeType, type INodeTypeDescription } from 'n8n-workflow';
import { issueDescription } from './resources/issue';
import { issueCommentDescription } from './resources/issueComment';
import { getRepositories } from './listSearch/getRepositories';
import { getUsers } from './listSearch/getUsers';
import { getIssues } from './listSearch/getIssues';
export class GithubIssues implements INodeType {
description: INodeTypeDescription = {
displayName: 'GitHub Issues',
name: 'githubIssues',
icon: { light: 'file:../../icons/github.svg', dark: 'file:../../icons/github.dark.svg' },
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume issues from the GitHub API',
defaults: {
name: 'GitHub Issues',
},
usableAsTool: true,
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
credentials: [
{
name: 'githubIssuesApi',
required: true,
displayOptions: {
show: {
authentication: ['accessToken'],
},
},
},
{
name: 'githubIssuesOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
requestDefaults: {
baseURL: 'https://api.github.com',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'accessToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Issue',
value: 'issue',
},
{
name: 'Issue Comment',
value: 'issueComment',
},
],
default: 'issue',
},
...issueDescription,
...issueCommentDescription,
],
};
methods = {
listSearch: {
getRepositories,
getUsers,
getIssues,
},
};
}

View File

@@ -0,0 +1,49 @@
import type {
ILoadOptionsFunctions,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type IssueSearchItem = {
number: number;
title: string;
html_url: string;
};
type IssueSearchResponse = {
items: IssueSearchItem[];
total_count: number;
};
export async function getIssues(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
let responseData: IssueSearchResponse = {
items: [],
total_count: 0,
};
const owner = this.getNodeParameter('owner', '', { extractValue: true });
const repository = this.getNodeParameter('repository', '', { extractValue: true });
const filters = [filter, `repo:${owner}/${repository}`];
responseData = await githubApiRequest.call(this, 'GET', '/search/issues', {
q: filters.filter(Boolean).join(' '),
page,
per_page,
});
const results: INodeListSearchItems[] = responseData.items.map((item: IssueSearchItem) => ({
name: item.title,
value: item.number,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View File

@@ -0,0 +1,50 @@
import type {
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type RepositorySearchItem = {
name: string;
html_url: string;
};
type RepositorySearchResponse = {
items: RepositorySearchItem[];
total_count: number;
};
export async function getRepositories(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
const q = `${filter ?? ''} user:${owner} fork:true`;
let responseData: RepositorySearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(this, 'GET', '/search/repositories', {
q,
page,
per_page,
});
} catch {
// will fail if the owner does not have any repositories
}
const results: INodeListSearchItems[] = responseData.items.map((item: RepositorySearchItem) => ({
name: item.name,
value: item.name,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View File

@@ -0,0 +1,49 @@
import type {
ILoadOptionsFunctions,
INodeListSearchResult,
INodeListSearchItems,
} from 'n8n-workflow';
import { githubApiRequest } from '../shared/transport';
type UserSearchItem = {
login: string;
html_url: string;
};
type UserSearchResponse = {
items: UserSearchItem[];
total_count: number;
};
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const page = paginationToken ? +paginationToken : 1;
const per_page = 100;
let responseData: UserSearchResponse = {
items: [],
total_count: 0,
};
try {
responseData = await githubApiRequest.call(this, 'GET', '/search/users', {
q: filter,
page,
per_page,
});
} catch {
// will fail if the owner does not have any users
}
const results: INodeListSearchItems[] = responseData.items.map((item: UserSearchItem) => ({
name: item.login,
value: item.login,
url: item.html_url,
}));
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}

View File

@@ -0,0 +1,74 @@
import type { INodeProperties } from 'n8n-workflow';
const showOnlyForIssueCreate = {
operation: ['create'],
resource: ['issue'],
};
export const issueCreateDescription: INodeProperties[] = [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
displayOptions: {
show: showOnlyForIssueCreate,
},
description: 'The title of the issue',
routing: {
send: {
type: 'body',
property: 'title',
},
},
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: showOnlyForIssueCreate,
},
description: 'The body of the issue',
routing: {
send: {
type: 'body',
property: 'body',
},
},
},
{
displayName: 'Labels',
name: 'labels',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Label',
},
displayOptions: {
show: showOnlyForIssueCreate,
},
default: { label: '' },
options: [
{
displayName: 'Label',
name: 'label',
type: 'string',
default: '',
description: 'Label to add to issue',
},
],
routing: {
send: {
type: 'body',
property: 'labels',
value: '={{$value.map((data) => data.label)}}',
},
},
},
];

View File

@@ -0,0 +1,14 @@
import type { INodeProperties } from 'n8n-workflow';
import { issueSelect } from '../../shared/descriptions';
const showOnlyForIssueGet = {
operation: ['get'],
resource: ['issue'],
};
export const issueGetDescription: INodeProperties[] = [
{
...issueSelect,
displayOptions: { show: showOnlyForIssueGet },
},
];

View File

@@ -0,0 +1,124 @@
import type { INodeProperties } from 'n8n-workflow';
import { parseLinkHeader } from '../../shared/utils';
const showOnlyForIssueGetMany = {
operation: ['getAll'],
resource: ['issue'],
};
export const issueGetManyDescription: INodeProperties[] = [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
...showOnlyForIssueGetMany,
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
routing: {
send: {
type: 'query',
property: 'per_page',
},
output: {
maxResults: '={{$value}}',
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: showOnlyForIssueGetMany,
},
default: false,
description: 'Whether to return all results or only up to a given limit',
routing: {
send: {
paginate: '={{ $value }}',
type: 'query',
property: 'per_page',
value: '100',
},
operations: {
pagination: {
type: 'generic',
properties: {
continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`,
request: {
url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`,
},
},
},
},
},
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
typeOptions: {
multipleValueButtonText: 'Add Filter',
},
displayOptions: {
show: showOnlyForIssueGetMany,
},
default: {},
options: [
{
displayName: 'Updated Since',
name: 'since',
type: 'dateTime',
default: '',
description: 'Return only issues updated at or after this time',
routing: {
request: {
qs: {
since: '={{$value}}',
},
},
},
},
{
displayName: 'State',
name: 'state',
type: 'options',
options: [
{
name: 'All',
value: 'all',
description: 'Returns issues with any state',
},
{
name: 'Closed',
value: 'closed',
description: 'Return issues with "closed" state',
},
{
name: 'Open',
value: 'open',
description: 'Return issues with "open" state',
},
],
default: 'open',
description: 'The issue state to filter on',
routing: {
request: {
qs: {
state: '={{$value}}',
},
},
},
},
],
},
];

View File

@@ -0,0 +1,75 @@
import type { INodeProperties } from 'n8n-workflow';
import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions';
import { issueGetManyDescription } from './getAll';
import { issueGetDescription } from './get';
import { issueCreateDescription } from './create';
const showOnlyForIssues = {
resource: ['issue'],
};
export const issueDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForIssues,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get issues in a repository',
description: 'Get many issues in a repository',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues',
},
},
},
{
name: 'Get',
value: 'get',
action: 'Get an issue',
description: 'Get the data of a single issue',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$parameter.issue}}',
},
},
},
{
name: 'Create',
value: 'create',
action: 'Create a new issue',
description: 'Create a new issue',
routing: {
request: {
method: 'POST',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues',
},
},
},
],
default: 'getAll',
},
{
...repoOwnerSelect,
displayOptions: {
show: showOnlyForIssues,
},
},
{
...repoNameSelect,
displayOptions: {
show: showOnlyForIssues,
},
},
...issueGetManyDescription,
...issueGetDescription,
...issueCreateDescription,
];

View File

@@ -0,0 +1,65 @@
import type { INodeProperties } from 'n8n-workflow';
import { parseLinkHeader } from '../../shared/utils';
const showOnlyForIssueCommentGetMany = {
operation: ['getAll'],
resource: ['issueComment'],
};
export const issueCommentGetManyDescription: INodeProperties[] = [
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
...showOnlyForIssueCommentGetMany,
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
routing: {
send: {
type: 'query',
property: 'per_page',
},
output: {
maxResults: '={{$value}}',
},
},
description: 'Max number of results to return',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: showOnlyForIssueCommentGetMany,
},
default: false,
description: 'Whether to return all results or only up to a given limit',
routing: {
send: {
paginate: '={{ $value }}',
type: 'query',
property: 'per_page',
value: '100',
},
operations: {
pagination: {
type: 'generic',
properties: {
continue: `={{ !!(${parseLinkHeader.toString()})($response.headers?.link).next }}`,
request: {
url: `={{ (${parseLinkHeader.toString()})($response.headers?.link)?.next ?? $request.url }}`,
},
},
},
},
},
},
];

View File

@@ -0,0 +1,47 @@
import type { INodeProperties } from 'n8n-workflow';
import { repoNameSelect, repoOwnerSelect } from '../../shared/descriptions';
import { issueCommentGetManyDescription } from './getAll';
const showOnlyForIssueComments = {
resource: ['issueComment'],
};
export const issueCommentDescription: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: showOnlyForIssueComments,
},
options: [
{
name: 'Get Many',
value: 'getAll',
action: 'Get issue comments',
description: 'Get issue comments',
routing: {
request: {
method: 'GET',
url: '=/repos/{{$parameter.owner}}/{{$parameter.repository}}/issues/comments',
},
},
},
],
default: 'getAll',
},
{
...repoOwnerSelect,
displayOptions: {
show: showOnlyForIssueComments,
},
},
{
...repoNameSelect,
displayOptions: {
show: showOnlyForIssueComments,
},
},
...issueCommentGetManyDescription,
];

View File

@@ -0,0 +1,151 @@
import type { INodeProperties } from 'n8n-workflow';
export const repoOwnerSelect: INodeProperties = {
displayName: 'Repository Owner',
name: 'owner',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Repository Owner',
name: 'list',
type: 'list',
placeholder: 'Select an owner...',
typeOptions: {
searchListMethod: 'getUsers',
searchable: true,
searchFilterRequired: false,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid GitHub URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n-io',
validation: [
{
type: 'regex',
properties: {
regex: '[-_a-zA-Z0-9]+',
errorMessage: 'Not a valid GitHub Owner Name',
},
},
],
url: '=https://github.com/{{$value}}',
},
],
};
export const repoNameSelect: INodeProperties = {
displayName: 'Repository Name',
name: 'repository',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
modes: [
{
displayName: 'Repository Name',
name: 'list',
type: 'list',
placeholder: 'Select an Repository...',
typeOptions: {
searchListMethod: 'getRepositories',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://github.com/n8n-io/n8n',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid GitHub Repository URL',
},
},
],
},
{
displayName: 'By Name',
name: 'name',
type: 'string',
placeholder: 'e.g. n8n',
validation: [
{
type: 'regex',
properties: {
regex: '[-_.0-9a-zA-Z]+',
errorMessage: 'Not a valid GitHub Repository Name',
},
},
],
url: '=https://github.com/{{$parameter["owner"]}}/{{$value}}',
},
],
displayOptions: {
hide: {
resource: ['user', 'organization'],
operation: ['getRepositories'],
},
},
};
export const issueSelect: INodeProperties = {
displayName: 'Issue',
name: 'issue',
type: 'resourceLocator',
default: {
mode: 'list',
value: '',
},
required: true,
modes: [
{
displayName: 'Issue',
name: 'list',
type: 'list',
placeholder: 'Select an Issue...',
typeOptions: {
searchListMethod: 'getIssues',
searchable: true,
},
},
{
displayName: 'By ID',
name: 'name',
type: 'string',
placeholder: 'e.g. 123',
url: '=https://github.com/{{$parameter.owner}}/{{$parameter.repository}}/issues/{{$value}}',
},
],
};

View File

@@ -0,0 +1,32 @@
import type {
IHookFunctions,
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IHttpRequestMethods,
IDataObject,
IHttpRequestOptions,
} from 'n8n-workflow';
export async function githubApiRequest(
this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
qs: IDataObject = {},
body: IDataObject | undefined = undefined,
) {
const authenticationMethod = this.getNodeParameter('authentication', 0);
const options: IHttpRequestOptions = {
method: method,
qs,
body,
url: `https://api.github.com${resource}`,
json: true,
};
const credentialType =
authenticationMethod === 'accessToken' ? 'githubIssuesApi' : 'githubIssuesOAuth2Api';
return this.helpers.httpRequestWithAuthentication.call(this, credentialType, options);
}

View File

@@ -0,0 +1,14 @@
export function parseLinkHeader(header?: string): { [rel: string]: string } {
const links: { [rel: string]: string } = {};
for (const part of header?.split(',') ?? []) {
const section = part.trim();
const match = section.match(/^<([^>]+)>\s*;\s*rel="?([^"]+)"?/);
if (match) {
const [, url, rel] = match;
links[rel] = url;
}
}
return links;
}

View File

@@ -0,0 +1,51 @@
{
"name": "{{nodePackageName}}",
"version": "0.1.0",
"description": "",
"license": "MIT",
"homepage": "",
"keywords": [
"n8n-community-node-package"
],
"author": {
"name": "{{user.name}}",
"email": "{{user.email}}"
},
"repository": {
"type": "git",
"url": "https://github.com/<...>/n8n-nodes-<...>.git"
},
"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"
],
"n8n": {
"n8nNodesApiVersion": 1,
"strict": true,
"credentials": [
"dist/credentials/GithubIssuesApi.credentials.js",
"dist/credentials/GithubIssuesOAuth2Api.credentials.js"
],
"nodes": [
"dist/nodes/GithubIssues/GithubIssues.node.js"
]
},
"devDependencies": {
"@n8n/node-cli": "*",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"n8n-workflow": "*"
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"outDir": "./dist/"
},
"include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
}

View File

@@ -0,0 +1,15 @@
export declare const templates: {
readonly declarative: {
readonly githubIssues: import("../core").TemplateWithRun<object>;
readonly custom: import("../core").TemplateWithRun<import("./declarative/custom/types").CustomTemplateConfig>;
};
readonly programmatic: {
readonly example: import("../core").TemplateWithRun<object>;
};
};
export type TemplateMap = typeof templates;
export type TemplateType = keyof TemplateMap;
export type TemplateName<T extends TemplateType> = keyof TemplateMap[T];
export declare function getTemplate<T extends TemplateType, N extends TemplateName<T>>(type: T, name: N): TemplateMap[T][N];
export declare function isTemplateType(val: unknown): val is TemplateType;
export declare function isTemplateName<T extends TemplateType>(type: T, name: unknown): name is TemplateName<T>;

View File

@@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.templates = void 0;
exports.getTemplate = getTemplate;
exports.isTemplateType = isTemplateType;
exports.isTemplateName = isTemplateName;
const template_1 = require("./declarative/custom/template");
const template_2 = require("./declarative/github-issues/template");
const template_3 = require("./programmatic/example/template");
exports.templates = {
declarative: {
githubIssues: template_2.githubIssuesTemplate,
custom: template_1.customTemplate,
},
programmatic: {
example: template_3.exampleTemplate,
},
};
function getTemplate(type, name) {
return exports.templates[type][name];
}
function isTemplateType(val) {
return typeof val === 'string' && val in exports.templates;
}
function isTemplateName(type, name) {
return typeof name === 'string' && name in exports.templates[type];
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/template/templates/index.ts"],"names":[],"mappings":";;;AAkBA,kCAKC;AAED,wCAEC;AAED,wCAKC;AAlCD,4DAA+D;AAC/D,mEAA4E;AAC5E,8DAAkE;AAErD,QAAA,SAAS,GAAG;IACxB,WAAW,EAAE;QACZ,YAAY,EAAE,+BAAoB;QAClC,MAAM,EAAE,yBAAc;KACtB;IACD,YAAY,EAAE;QACb,OAAO,EAAE,0BAAe;KACxB;CACQ,CAAC;AAMX,SAAgB,WAAW,CAC1B,IAAO,EACP,IAAO;IAEP,OAAO,iBAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY;IAC1C,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,IAAI,iBAAS,CAAC;AACpD,CAAC;AAED,SAAgB,cAAc,CAC7B,IAAO,EACP,IAAa;IAEb,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,iBAAS,CAAC,IAAI,CAAC,CAAC;AAC5D,CAAC"}

View File

@@ -0,0 +1,35 @@
import { customTemplate } from './declarative/custom/template';
import { githubIssuesTemplate } from './declarative/github-issues/template';
import { exampleTemplate } from './programmatic/example/template';
export const templates = {
declarative: {
githubIssues: githubIssuesTemplate,
custom: customTemplate,
},
programmatic: {
example: exampleTemplate,
},
} as const;
export type TemplateMap = typeof templates;
export type TemplateType = keyof TemplateMap;
export type TemplateName<T extends TemplateType> = keyof TemplateMap[T];
export function getTemplate<T extends TemplateType, N extends TemplateName<T>>(
type: T,
name: N,
): TemplateMap[T][N] {
return templates[type][name];
}
export function isTemplateType(val: unknown): val is TemplateType {
return typeof val === 'string' && val in templates;
}
export function isTemplateName<T extends TemplateType>(
type: T,
name: unknown,
): name is TemplateName<T> {
return typeof name === 'string' && name in templates[type];
}

View File

@@ -0,0 +1 @@
export declare const exampleTemplate: import("../../../core").TemplateWithRun<object>;

View File

@@ -0,0 +1,14 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.exampleTemplate = void 0;
const node_path_1 = __importDefault(require("node:path"));
const core_1 = require("../../../core");
exports.exampleTemplate = (0, core_1.createTemplate)({
name: 'Example',
description: 'Barebones example node',
path: node_path_1.default.join(__dirname, 'template'),
});
//# sourceMappingURL=template.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../../../../src/template/templates/programmatic/example/template.ts"],"names":[],"mappings":";;;;;;AAAA,0DAA6B;AAE7B,wCAA+C;AAElC,QAAA,eAAe,GAAG,IAAA,qBAAc,EAAC;IAC7C,IAAI,EAAE,SAAS;IACf,WAAW,EAAE,wBAAwB;IACrC,IAAI,EAAE,mBAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;CACtC,CAAC,CAAC"}

View File

@@ -0,0 +1,9 @@
import path from 'node:path';
import { createTemplate } from '../../../core';
export const exampleTemplate = createTemplate({
name: 'Example',
description: 'Barebones example node',
path: path.join(__dirname, 'template'),
});

View File

@@ -0,0 +1,46 @@
# {{nodePackageName}}
This is an n8n community node. It lets you use _app/service name_ in your n8n workflows.
_App/service name_ is _one or two sentences describing the service this node integrates with_.
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/sustainable-use-license/) workflow automation platform.
[Installation](#installation)
[Operations](#operations)
[Credentials](#credentials)
[Compatibility](#compatibility)
[Usage](#usage)
[Resources](#resources)
[Version history](#version-history)
## Installation
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
## Operations
_List the operations supported by your node._
## Credentials
_If users need to authenticate with the app/service, provide details here. You should include prerequisites (such as signing up with the service), available authentication methods, and how to set them up._
## Compatibility
_State the minimum n8n version, as well as which versions you test against. You can also include any known version incompatibility issues._
## Usage
_This is an optional section. Use it to help users with any difficult or confusing aspects of the node._
_By the time users are looking for community nodes, they probably already know n8n basics. But if you expect new users, you can link to the [Try it out](https://docs.n8n.io/try-it-out/) documentation to help them get started._
## Resources
* [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
* _Link to app/service documentation._
## Version history
_This is another optional section. If your node has multiple versions, include a short description of available versions and what changed, as well as any compatibility impact._

View File

@@ -0,0 +1,18 @@
{
"node": "{{nodePackageName}}",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Developer Tools"],
"resources": {
"credentialDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file#credentials"
}
],
"primaryDocumentation": [
{
"url": "https://github.com/org/repo?tab=readme-ov-file"
}
]
}
}

View File

@@ -0,0 +1,78 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
export class Example implements INodeType {
description: INodeTypeDescription = {
displayName: 'Example',
name: 'example',
icon: { light: 'file:example.svg', dark: 'file:example.dark.svg' },
group: ['input'],
version: 1,
description: 'Basic Example Node',
defaults: {
name: 'Example',
},
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
usableAsTool: true,
properties: [
// Node properties which the user gets displayed and
// can change on the node.
{
displayName: 'My String',
name: 'myString',
type: 'string',
default: '',
placeholder: 'Placeholder value',
description: 'The description text',
},
],
};
// The function below is responsible for actually doing whatever this node
// is supposed to do. In this case, we're just appending the `myString` property
// with whatever the user has entered.
// You can make async calls and use `await`.
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
let item: INodeExecutionData;
let myString: string;
// Iterates over all input items and add the key "myString" with the
// value the parameter "myString" resolves to.
// (This could be a different value for each item in case it contains an expression)
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
myString = this.getNodeParameter('myString', itemIndex, '') as string;
item = items[itemIndex];
item.json.myString = myString;
} catch (error) {
// This node should never fail but we want to showcase how
// to handle errors.
if (this.continueOnFail()) {
items.push({ json: this.getInputData(itemIndex)[0].json, error, pairedItem: itemIndex });
} else {
// Adding `itemIndex` allows other workflows to handle this error
if (error.context) {
// If the error thrown already contains the context property,
// only append the itemIndex
error.context.itemIndex = itemIndex;
throw error;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex,
});
}
}
}
return [items];
}
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="aquamarine"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="darkblue"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>

After

Width:  |  Height:  |  Size: 696 B

View File

@@ -0,0 +1,48 @@
{
"name": "{{nodePackageName}}",
"version": "0.1.0",
"description": "",
"license": "MIT",
"homepage": "",
"keywords": [
"n8n-community-node-package"
],
"author": {
"name": "{{user.name}}",
"email": "{{user.email}}"
},
"repository": {
"type": "git",
"url": "https://github.com/<...>/n8n-nodes-<...>.git"
},
"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"
],
"n8n": {
"n8nNodesApiVersion": 1,
"strict": true,
"credentials": [],
"nodes": [
"dist/nodes/Example/Example.node.js"
]
},
"devDependencies": {
"@n8n/node-cli": "*",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"n8n-workflow": "*"
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"outDir": "./dist/"
},
"include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
}

View File

@@ -0,0 +1,42 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleApi implements ICredentialType {
name = 'exampleApi';
displayName = 'Example API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
'x-api-key': '={{$credentials.apiKey}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.example.com/v2',
url: '/v1/user',
},
};
}

View File

@@ -0,0 +1,50 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleApi implements ICredentialType {
name = 'exampleApi';
displayName = 'Example API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'Username',
name: 'username',
type: 'string',
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string',
typeOptions: {
password: true,
},
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
auth: {
username: '={{$credentials.username}}',
password: '={{$credentials.password}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.example.com/v2',
url: '/v1/user',
},
};
}

View File

@@ -0,0 +1,42 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleApi implements ICredentialType {
name = 'exampleApi';
displayName = 'Example API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.accessToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.example.com/v2',
url: '/v1/user',
},
};
}

View File

@@ -0,0 +1,48 @@
import type {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class ExampleApi implements ICredentialType {
name = 'exampleApi';
displayName = 'Example API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
body: {
token: '={{$credentials.accessToken}}',
},
qs: {
token: '={{$credentials.accessToken}}',
},
headers: {
Authorization: '=Bearer {{$credentials.accessToken}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://api.example.com/v2',
url: '/v1/user',
},
};
}

View File

@@ -0,0 +1,51 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class ExampleOAuth2Api implements ICredentialType {
name = 'exampleOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'Example OAuth2 API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'authorizationCode',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden',
default: 'https://api.example.com/oauth/authorize',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://api.example.com/oauth/token',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'users:read users:write companies:read',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'header',
},
];
}

View File

@@ -0,0 +1,45 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
export class ExampleOAuth2Api implements ICredentialType {
name = 'exampleOAuth2Api';
extends = ['oAuth2Api'];
displayName = 'Example OAuth2 API';
// Link to your community node's README
documentationUrl = 'https://github.com/org/repo?tab=readme-ov-file#credentials';
properties: INodeProperties[] = [
{
displayName: 'Grant Type',
name: 'grantType',
type: 'hidden',
default: 'clientCredentials',
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden',
default: 'https://api.example.com/oauth/token',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden',
default: '',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
default: 'users:read users:write companies:read',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden',
default: 'body',
},
];
}

View File

@@ -0,0 +1,28 @@
name: CI
on:
pull_request:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: '{{packageManager.name}} {{packageManager.installCommand}}'
- name: Run lint
run: '{{packageManager.name}} run lint'
- name: Run build
run: '{{packageManager.name}} run build'

View File

@@ -0,0 +1,51 @@
module.exports = {
/**
* https://prettier.io/docs/en/options.html#semicolons
*/
semi: true,
/**
* https://prettier.io/docs/en/options.html#trailing-commas
*/
trailingComma: 'all',
/**
* https://prettier.io/docs/en/options.html#bracket-spacing
*/
bracketSpacing: true,
/**
* https://prettier.io/docs/en/options.html#tabs
*/
useTabs: true,
/**
* https://prettier.io/docs/en/options.html#tab-width
*/
tabWidth: 2,
/**
* https://prettier.io/docs/en/options.html#arrow-function-parentheses
*/
arrowParens: 'always',
/**
* https://prettier.io/docs/en/options.html#quotes
*/
singleQuote: true,
/**
* https://prettier.io/docs/en/options.html#quote-props
*/
quoteProps: 'as-needed',
/**
* https://prettier.io/docs/en/options.html#end-of-line
*/
endOfLine: 'lf',
/**
* https://prettier.io/docs/en/options.html#print-width
*/
printWidth: 100,
};

View File

@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to running n8n",
"processId": "${command:PickProcess}",
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
]
}

View File

@@ -0,0 +1,3 @@
import { config } from '@n8n/node-cli/eslint';
export default config;