Skip to content
53 changes: 53 additions & 0 deletions packages/cli/lib/commands/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ const command: NewCommandType = {
describe: "Project theme (depends on project type)",
type: "string"
})
.option("hosting", {
describe: "Blazor hosting model (Blazor projects only)",
type: "string",
choices: ["Server", "Wasm", "Auto"]
})
.option("variant", {
describe: "Theme variant (Blazor projects only)",
type: "string",
choices: ["light", "dark"]
})
.option("skip-git", {
alias: "sg",
describe: "Do not initialize a git repository for the project",
Expand Down Expand Up @@ -155,6 +165,49 @@ const command: NewCommandType = {
cd14: theme
});

if (typeof projTemplate.scaffold === "function") {
if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(argv.name)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regex doesn't allow spaces, apart from isAlphanumericExt . So if a user creates project with spaces, he won't get the warning abbout the namespace naming

This is also related to a bigger problem with having spaces in app names, that is marked in DotnetTemplateManager.scaffold()

Util.warn(`The project namespace will be derived from the name '${argv.name}'. ` +
"Use only letters, digits and dashes for a clean identifier.", "yellow");
}

const extraConfig: { [key: string]: any } = {};
if (argv.hosting) {
extraConfig.Hosting = argv.hosting;
}
if (argv.variant) {
extraConfig.Variant = argv.variant;
}

const success = await projTemplate.scaffold({
name: argv.name,
theme,
skipInstall: !!argv.skipInstall,
skipGit: !!argv.skipGit,
extraConfig
});
if (!success) {
return;
}

process.chdir(argv.name);
await configure(argv.framework, {
agents: argv.agents as (AIAgentTarget | "none")[],
assistants: argv.assistants as (AiCodingAssistant | "none")[]
});
process.chdir("..");

if (!argv["skip-git"] && !ProjectConfig.getConfig().skipGit) {
Util.gitInit(process.cwd(), argv.name);
}

Util.log("");
Util.log("Next Steps:");
Util.log(` cd ${argv.name}`);
Util.log(` dotnet run --project ${argv.name}`);
return;
}

