@nanoforge-dev/schematics 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/collection.json +5 -0
  3. package/dist/index.cjs +4 -2
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2 -2
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/libs/component/component.factory.d.ts +1 -4
  12. package/dist/libs/component/component.factory.d.ts.map +1 -1
  13. package/dist/libs/component/component.factory.js +2 -4
  14. package/dist/libs/component/component.factory.js.map +1 -1
  15. package/dist/libs/plugin/files/README.md +23 -0
  16. package/dist/libs/plugin/files/__dot__gitignore +2 -0
  17. package/dist/libs/plugin/files/nanoforge.manifest.json.template +49 -0
  18. package/dist/libs/plugin/files/package.json.template +21 -0
  19. package/dist/libs/plugin/files/src/Panel.svelte +35 -0
  20. package/dist/libs/plugin/files/src/counter.ts +4 -0
  21. package/dist/libs/plugin/files/src/index.ts +52 -0
  22. package/dist/libs/plugin/files/svelte.config.js +2 -0
  23. package/dist/libs/plugin/files/tsconfig.json +14 -0
  24. package/dist/libs/plugin/files/vite.config.ts +12 -0
  25. package/dist/libs/plugin/plugin.factory.d.ts +18 -0
  26. package/dist/libs/plugin/plugin.factory.d.ts.map +1 -0
  27. package/dist/libs/plugin/plugin.factory.js +111 -0
  28. package/dist/libs/plugin/plugin.factory.js.map +1 -0
  29. package/dist/libs/plugin/schema.json +25 -0
  30. package/dist/libs/plugin/theme-files/README.md +21 -0
  31. package/dist/libs/plugin/theme-files/__dot__gitignore +2 -0
  32. package/dist/libs/plugin/theme-files/build.mjs +29 -0
  33. package/dist/libs/plugin/theme-files/icons/leaf.svg +1 -0
  34. package/dist/libs/plugin/theme-files/nanoforge.manifest.json.template +30 -0
  35. package/dist/libs/plugin/theme-files/package.json.template +11 -0
  36. package/dist/libs/project/files/js/AGENTS.md.template +126 -0
  37. package/dist/libs/project/files/js/CLAUDE.md.template +1 -0
  38. package/dist/libs/project/files/js/__dot__gitignore +4 -0
  39. package/dist/libs/project/files/js/nanoforge.config.js +1 -1
  40. package/dist/libs/project/files/js/package.json.template +8 -3
  41. package/dist/libs/project/files/js/src/components/drawable-circle-2d.component.js +1 -1
  42. package/dist/libs/project/files/js/src/main.js +2 -2
  43. package/dist/libs/project/files/js/src/systems/draw-2d.system.js +1 -1
  44. package/dist/libs/project/files/js/src/systems/move-2d.system.js +1 -1
  45. package/dist/libs/project/files/js/src/systems/position-sync.system.js +1 -1
  46. package/dist/libs/project/files/ts/AGENTS.md.template +114 -0
  47. package/dist/libs/project/files/ts/CLAUDE.md.template +1 -0
  48. package/dist/libs/project/files/ts/__dot__gitignore +4 -0
  49. package/dist/libs/project/files/ts/nanoforge.config.ts +1 -1
  50. package/dist/libs/project/files/ts/package.json.template +8 -3
  51. package/dist/libs/project/files/ts/src/components/drawable-circle-2d.component.ts +1 -1
  52. package/dist/libs/project/files/ts/src/main.ts +2 -1
  53. package/dist/libs/project/files/ts/src/systems/draw-2d.system.ts +1 -1
  54. package/dist/libs/project/files/ts/src/systems/move-2d.system.ts +1 -1
  55. package/dist/libs/project/files/ts/src/systems/position-sync.system.ts +1 -1
  56. package/dist/libs/project/project.factory.d.ts +5 -4
  57. package/dist/libs/project/project.factory.d.ts.map +1 -1
  58. package/dist/libs/project/project.factory.js +14 -10
  59. package/dist/libs/project/project.factory.js.map +1 -1
  60. package/dist/libs/project/schema.json +5 -0
  61. package/dist/libs/system/system.factory.d.ts +1 -4
  62. package/dist/libs/system/system.factory.d.ts.map +1 -1
  63. package/dist/libs/system/system.factory.js +2 -4
  64. package/dist/libs/system/system.factory.js.map +1 -1
  65. package/dist/libs/workspace/files/js/AGENTS.md.template +49 -0
  66. package/dist/libs/workspace/files/js/CLAUDE.md.template +1 -0
  67. package/dist/libs/workspace/files/js/__dot__gitignore +4 -0
  68. package/dist/libs/workspace/files/js/nanoforge.config.js +1 -1
  69. package/dist/libs/workspace/files/js/package.json.template +1 -1
  70. package/dist/libs/workspace/files/ts/AGENTS.md.template +49 -0
  71. package/dist/libs/workspace/files/ts/CLAUDE.md.template +1 -0
  72. package/dist/libs/workspace/files/ts/__dot__gitignore +4 -0
  73. package/dist/libs/workspace/files/ts/nanoforge.config.ts +1 -1
  74. package/dist/libs/workspace/files/ts/package.json.template +1 -1
  75. package/dist/libs/workspace/schema.json +5 -0
  76. package/dist/libs/workspace/workspace.factory.d.ts +5 -4
  77. package/dist/libs/workspace/workspace.factory.d.ts.map +1 -1
  78. package/dist/libs/workspace/workspace.factory.js +7 -8
  79. package/dist/libs/workspace/workspace.factory.js.map +1 -1
  80. package/package.json +15 -16
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ # [3.0.0](https://github.com/NanoForge-dev/CLI/compare/2.3.1...3.0.0) - (2026-11-08)
6
+
7
+ ## Breaking Changes
8
+
9
+ - **cli:** Install, publish and unpublish on the shared registry lib (#263) ([9fb3abf](https://github.com/NanoForge-dev/CLI/commit/9fb3abfc4ff9c8630acb7a559be3129da04af221)) by @Exeloo
10
+ - **BREAKING CHANGE:** nf install drops --server, and installs into nf_modules instead of copying files into client/ or server/. Manifests must use the registry format (@scope/name, version, type package or plugin): publish.paths and the scope/name format are no longer read.
11
+
12
+ ## Features
13
+
14
+ - New CLI options for AGENTS.md documentation (#245) ([63a1446](https://github.com/NanoForge-dev/CLI/commit/63a144662840e3d7487b18f34feade8230507831)) by @hvrp3r
15
+ - Add theme plugin (#248) ([e62ec83](https://github.com/NanoForge-dev/CLI/commit/e62ec831e5a87d5221401a3382856738985a507a)) by @Exeloo
16
+ - **schematics:** Add support for generating NanoForge editor plugins (#241) ([0440154](https://github.com/NanoForge-dev/CLI/commit/044015451885e5ef2a3ad6475477ae326fc21406)) by @Exeloo
17
+
18
+ # [2.3.1](https://github.com/NanoForge-dev/CLI/compare/2.3.0...2.3.1) - (2026-09-25)
19
+
5
20
  # [2.3.0](https://github.com/NanoForge-dev/CLI/compare/1.6.2...2.3.0) - (2026-09-24)
6
21
 
7
22
  > `@nanoforge-dev/schematics` now lives in the CLI monorepo (`libs/schematics`) and is released
@@ -20,6 +20,11 @@
20
20
  "factory": "./libs/system/system.factory#main",
21
21
  "description": "Create a NanoForge system.",
22
22
  "schema": "./libs/system/schema.json"
23
+ },
24
+ "plugin": {
25
+ "factory": "./libs/plugin/plugin.factory#main",
26
+ "description": "Create a NanoForge editor plugin (manifest, Svelte panel, command, setting).",
27
+ "schema": "./libs/plugin/schema.json"
23
28
  }
24
29
  }
25
30
  }
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/defaults.ts
2
+ // src/defaults.ts
3
3
  const DEFAULT_APP_NAME = "nanoforge-app";
4
4
  const DEFAULT_LANGUAGE = "ts";
5
5
  const DEFAULT_PACKAGE_MANAGER = "npm";
@@ -19,9 +19,11 @@ const DEFAULT_TYPESCRIPT_VERSION = "6.0.3";
19
19
  * maliciously) published release isn't picked up immediately.
