@pikku/vite 0.0.0-stage → 0.12.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 - present Yasser Fadl and Pikku contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @pikku/vite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Vite plugins a Pikku frontend needs, behind one line.
4
+
5
+ ```ts
6
+ // vite.config.ts
7
+ import { pikkuVite } from '@pikku/vite'
8
+
9
+ export default defineConfig({
10
+ plugins: [tanstackStart(), pikkuVite(), react()],
11
+ })
12
+ ```
13
+
14
+ `pikkuVite()` reads `pikku.config.json` and applies:
15
+
16
+ - `testIds()` — adds `data-testid` anchors to JSX for tests and scenarios. With
17
+ `PIKKUSTUDIO_CHANGES_PANEL=true` it also adds the source anchors Pikku Studio's
18
+ changes panel uses.
19
+ - `crossSiteSession({ envFile })` from `@pikku/better-auth/vite` — in dev only, and only when
20
+ the project has Better Auth (its generated `auth/auth.gen.ts` exists). `envFile` is
21
+ `clientFiles.envFile`.
22
+
23
+ `testIds()` is also exported for apps that want it alone.
@@ -0,0 +1,6 @@
1
+ export { pikkuVite } from './pikku-vite.js';
2
+ export type { PikkuViteOptions } from './pikku-vite.js';
3
+ export { testIds } from './test-ids.js';
4
+ export { default as injectTestIds } from './inject-testids.js';
5
+ export { readPikkuViteProject, findPikkuConfig } from './pikku-config.js';
6
+ export type { PikkuViteProject } from './pikku-config.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { pikkuVite } from './pikku-vite.js';
2
+ export { testIds } from './test-ids.js';
3
+ export { default as injectTestIds } from './inject-testids.js';
4
+ export { readPikkuViteProject, findPikkuConfig } from './pikku-config.js';
@@ -0,0 +1,21 @@
1
+ import type { PluginObj, types as BabelTypes } from '@babel/core';
2
+ export interface InjectTestIdsOptions {
3
+ /**
4
+ * Stamp `data-cloud-src="src/routes/app.orders.tsx:42"` beside every testid.
5
+ *
6
+ * Off unless cloud turns it on for the build, because this writes the app's
7
+ * own source layout into markup an end user can read in view-source. It is
8
+ * also the expensive half of the changes panel: one attribute per addressable
9
+ * element, where the panel's own code is one chunk.
10
+ *
11
+ * The path is interned per module — one string constant the elements in that
12
+ * file concatenate their line onto — so a file with forty controls ships its
13
+ * path once rather than forty times.
14
+ */
15
+ sourceAnchors?: boolean;
16
+ /** Repo root the stamped paths are relative to. Defaults to the build's cwd. */
17
+ root?: string;
18
+ }
19
+ export default function injectTestIds({ types: t }: {
20
+ types: typeof BabelTypes;
21
+ }, options?: InjectTestIdsOptions): PluginObj;
@@ -0,0 +1,302 @@
1
+ import path from 'node:path';
2
+ /**
3
+ * Stamp the addresses browser scenarios use, at compile time.
4
+ *
5
+ * Two rules, because a scenario needs to name a control AND, when the same control appears
6
+ * twice on screen, say which one:
7
+ *
8
+ * - **A control** gets `data-testid` = its i18n MESSAGE KEY. Every other address the app
9
+ * already has is something it is free to change without meaning to: role+accessible-name
10
+ * reads the RENDERED COPY, which here is a paraglide message, so a scenario written
11
+ * against it is really asserting a translated string and breaks on a reworded button or a
12
+ * locale switch. A CSS selector breaks on a redesign. Message TEXT is copy and changes
13
+ * freely; the message KEY is the control's meaning, and only changes when the control's
14
+ * meaning does — at which point a scenario naming the old one SHOULD fail.
15
+ * - **A component's root element** gets `data-testid` = its component name, kebab-cased.
16
+ * Keys are NOT unique — `m.common__save()` is meant to be reused — so the id alone is
17
+ * ambiguous whenever two forms are open at once. That is not a flaw to design out: this
18
+ * app's own console steps already handle testids matching several nodes by filtering
19
+ * (`addon-console.steps.ts`), and pikku's TestIdSelector has `within`/`containing` for
20
+ * exactly this. Scoping only works if the containers are addressable too, which is what
21
+ * this second rule provides — `clicks common__save within entry-form`.
22
+ *
23
+ * Deriving both removes the discipline problem rather than policing it: nothing has to be
24
+ * added to a component, and the ids are identical on every build.
25
+ */
26
+ /** The shadcn controls, the router's link and the native elements, by tag. */
27
+ const INTERACTIVE = new Set([
28
+ 'Button',
29
+ 'Link',
30
+ 'Input',
31
+ 'InputOTP',
32
+ 'Textarea',
33
+ 'Checkbox',
34
+ 'Switch',
35
+ 'Slider',
36
+ 'Toggle',
37
+ 'ToggleGroupItem',
38
+ 'RadioGroupItem',
39
+ 'SelectTrigger',
40
+ 'SelectItem',
41
+ 'DropdownMenuItem',
42
+ 'DropdownMenuCheckboxItem',
43
+ 'DropdownMenuRadioItem',
44
+ 'TabsTrigger',
45
+ 'AccordionTrigger',
46
+ 'CommandItem',
47
+ 'button',
48
+ 'a',
49
+ 'input',
50
+ 'textarea',
51
+ 'select',
52
+ ]);
53
+ /** The elements whose text names the control beside them: `<Label htmlFor="email">`. */
54
+ const LABELS = new Set(['Label', 'FieldLabel', 'FormLabel', 'label']);
55
+ /**
56
+ * Where a control's meaning is written, most specific first.
57
+ *
58
+ * `aria-label` carries it for the icon-only buttons with no visible text — the same
59
+ * attribute a screen reader needs, so a control this cannot address is usually one a screen
60
+ * reader cannot announce either.
61
+ */
62
+ const LABEL_ATTRIBUTES = ['label', 'aria-label', 'placeholder', 'title'];
63
+ const kebab = (name) => name
64
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
65
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
66
+ .toLowerCase();
67
+ const isInteractive = (tag) => !!tag && INTERACTIVE.has(tag);
68
+ const tagName = (node) => {
69
+ if (node.type === 'JSXIdentifier')
70
+ return node.name;
71
+ if (node.type === 'JSXMemberExpression') {
72
+ const object = node.object;
73
+ if (object.type !== 'JSXIdentifier')
74
+ return null;
75
+ return `${object.name}.${node.property.name}`;
76
+ }
77
+ return null;
78
+ };
79
+ /**
80
+ * The paraglide message key behind an expression, e.g. `m.entry__save_label()` → the key.
81
+ *
82
+ * Searches rather than matching one shape, because a label is as often `cond ? m.a() : m.b()`
83
+ * as a bare call. The FIRST key wins: across a conditional the branches are one control
84
+ * saying two things, and an id that changed with state could not be addressed at all.
85
+ */
86
+ function messageKey(node) {
87
+ if (!node)
88
+ return null;
89
+ if (node.type === 'CallExpression') {
90
+ const callee = node.callee;
91
+ if (callee.type === 'MemberExpression' &&
92
+ callee.object.type === 'Identifier' &&
93
+ callee.object.name === 'm' &&
94
+ callee.property.type === 'Identifier') {
95
+ return callee.property.name;
96
+ }
97
+ }
98
+ const nested = [];
99
+ if (node.type === 'JSXExpressionContainer')
100
+ nested.push(node.expression);
101
+ if (node.type === 'ConditionalExpression')
102
+ nested.push(node.consequent, node.alternate);
103
+ if (node.type === 'LogicalExpression')
104
+ nested.push(node.left, node.right);
105
+ if (node.type === 'TemplateLiteral')
106
+ nested.push(...node.expressions);
107
+ if (node.type === 'CallExpression')
108
+ nested.push(...node.arguments);
109
+ for (const child of nested) {
110
+ const key = messageKey(child);
111
+ if (key)
112
+ return key;
113
+ }
114
+ return null;
115
+ }
116
+ /** A literal `name`/`id` — the stable identity a control has when its label is dynamic. */
117
+ function literalIdentity(attributes) {
118
+ return (literalAttribute(attributes, 'name') ?? literalAttribute(attributes, 'id'));
119
+ }
120
+ function literalAttribute(attributes, wanted) {
121
+ for (const attribute of attributes) {
122
+ if (attribute.type !== 'JSXAttribute')
123
+ continue;
124
+ const attributeName = attribute.name.type === 'JSXIdentifier' ? attribute.name.name : null;
125
+ if (attributeName !== wanted)
126
+ continue;
127
+ if (attribute.value?.type === 'StringLiteral')
128
+ return attribute.value.value;
129
+ }
130
+ return null;
131
+ }
132
+ /**
133
+ * The message key of the label that names this control from beside it, as shadcn forms do:
134
+ * `<Label htmlFor="email">{m.common__email()}</Label>` next to `<Input id="email" />`, in the
135
+ * same parent. A label pointing at a different id belongs to a different control.
136
+ */
137
+ function siblingLabelKey(element) {
138
+ const parent = element.parentPath.parentPath?.node;
139
+ if (parent?.type !== 'JSXElement')
140
+ return null;
141
+ const id = literalAttribute(element.node.attributes, 'id');
142
+ for (const child of parent.children) {
143
+ if (child.type !== 'JSXElement')
144
+ continue;
145
+ const label = child.openingElement;
146
+ if (!LABELS.has(tagName(label.name) ?? ''))
147
+ continue;
148
+ const target = literalAttribute(label.attributes, 'htmlFor');
149
+ if (target && id && target !== id)
150
+ continue;
151
+ for (const text of child.children) {
152
+ const key = messageKey(text);
153
+ if (key)
154
+ return key;
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ export default function injectTestIds({ types: t }, options = {}) {
160
+ const anchors = options.sourceAnchors === true;
161
+ const root = options.root ?? process.cwd();
162
+ let anchorIdentifier = null;
163
+ let anchorPath = null;
164
+ let anchorUsed = false;
165
+ const attributeName = (attribute) => attribute.type === 'JSXAttribute' && attribute.name.type === 'JSXIdentifier'
166
+ ? attribute.name.name
167
+ : null;
168
+ /**
169
+ * Whether this element is already spoken for. A hand-written id always wins — it is how a
170
+ * list row carries the id of the record it shows, which no static rule could derive. A
171
+ * spread may carry one, and merging into it would need a runtime helper for a case that is
172
+ * already addressable, so leave those to the author too.
173
+ */
174
+ const claimed = (element) => element.attributes.some((attribute) => attributeName(attribute) === 'data-testid' ||
175
+ attribute.type === 'JSXSpreadAttribute');
176
+ const stamp = (element, value) => {
177
+ element.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-testid'), t.stringLiteral(value)));
178
+ };
179
+ /**
180
+ * Anchors go on elements that are ALREADY claimed by a hand-written testid too
181
+ * — knowing an element's name and knowing where its JSX lives are different
182
+ * questions, and the author answering the first does not answer the second.
183
+ */
184
+ const anchor = (element) => {
185
+ if (!anchors || !anchorIdentifier)
186
+ return;
187
+ const line = element.loc?.start.line;
188
+ if (!line)
189
+ return;
190
+ if (element.attributes.some((attribute) => attributeName(attribute) === 'data-cloud-src')) {
191
+ return;
192
+ }
193
+ anchorUsed = true;
194
+ element.attributes.push(t.jsxAttribute(t.jsxIdentifier('data-cloud-src'), t.jsxExpressionContainer(t.binaryExpression('+', t.cloneNode(anchorIdentifier), t.stringLiteral(`:${line}`)))));
195
+ };
196
+ /** A component is a PascalCase function — the same rule React itself uses to tell them apart. */
197
+ const componentName = (path) => {
198
+ const node = path.node;
199
+ if (node.type === 'FunctionDeclaration' && node.id) {
200
+ return /^[A-Z]/.test(node.id.name) ? node.id.name : null;
201
+ }
202
+ const parent = path.parent;
203
+ if (parent.type === 'VariableDeclarator' &&
204
+ parent.id.type === 'Identifier') {
205
+ return /^[A-Z]/.test(parent.id.name) ? parent.id.name : null;
206
+ }
207
+ return null;
208
+ };
209
+ return {
210
+ name: 'pikku-inject-testids',
211
+ visitor: {
212
+ Program: {
213
+ enter(program, state) {
214
+ anchorIdentifier = null;
215
+ anchorPath = null;
216
+ anchorUsed = false;
217
+ if (!anchors || !state.filename)
218
+ return;
219
+ anchorPath = path
220
+ .relative(root, state.filename)
221
+ .split(path.sep)
222
+ .join('/');
223
+ anchorIdentifier = program.scope.generateUidIdentifier('cloudSrc');
224
+ },
225
+ exit(program) {
226
+ if (!anchorUsed || !anchorIdentifier || !anchorPath)
227
+ return;
228
+ program.unshiftContainer('body', t.variableDeclaration('const', [
229
+ t.variableDeclarator(anchorIdentifier, t.stringLiteral(anchorPath)),
230
+ ]));
231
+ },
232
+ },
233
+ JSXOpeningElement(path) {
234
+ if (!isInteractive(tagName(path.node.name)))
235
+ return;
236
+ anchor(path.node);
237
+ if (claimed(path.node))
238
+ return;
239
+ let key = null;
240
+ for (const wanted of LABEL_ATTRIBUTES) {
241
+ for (const attribute of path.node.attributes) {
242
+ if (attributeName(attribute) !== wanted ||
243
+ attribute.type !== 'JSXAttribute')
244
+ continue;
245
+ key = messageKey(attribute.value);
246
+ if (key)
247
+ break;
248
+ }
249
+ if (key)
250
+ break;
251
+ }
252
+ // A button says what it does in its children rather than a label prop.
253
+ if (!key && path.parentPath.node.type === 'JSXElement') {
254
+ for (const child of path.parentPath.node.children) {
255
+ key = messageKey(child);
256
+ if (key)
257
+ break;
258
+ }
259
+ }
260
+ key ??= siblingLabelKey(path);
261
+ key ??= literalIdentity(path.node.attributes);
262
+ // No key, no id — DELIBERATELY. The alternative is a positional fallback
263
+ // (`today-button-3`) that shifts the moment a control is added above it, turning
264
+ // every scenario using it into a flake. A control with no message key and no `name`
265
+ // has no stable identity to expose, and the fix belongs upstream: give it an
266
+ // `aria-label`, which it needs for a screen reader anyway.
267
+ if (key)
268
+ stamp(path.node, key);
269
+ },
270
+ Function(path) {
271
+ const name = componentName(path);
272
+ if (!name)
273
+ return;
274
+ const id = kebab(name);
275
+ // Every element this component can RETURN, so a component that renders a loading
276
+ // state and a loaded one is addressable in both. Returns inside a nested function
277
+ // belong to that function (a render prop, a map callback), not to this component.
278
+ path.traverse({
279
+ Function(inner) {
280
+ inner.skip();
281
+ },
282
+ ReturnStatement(statement) {
283
+ const returned = statement.node.argument;
284
+ if (returned?.type !== 'JSXElement')
285
+ return;
286
+ anchor(returned.openingElement);
287
+ if (claimed(returned.openingElement))
288
+ return;
289
+ // A component whose root IS a control (a lone `<Select>` wrapped in nothing)
290
+ // keeps its OWN key — the more specific address, and there is nothing inside it
291
+ // to scope anyway. Without this the container rule would win purely because
292
+ // babel reaches the function before its JSX, and the control would be
293
+ // addressable only by the name of the component wrapping it.
294
+ if (isInteractive(tagName(returned.openingElement.name)))
295
+ return;
296
+ stamp(returned.openingElement, id);
297
+ },
298
+ });
299
+ },
300
+ },
301
+ };
302
+ }
@@ -0,0 +1,16 @@
1
+ /** What the Vite plugins need to know about the project, read from `pikku.config.json`. */
2
+ export interface PikkuViteProject {
3
+ /** Directory holding `pikku.config.json`. */
4
+ configDir: string;
5
+ /** True when the project declares Better Auth, i.e. its generated auth wiring exists. */
6
+ betterAuth: boolean;
7
+ /** Absolute path of the generated `env.gen.ts` exporting `apiUrl()`, when configured. */
8
+ envFile?: string;
9
+ }
10
+ /** Walk up from `from` to the nearest `pikku.config.json`. */
11
+ export declare const findPikkuConfig: (from: string) => string | undefined;
12
+ /**
13
+ * Read the project's `pikku.config.json`, found from `from` upward. A project
14
+ * with none gets the defaults: test ids only.
15
+ */
16
+ export declare const readPikkuViteProject: (from?: string) => PikkuViteProject | undefined;
@@ -0,0 +1,46 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ /** Walk up from `from` to the nearest `pikku.config.json`. */
4
+ export const findPikkuConfig = (from) => {
5
+ let dir = resolve(from);
6
+ for (;;) {
7
+ const candidate = join(dir, 'pikku.config.json');
8
+ if (existsSync(candidate))
9
+ return candidate;
10
+ const parent = dirname(dir);
11
+ if (parent === dir)
12
+ return undefined;
13
+ dir = parent;
14
+ }
15
+ };
16
+ const absolute = (base, path) => isAbsolute(path) ? path : join(base, path);
17
+ /**
18
+ * Where the CLI writes the Better Auth wiring: `scaffold.pikkuDir`, or `scaffold/`
19
+ * beside the first source directory. Its presence is the project saying it uses
20
+ * Better Auth, the same signal `pikku all` acts on.
21
+ */
22
+ const authWiringFile = (config, configDir) => {
23
+ const rootDir = absolute(configDir, config.rootDir ?? '.');
24
+ const scaffoldDir = config.scaffold?.pikkuDir
25
+ ? absolute(rootDir, config.scaffold.pikkuDir)
26
+ : absolute(rootDir, join(config.srcDirectories?.[0] ?? 'src', 'scaffold'));
27
+ return join(scaffoldDir, 'auth', 'auth.gen.ts');
28
+ };
29
+ /**
30
+ * Read the project's `pikku.config.json`, found from `from` upward. A project
31
+ * with none gets the defaults: test ids only.
32
+ */
33
+ export const readPikkuViteProject = (from = process.cwd()) => {
34
+ const configPath = findPikkuConfig(from);
35
+ if (!configPath)
36
+ return undefined;
37
+ const configDir = dirname(configPath);
38
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
39
+ return {
40
+ configDir,
41
+ betterAuth: existsSync(authWiringFile(config, configDir)),
42
+ envFile: config.clientFiles?.envFile
43
+ ? absolute(configDir, config.clientFiles.envFile)
44
+ : undefined,
45
+ };
46
+ };
@@ -0,0 +1,17 @@
1
+ import type { PluginOption } from 'vite';
2
+ export interface PikkuViteOptions {
3
+ /** Where to start looking for `pikku.config.json`. Defaults to the directory vite runs in. */
4
+ from?: string;
5
+ }
6
+ /**
7
+ * The Pikku plugins a frontend needs, chosen from `pikku.config.json`:
8
+ *
9
+ * - `testIds()` always — scenarios drive built apps, not only the dev server.
10
+ * - the Better Auth cross-site session relay when the project declares Better
11
+ * Auth (dev server only; see `@pikku/better-auth/vite`).
12
+ *
13
+ * ```ts
14
+ * export default defineConfig({ plugins: [tanstackStart(), pikkuVite(), react()] })
15
+ * ```
16
+ */
17
+ export declare const pikkuVite: (options?: PikkuViteOptions) => PluginOption[];
@@ -0,0 +1,22 @@
1
+ import { readPikkuViteProject } from './pikku-config.js';
2
+ import { testIds } from './test-ids.js';
3
+ /**
4
+ * The Pikku plugins a frontend needs, chosen from `pikku.config.json`:
5
+ *
6
+ * - `testIds()` always — scenarios drive built apps, not only the dev server.
7
+ * - the Better Auth cross-site session relay when the project declares Better
8
+ * Auth (dev server only; see `@pikku/better-auth/vite`).
9
+ *
10
+ * ```ts
11
+ * export default defineConfig({ plugins: [tanstackStart(), pikkuVite(), react()] })
12
+ * ```
13
+ */
14
+ export const pikkuVite = (options = {}) => {
15
+ const project = readPikkuViteProject(options.from);
16
+ const plugins = [testIds()];
17
+ if (project?.betterAuth) {
18
+ // Loaded on demand: a project without Better Auth need not have it installed.
19
+ plugins.push(import('@pikku/better-auth/vite').then(({ crossSiteSession }) => crossSiteSession({ envFile: project.envFile })));
20
+ }
21
+ return plugins;
22
+ };
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Run the testid stamper over the app's JSX.
4
+ *
5
+ * A standalone pre-transform rather than a `babel` option on `@vitejs/plugin-react`, which
6
+ * mirrors how `apps/console` runs its `data-om` plugin and for the same reason:
7
+ * plugin-react@6 is the rolldown/oxc build and has NO babel option, so a transform hung off
8
+ * that option stops running on the version bump — silently, since the app still builds and
9
+ * only the scenarios notice. Owning the babel pass costs one extra parse of the app's own
10
+ * source in exchange for surviving that.
11
+ *
12
+ * NOT `apply: 'serve'`: scenarios drive the built sandbox app as well as the dev server, so
13
+ * a dev-only stamp would leave every deployed app unaddressable.
14
+ *
15
+ * The source anchors ride along on the same parse, but only when cloud asks
16
+ * for them — they put the app's file layout in view-source, and the panel that
17
+ * reads them is gated on the same flag.
18
+ */
19
+ export declare function testIds(): Plugin;
@@ -0,0 +1,43 @@
1
+ import * as babel from '@babel/core';
2
+ import injectTestIds from './inject-testids.js';
3
+ const JSX_FILE = /\.[jt]sx$/;
4
+ /**
5
+ * Run the testid stamper over the app's JSX.
6
+ *
7
+ * A standalone pre-transform rather than a `babel` option on `@vitejs/plugin-react`, which
8
+ * mirrors how `apps/console` runs its `data-om` plugin and for the same reason:
9
+ * plugin-react@6 is the rolldown/oxc build and has NO babel option, so a transform hung off
10
+ * that option stops running on the version bump — silently, since the app still builds and
11
+ * only the scenarios notice. Owning the babel pass costs one extra parse of the app's own
12
+ * source in exchange for surviving that.
13
+ *
14
+ * NOT `apply: 'serve'`: scenarios drive the built sandbox app as well as the dev server, so
15
+ * a dev-only stamp would leave every deployed app unaddressable.
16
+ *
17
+ * The source anchors ride along on the same parse, but only when cloud asks
18
+ * for them — they put the app's file layout in view-source, and the panel that
19
+ * reads them is gated on the same flag.
20
+ */
21
+ export function testIds() {
22
+ const sourceAnchors = process.env.PIKKUSTUDIO_CHANGES_PANEL === 'true';
23
+ return {
24
+ name: 'pikku:testids',
25
+ enforce: 'pre',
26
+ async transform(code, id) {
27
+ const file = id.split('?')[0];
28
+ if (!file || !JSX_FILE.test(file) || file.includes('/node_modules/'))
29
+ return null;
30
+ const result = await babel.transformAsync(code, {
31
+ filename: file,
32
+ babelrc: false,
33
+ configFile: false,
34
+ sourceMaps: true,
35
+ parserOpts: { plugins: ['jsx', 'typescript'] },
36
+ plugins: [[injectTestIds, { sourceAnchors }]],
37
+ });
38
+ if (!result?.code)
39
+ return null;
40
+ return { code: result.code, map: result.map };
41
+ },
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@pikku/vite",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.12.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/pikkujs/pikku.git",
7
+ "directory": "packages/frontend/vite"
8
+ },
9
+ "description": "Vite plugins for Pikku frontends — scenario test ids and the Better Auth cross-site session relay, applied from pikku.config.json",
10
+ "author": "yasser.fadl@gmail.com",
11
+ "license": "MIT",
12
+ "type": "module",
13
+ "main": "dist/index.js",
14
+ "module": "dist/index.js",
15
+ "types": "dist/index.d.ts",
16
+ "sideEffects": false,
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "!dist/**/*.tsbuildinfo"
27
+ ],
28
+ "scripts": {
29
+ "tsc": "tsc && tsc -p tsconfig.tests.json",
30
+ "build": "bun run clean && tsc -b",
31
+ "test": "bun test --timeout 120000 src",
32
+ "prepublishOnly": "bun run build",
33
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
34
+ },
35
+ "dependencies": {
36
+ "@babel/core": "^7.26.0"
37
+ },
38
+ "peerDependencies": {
39
+ "@pikku/better-auth": "^0.12.61",
40
+ "vite": "^5 || ^6 || ^7"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@pikku/better-auth": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@pikku/better-auth": "workspace:*",
49
+ "@types/babel__core": "^7.20.5",
50
+ "vite": "^7.0.0"
51
+ }
52
+ }