zitejs 0.9.120 → 0.9.121

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 (41) hide show
  1. package/dist/cjs/auth/config.d.ts +3 -0
  2. package/dist/cjs/auth/config.js +7 -0
  3. package/dist/cjs/auth/index.js +30 -4
  4. package/dist/cjs/auth/useAuth.test.js +5 -19
  5. package/dist/cjs/backend/index.d.ts +30 -3
  6. package/dist/cjs/backend/index.js +2 -2
  7. package/dist/cjs/bundle/index.d.ts +1 -1
  8. package/dist/cjs/bundle/index.js +6 -2
  9. package/dist/cjs/check/index.js +5 -13
  10. package/dist/cjs/cli.js +2 -2
  11. package/dist/cjs/dev/index.js +22 -29
  12. package/dist/cjs/sourceRoots.d.ts +19 -0
  13. package/dist/cjs/sourceRoots.js +33 -0
  14. package/dist/cjs/upload/index.js +19 -0
  15. package/dist/cjs/upload/index.test.js +30 -3
  16. package/dist/cjs/vite/domTagNames.d.ts +1 -0
  17. package/dist/cjs/vite/domTagNames.js +257 -0
  18. package/dist/cjs/vite/index.js +20 -11
  19. package/dist/cjs/vite/index.test.d.ts +1 -0
  20. package/dist/cjs/vite/index.test.js +53 -0
  21. package/dist/esm/auth/config.d.ts +3 -0
  22. package/dist/esm/auth/config.js +6 -0
  23. package/dist/esm/auth/index.js +28 -2
  24. package/dist/esm/auth/useAuth.test.js +5 -19
  25. package/dist/esm/backend/index.d.ts +30 -3
  26. package/dist/esm/backend/index.js +2 -2
  27. package/dist/esm/bundle/index.d.ts +1 -1
  28. package/dist/esm/bundle/index.js +6 -2
  29. package/dist/esm/check/index.js +6 -14
  30. package/dist/esm/cli.js +2 -2
  31. package/dist/esm/dev/index.js +22 -29
  32. package/dist/esm/sourceRoots.d.ts +19 -0
  33. package/dist/esm/sourceRoots.js +28 -0
  34. package/dist/esm/upload/index.js +19 -0
  35. package/dist/esm/upload/index.test.js +30 -3
  36. package/dist/esm/vite/domTagNames.d.ts +1 -0
  37. package/dist/esm/vite/domTagNames.js +254 -0
  38. package/dist/esm/vite/index.js +20 -11
  39. package/dist/esm/vite/index.test.d.ts +1 -0
  40. package/dist/esm/vite/index.test.js +48 -0
  41. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { watch } from "fs";
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
3
3
  import { join } from "path";
4
+ import { listSourceDirs } from "../sourceRoots.js";
4
5
  import { runSync } from "../sync/index.js";
5
6
  import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, } from "../sync/lib.js";
6
7
  const debounceTimers = new Map();
@@ -10,17 +11,9 @@ function debounce(key, fn, ms) {
10
11
  clearTimeout(existing);
11
12
  debounceTimers.set(key, setTimeout(fn, ms));
12
13
  }