20
20
  */
21
21
  const MIN_RELEASE_AGE_HOURS = 48;
22
- //#endregion
22
+ /** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */
23
+ const DEFAULT_EDITOR_VERSION = "^1.0.0";
23
24
  exports.DEFAULT_APP_NAME = DEFAULT_APP_NAME;
24
25
  exports.DEFAULT_CLI_DEPENDENCY_VERSION = DEFAULT_CLI_DEPENDENCY_VERSION;
26
+ exports.DEFAULT_EDITOR_VERSION = DEFAULT_EDITOR_VERSION;
25
27
  exports.DEFAULT_ENGINE_VERSION = DEFAULT_ENGINE_VERSION;
26
28
  exports.DEFAULT_LANGUAGE = DEFAULT_LANGUAGE;
27
29
  exports.DEFAULT_NANOFORGE_DEPENDENCY_VERSION = DEFAULT_NANOFORGE_DEPENDENCY_VERSION;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../src/defaults.ts"],"sourcesContent":["export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n"],"mappings":";;AAAA,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;AAChC,MAAa,0BAA0B;AACvC,MAAa,uCAAuC;AACpD,MAAa,yBAAyB;;AAGtC,MAAa,iCAAiC;;;;;AAM9C,MAAa,6BAA6B;;;;;;;AAQ1C,MAAa,wBAAwB"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../src/defaults.ts"],"sourcesContent":["export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n\n/** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */\nexport const DEFAULT_EDITOR_VERSION = \"^1.0.0\";\n"],"mappings":";;AAAA,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;AAChC,MAAa,0BAA0B;AACvC,MAAa,uCAAuC;AACpD,MAAa,yBAAyB;;AAGtC,MAAa,iCAAiC;;;;;AAM9C,MAAa,6BAA6B;;;;;;;AAQ1C,MAAa,wBAAwB;;AAGrC,MAAa,yBAAyB"}
package/dist/index.d.cts CHANGED
@@ -1,4 +1,3 @@
1
- //#region src/defaults.d.ts
2
1
  export declare const DEFAULT_APP_NAME = "nanoforge-app";
3
2
  export declare const DEFAULT_LANGUAGE = "ts";
4
3
  export declare const DEFAULT_PACKAGE_MANAGER = "npm";
@@ -18,5 +17,6 @@ export declare const DEFAULT_TYPESCRIPT_VERSION = "6.0.3";
18
17
  * maliciously) published release isn't picked up immediately.
19
18
  */
20
19
  export declare const MIN_RELEASE_AGE_HOURS = 48;
21
- //#endregion
20
+ /** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */
21
+ export declare const DEFAULT_EDITOR_VERSION = "^1.0.0";
22
22
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/defaults.ts"],"mappings":";qBAAa;qBACA;qBACA;qBACA;qBACA;;qBAGA;;;;;qBAMA;;;;;;;qBAQA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/defaults.ts"],"mappings":"A,qBAAa;qBACA;qBACA;qBACA;qBACA;;qBAGA;;;;;qBAMA;;;;;;;qBAQA;;qBAGA"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- //#region src/defaults.d.ts
2
1
  export declare const DEFAULT_APP_NAME = "nanoforge-app";
3
2
  export declare const DEFAULT_LANGUAGE = "ts";