const config = projTemplate.generateConfig(argv.name, theme);
for (const templatePath of projTemplate.templatePaths) {
await Util.processTemplates(templatePath, path.join(process.cwd(), argv.name),
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/lib/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface PositionalArgs {
/** Which theme to use when creating a new project. */
theme?: string;

/** Blazor hosting model (Blazor projects only). */
hosting?: string;

/** Theme variant (Blazor projects only). */
variant?: string;

template?: string;

module?: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/templates/blazor/igb/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class IgbBlazorProjectLibrary extends BaseProjectLibrary {
super(__dirname);
this.name = "Ignite UI for Blazor";
this.projectType = "igb";
this.themes = ["default"];
this.themes = ["bootstrap", "material", "fluent", "indigo"];
}
}
module.exports = new IgbBlazorProjectLibrary();
70 changes: 70 additions & 0 deletions packages/cli/templates/blazor/igb/projects/empty/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
ControlExtraConfiguration, ControlExtraConfigType, defaultDelimiters,
DotnetTemplateManager, ProjectTemplate, ScaffoldOptions
} from "@igniteui/cli-core";

export class EmptyIgbProject implements ProjectTemplate {

public id: string = "empty";
public name = "Blazor Web App";
public description = "Ignite UI for Blazor Web App, scaffolded via the IgniteUI.Blazor.Templates package";
public framework: string = "blazor";
public projectType: string = "igb";
public dependencies: string[] = [];
public hasExtraConfiguration: boolean = true;
public isHidden: boolean = false;
public delimiters = defaultDelimiters;
public templatePaths: string[] = [];

private extraConfiguration: { [key: string]: any } = {};

public getExtraConfiguration(): ControlExtraConfiguration[] {
return [
{
choices: ["Server", "Wasm", "Auto"],
default: "Server",
key: "Hosting",
message: "Choose the hosting model:",
type: ControlExtraConfigType.Choice
},
{
choices: ["light", "dark"],
default: "light",
key: "Variant",
message: "Choose the theme variant:",
type: ControlExtraConfigType.Choice
}
];
}

public setExtraConfiguration(extraConfigKeys: {} | any[]) {
if (Array.isArray(extraConfigKeys)) {
// the wizard supplies answers positionally, matching getExtraConfiguration() order
const keys = this.getExtraConfiguration().map(c => c.key);
extraConfigKeys.forEach((value, i) => {
if (keys[i] !== undefined) {
this.extraConfiguration[keys[i]] = value;
}
});
} else {
this.extraConfiguration = { ...this.extraConfiguration, ...extraConfigKeys };
}
}

public scaffold(options: ScaffoldOptions): Promise<boolean> {
const extraConfig = { ...this.extraConfiguration, ...options.extraConfig };
return Promise.resolve(DotnetTemplateManager.scaffold({ ...options, extraConfig }));
}

// Unused — the host calls scaffold() instead of the generateConfig pipeline when scaffold exists.
public installModules(): void {
throw new Error("Method not implemented.");
}
public upgradeIgniteUIPackages(_projectPath: string, _packagePath: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
public generateConfig(_name: string, _theme: string, ..._options: any[]): { [key: string]: any; } {
throw new Error("Method not implemented.");
}
}
export default new EmptyIgbProject();
2 changes: 1 addition & 1 deletion packages/cli/templates/blazor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class BlazorFramework implements Framework {
public id: string;
public name: string;
public projectLibraries: ProjectLibrary[];
public hidden = true;
public hidden = false;

constructor() {
this.id = "blazor";
Expand Down
71 changes: 58 additions & 13 deletions packages/core/prompt/BasePromptSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from "path";
import { Separator } from "@inquirer/prompts";
import { BaseTemplateManager } from "../templates";
import {
Component, Config, ControlExtraConfigType, ControlExtraConfiguration, Framework,
BaseTemplate, Component, Config, ControlExtraConfigType, ControlExtraConfiguration, Framework,
FrameworkId, ProjectLibrary, ProjectTemplate, Template
} from "../types";
import { App, ChoiceItem, GoogleAnalytics, ProjectConfig, Util } from "../util";
Expand Down Expand Up @@ -60,20 +60,44 @@ export abstract class BasePromptSession {
// project options:
theme = await this.getTheme(projLibrary);

Util.log(" Generating project structure.");
const config = projTemplate.generateConfig(projectName, theme);
for (const templatePath of projTemplate.templatePaths) {
await Util.processTemplates(templatePath, path.join(process.cwd(), projectName),
config, projTemplate.delimiters, false);
if (projTemplate.hasExtraConfiguration) {
await this.customizeTemplateTask(projTemplate);
}

Util.log(Util.greenCheck() + " Project structure generated.");
if (!this.config.skipGit) {
Util.gitInit(process.cwd(), projectName);
if (typeof projTemplate.scaffold === "function") {
Util.log(" Generating project structure.");
const success = await projTemplate.scaffold({
name: projectName,
theme,
skipInstall: false,
skipGit: this.config.skipGit
});
if (!success) {
return;
}
// the scaffold service never touches git, so initialize git here when requested
if (!this.config.skipGit) {
Util.gitInit(process.cwd(), projectName);
}
// move cwd to project folder
process.chdir(projectName);
await this.configureAI(framework.id);
Comment on lines +79 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was also flagged from copilot on my side. The whole point is that in new.ts the order of gitInit and configureAI is the other way around, which allegedly is the correct order.

new.ts:

process.chdir(argv.name);
await configure(argv.framework, { ... });   // ← AI config written FIRST
process.chdir("..");
Util.gitInit(process.cwd(), argv.name);     // ← then committed

} else {
Util.log(" Generating project structure.");
const config = projTemplate.generateConfig(projectName, theme);
for (const templatePath of projTemplate.templatePaths) {
await Util.processTemplates(templatePath, path.join(process.cwd(), projectName),
config, projTemplate.delimiters, false);
}

Util.log(Util.greenCheck() + " Project structure generated.");
if (!this.config.skipGit) {
Util.gitInit(process.cwd(), projectName);
}
// move cwd to project folder
process.chdir(projectName);
await this.configureAI(framework.id);
}
// move cwd to project folder
process.chdir(projectName);
await this.configureAI(framework.id);
}
await this.chooseActionLoop(projLibrary);
//TODO: restore cwd?
Expand Down Expand Up @@ -302,7 +326,7 @@ export abstract class BasePromptSession {
}

/** Create prompts from template extra configuration and assign user answers to the template */
protected async customizeTemplateTask(template: Template) {
protected async customizeTemplateTask(template: BaseTemplate) {
const extraPrompt = this.createQuestions(template.getExtraConfiguration());
const extraConfigAnswers = [];
for (const question of extraPrompt) {
Expand Down Expand Up @@ -428,6 +452,27 @@ export abstract class BasePromptSession {
case "Complete & Run":
const config = ProjectConfig.localConfig();

if (!config.project) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!config.project) {
if (!config.project || config.project.framework === "blazor") {

The current code is more like an assumption rather than a check if it's Blazor app

// Blazor (scaffolded via dotnet) has no cli-config — print next-steps instead of
// routing through completeAndRun (npm + start.start, which requires a cli-config).
const projectName = path.basename(process.cwd());
if (Util.canPrompt() && await InquirerWrapper.confirm({
message: "Run the app now (dotnet run)?",
default: false
})) {
const result = Util.spawnSync("dotnet", ["run", "--project", projectName], { stdio: "inherit" });
if (result.error || result.status !== 0) {
Util.error("dotnet run failed (see dotnet output above).", "red");
}
} else {
Util.log("");
Util.log("Next Steps:");
Util.log(` cd ${projectName}`);
Util.log(` dotnet run --project ${projectName}`);
Comment thread
dkalinovInfra marked this conversation as resolved.
}
break;
}

if (config.project.framework === "angular" &&
config.project.projectType === "igx-ts" &&
!config.packagesInstalled) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/templates/BaseTemplateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export abstract class BaseTemplateManager {
return this.frameworks.filter(f => includeHidden || !f.hidden).map(f => f.id);
}
public getFrameworkNames(includeHidden = false): string[] {
// exclude WebComponents from the Step-By-Step wizard
// hidden frameworks are excluded from the Step-By-Step wizard unless includeHidden is set
return this.frameworks.filter(f => includeHidden || !f.hidden).map(f => f.name);
}
/** Returns framework found by its name or undefined. */
Expand Down
22 changes: 22 additions & 0 deletions packages/core/types/ProjectTemplate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
import { BaseTemplate } from "./BaseTemplate";

/** Options passed to a project template's `scaffold` method. */
export interface ScaffoldOptions {
/** Project name (already validated alphanumeric-ext by the host). */
name: string;
/** Theme to apply, one of the library's themes (e.g. bootstrap|material|fluent|indigo). */
theme: string;
/** Skip restoring/installing packages after scaffolding. */
skipInstall?: boolean;
/** Skip git initialization. */
skipGit?: boolean;
/** Additional template-specific configuration (e.g. { Hosting, Variant }). */
extraConfig?: { [key: string]: any };
}

/** Interface for project templates */
export interface ProjectTemplate extends BaseTemplate {
/** This method should be called after generateConfig completes. */
Expand All @@ -15,4 +29,12 @@ export interface ProjectTemplate extends BaseTemplate {

/** Generates template files. */
generateConfig(name: string, theme: string, ...options: any[]): {[key: string]: any};

/**
* Optional alternative scaffolding strategy. When implemented, the host calls this
* instead of the generateConfig → processTemplates → installPackages pipeline.
* @param options Scaffold options
* @returns true on success, false on failure.
*/
scaffold?(options: ScaffoldOptions): Promise<boolean>;
}
Loading