@jskit-ai/ui-generator 0.1.5 → 0.1.6

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.
@@ -0,0 +1,219 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+ import { runGeneratorSubcommand } from "../src/server/subcommands/outlet.js";
7
+
8
+ async function withTempApp(run) {
9
+ const appRoot = await mkdtemp(path.join(tmpdir(), "ui-generator-outlet-"));
10
+ try {
11
+ return await run(appRoot);
12
+ } finally {
13
+ await rm(appRoot, { recursive: true, force: true });
14
+ }
15
+ }
16
+
17
+ test("ui-generator outlet injects ShellOutlet and RouterView into existing page", async () => {
18
+ await withTempApp(async (appRoot) => {
19
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
20
+ const targetPath = path.join(appRoot, targetFile);
21
+
22
+ await mkdir(path.dirname(targetPath), { recursive: true });
23
+ await writeFile(
24
+ targetPath,
25
+ `<script setup>
26
+ import { computed } from "vue";
27
+ </script>
28
+
29
+ <template>
30
+ <section>
31
+ <h1>Contact</h1>
32
+ </section>
33
+ </template>
34
+ `,
35
+ "utf8"
36
+ );
37
+
38
+ const result = await runGeneratorSubcommand({
39
+ appRoot,
40
+ subcommand: "outlet",
41
+ options: {
42
+ file: targetFile,
43
+ host: "contact-view"
44
+ }
45
+ });
46
+
47
+ assert.deepEqual(result.touchedFiles, [targetFile]);
48
+
49
+ const output = await readFile(targetPath, "utf8");
50
+ assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
51
+ assert.match(output, /import \{ RouterView \} from "vue-router";/);
52
+ assert.match(output, /<ShellOutlet host="contact-view" position="sub-pages" \/>/);
53
+ assert.equal((output.match(/<RouterView \/>/g) || []).length, 1);
54
+ assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
55
+
56
+ const rerun = await runGeneratorSubcommand({
57
+ appRoot,
58
+ subcommand: "outlet",
59
+ options: {
60
+ file: targetFile,
61
+ host: "contact-view"
62
+ }
63
+ });
64
+
65
+ assert.deepEqual(rerun.touchedFiles, []);
66
+ });
67
+ });
68
+
69
+ test("ui-generator outlet does not add second RouterView when one already exists", async () => {
70
+ await withTempApp(async (appRoot) => {
71
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
72
+ const targetPath = path.join(appRoot, targetFile);
73
+
74
+ await mkdir(path.dirname(targetPath), { recursive: true });
75
+ await writeFile(
76
+ targetPath,
77
+ `<script setup>
78
+ import { RouterView } from "vue-router";
79
+ </script>
80
+
81
+ <template>
82
+ <section>
83
+ <RouterView />
84
+ </section>
85
+ </template>
86
+ `,
87
+ "utf8"
88
+ );
89
+
90
+ await runGeneratorSubcommand({
91
+ appRoot,
92
+ subcommand: "outlet",
93
+ options: {
94
+ file: targetFile,
95
+ host: "contact-view"
96
+ }
97
+ });
98
+
99
+ const output = await readFile(targetPath, "utf8");
100
+ assert.match(output, /<ShellOutlet host="contact-view" position="sub-pages" \/>/);
101
+ assert.equal((output.match(/<RouterView \/>/g) || []).length, 1);
102
+ assert.equal((output.match(/import \{ RouterView \} from "vue-router";/g) || []).length, 1);
103
+ });
104
+ });
105
+
106
+ test("ui-generator outlet-only mode injects only ShellOutlet and creates script setup when missing", async () => {
107
+ await withTempApp(async (appRoot) => {
108
+ const targetFile = "src/components/ContactDetailsPanel.vue";
109
+ const targetPath = path.join(appRoot, targetFile);
110
+
111
+ await mkdir(path.dirname(targetPath), { recursive: true });
112
+ await writeFile(
113
+ targetPath,
114
+ `<template>
115
+ <div>Details</div>
116
+ </template>
117
+ `,
118
+ "utf8"
119
+ );
120
+
121
+ await runGeneratorSubcommand({
122
+ appRoot,
123
+ subcommand: "outlet",
124
+ options: {
125
+ file: targetFile,
126
+ host: "contact-view",
127
+ position: "sub-pages",
128
+ mode: "outlet-only"
129
+ }
130
+ });
131
+
132
+ const output = await readFile(targetPath, "utf8");
133
+ assert.match(output, /<script setup>/);
134
+ assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
135
+ assert.doesNotMatch(output, /import \{ RouterView \} from "vue-router";/);
136
+ assert.doesNotMatch(output, /<RouterView \/>/);
137
+ });
138
+ });
139
+
140
+ test("ui-generator outlet inserts generated script after existing route block", async () => {
141
+ await withTempApp(async (appRoot) => {
142
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
143
+ const targetPath = path.join(appRoot, targetFile);
144
+
145
+ await mkdir(path.dirname(targetPath), { recursive: true });
146
+ await writeFile(
147
+ targetPath,
148
+ `<route lang="json">
149
+ {
150
+ "meta": { "jskit": { "surface": "admin" } }
151
+ }
152
+ </route>
153
+
154
+ <template>
155
+ <section />
156
+ </template>
157
+ `,
158
+ "utf8"
159
+ );
160
+
161
+ await runGeneratorSubcommand({
162
+ appRoot,
163
+ subcommand: "outlet",
164
+ options: {
165
+ file: targetFile,
166
+ host: "contact-view",
167
+ mode: "outlet-only"
168
+ }
169
+ });
170
+
171
+ const output = await readFile(targetPath, "utf8");
172
+ const routeIndex = output.indexOf("<route");
173
+ const scriptIndex = output.indexOf("<script setup>");
174
+ const templateIndex = output.indexOf("<template>");
175
+ assert.ok(routeIndex >= 0);
176
+ assert.ok(scriptIndex > routeIndex);
177
+ assert.ok(templateIndex > scriptIndex);
178
+ });
179
+ });
180
+
181
+ test("ui-generator outlet keeps indentation when injected into nested template block", async () => {
182
+ await withTempApp(async (appRoot) => {
183
+ const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
184
+ const targetPath = path.join(appRoot, targetFile);
185
+
186
+ await mkdir(path.dirname(targetPath), { recursive: true });
187
+ await writeFile(
188
+ targetPath,
189
+ `<template>
190
+ <section>
191
+ <template v-else-if="view.isLoading">
192
+ <v-skeleton-loader type="heading, text@2, article" />
193
+ </template>
194
+
195
+ <template v-else>
196
+ <v-progress-linear v-if="view.isRefetching" indeterminate class="mb-4" />
197
+ </template>
198
+ </section>
199
+ </template>
200
+ `,
201
+ "utf8"
202
+ );
203
+
204
+ await runGeneratorSubcommand({
205
+ appRoot,
206
+ subcommand: "outlet",
207
+ options: {
208
+ file: targetFile,
209
+ host: "contact-view",
210
+ mode: "routed"
211
+ }
212
+ });
213
+
214
+ const output = await readFile(targetPath, "utf8");
215
+ assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet host="contact-view" position="sub-pages" \/>\n\s{2}<RouterView \/>\n<\/template>/);
216
+ assert.match(output, /<template v-else-if="view\.isLoading">\n\s*<v-skeleton-loader type="heading, text@2, article" \/>\n\s*<\/template>/);
217
+ assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
218
+ });
219
+ });