4
3
  export declare const DEFAULT_PACKAGE_MANAGER = "npm";
@@ -18,5 +17,6 @@ export declare const DEFAULT_TYPESCRIPT_VERSION = "6.0.3";
18
17
  * maliciously) published release isn't picked up immediately.
19
18
  */
20
19
  export declare const MIN_RELEASE_AGE_HOURS = 48;
21
- //#endregion
20
+ /** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */
21
+ export declare const DEFAULT_EDITOR_VERSION = "^1.0.0";
22
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/defaults.ts"],"mappings":";qBAAa;qBACA;qBACA;qBACA;qBACA;;qBAGA;;;;;qBAMA;;;;;;;qBAQA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/defaults.ts"],"mappings":"A,qBAAa;qBACA;qBACA;qBACA;qBACA;;qBAGA;;;;;qBAMA;;;;;;;qBAQA;;qBAGA"}
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/defaults.ts
1
+ // src/defaults.ts
2
2
  const DEFAULT_APP_NAME = "nanoforge-app";
3
3
  const DEFAULT_LANGUAGE = "ts";
4
4
  const DEFAULT_PACKAGE_MANAGER = "npm";
@@ -18,7 +18,8 @@ const DEFAULT_TYPESCRIPT_VERSION = "6.0.3";
18
18
  * maliciously) published release isn't picked up immediately.
19
19
  */
20
20
  const MIN_RELEASE_AGE_HOURS = 48;
21
- //#endregion
22
- export { DEFAULT_APP_NAME, DEFAULT_CLI_DEPENDENCY_VERSION, DEFAULT_ENGINE_VERSION, DEFAULT_LANGUAGE, DEFAULT_NANOFORGE_DEPENDENCY_VERSION, DEFAULT_PACKAGE_MANAGER, DEFAULT_TYPESCRIPT_VERSION, MIN_RELEASE_AGE_HOURS };
21
+ /** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */
22
+ const DEFAULT_EDITOR_VERSION = "^1.0.0";
23
+ export { DEFAULT_APP_NAME, DEFAULT_CLI_DEPENDENCY_VERSION, DEFAULT_EDITOR_VERSION, DEFAULT_ENGINE_VERSION, DEFAULT_LANGUAGE, DEFAULT_NANOFORGE_DEPENDENCY_VERSION, DEFAULT_PACKAGE_MANAGER, DEFAULT_TYPESCRIPT_VERSION, MIN_RELEASE_AGE_HOURS };
23
24
 
24
25
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/defaults.ts"],"sourcesContent":["export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n"],"mappings":";AAAA,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;AAChC,MAAa,0BAA0B;AACvC,MAAa,uCAAuC;AACpD,MAAa,yBAAyB;;AAGtC,MAAa,iCAAiC;;;;;AAM9C,MAAa,6BAA6B;;;;;;;AAQ1C,MAAa,wBAAwB"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/defaults.ts"],"sourcesContent":["export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n\n/** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */\nexport const DEFAULT_EDITOR_VERSION = \"^1.0.0\";\n"],"mappings":";AAAA,MAAa,mBAAmB;AAChC,MAAa,mBAAmB;AAChC,MAAa,0BAA0B;AACvC,MAAa,uCAAuC;AACpD,MAAa,yBAAyB;;AAGtC,MAAa,iCAAiC;;;;;AAM9C,MAAa,6BAA6B;;;;;;;AAQ1C,MAAa,wBAAwB;;AAGrC,MAAa,yBAAyB"}
@@ -1,5 +1,4 @@
1
1
  import { Rule } from "@angular-devkit/schematics";
2
- //#region src/libs/component/component.schema.d.ts
3
2
  interface ComponentSchema {
4
3
  /**
5
4
  * Component destination directory
@@ -18,8 +17,6 @@ interface ComponentSchema {
18
17
  */
19
18
  language: "js" | "ts";
20
19
  }
21
- //#endregion
22
- //#region src/libs/component/component.factory.d.ts
23
20
  export declare const main: (schema: ComponentSchema) => Rule;
24
- //#endregion
21
+ export {};
25
22
  //# sourceMappingURL=component.factory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.factory.d.ts","names":[],"sources":["../../../src/libs/component/component.schema.d.ts","../../../src/libs/component/component.factory.ts"],"mappings":";;UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;;;qBCiBW,OAAQ,QAAQ,oBAAkB"}
