UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

1,052 lines (1,050 loc) 129 kB
"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