@lenne.tech/cli
Version:
lenne.Tech CLI: lt
1,052 lines (1,050 loc) • 129 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Server = void 0;
const crypto = __importStar(require("crypto"));
const path_1 = require("path");
const ts = __importStar(require("typescript"));
const markdown_table_1 = require("../lib/markdown-table");
/**
* Server helper functions
*/
class Server {
/**
* Constructor for integration of toolbox
*/
constructor(toolbox) {
this.toolbox = toolbox;
// Specific imports for default modells
this.imports = {
CoreFileInfo: "import { CoreFileInfo } from '@lenne.tech/nest-server';",
FileUpload: "import type { FileUpload } from 'graphql-upload/processRequest.js';",
GraphQLUpload: "import * as GraphQLUpload from 'graphql-upload/GraphQLUpload.js';",
'Record<string, unknown>': "import { JSON } from '@lenne.tech/nest-server';",
};
// Specific types for properties in input fields
this.inputFieldTypes = {
Boolean: 'Boolean',
Date: 'Date',
File: 'GraphQLUpload',
FileInfo: 'GraphQLUpload',
ID: 'String',
Id: 'String',
JSON: 'JSON',
Json: 'JSON',
Number: 'Number',
ObjectId: 'String',
String: 'String',
Upload: 'GraphQLUpload',
};
// Specific types for properties in input classes
this.inputClassTypes = {
Boolean: 'boolean',
Date: 'Date',
File: 'FileUpload',
FileInfo: 'FileUpload',
ID: 'string',
Id: 'string',
JSON: 'Record<string, unknown>',
Json: 'Record<string, unknown>',
Number: 'number',
ObjectId: 'string',
String: 'string',
Upload: 'FileUpload',
};
// Specific types for properties in model fields
this.modelFieldTypes = {
Boolean: 'Boolean',
Date: 'Date',
File: 'CoreFileInfo',
FileInfo: 'CoreFileInfo',
ID: 'String',
Id: 'String',
JSON: 'JSON',
Json: 'JSON',
Number: 'Number',
ObjectId: 'String',
String: 'String',
Upload: 'CoreFileInfo',
};
// Specific types for properties in model class
this.modelClassTypes = {
Boolean: 'boolean',
Date: 'Date',
File: 'CoreFileInfo',
FileInfo: 'CoreFileInfo',
ID: 'string',
Id: 'string',
JSON: 'Record<string, unknown>',
Json: 'Record<string, unknown>',
Number: 'number',
ObjectId: 'string',
String: 'string',
Upload: 'CoreFileInfo',
};
// Additional string for ID properties
this.propertySuffixTypes = {
ID: 'Id',
Id: 'Id',
ObjectId: 'Id',
};
// Standard types: primitives and default JavaScript classes
this.standardTypes = ['boolean', 'string', 'number', 'Date'];
this.ask = toolbox.prompt.ask;
this.camelCase = toolbox.strings.camelCase;
this.confirm = toolbox.prompt.confirm;
this.filesystem = toolbox.filesystem;
this.kebabCase = toolbox.strings.kebabCase;
this.pascalCase = toolbox.strings.pascalCase;
this.info = toolbox.print.info;
}
/**
* Add properties to model
*/
addProperties(options) {
return __awaiter(this, void 0, void 0, function* () {
const { objectsToAdd, referencesToAdd } = Object.assign({ objectsToAdd: [], referencesToAdd: [] }, options);
// Set props
const props = {};
const setProps = true;
let refsSet = false;
let schemaSet = false;
while (setProps) {
const name = (yield this.ask({
message: 'Enter property name (e.g. myProperty) of the property or leave empty (ENTER)',
name: 'input',
type: 'input',
})).input;
if (!name.trim()) {
break;
}
let type = (yield this.ask([
{
choices: [
'boolean',
'string',
'number',
'ObjectId / Reference',
'Date',
'enum',
'SubObject',
'Use own',
'JSON / any',
],
message: 'Choose property type',
name: 'input',
type: 'select',
},
])).input;
if (type === 'ObjectId / Reference') {
type = 'ObjectId';
}
else if (type === 'JSON / any') {
type = 'JSON';
}
let schema;
if (type === 'SubObject') {
type = (yield this.ask({
initial: this.pascalCase(name),
message: 'Enter property type (e.g. MyClass)',
name: 'input',
type: 'input',
})).input;
schema = type;
schemaSet = true;
if (type) {
refsSet = true;
}
if (type === null || type === void 0 ? void 0 : type.trim()) {
let createObjAfter = false;
const cwd = this.filesystem.cwd();
const path = cwd.substr(0, cwd.lastIndexOf('src'));
const objectsDir = (0, path_1.join)(path, 'src', 'server', 'common', 'objects', this.kebabCase(type));
if (!this.filesystem.exists(objectsDir)) {
createObjAfter = yield this.confirm('Create this Object after all the other Properties?', true);
}
if (createObjAfter && !objectsToAdd.find((obj) => obj.object === this.kebabCase(type))) {
objectsToAdd.push({ object: this.kebabCase(type), property: name });
}
}
}
let reference;
let enumRef;
if (type === 'ObjectId') {
reference = (yield this.ask({
initial: this.pascalCase(name),
message: 'Enter reference for ObjectId',
name: 'input',
type: 'input',
})).input;
if (reference) {
refsSet = true;
}
if (reference === null || reference === void 0 ? void 0 : reference.trim()) {
let createRefAfter = false;
const cwd = this.filesystem.cwd();
const path = cwd.substr(0, cwd.lastIndexOf('src'));
const moduleDir = (0, path_1.join)(path, 'src', 'server', 'modules', this.kebabCase(reference));
if (!this.filesystem.exists(moduleDir)) {
createRefAfter = yield this.confirm('Create this Module after all the other Properties?', true);
}
if (createRefAfter && !referencesToAdd.find((ref) => ref.reference === this.kebabCase(reference))) {
referencesToAdd.push({ property: name, reference: this.kebabCase(reference) });
}
}
}
else if (type === 'enum') {
enumRef = (yield this.ask({
initial: `${this.pascalCase(name)}Enum`,
message: 'Enter enum type',
name: 'input',
type: 'input',
})).input;
if (enumRef) {
refsSet = true;
}
}
const arrayEnding = type.endsWith('[]');
type = type.replace('[]', '');
const isArray = arrayEnding || (yield this.confirm('Array?'));
const nullable = yield this.confirm('Nullable?', true);
props[name] = { enumRef, isArray, name, nullable, reference, schema, type };
}
return { objectsToAdd, props, referencesToAdd, refsSet, schemaSet };
});
}
useDefineForClassFieldsActivated() {
var _a;
// Walk UP from the current working directory to find the nearest
// tsconfig.json. gluegun's `filesystem.resolve('tsconfig.json')` resolves
// relative to cwd without checking existence, so it breaks when `lt
// server module` is invoked from inside `src/` (where no tsconfig lives)
// — it would then silently return `false`, causing the generator to
// emit class fields without the `override` modifier that the project's
// `noImplicitOverride` rule requires.
const path = require('path');
let current = this.filesystem.cwd();
const root = path.parse(current).root;
let tsConfigPath = null;
while (current && current !== root) {
const candidate = path.join(current, 'tsconfig.json');
if (this.filesystem.exists(candidate)) {
tsConfigPath = candidate;
break;
}
current = path.dirname(current);
}
if (tsConfigPath) {
const readConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
if (!readConfig.error) {
const tsConfig = readConfig.config;
if ((_a = tsConfig === null || tsConfig === void 0 ? void 0 : tsConfig.compilerOptions) === null || _a === void 0 ? void 0 : _a.useDefineForClassFields) {
return tsConfig.compilerOptions.useDefineForClassFields;
}
}
}
return false;
}
/**
* Determine GraphQL Field type for UnifiedField decorator
* Used for Model, Input, and CreateInput
*/
getInputFieldType(item, options) {
var _a, _b, _c;
const { create } = Object.assign({ create: false }, options);
const reference = ((_a = item.reference) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.reference.trim()) : '';
const schema = ((_b = item.schema) === null || _b === void 0 ? void 0 : _b.trim()) ? this.pascalCase(item.schema.trim()) : '';
const enumRef = ((_c = item.enumRef) === null || _c === void 0 ? void 0 : _c.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
if (schema) {
// SubObject → Schema + Input/CreateInput suffix
return schema + (create ? 'CreateInput' : 'Input');
}
else if (reference) {
// ObjectId/Reference → always String for IDs
return 'String';
}
else if (enumRef) {
// Enum → use enum name
return enumRef;
}
else {
// Standard types or custom types
let fieldType = this.inputFieldTypes[this.pascalCase(item.type)] ||
this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input');
fieldType = this.modelFieldTypes[item.type] ? this.modelFieldTypes[item.type] : fieldType;
return fieldType;
}
}
/**
* Determine TypeScript property type for Input/CreateInput classes
*/
getInputClassType(item, options) {
var _a, _b, _c;
const { create } = Object.assign({ create: false }, options);
const reference = ((_a = item.reference) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.reference.trim()) : '';
const schema = ((_b = item.schema) === null || _b === void 0 ? void 0 : _b.trim()) ? this.pascalCase(item.schema.trim()) : '';
const enumRef = ((_c = item.enumRef) === null || _c === void 0 ? void 0 : _c.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
if (schema) {
// SubObject → Schema + Input/CreateInput suffix
return schema + (create ? 'CreateInput' : 'Input');
}
else if (reference) {
// ObjectId/Reference → string for IDs
return 'string';
}
else if (enumRef) {
// Enum → use enum name
return enumRef;
}
else {
// Standard types or custom types
return (this.inputClassTypes[this.pascalCase(item.type)] ||
(this.standardTypes.includes(item.type)
? item.type
: this.pascalCase(item.type) + (create ? 'CreateInput' : 'Input')));
}
}
/**
* Determine GraphQL Field type for UnifiedField decorator in Model
*/
getModelFieldType(item) {
var _a, _b, _c;
const reference = ((_a = item.reference) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.reference.trim()) : '';
const schema = ((_b = item.schema) === null || _b === void 0 ? void 0 : _b.trim()) ? this.pascalCase(item.schema.trim()) : '';
const enumRef = ((_c = item.enumRef) === null || _c === void 0 ? void 0 : _c.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
if (reference) {
return reference;
}
else if (schema) {
return schema;
}
else if (enumRef) {
return enumRef;
}
else {
return this.modelFieldTypes[this.pascalCase(item.type)] || this.pascalCase(item.type);
}
}
/**
* Determine TypeScript property type for Model class
*/
getModelClassType(item) {
var _a, _b, _c;
const reference = ((_a = item.reference) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.reference.trim()) : '';
const schema = ((_b = item.schema) === null || _b === void 0 ? void 0 : _b.trim()) ? this.pascalCase(item.schema.trim()) : '';
const enumRef = ((_c = item.enumRef) === null || _c === void 0 ? void 0 : _c.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
if (reference) {
return reference;
}
else if (schema) {
return schema;
}
else if (enumRef) {
return enumRef;
}
else {
return (this.modelClassTypes[this.pascalCase(item.type)] ||
(this.standardTypes.includes(item.type) ? item.type : this.pascalCase(item.type)));
}
}
/**
* Generate enum configuration for UnifiedField decorator
*/
getEnumConfig(item) {
var _a;
const enumRef = ((_a = item.enumRef) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
if (!enumRef) {
return '';
}
return item.isArray
? `enum: { enum: ${enumRef}, options: { each: true } },\n `
: `enum: { enum: ${enumRef} },\n `;
}
/**
* Generate type configuration for UnifiedField decorator
*/
getTypeConfig(fieldType, isArray) {
// Type is always needed for all properties
return `type: () => ${isArray ? '[' : ''}${fieldType}${isArray ? ']' : ''}`;
}
/**
* Create template string for properties in model
*/
propsForModel(props, options) {
var _a, _b, _c;
// Preparations
const config = Object.assign({ useDefault: true }, options);
const { modelName, useDefault } = config;
// Only use = undefined when useDefineForClassFieldsActivated is false or override/declare keyword is set
const undefinedString = this.useDefineForClassFieldsActivated() ? ';' : ' = undefined;';
let result = '';
// Check parameters
if (!props || !(typeof props !== 'object') || !Object.keys(props).length) {
if (!useDefault) {
return { imports: '', mappings: 'this;', props: '' };
}
// Use default
if (!Object.keys(props).length && useDefault) {
return {
imports: '',
mappings: 'mapClasses(input, {user: User}, this);',
props: `
/**
* Description of properties
*/
@Restricted(RoleEnum.ADMIN, RoleEnum.S_CREATOR)
@Field(() => [String], { description: 'Properties of ${this.pascalCase(modelName)}', nullable: 'items'})
@UnifiedField({
description: 'Properties of ${this.pascalCase(modelName)}',
isOptional: false,
mongoose: [String],
roles: RoleEnum.S_EVERYONE,
type: () => [String],
})
properties: string[]${undefinedString}
/**
* User who has tested the ${this.pascalCase(modelName)}
*/
@UnifiedField({
description: 'User who has tested the ${this.pascalCase(modelName)}',
isOptional: true,
mongoose: { type: Schema.Types.ObjectId, ref: 'User' },
roles: RoleEnum.S_EVERYONE,
type: () => User,
})
testedBy: User${undefinedString}
`,
};
}
}
// Process configuration
const imports = {};
const mappings = {};
for (const [name, item] of Object.entries(props)) {
const propName = this.camelCase(name);
const reference = ((_a = item.reference) === null || _a === void 0 ? void 0 : _a.trim()) ? this.pascalCase(item.reference.trim()) : '';
const schema = ((_b = item.schema) === null || _b === void 0 ? void 0 : _b.trim()) ? this.pascalCase(item.schema.trim()) : '';
const enumRef = ((_c = item.enumRef) === null || _c === void 0 ? void 0 : _c.trim()) ? this.pascalCase(item.enumRef.trim()) : '';
// Use utility functions to determine types
const modelFieldType = this.getModelFieldType(item);
const modelClassType = this.getModelClassType(item);
const isArray = item.isArray;
const type = this.standardTypes.includes(item.type) ? item.type : this.pascalCase(item.type);
if (!this.standardTypes.includes(type) && type !== 'ObjectId' && type !== 'Enum' && type !== 'Json') {
mappings[propName] = type;
}
if (reference) {
mappings[propName] = reference;
}
if (schema) {
mappings[propName] = schema;
}
if (this.imports[modelClassType]) {
imports[modelClassType] = this.imports[modelClassType];
}
// Use utility functions for enum and type config
// For enums, only use enum property (not type property)
const enumConfig = this.getEnumConfig(item);
const typeConfig = enumConfig ? '' : this.getTypeConfig(modelFieldType, isArray);
// Build mongoose configuration
const mongooseConfig = reference
? `${isArray ? '[' : ''}{ ref: '${reference}', type: Schema.Types.ObjectId }${isArray ? ']' : ''}`
: schema
? `${isArray ? '[' : ''}{ type: ${schema}Schema }${isArray ? ']' : ''}`
: enumRef
? `${isArray ? '[' : ''}{ enum: ${item.nullable ? `Object.values(${enumRef}).concat([null])` : enumRef}, type: String }${isArray ? ']' : ''}`
: type === 'Json'
? `${isArray ? '[' : ''}{ type: Object }${isArray ? ']' : ''}`
: 'true';
result += `
/**
* ${this.pascalCase(propName) + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}
*/
@UnifiedField({
description: '${this.pascalCase(propName) + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}',
${enumConfig}isOptional: ${item.nullable},
mongoose: ${mongooseConfig},
roles: RoleEnum.S_EVERYONE,${typeConfig
? `
${typeConfig}`
: ''}
})
${propName}: ${(reference ? reference : schema ? schema : enumRef || modelClassType) + (isArray ? '[]' : '')}${undefinedString}
`;
}
// Process imports
let importsResult = '';
for (const value of Object.values(imports)) {
importsResult += `\n${value}`;
}
// Process mappings
const mappingsResult = [];
for (const [key, value] of Object.entries(mappings)) {
mappingsResult.push(`${key}: ${value}`);
}
// Return template data
return {
imports: importsResult,
mappings: mappingsResult.length ? `mapClasses(input, { ${mappingsResult.join(', ')} }, this);` : 'this;',
props: result,
};
}
/**
* Create template string for properties in input
*/
propsForInput(props, options) {
// Preparations
const config = Object.assign({ useDefault: true }, options);
const { create, modelName, nullable, useDefault } = config;
// Only use = undefined when useDefineForClassFieldsActivated is false or override keyword is set
const undefinedString = this.useDefineForClassFieldsActivated() ? ';' : ' = undefined;';
let result = '';
// Check parameters
if (!props || !(typeof props !== 'object') || !Object.keys(props).length) {
if (!useDefault) {
return { imports: '', props: '' };
}
// Use default
if (!Object.keys(props).length && useDefault) {
return {
imports: '',
props: `
/**
* Description of properties
*/
@Restricted(RoleEnum.ADMIN, RoleEnum.S_CREATOR)
@Field(() => [String], { description: 'Properties of ${this.pascalCase(modelName)}', nullable: ${config.nullable ? config.nullable : "'items'"}})
properties: string[]${undefinedString}
/**
* User who has tested the ${this.pascalCase(modelName)}
*/
@UnifiedField({
description: 'User who has tested the ${this.pascalCase(modelName)}',
isOptional: ${config.nullable},
})
testedBy: User${undefinedString}
`,
};
}
// Process configuration
const imports = {};
for (const [name, item] of Object.entries(props)) {
// Skip optional properties in CreateInput (they are inherited from Input)
if (create && item.nullable) {
continue;
}
// Use utility functions to determine types
const inputFieldType = this.getInputFieldType(item, { create });
const inputClassType = this.getInputClassType(item, { create });
const propertySuffix = this.propertySuffixTypes[this.pascalCase(item.type)] || '';
// Use override (not declare) for decorator properties when useDefineForClassFieldsActivated is true
const overrideString = this.useDefineForClassFieldsActivated() ? 'override ' : '';
const overrideFlag = create ? overrideString : '';
// When override is set, always use = undefined
const propertyUndefinedString = overrideFlag ? ' = undefined;' : undefinedString;
if (this.imports[inputFieldType]) {
imports[inputFieldType] = this.imports[inputFieldType];
}
if (this.imports[inputClassType]) {
imports[inputClassType] = this.imports[inputClassType];
}
// Use utility functions for enum and type config
// For enums, only use enum property (not type property)
const enumConfig = this.getEnumConfig(item);
const typeConfig = enumConfig ? '' : this.getTypeConfig(inputFieldType, item.isArray);
result += `
/**
* ${this.pascalCase(name) + propertySuffix + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}
*/
@UnifiedField({
description: '${this.pascalCase(name) + propertySuffix + (modelName ? ` of ${this.pascalCase(modelName)}` : '')}',
${enumConfig}isOptional: ${nullable || item.nullable},
roles: RoleEnum.S_EVERYONE,${typeConfig
? `
${typeConfig}`
: ''}
})
${overrideFlag + this.camelCase(name)}${nullable || item.nullable ? '?' : ''}: ${inputClassType}${item.isArray ? '[]' : ''}${propertyUndefinedString}
`;
}
// Process imports
let importsResult = '';
for (const value of Object.values(imports)) {
importsResult += `\n${value}`;
}
// Return template data
return {
imports: importsResult,
props: result,
};
}
}
/**
* Setup a new server project
* Handles template setup and file patching (config.env.ts, package.json, main.ts, meta.json)
*
* @param dest - Destination directory path
* @param options - Setup options
* @returns Setup result with success status
*/
setupServer(dest, options) {
return __awaiter(this, void 0, void 0, function* () {
const { apiMode: apiModeHelper, patching, system, template, templateHelper } = this.toolbox;
const { apiMode, author = '', branch, copyPath, description = '', experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
const repoUrl = experimental
? 'https://github.com/lenneTech/nest-base.git'
: 'https://github.com/lenneTech/nest-server-starter.git';
// Setup template
const result = yield templateHelper.setup(dest, {
branch,
copyPath,
linkPath,
repoUrl,
});
if (!result.success) {
return { method: result.method, path: result.path, success: false };
}
// Link mode: skip all post-processing
if (result.method === 'link') {
return { method: 'link', path: result.path, success: true };
}
// Apply patches (config.env.ts, package.json, main.ts, meta.json)
if (!skipPatching) {
try {
if (!experimental) {
// Generate README
yield template.generate({
props: { description, name },
target: `${dest}/README.md`,
template: 'nest-server-starter/README.md.ejs',
});
// Replace secret or private keys and update database names via AST
this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
// Update Swagger configuration in main.ts
yield patching.update(`${dest}/src/main.ts`, (content) => content
.replace(/\.setTitle\('.*?'\)/, `.setTitle('${name}')`)
.replace(/\.setDescription\('.*?'\)/, `.setDescription('${description || name}')`));
}
// Update package.json
yield patching.update(`${dest}/package.json`, (config) => {
config.author = author;
config.bugs = { url: '' };
config.description = description || name;
config.homepage = '';
config.name = projectDir;
config.repository = { type: 'git', url: '' };
config.version = '0.0.1';
return config;
});
if (!experimental) {
// Update meta.json if exists
if (this.filesystem.exists(`${dest}/src/meta`)) {
yield patching.update(`${dest}/src/meta`, (config) => {
config.name = name;
config.description = description;
return config;
});
}
}
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
}
// Clean up copied template artifacts (prevents install issues with stale node_modules)
if (result.method === 'copy') {
this.filesystem.remove(`${dest}/node_modules`);
this.filesystem.remove(`${dest}/package-lock.json`);
this.filesystem.remove(`${dest}/pnpm-lock.yaml`);
this.filesystem.remove(`${dest}/yarn.lock`);
this.filesystem.remove(`${dest}/.yalc`);
this.filesystem.remove(`${dest}/yalc.lock`);
}
// Vendor-mode transformation — identical to setupServerForFullstack, so
// a standalone `lt server create --framework-mode vendor` produces the
// same project layout as `lt fullstack init --framework-mode vendor`.
// Essentials list is captured BEFORE processApiMode deletes the
// manifest (same dance as in setupServerForFullstack).
let standaloneVendorUpstreamDeps = {};
let standaloneVendorCoreEssentials = [];
if (!experimental && frameworkMode === 'vendor') {
try {
const converted = yield this.convertCloneToVendored({
dest,
projectName: name,
upstreamBranch: frameworkUpstreamBranch,
});
standaloneVendorUpstreamDeps = converted.upstreamDeps;
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
standaloneVendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
}
// Process API mode (before install so package.json is correct)
if (!experimental && apiMode) {
try {
yield apiModeHelper.processApiMode(dest, apiMode);
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
}
// Restore core essentials after processApiMode stripped them (vendor + REST only).
if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
try {
this.restoreVendorCoreEssentials({
dest,
essentials: standaloneVendorCoreEssentials,
upstreamDeps: standaloneVendorUpstreamDeps,
});
}
catch (_a) {
// Non-fatal.
}
}
// Patch CLAUDE.md with API mode info
if (!experimental) {
this.patchClaudeMdApiMode(dest, apiMode);
}
// Install packages
if (!skipInstall && !experimental) {
try {
const { pm } = this.toolbox;
yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
// Post-install format pass. processApiMode may have left whitespace
// artifacts (multi-line arrays/imports) that the formatter flags in
// format:check; oxfmt is only available after install, so we run it
// here.
if (apiMode) {
yield apiModeHelper.formatProject(dest);
}
}
return { method: result.method, path: dest, success: true };
});
}
/**
* Setup server for fullstack project (simplified version without package.json/main.ts patching)
*
* @param dest - Destination directory path
* @param options - Setup options
* @returns Setup result with success status
*/
setupServerForFullstack(dest, options) {
return __awaiter(this, void 0, void 0, function* () {
const { apiMode: apiModeHelper, templateHelper } = this.toolbox;
const { apiMode, branch, copyPath, experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
const repoUrl = experimental
? 'https://github.com/lenneTech/nest-base.git'
: 'https://github.com/lenneTech/nest-server-starter';
// Both npm and vendor mode clone nest-server-starter as the base. The
// starter ships the minimal consumer conventions a project needs
// (src/server/common/models/persistence.model.ts, src/server/modules/user/,
// file/, meta/, tests/, migrations/, env files, etc.).
//
// In vendor mode we additionally clone @lenne.tech/nest-server to
// obtain the framework `core/` tree, copy it into the project at
// src/core/ (with the flatten-fix), remove the `@lenne.tech/nest-server`
// npm dependency, merge its transitive deps into the project
// package.json, and run a codemod that rewrites every
// `from '@lenne.tech/nest-server'` import to a relative path pointing
// at the vendored core.
//
// See convertCloneToVendored() below for the exact transformation.
// Setup template
const result = yield templateHelper.setup(dest, {
branch,
copyPath,
linkPath,
repoUrl,
});
if (!result.success) {
return { method: result.method, path: result.path, success: false };
}
// Link mode: skip all post-processing
if (result.method === 'link') {
return { method: 'link', path: result.path, success: true };
}
// Apply minimal patches for fullstack
if (!experimental) {
try {
// Write meta.json
this.filesystem.write(`${dest}/src/meta.json`, {
description: `API for ${name} app`,
name: `${name}-api-server`,
version: '0.0.0',
});
// Replace secret or private keys and update database names via AST
this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
}
else {
try {
yield this.toolbox.patching.update(`${dest}/package.json`, (config) => {
config.name = projectDir;
config.description = `API for ${name} app`;
config.version = '0.0.0';
return config;
});
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
}
// Clean up copied template artifacts
if (result.method === 'copy') {
this.filesystem.remove(`${dest}/node_modules`);
this.filesystem.remove(`${dest}/package-lock.json`);
this.filesystem.remove(`${dest}/pnpm-lock.yaml`);
this.filesystem.remove(`${dest}/yarn.lock`);
this.filesystem.remove(`${dest}/.yalc`);
this.filesystem.remove(`${dest}/yalc.lock`);
}
// Vendor-mode transformation: strip framework-internal content and wire
// the remaining files so they behave like a project that consumed the
// framework's core/ directory directly. Idempotent; safe to skip in
// npm mode.
//
// We capture the framework package.json snapshot from the temp clone so
// the post-apiMode step can restore upstream-declared core essentials
// without hard-coding package lists.
let vendorUpstreamDeps = {};
let vendorCoreEssentials = [];
if (!experimental && frameworkMode === 'vendor') {
try {
const converted = yield this.convertCloneToVendored({
dest,
projectName: name,
upstreamBranch: frameworkUpstreamBranch,
});
vendorUpstreamDeps = converted.upstreamDeps;
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
// Read the graphql-only package list from the starter's
// api-mode.manifest.json BEFORE processApiMode runs and deletes it.
// These are exactly the packages processApiMode will strip in REST
// mode — and in vendor mode they must come back afterwards, because
// src/core/** still imports them even when the consumer project is
// REST-only (e.g. PubSub in core-auth.module.ts, GraphQLUpload in
// core-file.service.ts). List is dynamic; new additions surface
// automatically.
vendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
}
// Process API mode (before install which happens at monorepo level)
if (!experimental && apiMode) {
try {
yield apiModeHelper.processApiMode(dest, apiMode);
}
catch (err) {
return { method: result.method, path: dest, success: false };
}
}
// In vendor mode + REST, re-add the graphql essentials that
// processApiMode just stripped. Both and GraphQL keep all packages
// by construction and don't need restoration.
if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
try {
this.restoreVendorCoreEssentials({
dest,
essentials: vendorCoreEssentials,
upstreamDeps: vendorUpstreamDeps,
});
}
catch (err) {
// Non-fatal — install may still succeed if the core never imports
// the restored packages at the current version.
}
}
// Patch CLAUDE.md with API mode info
if (!experimental) {
this.patchClaudeMdApiMode(dest, apiMode);
}
return { method: result.method, path: dest, success: true };
});
}
/**
* Converts a freshly cloned `nest-server-starter` working tree into a
* vendored-mode consumer project.
*
* The starter ships all consumer conventions a project needs (a
* working `src/server/` with `common/models/persistence.model.ts`,
* `modules/user/`, `modules/file/`, `modules/meta/`, sample tests,
* migrations, env files). In npm mode it relies on the
* `@lenne.tech/nest-server` npm dependency to provide the framework
* source via `node_modules/@lenne.tech/nest-server/dist/**`.
*
* In vendor mode we additionally clone `@lenne.tech/nest-server` to
* /tmp, copy its framework kernel (`src/core/`, `src/index.ts`,
* `src/core.module.ts`, `src/test/`, `src/types/`, `LICENSE`,
* `bin/migrate.js`) into the project at `src/core/` applying the
* flatten-fix, place upstream `src/templates/` at `<project>/src/templates/`
* (outside core/ so the runtime resolver finds it at the same relative
* path as in npm mode), remove `@lenne.tech/nest-server` from the
* project's `package.json`, merge the framework's transitive deps into
* the project's own deps, and run an AST-based codemod that rewrites
* every `from '@lenne.tech/nest-server'` import in consumer code
* (src/server, src/main.ts, tests/, migrations/, scripts/) to a
* relative path pointing at the vendored `src/core/`.
*
* The resulting tree matches the layout produced by the imo vendoring
* pilot, so the `nest-server-core-updater` and
* `nest-server-core-contributor` agents work without any further
* post-processing.
*
* Idempotent — running twice is a no-op.
*/
convertCloneToVendored(options) {
return __awaiter(this, void 0, void 0, function* () {
const { dest, upstreamBranch, upstreamRepoUrl = 'https://github.com/lenneTech/nest-server.git' } = options;
const filesystem = this.filesystem;
const { system } = this.toolbox;
const os = require('os');
const path = require('path');
const { Project, SyntaxKind } = require('ts-morph');
const srcDir = `${dest}/src`;
const coreDir = `${srcDir}/core`;
// ── 1. Clone @lenne.tech/nest-server into a temp directory ───────────
//
// We clone the framework repo shallowly to get the `src/core/` tree,
// `bin/migrate.js`, and associated meta files. The clone lives in a
// throw-away tmp dir that gets cleaned up at the end.
const tmpClone = path.join(os.tmpdir(), `lt-vendor-nest-server-${Date.now()}`);
const branchArg = upstreamBranch ? `--branch ${upstreamBranch} ` : '';
try {
yield system.run(`git clone --depth 1 ${branchArg}${upstreamRepoUrl} ${tmpClone}`);
}
catch (err) {
// Clone failures usually boil down to one of four causes — network,
// auth, unknown ref, or a pre-existing tmp dir. Give the user a
// pointed error message rather than the raw `git clone` stderr.
const raw = err.message || '';
const hints = [];
if (/Could not resolve host|getaddrinfo|ECONNREFUSED|Network is unreachable/i.test(raw)) {
hints.push('Network issue reaching github.com — check your connection or proxy settings.');
}
if (/Permission denied|authentication failed|publickey|403|401/i.test(raw)) {
hints.push('Authentication issue — the CLI uses an anonymous HTTPS clone; verify GitHub is reachable.');
}
if (upstreamBranch && /Remote branch .* not found|did not match any file\(s\) known to git/i.test(raw)) {
hints.push(`Upstream ref "${upstreamBranch}" does not exist. Check ${upstreamRepoUrl}/tags or /branches for valid refs. ` +
'Note: nest-server tags have NO "v" prefix — use e.g. "11.24.1", not "v11.24.1".');
}
if (/already exists and is not an empty/i.test(raw)) {
hints.push(`Target directory ${tmpClone} already exists. This usually indicates a stale previous run — rm -rf /tmp/lt-vendor-nest-server-* and retry.`);
}
const hintBlock = hints.length > 0 ? `\n Hints:\n - ${hints.join('\n - ')}` : '';
throw new Error(`Failed to clone ${upstreamRepoUrl}${upstreamBranch ? ` (branch/tag: ${upstreamBranch})` : ''}.\n Raw git error: ${raw.trim()}${hintBlock}`);
}
// Snapshot upstream package.json before cleanup so we can merge its
// transitive deps into the project's package.json (step 5 below).
let upstreamDeps = {};
let upstreamDevDeps = {};
let upstreamVersion = '';
try {
const upstreamPkg = filesystem.read(`${tmpClone}/package.json`, 'json');
if (upstreamPkg && typeof upstreamPkg === 'object') {
upstreamDeps = upstreamPkg.dependencies || {};
upstreamDevDeps = upstreamPkg.devDependencies || {};
upstreamVersion = upstreamPkg.version || '';
}
}
catch (_a) {
// Best-effort — if we can't read upstream pkg, the starter's own
// deps should still cover most of the framework's needs.
}
// Snapshot the upstream CLAUDE.md for section-merge into projects/api/CLAUDE.md.
// The nest-server CLAUDE.md contains framework-specific instructions that
// Claude Code needs to work correctly with the vendored source (API conventions,
// UnifiedField usage, CrudService patterns, etc.). We capture it before the
// temp clone is deleted and merge it after the vendor-marker block.
let upstreamClaudeMd = '';
try {
const claudeMdContent = filesystem.read(`${tmpClone}/CLAUDE.md`);
if (typeof claudeMdContent === 'string') {
upstreamClaudeMd = claudeMdContent;
}
}
catch (_b) {
// Non-fatal — if missing, the project CLAUDE.md just won't get upstream sections.
}
// Snapshot the upstream commit SHA for traceability in VENDOR.md.
let upstreamCommit = '';
try {
const sha = yield system.run(`git -C ${tmpClone} rev-parse HEAD`);
upstreamCommit = (sha || '').trim();
}
catch (_c) {
// Non-fatal — VENDOR.md will just show an empty SHA.
}
try {
// ── 2. Copy framework kernel into project src/core/ (flatten-fix) ──
//
// Upstream layout: src/core/ (framework sub-dir) + src/index.ts +
// src/core.module.ts + src/test/ + src/templates/ + src/types/.
// Target layout: most things flat under <project>/src/core/, with
// one exception: src/templates/ stays at the same upstream location
// (<project>/src/templates/) because the runtime email-template
// resolver uses __dirname-relative lookup that must match npm mode.
//
// We WIPE the starter's (non-existent in npm mode) src/core/ first
// just to guarantee idempotency when users run this twice.
if (filesystem.exists(coreDir)) {
filesystem.remove(coreDir);
}
const copies = [
[`${tmpClone}/src/core`, coreDir],
[`${tmpClone}/src/index.ts`, `${coreDir}/index.ts`],
[`${tmpClone}/src/core.module.ts`, `${coreDir}/core.module.ts`],
[`${tmpClone}/src/test`, `${coreDir}/test`],
// src/templates/ stays OUTSIDE src/core/ at its upstream location so
// the run