13
- function findAppDirs() {
14
- const appsDir = "apps";
15
- if (!existsSync(appsDir))
16
- return [];
17
- return readdirSync(appsDir, { withFileTypes: true })
18
- .filter((d) => d.isDirectory())
19
- .map((d) => d.name);
20
- }
21
14
  function getFlowId(appDir) {
22
15
  try {
23
- const configPath = join("apps", appDir, "zite.config.json");
16
+ const configPath = join(appDir.path, "zite.config.json");
24
17
  if (existsSync(configPath)) {
25
18
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
26
19
  return config.id;
@@ -40,11 +33,11 @@ const readJsonFile = (path) => {
40
33
  }
41
34
  };
42
35
  function getEmailIntegrationId(appDir) {
43
- return findEmailIntegrationId(readJsonFile(join("apps", appDir, "zite.config.json")), readJsonFile("zite.config.json"));
36
+ return findEmailIntegrationId(readJsonFile(join(appDir.path, "zite.config.json")), readJsonFile("zite.config.json"));
44
37
  }
45
38
  function getDeclaredEnvVarNames(appDir) {
46
39
  try {
47
- const configPath = join("apps", appDir, "zite.config.json");
40
+ const configPath = join(appDir.path, "zite.config.json");
48
41
  if (!existsSync(configPath))
49
42
  return [];
50
43
  const config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -55,7 +48,7 @@ function getDeclaredEnvVarNames(appDir) {
55
48
  }
56
49
  }
57
50
  function regenerateAppTypedWrappers(appDir) {
58
- const outDir = join("apps", appDir, ".zite");
51
+ const outDir = join(appDir.path, ".zite");
59
52
  mkdirSync(outDir, { recursive: true });
60
53
  // user.ts and auth.ts no longer generated — User type is fixed in zitejs/auth
61
54
  // Email integration: generate the Email client at .zite/integrations/email.ts.
@@ -70,7 +63,7 @@ function regenerateAppTypedWrappers(appDir) {
70
63
  writeFileSync(join(outDir, "backend.ts"), generateBackendWrapperTs(getDeclaredEnvVarNames(appDir)));
71
64
  }
72
65
  function regenerateAppApiTs(appDir) {
73
- const apiDir = join("apps", appDir, "src", "api");
66
+ const apiDir = join(appDir.path, "src", "api");
74
67
  if (!existsSync(apiDir))
75
68
  return;
76
69
  // Sorted because `readdir` order is unspecified — it is whatever the
@@ -90,14 +83,14 @@ function regenerateAppApiTs(appDir) {
90
83
  }));
91
84
  const content = generateApiTs(endpointFiles);
92
85
  if (content) {
93
- const outDir = join("apps", appDir, ".zite");
86
+ const outDir = join(appDir.path, ".zite");
94
87
  mkdirSync(outDir, { recursive: true });
95
88
  writeFileSync(join(outDir, "api.ts"), content);
96
- console.log(`Updated apps/${appDir}/.zite/api.ts`);
89
+ console.log(`Updated ${appDir.path}/.zite/api.ts`);
97
90
  }
98
91
  }
99
92
  function regenerateAppAirtableSdk(appDir) {
100
- const lockPath = join("apps", appDir, "zite.lock");
93
+ const lockPath = join(appDir.path, "zite.lock");
101
94
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${existsSync(lockPath)}`);
102
95
  if (!existsSync(lockPath))
103
96
  return;
@@ -118,15 +111,15 @@ function regenerateAppAirtableSdk(appDir) {
118
111
  };
119
112
  const content = generateAirtableTs(airtableLock);
120
113
  if (content) {
121
- const outDir = join("apps", appDir, ".zite", "integrations");
114
+ const outDir = join(appDir.path, ".zite", "integrations");
122
115
  mkdirSync(outDir, { recursive: true });
123
116
  writeFileSync(join(outDir, "airtable.ts"), content);
124
- console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
117
+ console.log(`Updated ${appDir.path}/.zite/integrations/airtable.ts`);
125
118
  }
126
119
  }
127
120
  }
128
121
  catch (err) {
129
- console.error(`[airtable-sdk] Error processing lock for ${appDir}:`, err);
122
+ console.error(`[airtable-sdk] Error processing lock for ${appDir.path}:`, err);
130
123
  }
131
124
  }
132
125
  export async function runGenerate() {
@@ -150,8 +143,8 @@ export async function runGenerate() {
150
143
  catch (err) {
151
144
  console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
152
145
  }
153
- // 2. Find all apps and regenerate their .zite/ files
154
- const appDirs = findAppDirs();
146
+ // 2. Find every app and automation and regenerate their .zite/ files
147
+ const appDirs = listSourceDirs();
155
148
  for (const app of appDirs) {
156
149
  regenerateAppApiTs(app);
157
150
  regenerateAppTypedWrappers(app);
@@ -171,33 +164,33 @@ export async function runDev() {
171
164
  console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
172
165
  }
173
166
  console.log("");
174
- // 2. Find all apps and regenerate their .zite/ files
175
- const appDirs = findAppDirs();
167
+ // 2. Find every app and automation and regenerate their .zite/ files
168
+ const appDirs = listSourceDirs();
176
169
  for (const app of appDirs) {
177
170
  regenerateAppApiTs(app);
178
171
  regenerateAppTypedWrappers(app);
179
172
  }
180
- // 3. Watch each app's src/api/ for endpoint changes
173
+ // 3. Watch each dir's src/api/ for endpoint changes
181
174
  let watchingAny = false;
182
175
  for (const app of appDirs) {
183
- const apiDir = join("apps", app, "src", "api");
176
+ const apiDir = join(app.path, "src", "api");
184
177
  if (!existsSync(apiDir))
185
178
  continue;
186
179
  watchingAny = true;
187
- console.log(`Watching apps/${app}/src/api/ for endpoint changes...`);
180
+ console.log(`Watching ${app.path}/src/api/ for endpoint changes...`);
188
181
  watch(apiDir, { recursive: true }, (_event, filename) => {
189
182
  if (!filename)
190
183
  return;
191
184
  if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
192
185
  return;
193
- debounce(app, () => {
194
- console.log(`Endpoint changed in ${app}: ${filename}`);
186
+ debounce(app.path, () => {
187
+ console.log(`Endpoint changed in ${app.path}: ${filename}`);
195
188
  regenerateAppApiTs(app);
196
189
  }, 200);
197
190
  });
198
191
  }
199
192
  if (!watchingAny) {
200
- console.log("No apps with src/api/ found.");
193
+ console.log("No apps or automations with src/api/ found.");
201
194
  }
202
195
  // 4. Watch zite.schema.json for schema drift
203
196
  if (existsSync("zite.schema.json")) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The repo-root directories a project's own source lives under: `apps/` for
3
+ * apps and `automations/` for automations (endpoints with no frontend). The
4
+ * backend keeps the same list; a dir name is unique across both roots, so a
5
+ * bare name still identifies one dir.
6
+ */
7
+ export declare const SOURCE_ROOTS: readonly ["apps", "automations"];
8
+ export type SourceRoot = (typeof SOURCE_ROOTS)[number];
9
+ export interface SourceDir {
10
+ root: SourceRoot;
11
+ /** The bare directory name. */
12
+ dir: string;
13
+ /** `<root>/<dir>`, relative to the repo root. */
14
+ path: string;
15
+ }
16
+ /** Every source dir under every root, in root order then name order. */
17
+ export declare function listSourceDirs(cwd?: string): SourceDir[];
18
+ /** The source dir with this bare name, under whichever root holds it. */
19
+ export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
@@ -0,0 +1,28 @@
1
+ import { existsSync, readdirSync } from "fs";
2
+ import { join } from "path";
3
+ /**
4
+ * The repo-root directories a project's own source lives under: `apps/` for
5
+ * apps and `automations/` for automations (endpoints with no frontend). The
6
+ * backend keeps the same list; a dir name is unique across both roots, so a
7
+ * bare name still identifies one dir.
8
+ */
9
+ export const SOURCE_ROOTS = ["apps", "automations"];
10
+ /** Every source dir under every root, in root order then name order. */
11
+ export function listSourceDirs(cwd = ".") {
12
+ const dirs = [];
13
+ for (const root of SOURCE_ROOTS) {
14
+ const rootPath = join(cwd, root);
15
+ if (!existsSync(rootPath))
16
+ continue;
17
+ for (const entry of readdirSync(rootPath, { withFileTypes: true })) {
18
+ if (!entry.isDirectory())
19
+ continue;
20
+ dirs.push({ root, dir: entry.name, path: join(root, entry.name) });
21
+ }
22
+ }
23
+ return dirs;
24
+ }
25
+ /** The source dir with this bare name, under whichever root holds it. */
26
+ export function findSourceDir(name, cwd = ".") {
27
+ return listSourceDirs(cwd).find((d) => d.dir === name);
28
+ }
@@ -106,6 +106,25 @@ async function performUpload(data, filename) {
106
106
  if (!putRes.ok) {
107
107
  throw new FileUploadError('Upload failed');
108
108
  }
109
+ // Gateways that predate the storage ledger omit the token. The file is
110
+ // already at fileUrl, so those uploads still succeed.
111
+ if (session.completionToken) {
112
+ const completeRes = await fetch(getApiUrl() +
113
+ '/v1/zite/public/' +
114
+ flowId +
115
+ '/complete-upload?mode=' +
116
+ mode, {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ ...authHeaders(),
121
+ },
122
+ body: JSON.stringify({ completionToken: session.completionToken }),
123
+ });
124
+ if (!completeRes.ok) {
125
+ throw new FileUploadError(await readErrorMessage(completeRes, 'Upload failed'));
126
+ }
127
+ }
109
128
  return session.fileUrl;
110
129
  }
111
130
  export async function uploadFile({ data, filename, }) {
@@ -23,7 +23,7 @@ describe('toUploadBlob', () => {
23
23
  });
24
24
  });
25
25
  describe('uploadFile', () => {
26
- it('requests a session then PUTs the bytes to the presigned URL', async () => {
26
+ it('requests a session, PUTs the bytes, then completes the upload', async () => {
27
27
  const file = new File(['hello-world'], 'hello.txt', { type: 'text/plain' });
28
28
  fetchMock
29
29
  .mockResolvedValueOnce({
@@ -33,12 +33,14 @@ describe('uploadFile', () => {
33
33
  presignedUrl: 'https://s3.example.com/put',
34
34
  fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
35
35
  contentType: 'text/plain',
36
+ completionToken: 'signed-token',
36
37
  }),
37
38
  })
38
- .mockResolvedValueOnce({ ok: true });
39
+ .mockResolvedValueOnce({ ok: true })
40
+ .mockResolvedValueOnce({ ok: true, json: async () => ({ success: true }) });
39
41
  const result = await uploadFile({ data: file, filename: 'hello.txt' });
40
42
  expect(result.fileUrl).toContain('hello.txt');
41
- expect(fetchMock).toHaveBeenCalledTimes(2);
43
+ expect(fetchMock).toHaveBeenCalledTimes(3);
42
44
  expect(fetchMock.mock.calls[0][0]).toBe('https://api.example.com/v1/zite/public/app_123/upload-session?mode=live');
43
45
  const sessionInit = fetchMock.mock.calls[0][1];
44
46
  expect(JSON.parse(sessionInit.body)).toEqual({
@@ -48,6 +50,31 @@ describe('uploadFile', () => {
48
50
  });
49
51
  expect(fetchMock.mock.calls[1][0]).toBe('https://s3.example.com/put');
50
52
  expect(fetchMock.mock.calls[1][1].method).toBe('PUT');
53
+ expect(fetchMock.mock.calls[2][0]).toBe('https://api.example.com/v1/zite/public/app_123/complete-upload?mode=live');
54
+ const completeInit = fetchMock.mock.calls[2][1];
55
+ expect(completeInit.method).toBe('POST');
56
+ expect(JSON.parse(completeInit.body)).toEqual({
57
+ completionToken: 'signed-token',
58
+ });
59
+ });
60
+ it('skips completion when the gateway does not issue a token', async () => {
61
+ fetchMock
62
+ .mockResolvedValueOnce({
63
+ ok: true,
64
+ json: async () => ({
65
+ success: true,
66
+ presignedUrl: 'https://s3.example.com/put',
67
+ fileUrl: 'https://uploads.zite.com/orgid-1/zite-uploads/app_123/hello.txt',
68
+ contentType: 'text/plain',
69
+ }),
70
+ })
71
+ .mockResolvedValueOnce({ ok: true });
72
+ const result = await uploadFile({
73
+ data: new File(['hello-world'], 'hello.txt', { type: 'text/plain' }),
74
+ filename: 'hello.txt',
75
+ });
76
+ expect(result.fileUrl).toContain('hello.txt');
77
+ expect(fetchMock).toHaveBeenCalledTimes(2);
51
78
  });
52
79
  it('surfaces the plan-limit message from the session endpoint', async () => {
53
80
  fetchMock.mockResolvedValueOnce({
@@ -0,0 +1 @@
1
+ export declare const domTagNames: ReadonlySet<string>;
@@ -0,0 +1,254 @@
1
+ export const domTagNames = new Set([
2
+ "a",
3
+ "abbr",
4
+ "acronym",
5
+ "address",
6
+ "altGlyph",
7
+ "altGlyphDef",
8
+ "altGlyphItem",
9
+ "animate",
10
+ "animateColor",
11
+ "animateMotion",
12
+ "animateTransform",
13
+ "annotation",
14
+ "annotation-xml",
15
+ "applet",
16
+ "area",
17
+ "article",
18
+ "aside",
19
+ "audio",
20
+ "b",
21
+ "base",
22
+ "basefont",
23
+ "bdi",
24
+ "bdo",
25
+ "bgsound",
26
+ "big",
27
+ "blink",
28
+ "blockquote",
29
+ "body",
30
+ "br",
31
+ "button",
32
+ "canvas",
33
+ "caption",
34
+ "center",
35
+ "circle",
36
+ "cite",
37
+ "clipPath",
38
+ "code",
39
+ "col",
40
+ "colgroup",
41
+ "color-profile",
42
+ "command",
43
+ "content",
44
+ "cursor",
45
+ "data",
46
+ "datalist",
47
+ "dd",
48
+ "defs",
49
+ "del",
50
+ "desc",
51
+ "details",
52
+ "dfn",
53
+ "dialog",
54
+ "dir",
55
+ "discard",
56
+ "div",
57
+ "dl",
58
+ "dt",
59
+ "ellipse",
60
+ "em",
61
+ "embed",
62
+ "feBlend",
63
+ "feColorMatrix",
64
+ "feComponentTransfer",
65
+ "feComposite",
66
+ "feConvolveMatrix",
67
+ "feDiffuseLighting",
68
+ "feDisplacementMap",
69
+ "feDistantLight",
70
+ "feDropShadow",
71
+ "feFlood",
72
+ "feFuncA",
73
+ "feFuncB",
74
+ "feFuncG",
75
+ "feFuncR",
76
+ "feGaussianBlur",
77
+ "feImage",
78
+ "feMerge",
79
+ "feMergeNode",
80
+ "feMorphology",
81
+ "feOffset",
82
+ "fePointLight",
83
+ "feSpecularLighting",
84
+ "feSpotLight",
85
+ "feTile",
86
+ "feTurbulence",
87
+ "fieldset",
88
+ "figcaption",
89
+ "figure",
90
+ "filter",
91
+ "font",
92
+ "font-face",
93
+ "font-face-format",
94
+ "font-face-name",
95
+ "font-face-src",
96
+ "font-face-uri",
97
+ "footer",
98
+ "foreignObject",
99
+ "form",
100
+ "frame",
101
+ "frameset",
102
+ "g",
103
+ "glyph",
104
+ "glyphRef",
105
+ "h1",
106
+ "h2",
107
+ "h3",
108
+ "h4",
109
+ "h5",
110
+ "h6",
111
+ "head",
112
+ "header",
113
+ "hgroup",
114
+ "hkern",
115
+ "hr",
116
+ "html",
117
+ "i",
118
+ "iframe",
119
+ "image",
120
+ "img",
121
+ "input",
122
+ "ins",
123
+ "isindex",
124
+ "kbd",
125
+ "keygen",
126
+ "label",
127
+ "legend",
128
+ "li",
129
+ "line",
130
+ "linearGradient",
131
+ "link",
132
+ "listing",
133
+ "maction",
134
+ "main",
135
+ "map",
136
+ "mark",
137
+ "marker",
138
+ "marquee",
139
+ "mask",
140
+ "math",
141
+ "menu",
142
+ "menuitem",
143
+ "merror",
144
+ "meta",
145
+ "metadata",
146
+ "meter",
147
+ "mfrac",
148
+ "mi",
149
+ "missing-glyph",
150
+ "mmultiscripts",
151
+ "mn",
152
+ "mo",
153
+ "mover",
154
+ "mpadded",
155
+ "mpath",
156
+ "mphantom",
157
+ "mprescripts",
158
+ "mroot",
159
+ "mrow",
160
+ "ms",
161
+ "mspace",
162
+ "msqrt",
163
+ "mstyle",
164
+ "msub",
165
+ "msubsup",
166
+ "msup",
167
+ "mtable",
168
+ "mtd",
169
+ "mtext",
170
+ "mtr",
171
+ "multicol",
172
+ "munder",
173
+ "munderover",
174
+ "nav",
175
+ "nextid",
176
+ "nobr",
177
+ "noembed",
178
+ "noframes",
179
+ "noscript",
180
+ "object",
181
+ "ol",
182
+ "optgroup",
183
+ "option",
184
+ "output",
185
+ "p",
186
+ "param",
187
+ "path",
188
+ "pattern",
189
+ "picture",
190
+ "plaintext",
191
+ "polygon",
192
+ "polyline",
193
+ "pre",
194
+ "progress",
195
+ "q",
196
+ "radialGradient",
197
+ "rb",
198
+ "rbc",
199
+ "rect",
200
+ "rp",
201
+ "rt",
202
+ "rtc",
203
+ "ruby",
204
+ "s",
205
+ "samp",
206
+ "script",
207
+ "search",
208
+ "section",
209
+ "select",
210
+ "semantics",
211
+ "set",
212
+ "shadow",
213
+ "slot",
214
+ "small",
215
+ "source",
216
+ "spacer",
217
+ "span",
218
+ "stop",
219
+ "strike",
220
+ "strong",
221
+ "style",
222
+ "sub",
223
+ "summary",
224
+ "sup",
225
+ "svg",
226
+ "switch",
227
+ "symbol",
228
+ "table",
229
+ "tbody",
230
+ "td",
231
+ "template",
232
+ "text",
233
+ "textPath",
234
+ "textarea",
235
+ "tfoot",
236
+ "th",
237
+ "thead",
238
+ "time",
239
+ "title",
240
+ "tr",
241
+ "track",
242
+ "tref",
243
+ "tspan",
244
+ "tt",
245
+ "u",
246
+ "ul",
247
+ "use",
248
+ "var",
249
+ "video",
250
+ "view",
251
+ "vkern",
252
+ "wbr",
253
+ "xmp",
254
+ ]);
@@ -2,7 +2,9 @@ import { parse } from "@babel/parser";
2
2
  import MagicString from "magic-string";
3
3
  import path from "path";
4
4
  import { walk } from "estree-walker";
5
+ import { domTagNames } from "./domTagNames.js";
5
6
  const validExtensions = new Set([".jsx", ".tsx"]);
7
+ const fragmentNames = new Set(["Fragment", "React.Fragment"]);
6
8
  function getComponentName(elementName) {
7
9
  if (elementName.type === "JSXIdentifier") {
8
10
  return elementName.name;
@@ -14,8 +16,17 @@ function getComponentName(elementName) {
14
16
  }
15
17
  return null;
16
18
  }
17
- function isNativeElement(name) {
18
- return /^[a-z]/.test(name) && !name.includes(".");
19
+ function classifyElement(name) {
20
+ if (fragmentNames.has(name)) {
21
+ return null;
22
+ }
23
+ if (!/^[a-z]/.test(name) || name.includes(".")) {
24
+ return "component";
25
+ }
26
+ if (domTagNames.has(name) || name.includes("-")) {
27
+ return "dom";
28
+ }
29
+ return null;
19
30
  }
20
31
  export function ziteId() {
21
32
  const cwd = process.cwd();
@@ -40,21 +51,19 @@ export function ziteId() {
40
51
  if (node.type === "JSXElement") {
41
52
  const openingElement = node.openingElement;
42
53
  const children = node.children;
43
- const elementName = openingElement.name;
44
- const isFragment = (elementName.type === "JSXMemberExpression" &&
45
- elementName.object.name === "React" &&
46
- elementName.property.name === "Fragment") ||
47
- (elementName.type === "JSXIdentifier" &&
48
- elementName.name === "Fragment");
49
- if (isFragment) {
54
+ const componentName = getComponentName(openingElement.name);
55
+ if (!componentName) {
56
+ return;
57
+ }
58
+ const kind = classifyElement(componentName);
59
+ if (!kind) {
50
60
  return;
51
61
  }
52
62
  const line = openingElement.loc?.start?.line ?? 0;
53
63
  const col = openingElement.loc?.start?.column ?? 0;
54
64
  const dataComponentId = `${relativePath}|${line}|${col}`;
55
65
  let attributes = ` data-zite-id="${dataComponentId}"`;
56
- const componentName = getComponentName(openingElement.name);
57
- if (componentName && !isNativeElement(componentName)) {
66
+ if (kind === "component") {
58
67
  attributes += ` data-zite-component="${componentName}"`;
59
68
  }
60
69
  if (children.length === 1 &&
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import path from 'path';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { ziteId } from './index.js';
4
+ async function transform(code) {
5
+ const plugin = ziteId();
6
+ const handler = plugin.transform;
7
+ const result = await handler(code, path.join(process.cwd(), 'src/App.tsx'));
8
+ if (!result) {
9
+ throw new Error('transform returned null');
10
+ }
11
+ return result.code;
12
+ }
13
+ describe('ziteId', () => {
14
+ it('tags html and svg elements without a component name', async () => {
15
+ const code = await transform('const a = <div><svg><path d="M0" /></svg><p>Hello</p></div>;');
16
+ expect(code).toContain('<div data-zite-id="src/App.tsx|1|10">');
17
+ expect(code).toContain('<svg data-zite-id="src/App.tsx|1|15">');
18
+ expect(code).toContain('<path data-zite-id="src/App.tsx|1|20" d="M0" />');
19
+ expect(code).toContain('<p data-zite-id="src/App.tsx|1|41" data-zite-editable="true">');
20
+ expect(code).not.toContain('data-zite-component');
21
+ });
22
+ it('tags components with their name', async () => {
23
+ const code = await transform('const a = <><Card /><motion.div /><_Private /></>;');
24
+ expect(code).toContain('<Card data-zite-id="src/App.tsx|1|12" data-zite-component="Card" />');
25
+ expect(code).toContain('<motion.div data-zite-id="src/App.tsx|1|20" data-zite-component="motion.div" />');
26
+ expect(code).toContain('<_Private data-zite-id="src/App.tsx|1|34" data-zite-component="_Private" />');
27
+ });
28
+ it('tags MathML elements', async () => {
29
+ const code = await transform('const a = <math><mi>x</mi></math>;');
30
+ expect(code).toBe('const a = <math data-zite-id="src/App.tsx|1|10"><mi data-zite-id="src/App.tsx|1|16" data-zite-editable="true">x</mi></math>;');
31
+ });
32
+ it('tags custom elements', async () => {
33
+ const code = await transform('const a = <my-widget />;');
34
+ expect(code).toBe('const a = <my-widget data-zite-id="src/App.tsx|1|10" />;');
35
+ });
36
+ it('leaves non-DOM intrinsic elements untouched', async () => {
37
+ const source = 'const a = <group><mesh position-x={1} /><ambientLight intensity={0.5} /></group>;';
38
+ expect(await transform(source)).toBe(source);
39
+ });
40
+ it('tags DOM elements nested inside non-DOM intrinsics', async () => {
41
+ const code = await transform('const a = <group><Html><div /></Html></group>;');
42
+ expect(code).toBe('const a = <group><Html data-zite-id="src/App.tsx|1|17" data-zite-component="Html"><div data-zite-id="src/App.tsx|1|23" /></Html></group>;');
43
+ });
44
+ it('leaves fragments untouched', async () => {
45
+ const source = 'const a = <Fragment><React.Fragment key="a" /></Fragment>;';
46
+ expect(await transform(source)).toBe(source);
47
+ });
48
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.120",
3
+ "version": "0.9.121",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",