@cosmicdrift/kumiko-feature-ast 0.394.0 → 0.395.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-feature-ast",
3
- "version": "0.394.0",
3
+ "version": "0.395.0",
4
4
  "description": "Feature-file AST for Kumiko: parse, patch and render feature definitions, the pattern library, and the codemods kumiko-upgrade runs.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -22,8 +22,8 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@cosmicdrift/kumiko-framework": "0.394.0",
26
- "@cosmicdrift/kumiko-types": "0.394.0",
25
+ "@cosmicdrift/kumiko-framework": "0.395.0",
26
+ "@cosmicdrift/kumiko-types": "0.395.0",
27
27
  "ts-morph": "^28.0.0",
28
28
  "zod": "^4.6.7"
29
29
  },
@@ -236,3 +236,14 @@ type's moved members are nested (`{ dispatch: { query: ... } }`) and the body is
236
236
  when a handler or job context is passed to that parameter somewhere in the project. A structural
237
237
  type declared in another file is reported as `manual`, and structural parameters with other names are
238
238
  invisible to it and show up as type errors afterwards.
239
+
240
+ ## content-collection-format.ts
241
+
242
+ Content collections know two editor formats, `plain` and `document`. The codemod rewrites
243
+ `contentFormat: "rich"` and `contentFormat: "markdown"` to `"document"` in the collection declarations of
244
+ `createTemplateResolverFeature({ collections: [...] })` and `r.contentCollection({...})`. Record storage
245
+ literals such as `contentFormat: "markdown"` in write payloads or seeds use a different vocabulary and stay.
246
+
247
+ ```bash
248
+ bun node_modules/@cosmicdrift/kumiko-feature-ast/src/scripts/codemod/content-collection-format.ts <dir> [--check | --dry-run | --write] [--no-format]
249
+ ```
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env bun
2
+ // content-collection-format — the editor format of a content collection is "plain" or "document":
3
+ // createTemplateResolverFeature({ collections: [{ contentFormat: "rich" | "markdown" }] })
4
+ // r.contentCollection({ contentFormat: "rich" | "markdown" }) -> contentFormat: "document"
5
+ // Only collection declarations are touched. Record storage literals (`contentFormat: "markdown"`
6
+ // in write payloads, seeds, upserts) use a different vocabulary and stay unchanged.
7
+ //
8
+ // Usage:
9
+ // bun scripts/codemod/content-collection-format.ts <targetDir...> [--check | --dry-run | --write] [--no-format]
10
+
11
+ import { Node, type ObjectLiteralExpression, Project, SyntaxKind } from "ts-morph";
12
+ import {
13
+ applyEdits,
14
+ type CodemodResult,
15
+ runCodemodCli,
16
+ type SiteLists,
17
+ type TextEdit,
18
+ } from "./codemod-support.ts";
19
+
20
+ const CHEAP_PREFILTER = /contentCollection|createTemplateResolverFeature/;
21
+ const LEGACY_EDITOR_FORMATS: ReadonlySet<string> = new Set(["rich", "markdown"]);
22
+
23
+ function firstObjectArgument(call: Node): ObjectLiteralExpression | undefined {
24
+ if (!Node.isCallExpression(call)) return undefined;
25
+ const first = call.getArguments()[0];
26
+ return first !== undefined && Node.isObjectLiteralExpression(first) ? first : undefined;
27
+ }
28
+
29
+ function collectionDeclarations(call: Node): ObjectLiteralExpression[] {
30
+ const callee = call.asKind(SyntaxKind.CallExpression)?.getExpression().getText();
31
+ if (callee === undefined) return [];
32
+ if (callee === "createTemplateResolverFeature") {
33
+ const collections = firstObjectArgument(call)?.getProperty("collections");
34
+ const array = collections?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer();
35
+ if (array === undefined || !Node.isArrayLiteralExpression(array)) return [];
36
+ return array.getElements().filter(Node.isObjectLiteralExpression);
37
+ }
38
+ if (callee.endsWith(".contentCollection")) {
39
+ const declaration = firstObjectArgument(call);
40
+ return declaration === undefined ? [] : [declaration];
41
+ }
42
+ return [];
43
+ }
44
+
45
+ export function transformContentCollectionFormatSource(
46
+ source: string,
47
+ filePath = "source.ts",
48
+ ): CodemodResult {
49
+ const result: SiteLists = { rewrites: [], manual: [] };
50
+ if (!CHEAP_PREFILTER.test(source)) return { output: source, ...result };
51
+ const project = new Project({
52
+ useInMemoryFileSystem: true,
53
+ skipAddingFilesFromTsConfig: true,
54
+ skipFileDependencyResolution: true,
55
+ });
56
+ const sourceFile = project.createSourceFile(filePath, source, { overwrite: true });
57
+ const edits: TextEdit[] = [];
58
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
59
+ for (const declaration of collectionDeclarations(call)) {
60
+ const property = declaration
61
+ .getProperty("contentFormat")
62
+ ?.asKind(SyntaxKind.PropertyAssignment);
63
+ const value = property?.getInitializer();
64
+ if (value === undefined || !Node.isStringLiteral(value)) continue;
65
+ const current = value.getLiteralValue();
66
+ if (!LEGACY_EDITOR_FORMATS.has(current)) continue;
67
+ result.rewrites.push({
68
+ file: filePath,
69
+ line: value.getStartLineNumber(),
70
+ description: `contentFormat "${current}" -> "document"`,
71
+ });
72
+ edits.push({ start: value.getStart() + 1, end: value.getEnd() - 1, text: "document" });
73
+ }
74
+ }
75
+ return { output: applyEdits(source, edits), ...result };
76
+ }
77
+
78
+ if (import.meta.main) {
79
+ await runCodemodCli(
80
+ {
81
+ name: "content-collection-format",
82
+ transform: transformContentCollectionFormatSource,
83
+ isSkippedPath: () => false,
84
+ },
85
+ process.argv.slice(2),
86
+ );
87
+ }