1
+ {"version":3,"file":"component.factory.d.ts","names":[],"sources":["../../../src/libs/component/component.schema.d.ts","../../../src/libs/component/component.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;;;EAKA;;qBCiBW,OAAQ,QAAQ,oBAAkB"}
@@ -1,6 +1,6 @@
1
1
  import { join, normalize, strings } from "@angular-devkit/core";
2
2
  import { apply, mergeWith, move, template, url } from "@angular-devkit/schematics";
3
- //#region src/utils/formatting.ts
3
+ // src/utils/formatting.ts
4
4
  const toWords = (str) => {
5
5
  return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
6
6
  };
@@ -10,8 +10,7 @@ const toKebabCase = (str) => {
10
10
  const toPascalCase = (str) => {
11
11
  return toWords(str).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
12
12
  };
13
- //#endregion
14
- //#region src/libs/component/component.factory.ts
13
+ // src/libs/component/component.factory.ts
15
14
  const transform = (schema) => {
16
15
  return {
17
16
  part: schema.part,
@@ -30,7 +29,6 @@ const main = (schema) => {
30
29
  const options = transform(schema);
31
30
  return mergeWith(generate(options, schema.directory, schema.language));
32
31
  };
33
- //#endregion
34
32
  export { main };
35
33
 
36
34
  //# sourceMappingURL=component.factory.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"component.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/component/component.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase, toPascalCase } from \"@utils/formatting\";\n\nimport { type ComponentOptions } from \"./component.options\";\nimport { type ComponentSchema } from \"./component.schema\";\n\nconst transform = (schema: ComponentSchema): ComponentOptions => {\n return {\n part: schema.part,\n className: toPascalCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: ComponentOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: ComponentSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAEA,MAAa,gBAAgB,QAAwB;CACnD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CACzE,KAAK,EAAE;AACZ;;;ACJA,MAAM,aAAa,WAA8C;CAC/D,OAAO;EACL,MAAM,OAAO;EACb,WAAW,aAAa,OAAO,IAAI;EACnC,UAAU,YAAY,OAAO,IAAI;CACnC;AACF;AAEA,MAAM,YAAY,SAA2B,MAAc,aAAkC;CAC3F,MAAM,QAAQ,CACZ,SAAS;EACP,GAAG;EACH,GAAG;CACL,CAAC,GACD,KAAK,UAAU,IAAI,CAAC,CACtB;CAEA,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,CAAC,GAAG,KAAK;AAC5D;AAEA,MAAa,QAAQ,WAAkC;CACrD,MAAM,UAAU,UAAU,MAAM;CAEhC,OAAO,UAAU,SAAS,SAAS,OAAO,WAAW,OAAO,QAAQ,CAAC;AACvE"}
1
+ {"version":3,"file":"component.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/libs/component/component.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","import { type Path, join, normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n type Source,\n apply,\n mergeWith,\n move,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toKebabCase, toPascalCase } from \"@utils/formatting\";\n\nimport { type ComponentOptions } from \"./component.options\";\nimport { type ComponentSchema } from \"./component.schema\";\n\nconst transform = (schema: ComponentSchema): ComponentOptions => {\n return {\n part: schema.part,\n className: toPascalCase(schema.name),\n fileName: toKebabCase(schema.name),\n };\n};\n\nconst generate = (options: ComponentOptions, path: string, language: \"ts\" | \"js\"): Source => {\n const rules = [\n template({\n ...strings,\n ...options,\n }),\n move(normalize(path)),\n ];\n\n return apply(url(join(\"./files\" as Path, language)), rules);\n};\n\nexport const main = (schema: ComponentSchema): Rule => {\n const options = transform(schema);\n\n return mergeWith(generate(options, schema.directory, schema.language));\n};\n"],"mappings":";;;AAAA,MAAM,WAAW,QAA0B;CACzC,OAAO,IACJ,QAAQ,mBAAmB,OAAO,CAAC,CACnC,QAAQ,yBAAyB,OAAO,CAAC,CACzC,QAAQ,UAAU,GAAG,CAAC,CACtB,KAAK,CAAC,CACN,MAAM,KAAK,CAAC,CACZ,OAAO,OAAO;AACnB;AAEA,MAAa,eAAe,QAAwB;CAClD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CACjC,KAAK,GAAG;AACb;AAEA,MAAa,gBAAgB,QAAwB;CACnD,OAAO,QAAQ,GAAG,CAAC,CAChB,KAAK,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CACzE,KAAK,EAAE;AACZ;;ACJA,MAAM,aAAa,WAA8C;CAC/D,OAAO;EACL,MAAM,OAAO;EACb,WAAW,aAAa,OAAO,IAAI;EACnC,UAAU,YAAY,OAAO,IAAI;CACnC;AACF;AAEA,MAAM,YAAY,SAA2B,MAAc,aAAkC;CAC3F,MAAM,QAAQ,CACZ,SAAS;EACP,GAAG;EACH,GAAG;CACL,CAAC,GACD,KAAK,UAAU,IAAI,CAAC,CACtB;CAEA,OAAO,MAAM,IAAI,KAAK,WAAmB,QAAQ,CAAC,GAAG,KAAK;AAC5D;AAEA,MAAa,QAAQ,WAAkC;CACrD,MAAM,UAAU,UAAU,MAAM;CAEhC,OAAO,UAAU,SAAS,SAAS,OAAO,WAAW,OAAO,QAAQ,CAAC;AACvE"}
@@ -0,0 +1,23 @@
1
+ # <%= title %>
2
+
3
+ A NanoForge editor plugin (`<%= name %>`).
4
+
5
+ - `nanoforge.manifest.json`: what the plugin adds to the editor (a panel, a command, a menu entry, a setting).
6
+ - `src/index.ts`: `activate`, run when the panel is first shown or the command first runs.
7
+ - `src/Panel.svelte`: the panel.
8
+
9
+ ## Run it in the editor
10
+
11
+ ```sh
12
+ pnpm install
13
+ pnpm dev # rebuilds dist/ on every change
14
+ DEV_PLUGINS=$(pwd) nf editor <a project> # in another terminal: the editor reloads the plugin
15
+ ```
16
+
17
+ The <%= title %> panel opens in the bottom right dock. _View › Panels › <%= title %>_ shows it again once closed, and _Edit › Add to the counter_ runs its command.
18
+
19
+ ## Install it without the dev mode
20
+
21
+ Build it (`pnpm build`), then copy `dist/` to `~/.nanoforge/editor/plugins/<%= name %>` (every project) or to `<project>/.nanoforge/plugins/<%= name %>` (that project).
22
+
23
+ The plugin authoring guide has the rest: the manifest, the SDK, extension points, settings, history.
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,49 @@
1
+ {
2
+ "type": "plugin",
3
+ "name": "<%= name %>",
4
+ "version": "0.1.0",
5
+ "displayName": "<%= title %>",
6
+ "description": "Counts, to show how an editor plugin is made.",
7
+ "engines": {
8
+ "editor": ">=0.1.0"
9
+ },
10
+ "entry": {
11
+ "client": "index.js"
12
+ },
13
+ "activation": ["onWidget:<%= id %>.panel", "onCommand:<%= id %>.add"],
14
+ "contributes": {
15
+ "widgets": [
16
+ {
17
+ "id": "<%= id %>.panel",
18
+ "title": "<%= title %>",
19
+ "icon": "plus",
20
+ "defaultSlot": "rightBottom",
21
+ "order": 900
22
+ }
23
+ ],
24
+ "commands": [
25
+ {
26
+ "id": "<%= id %>.add",
27
+ "title": "Add to the counter",
28
+ "category": "<%= title %>"
29
+ }
30
+ ],
31
+ "menus": [
32
+ {
33
+ "menu": "edit",
34
+ "command": "<%= id %>.add",
35
+ "group": "<%= id %>"
36
+ }
37
+ ],
38
+ "settings": [
39
+ {
40
+ "key": "step",
41
+ "type": "number",
42
+ "default": 1,
43
+ "title": "Step",
44
+ "description": "How much each click adds.",
45
+ "category": "<%= title %>"
46
+ }
47
+ ]
48
+ }
49
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "<%= short %>-editor-plugin",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "<%= title %>: a NanoForge editor plugin",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "dev": "vite build --watch",
10
+ "typecheck": "svelte-check --tsconfig ./tsconfig.json"
11
+ },
12
+ "devDependencies": {
13
+ "@nanoforge-dev/editor-sdk": "<%= editorSdkVersion %>",
14
+ "@nanoforge-dev/editor-vite-plugin": "<%= editorVitePluginVersion %>",
15
+ "@sveltejs/vite-plugin-svelte": "^6.0.0 || ^7.0.0",
16
+ "svelte": "^5.0.0",
17
+ "svelte-check": "^4.0.0",
18
+ "typescript": "^6.0.0",
19
+ "vite": "^7.0.0 || ^8.0.0"
20
+ }
21
+ }
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import { EditorServices } from '@nanoforge-dev/editor-sdk';
3
+ import { Button, type WidgetInstance } from '@nanoforge-dev/editor-sdk/ui';
4
+
5
+ import { count } from './counter';
6
+ import { HISTORY } from './index';
7
+
8
+ const { instance }: { instance: WidgetInstance } = $props();
9
+ const add = () => instance.services.get(EditorServices.Commands).execute('<%= id %>.add');
10
+
11
+ // While this panel has focus, undo and redo act on the plugin's history.
12
+ $effect(() => {
13
+ instance.setHistoryContext(HISTORY);
14
+ return () => instance.setHistoryContext(undefined);
15
+ });
16
+ </script>
17
+
18
+ <section class="<%= short %>" aria-label="<%= title %>">
19
+ <p class="count" role="status">{$count}</p>
20
+ <Button variant="primary" onclick={() => void add()}>Add</Button>
21
+ </section>
22
+
23
+ <style>
24
+ .<%= short %> {
25
+ display: grid;
26
+ gap: var(--nf-space-3);
27
+ justify-items: center;
28
+ padding: var(--nf-space-4);
29
+ }
30
+ .count {
31
+ margin: 0;
32
+ font-size: 2.5rem;
33
+ font-weight: 600;
34
+ }
35
+ </style>
@@ -0,0 +1,4 @@
1
+ import { ObservableValue } from '@nanoforge-dev/editor-sdk';
2
+
3
+ /** The plugin's state, shared by its command and its panel. */
4
+ export const count = new ObservableValue(0);
@@ -0,0 +1,52 @@
1
+ import type { Component } from 'svelte';
2
+
3
+ import { HistoryServiceToken, SettingsServiceToken, definePlugin } from '@nanoforge-dev/editor-sdk';
4
+ import { StyleServiceToken, WIDGET_VIEWS, type WidgetInstance } from '@nanoforge-dev/editor-sdk/ui';
5
+
6
+ import Panel from './Panel.svelte';
7
+ import { count } from './counter';
8
+
9
+ /** Undo and redo target of the panel (see Panel.svelte). */
10
+ export const HISTORY = '<%= id %>';
11
+
12
+ export default definePlugin({
13
+ async activate(context) {
14
+ const { services } = context;
15
+
16
+ // The panel's styles are built into <%= short %>.css: the plugin injects them when it starts.
17
+ const css = await fetch(context.resolveAsset('<%= short %>.css')).then((response) =>
18
+ response.ok ? response.text() : '',
19
+ );
20
+ context.subscriptions.add(services.get(StyleServiceToken).inject(context.name, css));
21
+
22
+ // The view of the widget declared in the manifest.
23
+ context.subscriptions.add(
24
+ context.contribute(WIDGET_VIEWS, {
25
+ id: '<%= id %>.panel',
26
+ component: Panel as Component<{ instance: WidgetInstance }>,
27
+ }),
28
+ );
29
+
30
+ // A history of its own: each addition can be undone with Ctrl+Z while the panel has focus.
31
+ const history = services.get(HistoryServiceToken).registerContext({
32
+ id: HISTORY,
33
+ label: '<%= title %>',
34
+ owner: context.name,
35
+ });
36
+ context.subscriptions.add(history);
37
+
38
+ // The command declared in the manifest. Settings keys carry the plugin's name.
39
+ const settings = services.get(SettingsServiceToken);
40
+ context.subscriptions.add(
41
+ context.registerCommand('<%= id %>.add', async () => {
42
+ const step = Number(settings.get('<%= name %>.step')) || 1;
43
+ await history.stack.push({
44
+ label: `Add ${step}`,
45
+ do: () => count.set(count.get() + step),
46
+ undo: () => count.set(count.get() - step),
47
+ });
48
+ context.logger.info(`Counter: ${count.get()}`);
49
+ }),
50
+ );
51
+ },
52
+ });
@@ -0,0 +1,2 @@
1
+ /** Plugins compile Svelte in runes mode, like the editor. */
2
+ export default { compilerOptions: { runes: true } };
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "verbatimModuleSyntax": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "types": ["vite/client"]
12
+ },
13
+ "include": ["src/**/*.ts", "src/**/*.svelte"]
14
+ }
@@ -0,0 +1,12 @@
1
+ import { svelte } from '@sveltejs/vite-plugin-svelte';
2
+ import { defineConfig } from 'vite';
3
+
4
+ import { nanoforgeEditorPlugin } from '@nanoforge-dev/editor-vite-plugin';
5
+
6
+ // Builds dist/: index.js, <%= short %>.css and the manifest. The editor loads that folder.
7
+ export default defineConfig({
8
+ plugins: [svelte(), nanoforgeEditorPlugin()],
9
+ build: {
10
+ lib: { entry: { index: 'src/index.ts' }, formats: ['es'], cssFileName: '<%= short %>' },
11
+ },
12
+ });
@@ -0,0 +1,18 @@
1
+ import { Rule } from "@angular-devkit/schematics";
2
+ interface PluginSchema {
3
+ /**
4
+ * Plugin name in the registry, as @scope/name
5
+ */
6
+ name: string;
7
+ /**
8
+ * Plugin destination directory. Defaults to the plugin's short name.
9
+ */
10
+ directory?: string | null;
11
+ /**
12
+ * What to start from: a panel (default) or a color theme with no code
13
+ */
14
+ template?: "panel" | "theme";
15
+ }
16
+ export declare const main: (schema: PluginSchema) => Rule;
17
+ export {};
18
+ //# sourceMappingURL=plugin.factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.factory.d.ts","names":[],"sources":["../../../src/libs/plugin/plugin.schema.d.ts","../../../src/libs/plugin/plugin.factory.ts"],"mappings":";UAAiB;;;;EAIf;;;;EAKA;;;;EAKA;;qBCuDW,OAAQ,QAAQ,iBAAe"}
@@ -0,0 +1,111 @@
1
+ import { normalize, strings } from "@angular-devkit/core";
2
+ import { SchematicsException, apply, mergeWith, move, renameTemplateFiles, template, url } from "@angular-devkit/schematics";
3
+ // src/utils/formatting.ts
4
+ const toWords = (str) => {
5
+ return str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/[-_]+/g, " ").trim().split(/\s+/).filter(Boolean);
6
+ };
7
+ const toCamelCase = (str) => {
8
+ return toWords(str).map((word, i) => i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
9
+ };
10
+ /** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */
11
+ const DEFAULT_EDITOR_VERSION = "^1.0.0";
12
+ // src/utils/registry.ts
13
+ const REGISTRY_URL = "https://registry.npmjs.org";
14
+ const DEFAULT_TIMEOUT_MS = 3e3;
15
+ const RELEASE_VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
16
+ const compareVersions = (a, b) => {
17
+ const partsA = a.split(".").map(Number);
18
+ const partsB = b.split(".").map(Number);
19
+ for (let i = 0; i < 3; i++) {
20
+ const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);
21
+ if (diff !== 0) return diff;
22
+ }
23
+ return 0;
24
+ };
25
+ /**
26
+ * Resolves the newest published version of an npm package that is at least
27
+ * `minAgeHours` old, so freshly (and potentially maliciously) published
28
+ * versions aren't picked up immediately. Falls back to `fallback` on any
29
+ * network error, timeout, missing package, or when no version satisfies the
30
+ * constraints, so callers never have to handle a rejected promise.
31
+ */
32
+ const fetchTrustedVersion = async (packageName, fallback, options = {}) => {
33
+ const { minAgeHours = 48, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
34
+ const controller = new AbortController();
35
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, { signal: controller.signal });
38
+ if (!response.ok) return fallback;
39
+ const time = (await response.json()).time;
40
+ if (!time) return fallback;
41
+ const cutoff = Date.now() - minAgeHours * 60 * 60 * 1e3;
42
+ const eligible = Object.entries(time).filter(([version]) => RELEASE_VERSION_PATTERN.test(version)).filter(([version]) => major === void 0 || Number(version.split(".")[0]) === major).filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff).map(([version]) => version).sort(compareVersions);
43
+ return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;
44
+ } catch {
45
+ return fallback;
46
+ } finally {
47
+ clearTimeout(timeout);
48
+ }
49
+ };
50
+ /**
51
+ * Resolves several packages' trusted versions in parallel. Keys of `specs`
52
+ * are the package name to query the registry for; values are the fallback
53
+ * to use if that lookup fails. Returns a map from the *same* keys to the
54
+ * resolved (or fallback) version — typed so callers don't get `| undefined`
55
+ * back for a key they just passed in.
56
+ */
57
+ const fetchTrustedVersions = async (specs) => {
58
+ const entries = await Promise.all(Object.entries(specs).map(async ([packageName, { fallback, major }]) => {
59
+ return [packageName, await fetchTrustedVersion(packageName, fallback, { major })];
60
+ }));
61
+ return Object.fromEntries(entries);
62
+ };
63
+ // src/libs/plugin/plugin.factory.ts
64
+ const PLUGIN_NAME_PATTERN = /^@([a-z0-9][a-z0-9-]*)\/([a-z0-9][a-z0-9-]*)$/;
65
+ const transform = async (schema) => {
66
+ const theme = schema.template === "theme";
67
+ const name = schema.name.trim();
68
+ const short = PLUGIN_NAME_PATTERN.exec(name)?.[2];
69
+ if (!short) throw new SchematicsException(`"${name}" is not a plugin name. Use @scope/name, in lowercase: @acme/counter.`);
70
+ const versions = theme ? {
71
+ "@nanoforge-dev/editor-sdk": DEFAULT_EDITOR_VERSION,
72
+ "@nanoforge-dev/editor-vite-plugin": DEFAULT_EDITOR_VERSION
73
+ } : await fetchTrustedVersions({
74
+ "@nanoforge-dev/editor-sdk": {
75
+ fallback: DEFAULT_EDITOR_VERSION,
76
+ major: 1
77
+ },
78
+ "@nanoforge-dev/editor-vite-plugin": {
79
+ fallback: DEFAULT_EDITOR_VERSION,
80
+ major: 1
81
+ }
82
+ });
83
+ return {
84
+ name,
85
+ short,
86
+ title: short.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "),
87
+ id: toCamelCase(short),
88
+ editorSdkVersion: versions["@nanoforge-dev/editor-sdk"],
89
+ editorVitePluginVersion: versions["@nanoforge-dev/editor-vite-plugin"]
90
+ };
91
+ };
92
+ const generate = (options, path, theme) => {
93
+ return apply(url(theme ? "./theme-files" : "./files"), [
94
+ template({
95
+ dot: ".",
96
+ ...strings,
97
+ ...options
98
+ }),
99
+ renameTemplateFiles(),
100
+ move(normalize(path))
101
+ ]);
102
+ };
103
+ const main = (schema) => {
104
+ return async () => {
105
+ const options = await transform(schema);
106
+ return mergeWith(generate(options, schema.directory ?? options.short, schema.template === "theme"));
107
+ };
108
+ };
109
+ export { main };
110
+
111
+ //# sourceMappingURL=plugin.factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.factory.js","names":[],"sources":["../../../src/utils/formatting.ts","../../../src/defaults.ts","../../../src/utils/registry.ts","../../../src/libs/plugin/plugin.factory.ts"],"sourcesContent":["const toWords = (str: string): string[] => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1 $2\")\n .replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .trim()\n .split(/\\s+/)\n .filter(Boolean);\n};\n\nexport const toKebabCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.toLowerCase())\n .join(\"-\");\n};\n\nexport const toPascalCase = (str: string): string => {\n return toWords(str)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n};\n\nexport const toCamelCase = (str: string): string => {\n const words = toWords(str);\n return words\n .map((word, i) =>\n i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),\n )\n .join(\"\");\n};\n","export const DEFAULT_APP_NAME = \"nanoforge-app\";\nexport const DEFAULT_LANGUAGE = \"ts\";\nexport const DEFAULT_PACKAGE_MANAGER = \"npm\";\nexport const DEFAULT_NANOFORGE_DEPENDENCY_VERSION = \"^2\";\nexport const DEFAULT_ENGINE_VERSION = \"^2\";\n\n/** Fallback used when the registry lookup for `@nanoforge-dev/cli` fails. */\nexport const DEFAULT_CLI_DEPENDENCY_VERSION = \"latest\";\n\n/**\n * Fallback used when the registry lookup for `typescript` fails. A specific\n * version, not a range — non-nanoforge packages are pinned exactly.\n */\nexport const DEFAULT_TYPESCRIPT_VERSION = \"6.0.3\";\n\n/**\n * Mirrors pnpm's own `minimumReleaseAge` supply-chain protection: when\n * resolving a pinned dependency version at generation time, only consider\n * versions published at least this long ago, so a freshly (and potentially\n * maliciously) published release isn't picked up immediately.\n */\nexport const MIN_RELEASE_AGE_HOURS = 48;\n\n/** Fallback for the editor packages (`@nanoforge-dev/editor-*`), which share one version line. */\nexport const DEFAULT_EDITOR_VERSION = \"^1.0.0\";\n","import { MIN_RELEASE_AGE_HOURS } from \"~/defaults\";\n\nconst REGISTRY_URL = \"https://registry.npmjs.org\";\nconst DEFAULT_TIMEOUT_MS = 3000;\n\nconst RELEASE_VERSION_PATTERN = /^\\d+\\.\\d+\\.\\d+$/;\n\nconst compareVersions = (a: string, b: string): number => {\n const partsA = a.split(\".\").map(Number);\n const partsB = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) {\n const diff = (partsA[i] ?? 0) - (partsB[i] ?? 0);\n if (diff !== 0) {\n return diff;\n }\n }\n return 0;\n};\n\nexport interface FetchTrustedVersionOptions {\n /**\n * Only consider versions published at least this many hours ago, mirroring\n * pnpm's `minimumReleaseAge` supply-chain protection for a freshly\n * published (and potentially compromised) release.\n * @default MIN_RELEASE_AGE_HOURS\n */\n minAgeHours?: number;\n\n /**\n * Restrict candidates to this major version (e.g. `6` only considers\n * `6.x.x`). Useful to avoid silently jumping a generated project onto a\n * new major release.\n */\n major?: number;\n\n /** Network timeout in milliseconds. @default 3000 */\n timeoutMs?: number;\n}\n\n/**\n * Resolves the newest published version of an npm package that is at least\n * `minAgeHours` old, so freshly (and potentially maliciously) published\n * versions aren't picked up immediately. Falls back to `fallback` on any\n * network error, timeout, missing package, or when no version satisfies the\n * constraints, so callers never have to handle a rejected promise.\n */\nexport const fetchTrustedVersion = async (\n packageName: string,\n fallback: string,\n options: FetchTrustedVersionOptions = {},\n): Promise<string> => {\n const { minAgeHours = MIN_RELEASE_AGE_HOURS, major, timeoutMs = DEFAULT_TIMEOUT_MS } = options;\n\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(`${REGISTRY_URL}/${encodeURIComponent(packageName)}`, {\n signal: controller.signal,\n });\n if (!response.ok) {\n return fallback;\n }\n\n const data = (await response.json()) as { time?: Record<string, string> };\n const time = data.time;\n if (!time) {\n return fallback;\n }\n\n const cutoff = Date.now() - minAgeHours * 60 * 60 * 1000;\n const eligible = Object.entries(time)\n .filter(([version]) => RELEASE_VERSION_PATTERN.test(version))\n .filter(([version]) => major === undefined || Number(version.split(\".\")[0]) === major)\n .filter(([, publishedAt]) => new Date(publishedAt).getTime() <= cutoff)\n .map(([version]) => version)\n .sort(compareVersions);\n\n return eligible.at(-1) ? `^${eligible.at(-1)}` : fallback;\n } catch {\n return fallback;\n } finally {\n clearTimeout(timeout);\n }\n};\n\n/**\n * Resolves several packages' trusted versions in parallel. Keys of `specs`\n * are the package name to query the registry for; values are the fallback\n * to use if that lookup fails. Returns a map from the *same* keys to the\n * resolved (or fallback) version — typed so callers don't get `| undefined`\n * back for a key they just passed in.\n */\nexport const fetchTrustedVersions = async <\n T extends Record<string, { fallback: string; major?: number }>,\n>(\n specs: T,\n): Promise<{ [K in keyof T]: string }> => {\n const entries = await Promise.all(\n Object.entries(specs).map(async ([packageName, { fallback, major }]) => {\n const version = await fetchTrustedVersion(packageName, fallback, { major });\n return [packageName, version] as const;\n }),\n );\n return Object.fromEntries(entries) as { [K in keyof T]: string };\n};\n","import { normalize, strings } from \"@angular-devkit/core\";\nimport {\n type Rule,\n SchematicsException,\n type Source,\n apply,\n mergeWith,\n move,\n renameTemplateFiles,\n template,\n url,\n} from \"@angular-devkit/schematics\";\n\nimport { toCamelCase } from \"@utils/formatting\";\nimport { fetchTrustedVersions } from \"@utils/registry\";\n\nimport { DEFAULT_EDITOR_VERSION } from \"~/defaults\";\n\nimport { type PluginOptions } from \"./plugin.options\";\nimport { type PluginSchema } from \"./plugin.schema\";\n\nconst PLUGIN_NAME_PATTERN = /^@([a-z0-9][a-z0-9-]*)\\/([a-z0-9][a-z0-9-]*)$/;\n\nconst transform = async (schema: PluginSchema): Promise<PluginOptions> => {\n const theme = schema.template === \"theme\";\n const name = schema.name.trim();\n const short = PLUGIN_NAME_PATTERN.exec(name)?.[2];\n if (!short) {\n throw new SchematicsException(\n `\"${name}\" is not a plugin name. Use @scope/name, in lowercase: @acme/counter.`,\n );\n }\n\n // A theme has no code: it needs no editor package.\n const versions = theme\n ? {\n \"@nanoforge-dev/editor-sdk\": DEFAULT_EDITOR_VERSION,\n \"@nanoforge-dev/editor-vite-plugin\": DEFAULT_EDITOR_VERSION,\n }\n : await fetchTrustedVersions({\n \"@nanoforge-dev/editor-sdk\": { fallback: DEFAULT_EDITOR_VERSION, major: 1 },\n \"@nanoforge-dev/editor-vite-plugin\": { fallback: DEFAULT_EDITOR_VERSION, major: 1 },\n });\n\n return {\n name,\n short,\n title: short\n .split(\"-\")\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \"),\n id: toCamelCase(short),\n editorSdkVersion: versions[\"@nanoforge-dev/editor-sdk\"],\n editorVitePluginVersion: versions[\"@nanoforge-dev/editor-vite-plugin\"],\n };\n};\n\nconst generate = (options: PluginOptions, path: string, theme: boolean): Source => {\n return apply(url(theme ? \"./theme-files\" : \"./files\"), [\n template({\n dot: \".\",\n ...strings,\n ...options,\n }),\n renameTemplateFiles(),\n move(normalize(path)),\n ]);\n};\n\nexport const main = (schema: PluginSchema): Rule => {\n return async () => {\n const options = await transform(schema);\n\n return mergeWith(\n generate(options, schema.directory ?? options.short, schema.template === \"theme\"),\n );\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "SchematicsNanoForgePlugin",
4
+ "title": "NanoForge Editor Plugin Options Schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "name": {
8
+ "type": "string",
9
+ "description": "Plugin name in the registry, as @scope/name (lowercase letters, digits and dashes)",
10
+ "pattern": "^@[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$"
11
+ },
12
+ "directory": {
13
+ "type": ["string", "null"],
14
+ "description": "Plugin destination directory. Defaults to the plugin's short name (the part after the scope).",
15
+ "default": null
16
+ },
17
+ "template": {
18
+ "type": "string",
19
+ "enum": ["panel", "theme"],
20
+ "description": "What to start from: a panel with a command and a setting (panel), or a color theme with no code (theme).",
21
+ "default": "panel"
22
+ }
23
+ },
24
+ "required": ["name"]
25
+ }
@@ -0,0 +1,21 @@
1
+ # <%= title %>
2
+
3
+ A NanoForge editor theme (`<%= name %>`), with no code: everything is in `nanoforge.manifest.json`.
4
+
5
+ - `contributes.themes`: the theme. It `extends` the editor's dark theme and gives only the colors it changes; give `kind` and every color to start from nothing. `syntax` colors the code, `tokens` changes fonts and corners.
6
+ - `contributes.icons`: icons as SVG files of the plugin, by name. `contributes.iconThemes` replaces the editor's icons and gives file and folder icons.
7
+
8
+ The editor checks the theme's contrast when it is applied: text that falls under 4.5:1 is named in its Console.
9
+
10
+ ## Run it in the editor
11
+
12
+ ```sh
13
+ pnpm dev # copies the manifest and icons to dist/ on every change
14
+ DEV_PLUGINS=$(pwd) nf editor <a project> # in another terminal: the editor reloads the theme
15
+ ```
16
+
17
+ Choose it in _Settings › Editor › Appearance_.
18
+
19
+ ## Publish it
20
+
21
+ `keywords: ["theme"]` lists it under _Get more themes_ in the editor's marketplace. The plugin authoring guide has the list of colors and tokens.