Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 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 @@
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,46 @@
cd14: theme
});

if (typeof projTemplate.scaffold === "function") {
if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(argv.name)) {
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, argv.agents as (AIAgentTarget | "none")[], argv.assistants as (AiCodingAssistant | "none")[]);

Check failure on line 194 in packages/cli/lib/commands/new.ts

View workflow job for this annotation

GitHub Actions / run-tests (24.x)

Expected 1-2 arguments, but got 3.
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
70 changes: 57 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);
} 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 @@ -279,7 +303,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 @@ -405,6 +429,26 @@ export abstract class BasePromptSession {
case "Complete & Run":
const config = ProjectConfig.localConfig();

if (!config.project) {
// 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(` dotnet run --project ${projectName}`);
}
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>;
}
87 changes: 87 additions & 0 deletions packages/core/util/DotnetTemplateManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ScaffoldOptions } from "../types/ProjectTemplate";
import { Util } from "./Util";

const TEMPLATE_PACKAGE = "IgniteUI.Blazor.Templates";
const TEMPLATE_SHORT_NAME = "igb-blazor";
const MIN_SDK_MAJOR = 8;

/**
* Thin wrapper around the `dotnet` CLI used to scaffold Blazor projects from the
* published `IgniteUI.Blazor.Templates` NuGet template package.
*/
export class DotnetTemplateManager {

/** Returns the major version of the installed .NET SDK, or null if dotnet is unavailable. */
public static getSdkMajorVersion(): number | null {
const result = Util.spawnSync("dotnet", ["--version"], { encoding: "utf8" });
if (result.error || result.status !== 0 || !result.stdout) {
return null;
}
const major = parseInt(result.stdout.toString().trim().split(".")[0], 10);
return isNaN(major) ? null : major;
}

/** Checks whether the IgniteUI.Blazor.Templates package is already installed. */
public static isTemplateInstalled(): boolean {
const result = Util.spawnSync("dotnet", ["new", "list", TEMPLATE_SHORT_NAME], { encoding: "utf8" });
return result.status === 0 && !!result.stdout && result.stdout.toString().includes(TEMPLATE_SHORT_NAME);
}

/** Installs the latest IgniteUI.Blazor.Templates package. Returns false on failure. */
public static installTemplate(): boolean {
Util.log(`Installing ${TEMPLATE_PACKAGE}…`);
const result = Util.spawnSync("dotnet", ["new", "install", TEMPLATE_PACKAGE], { stdio: "inherit" });
if (result.error || result.status !== 0) {
Util.error(`Failed to install the ${TEMPLATE_PACKAGE} package. ` +
"Check your network/NuGet feed and try again.", "red");
return false;
}
return true;
}

/**
* Scaffolds a Blazor project via `dotnet new igb-blazor`. Ensures the SDK and template
* package are available first. Returns false on any failure (no throwing).
*/
public static scaffold(options: ScaffoldOptions): boolean {
const sdkMajor = this.getSdkMajorVersion();
if (sdkMajor === null) {
Util.error("The .NET SDK is required to create a Blazor project but was not found. " +
"Install it from https://dotnet.microsoft.com/download and try again.", "red");
return false;
}
if (sdkMajor < MIN_SDK_MAJOR) {
Util.error(`The Blazor Web App template requires .NET ${MIN_SDK_MAJOR}+ but found .NET ${sdkMajor}. ` +
"Install a newer SDK from https://dotnet.microsoft.com/download and try again.", "red");
return false;
}

if (!this.isTemplateInstalled() && !this.installTemplate()) {
return false;
}

const extra = options.extraConfig ?? {};
const hosting = extra.Hosting ?? "Server";
const variant = extra.Variant ?? "light";
const args = [
"new", TEMPLATE_SHORT_NAME,
"-n", options.name,
"-o", options.name,
"--Hosting", hosting,
"--Theme", options.theme,
"--Variant", variant
];
if (options.skipInstall) {
args.push("--SkipRestore");
}

const result = Util.spawnSync("dotnet", args, { stdio: "inherit" });
if (result.error || result.status !== 0) {
Util.error("Project creation failed (see dotnet output above).", "red");
return false;
}

Util.log(Util.greenCheck() + " Project Created");
return true;
}
}
Loading
Loading