diff --git a/README.md b/README.md index fbf975e..a725487 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,12 @@ IntelliJ IDEA 一样按项目列出任务分组和依赖配置,运行任务、 | 内容根 | source set 的 Java/Kotlin/Groovy/Scala 源码、资源和注解处理器生成目录;`build` 与 `.gradle` 标为排除 | | 同步 | 构建脚本、`gradle.properties`、`gradle/libs.versions.toml`、Wrapper 配置变化后重新读取项目 | +## 新建项目 + +插件在 New Project 窗口提供 “Gradle” 条目,生成一个 Gradle Java 项目:可选 Kotlin 或 Groovy DSL、JDK、是否添加示例代码, +以及 GroupId 和 ArtifactId。模板在 `io.github.lithe-idea.gradle-build/templates/java/`,从 Java Support 插件移来, +生成的文件与原来相同。条目要求 `java` 语言和 `gradle` 构建系统同时可用,所以生成的项目需要安装 Java Support 插件提供 Java 语言支持。 + ## 依赖的程序 插件不带 Gradle,使用你项目或电脑上的 Gradle,按以下顺序查找: @@ -67,6 +73,7 @@ Lithe 已预置 Lithe Store 社区插件源;如果你之前移除了它,先 io.github.lithe-idea.gradle-build/plugin.json # 插件描述,目录名必须等于插件 id io.github.lithe-idea.gradle-build/lithe.init.gradle # 初始化脚本,导出模型和依赖树 io.github.lithe-idea.gradle-build/icons/ # 工具窗口和工具栏图标(来源见 NOTICE) +io.github.lithe-idea.gradle-build/templates/java/ # New Project 的 Gradle Java 项目模板(Kotlin/Groovy DSL、示例代码) test/fixtures/multi/ # 两个子项目的示例构建,供集成测试使用 scripts/package.mjs # 打包成 dist/gradle-build-<版本>-darwin.zip scripts/test.mjs # 用真实 Gradle 运行清单里的命令并检查输出 diff --git a/io.github.lithe-idea.gradle-build/plugin.json b/io.github.lithe-idea.gradle-build/plugin.json index 1ec8a4d..34e9550 100644 --- a/io.github.lithe-idea.gradle-build/plugin.json +++ b/io.github.lithe-idea.gradle-build/plugin.json @@ -4,13 +4,13 @@ "displayName": "Gradle", "version": "0.1.1", "pluginAPIVersion": 3, - "requires": { "buildSystems": 1 }, + "requires": { "buildSystems": 1, "projectGenerators": 1 }, "vendor": { "id": "io.github.lithe-idea", "displayName": "Lithe Community", "signatureRequirement": "none" }, - "permissions": ["process.build"], + "permissions": ["process.build", "project.generate"], "activationEvents": ["onBuildSystem:gradle"], "languages": [], "buildSystems": [ @@ -173,5 +173,79 @@ "layout": { "excluded": [".gradle", "build"] }, "languageImport": { "kind": "gradle", "options": { "offline": "offline", "gradleUserHome": "gradleUserHome" } } } + ], + "projectGenerators": [ + { + "id": "gradle-java", + "displayName": "Gradle", + "description": "A Java project built with Gradle.", + "section": "project", + "order": 30, + "icon": { "path": "icons/gradle.svg" }, + "fields": [ + { + "id": "gradleDsl", + "type": "enum", + "title": "Gradle DSL", + "presentation": "segmented", + "options": [ + { "value": "kotlin", "title": "Kotlin" }, + { "value": "groovy", "title": "Groovy" } + ] + }, + { + "id": "jdk", + "type": "runtime", + "title": "JDK", + "runtime": "java", + "minimumVersion": "11" + }, + { + "id": "addSampleCode", + "type": "boolean", + "title": "Add sample code", + "default": true + }, + { + "id": "groupId", + "type": "text", + "title": "GroupId", + "description": "Usually the reversed domain name of your organization, such as com.example.", + "advanced": true, + "default": "org.example", + "pattern": "[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*", + "maxLength": 200 + }, + { + "id": "artifactId", + "type": "text", + "title": "ArtifactId", + "description": "The name of the artifact the build produces.", + "advanced": true, + "default": "${project.name}", + "pattern": "[A-Za-z0-9_][A-Za-z0-9_.-]*", + "maxLength": 200 + } + ], + "requirements": [{ "languageID": "java" }, { "buildSystemID": "gradle" }], + "steps": [ + { "kind": "files", "source": "templates/java/gradle-kotlin", "when": { "field": "gradleDsl", "equals": "kotlin" } }, + { "kind": "files", "source": "templates/java/gradle-groovy", "when": { "field": "gradleDsl", "equals": "groovy" } }, + { "kind": "files", "source": "templates/java/standard-sample", "when": { "field": "addSampleCode", "equals": true } }, + { + "kind": "files", + "source": "templates/java/standard-sample-main", + "target": "src/main/java/${field.groupId|package|path}", + "when": { "field": "addSampleCode", "equals": true } + }, + { "kind": "files", "source": "templates/java/standard-empty", "when": { "field": "addSampleCode", "equals": false } } + ], + "openFiles": [ + { + "path": "src/main/java/${field.groupId|package|path}/Main.java", + "when": { "field": "addSampleCode", "equals": true } + } + ] + } ] } diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/build.gradle b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/build.gradle new file mode 100644 index 0000000..8c4bd09 --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'java' +} + +group = '{{ field.groupId }}' +version = '1.0-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of({{ field.jdk.major }}) + } +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.13.4') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +test { + useJUnitPlatform() +} diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/dot-gitignore b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/dot-gitignore new file mode 100644 index 0000000..2246277 --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/dot-gitignore @@ -0,0 +1,23 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### Lithe ### +.lithe/workspace/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### macOS ### +.DS_Store diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/settings.gradle b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/settings.gradle new file mode 100644 index 0000000..19318ab --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-groovy/settings.gradle @@ -0,0 +1 @@ +rootProject.name = '{{ field.artifactId }}' diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/build.gradle.kts b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/build.gradle.kts new file mode 100644 index 0000000..578317c --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + java +} + +group = "{{ field.groupId }}" +version = "1.0-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of({{ field.jdk.major }}) + } +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.13.4")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/dot-gitignore b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/dot-gitignore new file mode 100644 index 0000000..2246277 --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/dot-gitignore @@ -0,0 +1,23 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### Lithe ### +.lithe/workspace/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### macOS ### +.DS_Store diff --git a/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/settings.gradle.kts b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/settings.gradle.kts new file mode 100644 index 0000000..e95732b --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/gradle-kotlin/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "{{ field.artifactId }}" diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/main/java/dot-gitkeep b/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/main/java/dot-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/main/resources/dot-gitkeep b/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/main/resources/dot-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/test/java/dot-gitkeep b/io.github.lithe-idea.gradle-build/templates/java/standard-empty/src/test/java/dot-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-sample-main/Main.java b/io.github.lithe-idea.gradle-build/templates/java/standard-sample-main/Main.java new file mode 100644 index 0000000..0550c62 --- /dev/null +++ b/io.github.lithe-idea.gradle-build/templates/java/standard-sample-main/Main.java @@ -0,0 +1,7 @@ +package {{ field.groupId | package }}; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-sample/src/main/resources/dot-gitkeep b/io.github.lithe-idea.gradle-build/templates/java/standard-sample/src/main/resources/dot-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/io.github.lithe-idea.gradle-build/templates/java/standard-sample/src/test/java/dot-gitkeep b/io.github.lithe-idea.gradle-build/templates/java/standard-sample/src/test/java/dot-gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/package.mjs b/scripts/package.mjs index 85ae8e9..5a3f35e 100644 --- a/scripts/package.mjs +++ b/scripts/package.mjs @@ -2,8 +2,9 @@ // Packages the plugin into dist/--darwin.zip, the asset name // Lithe Store's release tracker recognizes (docs/publishing.md there). // -// The package is plain data: plugin.json, icons, and the init script that -// runs inside the user's own Gradle, so one archive serves every Mac. The +// The package is plain data: plugin.json, icons, New Project templates, and +// the init script that runs inside the user's own Gradle, so one archive +// serves every Mac. The // unpacked package stays in dist/package/ for installing a local // development copy into Lithe. // @@ -11,14 +12,28 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { cpSync, lstatSync, readdirSync, readFileSync, rmSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { cpSync, lstatSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; +import { isAbsolute, join, resolve } from "node:path"; const PACKAGE_DIR = "io.github.lithe-idea.gradle-build"; const ASSET_PREFIX = "gradle-build"; const PLATFORM = "darwin"; const PACKAGE_ROOT = join("dist", "package"); +/** + * Why `path` is not an existing `kind` ("icon file" or "template directory") + * inside the plugin package, or undefined when it is one. Absolute paths and + * `..` segments are refused because the package is copied and zipped as is. + */ +function packageEntryProblem(path, kind) { + if (typeof path !== "string" || path === "" || isAbsolute(path) || path.split(/[\\/]/).includes("..")) { + return `${kind} ${path} must be a relative path inside the package`; + } + const stat = statSync(join(PACKAGE_DIR, path), { throwIfNoEntry: false }); + const found = kind === "template directory" ? stat?.isDirectory() : stat?.isFile(); + return found ? undefined : `missing ${kind} ${path}`; +} + const manifest = JSON.parse(readFileSync(`${PACKAGE_DIR}/plugin.json`, "utf8")); const problems = []; if (manifest.id !== PACKAGE_DIR) problems.push(`plugin.json id ${manifest.id} must equal the directory ${PACKAGE_DIR}`); @@ -31,6 +46,22 @@ if (tagFlag >= 0) { const tag = process.argv[tagFlag + 1]; if (tag !== `v${manifest.version}`) problems.push(`tag ${tag} does not match plugin.json version ${manifest.version} (expected v${manifest.version})`); } + +// New Project entries point at icons and template directories by path; a typo +// there would only surface when a user opens the New Project window. +for (const generator of manifest.projectGenerators ?? []) { + const entries = []; + if (generator.icon?.path !== undefined) entries.push(["icon file", generator.icon.path]); + if (generator.icon?.darkPath !== undefined) entries.push(["icon file", generator.icon.darkPath]); + for (const step of generator.steps ?? []) { + if (step.source !== undefined) entries.push(["template directory", step.source]); + } + for (const [kind, path] of entries) { + const problem = packageEntryProblem(path, kind); + if (problem !== undefined) problems.push(`generator ${generator.id}: ${problem}`); + } +} + if (problems.length > 0) { for (const problem of problems) console.error(`✗ ${problem}`); process.exit(1);