@wix/ditto-codegen-public 1.0.483 → 1.0.484

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.
@@ -1,5 +1,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
- import { readdir } from "node:fs/promises";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
3
4
  import { join } from "node:path";
4
5
 
5
6
  const EDITOR_COMPONENTS_DIR = "src/extensions/site/components";
@@ -15,12 +16,173 @@ async function containsTypeScriptFile(directory: string): Promise<boolean> {
15
16
 
16
17
  for (const entry of entries) {
17
18
  const path = join(directory, entry.name);
18
- if (entry.isDirectory() && (await containsTypeScriptFile(path))) return true;
19
+ if (entry.isDirectory() && (await containsTypeScriptFile(path)))
20
+ return true;
19
21
  if (entry.isFile() && /\.tsx?$/.test(entry.name)) return true;
20
22
  }
21
23
  return false;
22
24
  }
23
25
 
26
+ const SAFE_SPECIFIER = /^[\w@./-]{1,100}$/;
27
+
28
+ const MISSING_SPECIFIER_FIX: Record<string, string> = {
29
+ contextProvider:
30
+ "add `moduleSpecifier: '<prefix><provider-kebab-case-name>'` to resources.contextSpecifier",
31
+ functionLibrary:
32
+ "add `moduleSpecifier: '<prefix><library-name>'` to resources",
33
+ };
34
+
35
+ type TypeScript = typeof import("typescript");
36
+
37
+ // The project's own TypeScript (the one its tsc step uses), else ours.
38
+ function loadTypeScript(cwd: string): TypeScript | undefined {
39
+ for (const base of [cwd, process.cwd()]) {
40
+ try {
41
+ return createRequire(join(base, "package.json"))(
42
+ "typescript",
43
+ ) as TypeScript;
44
+ } catch {
45
+ continue;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ // Reads moduleSpecifier only from the object passed to contextProvider() or
52
+ // functionLibrary(), so comments, strings and other calls never count. A value
53
+ // is undefined unless it is a plain single-line quoted string.
54
+ function readModuleSpecifiers(
55
+ ts: TypeScript,
56
+ file: string,
57
+ source: string,
58
+ ): { kind?: string; values: (string | undefined)[] } {
59
+ const found: { kind?: string; values: (string | undefined)[] } = {
60
+ values: [],
61
+ };
62
+ const visit = (node: import("typescript").Node, inCall: boolean): void => {
63
+ if (ts.isCallExpression(node)) {
64
+ const callee = node.expression;
65
+ const name = ts.isPropertyAccessExpression(callee)
66
+ ? callee.name.text
67
+ : ts.isIdentifier(callee)
68
+ ? callee.text
69
+ : undefined;
70
+ if (name === "contextProvider" || name === "functionLibrary") {
71
+ found.kind ??= name;
72
+ node.arguments.forEach((argument) => visit(argument, true));
73
+ return;
74
+ }
75
+ }
76
+ if (
77
+ inCall &&
78
+ (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node))
79
+ ) {
80
+ const key =
81
+ ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)
82
+ ? node.name.text
83
+ : undefined;
84
+ if (key === "moduleSpecifier") {
85
+ const value =
86
+ ts.isPropertyAssignment(node) &&
87
+ ts.isStringLiteral(node.initializer) &&
88
+ !/['"`\r\n]/.test(node.initializer.text)
89
+ ? node.initializer.text
90
+ : undefined;
91
+ found.values.push(value);
92
+ return;
93
+ }
94
+ }
95
+ ts.forEachChild(node, (child) => visit(child, inCall));
96
+ };
97
+ visit(ts.createSourceFile(file, source, ts.ScriptTarget.Latest), false);
98
+ return found;
99
+ }
100
+
101
+ async function checkModuleSpecifiers(cwd: string): Promise<string | undefined> {
102
+ const prefix = process.env.MODULE_SPECIFIER_PREFIX;
103
+ if (!prefix) return undefined;
104
+
105
+ const gitEnv = { ...process.env, LC_ALL: "C" };
106
+ const listings = await Promise.all([
107
+ Bun.$`git ls-files --others --exclude-standard -- src`
108
+ .cwd(cwd)
109
+ .env(gitEnv)
110
+ .nothrow()
111
+ .quiet(),
112
+ Bun.$`git diff --cached --name-only --diff-filter=A --relative -- src`
113
+ .cwd(cwd)
114
+ .env(gitEnv)
115
+ .nothrow()
116
+ .quiet(),
117
+ ]);
118
+ for (const listing of listings) {
119
+ const stderr = listing.stderr.toString();
120
+ if (listing.exitCode === 0) continue;
121
+ if (
122
+ listing.exitCode === 128 &&
123
+ /^fatal: not a git repository\b/m.test(stderr)
124
+ )
125
+ return undefined;
126
+ return `FAILED at moduleSpecifier prefix check: git exited ${listing.exitCode}:\n${stderr}`;
127
+ }
128
+ const newExtensionFiles = [
129
+ ...new Set(
130
+ listings
131
+ .flatMap((listing) => listing.stdout.toString().split("\n"))
132
+ .map((file) => file.trim())
133
+ .filter((file) => file.endsWith(".extension.ts")),
134
+ ),
135
+ ];
136
+
137
+ const violations: string[] = [];
138
+ const ts = newExtensionFiles.length > 0 ? loadTypeScript(cwd) : undefined;
139
+ if (newExtensionFiles.length > 0 && !ts) {
140
+ violations.push(
141
+ "- cannot load the project's typescript package to read moduleSpecifier; run npm install",
142
+ );
143
+ }
144
+ for (const file of newExtensionFiles) {
145
+ if (!ts) break;
146
+ const source = await readFile(join(cwd, file), "utf8").catch(() => "");
147
+ const { kind, values } = readModuleSpecifiers(ts, file, source);
148
+ if (!kind) continue;
149
+ if (values.length === 0) {
150
+ violations.push(
151
+ `- ${file}: no moduleSpecifier; ${MISSING_SPECIFIER_FIX[kind].replace("<prefix>", prefix)}`,
152
+ );
153
+ continue;
154
+ }
155
+ for (const specifier of values) {
156
+ if (specifier === undefined) {
157
+ violations.push(
158
+ `- ${file}: moduleSpecifier is not a plain string literal; write it as '${prefix}<name>'`,
159
+ );
160
+ } else if (specifier.length > 100) {
161
+ violations.push(
162
+ `- ${file}: moduleSpecifier is over 100 characters; shorten it to '${prefix}<name>'`,
163
+ );
164
+ } else if (!specifier.startsWith(prefix)) {
165
+ violations.push(
166
+ SAFE_SPECIFIER.test(specifier) &&
167
+ prefix.length + specifier.length <= 100
168
+ ? `- ${file}: '${specifier}' -> '${prefix}${specifier}'`
169
+ : `- ${file}: moduleSpecifier must be '${prefix}<name>' (letters, digits, @ . _ / - only, at most 100 characters)`,
170
+ );
171
+ }
172
+ }
173
+ }
174
+ if (violations.length === 0) return undefined;
175
+
176
+ return [
177
+ "FAILED at moduleSpecifier prefix check:",
178
+ ...violations,
179
+ `Every new context provider and function library must declare a moduleSpecifier that starts with '${prefix}'. ` +
180
+ "Module specifiers are unique across all of Wix, so an unprefixed one can belong to " +
181
+ 'another app and fail the checkpoint with "Error during component specifier upsert". ' +
182
+ "Only new extensions are checked: never change the moduleSpecifier of an existing one.",
183
+ ].join("\n");
184
+ }
185
+
24
186
  export default tool({
25
187
  description:
26
188
  "Run project validation: TypeScript check by default, optionally a full build " +
@@ -74,6 +236,9 @@ export default tool({
74
236
  }
75
237
  steps.push("tsc --noEmit -p .: OK");
76
238
 
239
+ const specifierError = await checkModuleSpecifiers(cwd);
240
+ if (specifierError) return [...steps, specifierError].join("\n");
241
+
77
242
  if (!args.runBuild) {
78
243
  steps.push("wix build: skipped (pass runBuild: true to verify build)");
79
244
  return steps.join("\n");
@@ -94,9 +259,7 @@ export default tool({
94
259
  join(cwd, EDITOR_COMPONENTS_DIR),
95
260
  );
96
261
  if (!hasEditorComponents) {
97
- steps.push(
98
- "wix generate manifest: skipped (no editor components)",
99
- );
262
+ steps.push("wix generate manifest: skipped (no editor components)");
100
263
  return steps.join("\n");
101
264
  }
102
265