@qwik-custom-elements/core 1.0.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +596 -0
- package/dist/__tests__/generator.test.d.ts +1 -0
- package/dist/__tests__/generator.test.js +1331 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +271 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +183 -0
- package/dist/generator.d.ts +6 -0
- package/dist/generator.js +720 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.js +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { GenerationError, generateFromConfig } from '../generator.js';
|
|
7
|
+
const validStencilAdapterOptions = {
|
|
8
|
+
runtime: {
|
|
9
|
+
loaderImport: '@acme/stencil-lib/loader',
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
async function withTempDir(run) {
|
|
13
|
+
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'qce-core-'));
|
|
14
|
+
try {
|
|
15
|
+
await run(tempDir);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createSingleProjectConfig(tempDir, dryRun = false) {
|
|
22
|
+
return {
|
|
23
|
+
dryRun,
|
|
24
|
+
projects: [
|
|
25
|
+
{
|
|
26
|
+
id: 'demo',
|
|
27
|
+
adapter: 'stencil',
|
|
28
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
29
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
30
|
+
adapterOptions: validStencilAdapterOptions,
|
|
31
|
+
outDir: './src/generated',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function createFixturePackage(tempDir, packageName) {
|
|
37
|
+
const packageRoot = path.join(tempDir, 'node_modules', ...packageName.split('/'));
|
|
38
|
+
await mkdir(packageRoot, { recursive: true });
|
|
39
|
+
await writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: packageName, version: '1.0.0' }, null, 2), 'utf8');
|
|
40
|
+
return packageRoot;
|
|
41
|
+
}
|
|
42
|
+
describe('generateFromConfig', () => {
|
|
43
|
+
it('produces deterministic planned writes in dry-run mode without mutating files', async () => {
|
|
44
|
+
await withTempDir(async (tempDir) => {
|
|
45
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
46
|
+
modules: [
|
|
47
|
+
{
|
|
48
|
+
declarations: [
|
|
49
|
+
{ tagName: 'z-card' },
|
|
50
|
+
{
|
|
51
|
+
tagName: 'a-button',
|
|
52
|
+
attributes: [
|
|
53
|
+
{
|
|
54
|
+
name: 'size',
|
|
55
|
+
fieldName: 'size',
|
|
56
|
+
type: { text: '"lg" | "md" | "sm"' },
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
members: [
|
|
60
|
+
{
|
|
61
|
+
kind: 'field',
|
|
62
|
+
name: '#internalCount',
|
|
63
|
+
privacy: 'private',
|
|
64
|
+
type: { text: 'number' },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: 'field',
|
|
68
|
+
name: 'variant',
|
|
69
|
+
privacy: 'public',
|
|
70
|
+
type: { text: '"primary" | "secondary"' },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
events: [
|
|
74
|
+
{
|
|
75
|
+
name: 'tripleClick',
|
|
76
|
+
type: { text: 'CustomEvent<MouseEvent>' },
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
slots: [
|
|
80
|
+
{
|
|
81
|
+
name: 'footer',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'footer',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{ tagName: 'z-card' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}, null, 2), 'utf8');
|
|
93
|
+
const result = await generateFromConfig(createSingleProjectConfig(tempDir, true), { cwd: tempDir });
|
|
94
|
+
expect(result.dryRun).toBe(true);
|
|
95
|
+
expect(result.projects).toHaveLength(1);
|
|
96
|
+
expect(result.projects[0].wroteFiles).toBe(false);
|
|
97
|
+
expect(result.projects[0].componentTags).toEqual(['a-button', 'z-card']);
|
|
98
|
+
expect(result.projects[0].plannedWrites).toHaveLength(5);
|
|
99
|
+
const indexWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
100
|
+
const runtimeBarrelWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime.ts')));
|
|
101
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-csr.generated.ts')));
|
|
102
|
+
const buttonWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'a-button.tsx')));
|
|
103
|
+
const cardWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'z-card.tsx')));
|
|
104
|
+
expect(indexWrite?.content).toContain('// Generated by @qwik-custom-elements/core. Project: demo.');
|
|
105
|
+
expect(indexWrite?.content).toContain('export const generatedComponentTags = ["a-button","z-card"] as const;');
|
|
106
|
+
expect(indexWrite?.content).toContain("export { QwikAButton } from './a-button';");
|
|
107
|
+
expect(indexWrite?.content).toContain("export { QwikZCard } from './z-card';");
|
|
108
|
+
expect(buttonWrite?.content).toContain("import { Slot, component$ } from '@builder.io/qwik';");
|
|
109
|
+
expect(buttonWrite?.content).toContain("import type { QRL } from '@builder.io/qwik';");
|
|
110
|
+
expect(buttonWrite?.content).toContain("import { GeneratedStencilCSRComponent } from './runtime';");
|
|
111
|
+
expect(buttonWrite?.content).not.toContain('useStencilClientSetup();');
|
|
112
|
+
expect(buttonWrite?.content).toContain('export interface QwikAButtonProps {');
|
|
113
|
+
expect(buttonWrite?.content).toContain(' size?: "lg" | "md" | "sm";');
|
|
114
|
+
expect(buttonWrite?.content).toContain(' variant?: "primary" | "secondary";');
|
|
115
|
+
expect(buttonWrite?.content).not.toContain('#internalCount');
|
|
116
|
+
expect(buttonWrite?.content).toContain(' onTripleClick$?: QRL<(event: CustomEvent<MouseEvent>) => void>;');
|
|
117
|
+
expect(buttonWrite?.content).toContain('export const QwikAButton = component$<QwikAButtonProps>');
|
|
118
|
+
expect(buttonWrite?.content).toContain(' const eventProps: Record<string, unknown> = {};');
|
|
119
|
+
expect(buttonWrite?.content).toContain(' if (isEventBindingKey(key)) {');
|
|
120
|
+
expect(buttonWrite?.content).toContain(' <GeneratedStencilCSRComponent');
|
|
121
|
+
expect(buttonWrite?.content).toContain(' tagName="a-button"');
|
|
122
|
+
expect(buttonWrite?.content).toContain(' props={elementProps}');
|
|
123
|
+
expect(buttonWrite?.content).toContain(' events={mappedEvents}');
|
|
124
|
+
expect(buttonWrite?.content).toContain(' slots={["footer"]}');
|
|
125
|
+
expect(buttonWrite?.content).toContain(' {...passthroughEventProps}');
|
|
126
|
+
expect(buttonWrite?.content).toContain(' </GeneratedStencilCSRComponent>');
|
|
127
|
+
expect(buttonWrite?.content).toContain(' <Slot />');
|
|
128
|
+
expect(buttonWrite?.content).toContain(' <Slot name="footer" />');
|
|
129
|
+
expect(cardWrite?.content).toContain('export interface QwikZCardProps {');
|
|
130
|
+
expect(runtimeBarrelWrite?.content).toContain("export * from './runtime-csr.generated';");
|
|
131
|
+
expect(runtimeWrite?.content).toContain("import { defineCustomElements as runtimeDefineCustomElements } from '@acme/stencil-lib/loader';");
|
|
132
|
+
await expect(readFile(indexWrite.path, 'utf8')).rejects.toThrow();
|
|
133
|
+
await expect(readFile(runtimeBarrelWrite.path, 'utf8')).rejects.toThrow();
|
|
134
|
+
await expect(readFile(runtimeWrite.path, 'utf8')).rejects.toThrow();
|
|
135
|
+
await expect(readFile(buttonWrite.path, 'utf8')).rejects.toThrow();
|
|
136
|
+
await expect(readFile(cardWrite.path, 'utf8')).rejects.toThrow();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it('writes generated index.ts in non-dry-run mode', async () => {
|
|
140
|
+
await withTempDir(async (tempDir) => {
|
|
141
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
142
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
143
|
+
}), 'utf8');
|
|
144
|
+
const result = await generateFromConfig(createSingleProjectConfig(tempDir), { cwd: tempDir });
|
|
145
|
+
const indexWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
146
|
+
const wrapperWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'app-root.tsx')));
|
|
147
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-csr.generated.ts')));
|
|
148
|
+
const runtimeBarrelWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime.ts')));
|
|
149
|
+
const indexDiskContent = await readFile(indexWrite.path, 'utf8');
|
|
150
|
+
const runtimeBarrelDiskContent = await readFile(runtimeBarrelWrite.path, 'utf8');
|
|
151
|
+
const runtimeDiskContent = await readFile(runtimeWrite.path, 'utf8');
|
|
152
|
+
const wrapperDiskContent = await readFile(wrapperWrite.path, 'utf8');
|
|
153
|
+
expect(result.projects[0].wroteFiles).toBe(true);
|
|
154
|
+
expect(result.projects[0].plannedWrites).toHaveLength(4);
|
|
155
|
+
expect(indexDiskContent).toBe(indexWrite.content);
|
|
156
|
+
expect(runtimeBarrelDiskContent).toBe(runtimeBarrelWrite.content);
|
|
157
|
+
expect(runtimeDiskContent).toBe(runtimeWrite.content);
|
|
158
|
+
expect(wrapperDiskContent).toBe(wrapperWrite.content);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
it('keeps generated files byte-stable across repeated write runs', async () => {
|
|
162
|
+
await withTempDir(async (tempDir) => {
|
|
163
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
164
|
+
modules: [
|
|
165
|
+
{
|
|
166
|
+
declarations: [
|
|
167
|
+
{ tagName: 'card-panel' },
|
|
168
|
+
{ tagName: 'app-root' },
|
|
169
|
+
],
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
}, null, 2), 'utf8');
|
|
173
|
+
const config = createSingleProjectConfig(tempDir);
|
|
174
|
+
const firstRun = await generateFromConfig(config, { cwd: tempDir });
|
|
175
|
+
const firstIndexWrite = firstRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
176
|
+
const firstAppRootWrite = firstRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'app-root.tsx')));
|
|
177
|
+
const firstCardPanelWrite = firstRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'card-panel.tsx')));
|
|
178
|
+
const firstIndexDiskContent = await readFile(firstIndexWrite.path, 'utf8');
|
|
179
|
+
const firstAppRootDiskContent = await readFile(firstAppRootWrite.path, 'utf8');
|
|
180
|
+
const firstCardPanelDiskContent = await readFile(firstCardPanelWrite.path, 'utf8');
|
|
181
|
+
const secondRun = await generateFromConfig(config, { cwd: tempDir });
|
|
182
|
+
const secondIndexWrite = secondRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
183
|
+
const secondAppRootWrite = secondRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'app-root.tsx')));
|
|
184
|
+
const secondCardPanelWrite = secondRun.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'card-panel.tsx')));
|
|
185
|
+
const secondIndexDiskContent = await readFile(secondIndexWrite.path, 'utf8');
|
|
186
|
+
const secondAppRootDiskContent = await readFile(secondAppRootWrite.path, 'utf8');
|
|
187
|
+
const secondCardPanelDiskContent = await readFile(secondCardPanelWrite.path, 'utf8');
|
|
188
|
+
expect(secondRun.projects[0].plannedWrites.map((write) => write.path)).toEqual(firstRun.projects[0].plannedWrites.map((write) => write.path));
|
|
189
|
+
expect(secondIndexWrite.content).toBe(firstIndexWrite.content);
|
|
190
|
+
expect(secondAppRootWrite.content).toBe(firstAppRootWrite.content);
|
|
191
|
+
expect(secondCardPanelWrite.content).toBe(firstCardPanelWrite.content);
|
|
192
|
+
expect(secondIndexDiskContent).toBe(firstIndexDiskContent);
|
|
193
|
+
expect(secondAppRootDiskContent).toBe(firstAppRootDiskContent);
|
|
194
|
+
expect(secondCardPanelDiskContent).toBe(firstCardPanelDiskContent);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
it('fails with deterministic code when CEM source cannot be read', async () => {
|
|
198
|
+
await withTempDir(async (tempDir) => {
|
|
199
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
200
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({ code: 'QCE_CEM_READ_FAILED' });
|
|
201
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toBeInstanceOf(GenerationError);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
it('fails when the adapter rejects a stencil CEM project without loaderImport', async () => {
|
|
205
|
+
await withTempDir(async (tempDir) => {
|
|
206
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
207
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
208
|
+
}), 'utf8');
|
|
209
|
+
await writeFile(path.join(tempDir, 'adapter-validating.mjs'), [
|
|
210
|
+
'export const metadata = {',
|
|
211
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
212
|
+
' supportsSsrProbe: true,',
|
|
213
|
+
" ssrRuntimeSubpath: './ssr',",
|
|
214
|
+
'};',
|
|
215
|
+
'',
|
|
216
|
+
'export function validateProject({ source, adapterOptions }) {',
|
|
217
|
+
" if (source.type !== 'CEM') return;",
|
|
218
|
+
' const loaderImport = adapterOptions?.runtime?.loaderImport;',
|
|
219
|
+
" if (typeof loaderImport === 'string' && loaderImport.trim() !== '') return;",
|
|
220
|
+
" throw Object.assign(new Error('Stencil CEM projects must provide adapterOptions.runtime.loaderImport.'), {",
|
|
221
|
+
" code: 'QCE_STENCIL_RUNTIME_LOADER_REQUIRED',",
|
|
222
|
+
' });',
|
|
223
|
+
'}',
|
|
224
|
+
'',
|
|
225
|
+
'export async function probeSSR() {',
|
|
226
|
+
' return { available: true };',
|
|
227
|
+
'}',
|
|
228
|
+
'',
|
|
229
|
+
].join('\n'), 'utf8');
|
|
230
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
231
|
+
config.projects[0].adapterPackage = './adapter-validating.mjs';
|
|
232
|
+
config.projects[0].adapterOptions = {};
|
|
233
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
234
|
+
code: 'QCE_STENCIL_RUNTIME_LOADER_REQUIRED',
|
|
235
|
+
message: 'Stencil CEM projects must provide adapterOptions.runtime.loaderImport.',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
it('falls back to CEM-only generation when adapter SSR is unavailable', async () => {
|
|
240
|
+
await withTempDir(async (tempDir) => {
|
|
241
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
242
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
243
|
+
}), 'utf8');
|
|
244
|
+
await writeFile(path.join(tempDir, 'adapter-unsupported.mjs'), [
|
|
245
|
+
'export async function probeSSR() {',
|
|
246
|
+
' return { available: false };',
|
|
247
|
+
'}',
|
|
248
|
+
'',
|
|
249
|
+
'export function createGeneratedOutput() {',
|
|
250
|
+
' return [];',
|
|
251
|
+
'}',
|
|
252
|
+
'',
|
|
253
|
+
].join('\n'), 'utf8');
|
|
254
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
255
|
+
config.projects[0].adapterPackage = './adapter-unsupported.mjs';
|
|
256
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
257
|
+
expect(result.projects[0].status).toBe('success');
|
|
258
|
+
expect(result.projects[0].plannedWrites).toHaveLength(0);
|
|
259
|
+
expect(result.projects[0].ssrCapabilities).toEqual({
|
|
260
|
+
available: false,
|
|
261
|
+
supportsSsrProbe: false,
|
|
262
|
+
ssrRuntimeSubpath: null,
|
|
263
|
+
});
|
|
264
|
+
expect(result.projects[0].observedErrorCodes).toEqual([
|
|
265
|
+
'QCE_SSR_UNSUPPORTED_FALLBACK',
|
|
266
|
+
]);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
it('emits generatedMode csr and QCE_SSR_UNSUPPORTED_FALLBACK together when SSR probe returns unavailable', async () => {
|
|
270
|
+
await withTempDir(async (tempDir) => {
|
|
271
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
272
|
+
modules: [{ declarations: [{ tagName: 'lit-button' }] }],
|
|
273
|
+
}), 'utf8');
|
|
274
|
+
await writeFile(path.join(tempDir, 'adapter-ssr-probe-unavailable.mjs'), [
|
|
275
|
+
'export const metadata = {',
|
|
276
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
277
|
+
' supportsSsrProbe: true,',
|
|
278
|
+
" ssrRuntimeSubpath: './ssr',",
|
|
279
|
+
'};',
|
|
280
|
+
'',
|
|
281
|
+
'export async function probeSSR() {',
|
|
282
|
+
' return { available: false };',
|
|
283
|
+
'}',
|
|
284
|
+
'',
|
|
285
|
+
'export function createGeneratedOutput({ componentDefinitions, ssrAvailable }) {',
|
|
286
|
+
' const tags = componentDefinitions.map((d) => JSON.stringify(d.tagName)).join(",");',
|
|
287
|
+
" const mode = ssrAvailable ? 'ssr' : 'csr';",
|
|
288
|
+
' return [{',
|
|
289
|
+
" relativePath: 'index.ts',",
|
|
290
|
+
" content: `export const generatedComponentTags = [${tags}] as const;\\nexport const generatedMode = '${mode}' as const;\\n`,",
|
|
291
|
+
' }];',
|
|
292
|
+
'}',
|
|
293
|
+
'',
|
|
294
|
+
].join('\n'), 'utf8');
|
|
295
|
+
const config = {
|
|
296
|
+
dryRun: true,
|
|
297
|
+
projects: [
|
|
298
|
+
{
|
|
299
|
+
id: 'demo',
|
|
300
|
+
adapter: 'lit',
|
|
301
|
+
adapterPackage: './adapter-ssr-probe-unavailable.mjs',
|
|
302
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
303
|
+
outDir: './src/generated',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
308
|
+
expect(result.projects[0].status).toBe('success');
|
|
309
|
+
expect(result.projects[0].observedErrorCodes).toContain('QCE_SSR_UNSUPPORTED_FALLBACK');
|
|
310
|
+
const indexWrite = result.projects[0].plannedWrites.find((w) => w.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
311
|
+
expect(indexWrite?.content).toContain("export const generatedMode = 'csr' as const;");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
it('uses the primary adapter generation contract when the adapter exposes createGeneratedOutput', async () => {
|
|
315
|
+
await withTempDir(async (tempDir) => {
|
|
316
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
317
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
318
|
+
}), 'utf8');
|
|
319
|
+
await writeFile(path.join(tempDir, 'adapter-primary-generation.mjs'), [
|
|
320
|
+
'export const metadata = {',
|
|
321
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
322
|
+
' supportsSsrProbe: false,',
|
|
323
|
+
' ssrRuntimeSubpath: null,',
|
|
324
|
+
'};',
|
|
325
|
+
'',
|
|
326
|
+
'export function createGeneratedOutput({ projectId, componentDefinitions }) {',
|
|
327
|
+
' return componentDefinitions.map((componentDefinition) => ({',
|
|
328
|
+
' relativePath: `${componentDefinition.tagName}.txt`,',
|
|
329
|
+
' content: `primary:${projectId}:${componentDefinition.tagName}` ,',
|
|
330
|
+
' }));',
|
|
331
|
+
'}',
|
|
332
|
+
'',
|
|
333
|
+
].join('\n'), 'utf8');
|
|
334
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
335
|
+
config.projects[0].adapterPackage = './adapter-primary-generation.mjs';
|
|
336
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
337
|
+
expect(result.projects[0].plannedWrites).toEqual([
|
|
338
|
+
expect.objectContaining({
|
|
339
|
+
path: path.join(tempDir, 'src', 'generated', 'app-root.txt'),
|
|
340
|
+
content: 'primary:demo:app-root',
|
|
341
|
+
}),
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
it('passes optional libraryName to createGeneratedOutput when configured', async () => {
|
|
346
|
+
await withTempDir(async (tempDir) => {
|
|
347
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
348
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
349
|
+
}), 'utf8');
|
|
350
|
+
await writeFile(path.join(tempDir, 'adapter-library-name.mjs'), [
|
|
351
|
+
'export const metadata = {',
|
|
352
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
353
|
+
' supportsSsrProbe: false,',
|
|
354
|
+
' ssrRuntimeSubpath: null,',
|
|
355
|
+
'};',
|
|
356
|
+
'',
|
|
357
|
+
'export function createGeneratedOutput({ projectId, libraryName, componentDefinitions }) {',
|
|
358
|
+
' return componentDefinitions.map((componentDefinition) => ({',
|
|
359
|
+
' relativePath: `${componentDefinition.tagName}.txt`,',
|
|
360
|
+
' content: `library:${projectId}:${libraryName ?? "missing"}:${componentDefinition.tagName}` ,',
|
|
361
|
+
' }));',
|
|
362
|
+
'}',
|
|
363
|
+
'',
|
|
364
|
+
].join('\n'), 'utf8');
|
|
365
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
366
|
+
config.projects[0].adapterPackage = './adapter-library-name.mjs';
|
|
367
|
+
config.projects[0].libraryName = 'test-stencil-lib';
|
|
368
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
369
|
+
expect(result.projects[0].plannedWrites).toEqual([
|
|
370
|
+
expect.objectContaining({
|
|
371
|
+
path: path.join(tempDir, 'src', 'generated', 'app-root.txt'),
|
|
372
|
+
content: 'library:demo:test-stencil-lib:app-root',
|
|
373
|
+
}),
|
|
374
|
+
]);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
it('returns only adapter-generated files without adding core fallback output', async () => {
|
|
378
|
+
await withTempDir(async (tempDir) => {
|
|
379
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
380
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
381
|
+
}, null, 2), 'utf8');
|
|
382
|
+
await writeFile(path.join(tempDir, 'adapter-owned-output.mjs'), [
|
|
383
|
+
'export const metadata = {',
|
|
384
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
385
|
+
' supportsSsrProbe: false,',
|
|
386
|
+
' ssrRuntimeSubpath: null,',
|
|
387
|
+
'};',
|
|
388
|
+
'',
|
|
389
|
+
'export function createGeneratedOutput({ projectId, componentDefinitions }) {',
|
|
390
|
+
' return [',
|
|
391
|
+
' {',
|
|
392
|
+
" relativePath: 'owned/runtime-entry.ts',",
|
|
393
|
+
' content: `owned:${projectId}:${componentDefinitions[0]?.tagName ?? "missing"}`,',
|
|
394
|
+
' },',
|
|
395
|
+
' ];',
|
|
396
|
+
'}',
|
|
397
|
+
'',
|
|
398
|
+
].join('\n'), 'utf8');
|
|
399
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
400
|
+
config.projects[0].adapterPackage = './adapter-owned-output.mjs';
|
|
401
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
402
|
+
expect(result.projects[0].plannedWrites).toEqual([
|
|
403
|
+
{
|
|
404
|
+
path: path.join(tempDir, 'src', 'generated', 'owned', 'runtime-entry.ts'),
|
|
405
|
+
content: 'owned:demo:app-root',
|
|
406
|
+
},
|
|
407
|
+
]);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
it('fails when an adapter only exposes the deprecated additional planned writes contract', async () => {
|
|
411
|
+
await withTempDir(async (tempDir) => {
|
|
412
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
413
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
414
|
+
}), 'utf8');
|
|
415
|
+
await writeFile(path.join(tempDir, 'adapter-legacy-generation.mjs'), [
|
|
416
|
+
'export const metadata = {',
|
|
417
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
418
|
+
' supportsSsrProbe: false,',
|
|
419
|
+
' ssrRuntimeSubpath: null,',
|
|
420
|
+
'};',
|
|
421
|
+
'',
|
|
422
|
+
'export function createAdditionalPlannedWrites({ projectId, componentDefinitions }) {',
|
|
423
|
+
' return componentDefinitions.map((componentDefinition) => ({',
|
|
424
|
+
' relativePath: `${componentDefinition.tagName}.txt`,',
|
|
425
|
+
' content: `legacy:${projectId}:${componentDefinition.tagName}` ,',
|
|
426
|
+
' }));',
|
|
427
|
+
'}',
|
|
428
|
+
'',
|
|
429
|
+
].join('\n'), 'utf8');
|
|
430
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
431
|
+
config.projects[0].adapterPackage = './adapter-legacy-generation.mjs';
|
|
432
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
433
|
+
code: 'QCE_ADAPTER_GENERATION_CONTRACT_REQUIRED',
|
|
434
|
+
message: expect.stringContaining('createGeneratedOutput'),
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
it('passes adapter-resolved runtime imports into SSR probing', async () => {
|
|
439
|
+
await withTempDir(async (tempDir) => {
|
|
440
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
441
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
442
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
443
|
+
}), 'utf8');
|
|
444
|
+
await writeFile(path.join(tempDir, 'adapter-runtime-resolution.mjs'), [
|
|
445
|
+
'export const metadata = {',
|
|
446
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
447
|
+
' supportsSsrProbe: true,',
|
|
448
|
+
" ssrRuntimeSubpath: './ssr',",
|
|
449
|
+
'};',
|
|
450
|
+
'',
|
|
451
|
+
'export function resolveRuntimeImports({ source, adapterOptions }) {',
|
|
452
|
+
' return {',
|
|
453
|
+
' loaderImport: adapterOptions?.runtime?.loaderImport ?? `${source.packageName}/loader`,',
|
|
454
|
+
' hydrateImport: adapterOptions?.runtime?.hydrateImport ?? `${source.packageName}/hydrate`,',
|
|
455
|
+
' };',
|
|
456
|
+
'}',
|
|
457
|
+
'',
|
|
458
|
+
'export async function probeSSR({ runtimeImports }) {',
|
|
459
|
+
" return { available: runtimeImports?.loaderImport === '@acme/stencil-lib/loader' && runtimeImports?.hydrateImport === '@acme/stencil-lib/hydrate' };",
|
|
460
|
+
'}',
|
|
461
|
+
'',
|
|
462
|
+
'export function createGeneratedOutput() {',
|
|
463
|
+
' return [];',
|
|
464
|
+
'}',
|
|
465
|
+
'',
|
|
466
|
+
].join('\n'), 'utf8');
|
|
467
|
+
const config = {
|
|
468
|
+
dryRun: true,
|
|
469
|
+
projects: [
|
|
470
|
+
{
|
|
471
|
+
id: 'demo',
|
|
472
|
+
adapter: 'stencil',
|
|
473
|
+
adapterPackage: './adapter-runtime-resolution.mjs',
|
|
474
|
+
source: {
|
|
475
|
+
type: 'PACKAGE_NAME',
|
|
476
|
+
packageName: '@acme/stencil-lib',
|
|
477
|
+
},
|
|
478
|
+
outDir: './src/generated',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
483
|
+
expect(result.projects[0].ssrCapabilities).toEqual({
|
|
484
|
+
available: true,
|
|
485
|
+
supportsSsrProbe: true,
|
|
486
|
+
ssrRuntimeSubpath: './ssr',
|
|
487
|
+
});
|
|
488
|
+
expect(result.projects[0].observedErrorCodes).toEqual([]);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
it('fails when a PACKAGE_NAME stencil loader runtime import cannot be resolved', async () => {
|
|
492
|
+
await withTempDir(async (tempDir) => {
|
|
493
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
494
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
495
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
496
|
+
}), 'utf8');
|
|
497
|
+
const config = {
|
|
498
|
+
dryRun: true,
|
|
499
|
+
projects: [
|
|
500
|
+
{
|
|
501
|
+
id: 'demo',
|
|
502
|
+
adapter: 'stencil',
|
|
503
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
504
|
+
source: {
|
|
505
|
+
type: 'PACKAGE_NAME',
|
|
506
|
+
packageName: '@acme/stencil-lib',
|
|
507
|
+
},
|
|
508
|
+
outDir: './src/generated',
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
513
|
+
code: 'QCE_STENCIL_RUNTIME_LOADER_RESOLVE_FAILED',
|
|
514
|
+
message: expect.stringContaining('@acme/stencil-lib/loader'),
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
it('records a hydrate runtime diagnostic and falls back when a PACKAGE_NAME stencil hydrate import cannot be resolved', async () => {
|
|
519
|
+
await withTempDir(async (tempDir) => {
|
|
520
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
521
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
522
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
523
|
+
}), 'utf8');
|
|
524
|
+
await mkdir(path.join(fixturePackageRoot, 'loader'), { recursive: true });
|
|
525
|
+
await writeFile(path.join(fixturePackageRoot, 'loader', 'index.js'), 'export const defineCustomElements = () => undefined;\n', 'utf8');
|
|
526
|
+
const config = {
|
|
527
|
+
dryRun: true,
|
|
528
|
+
projects: [
|
|
529
|
+
{
|
|
530
|
+
id: 'demo',
|
|
531
|
+
adapter: 'stencil',
|
|
532
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
533
|
+
source: {
|
|
534
|
+
type: 'PACKAGE_NAME',
|
|
535
|
+
packageName: '@acme/stencil-lib',
|
|
536
|
+
},
|
|
537
|
+
outDir: './src/generated',
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
};
|
|
541
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
542
|
+
expect(result.projects[0].status).toBe('success');
|
|
543
|
+
expect(result.projects[0].ssrCapabilities).toEqual({
|
|
544
|
+
available: false,
|
|
545
|
+
supportsSsrProbe: true,
|
|
546
|
+
ssrRuntimeSubpath: './ssr',
|
|
547
|
+
clientOnlyMode: true,
|
|
548
|
+
});
|
|
549
|
+
expect(result.projects[0].observedErrorCodes).toEqual([
|
|
550
|
+
'QCE_SSR_UNSUPPORTED_FALLBACK',
|
|
551
|
+
'QCE_STENCIL_RUNTIME_HYDRATE_RESOLVE_FAILED',
|
|
552
|
+
]);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
it('generates a loader-only runtime barrel and CSR wrapper when PACKAGE_NAME hydrate resolution fails', async () => {
|
|
556
|
+
await withTempDir(async (tempDir) => {
|
|
557
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
558
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
559
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
560
|
+
}), 'utf8');
|
|
561
|
+
await mkdir(path.join(fixturePackageRoot, 'loader'), { recursive: true });
|
|
562
|
+
await writeFile(path.join(fixturePackageRoot, 'loader', 'index.js'), 'export const defineCustomElements = () => undefined;\n', 'utf8');
|
|
563
|
+
const config = {
|
|
564
|
+
dryRun: true,
|
|
565
|
+
projects: [
|
|
566
|
+
{
|
|
567
|
+
id: 'demo',
|
|
568
|
+
adapter: 'stencil',
|
|
569
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
570
|
+
source: {
|
|
571
|
+
type: 'PACKAGE_NAME',
|
|
572
|
+
packageName: '@acme/stencil-lib',
|
|
573
|
+
},
|
|
574
|
+
outDir: './src/generated',
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
};
|
|
578
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
579
|
+
const runtimeBarrelWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime.ts')));
|
|
580
|
+
const runtimeCsrWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-csr.generated.ts')));
|
|
581
|
+
const wrapperWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'app-root.tsx')));
|
|
582
|
+
expect(result.projects[0].plannedWrites).not.toEqual(expect.arrayContaining([
|
|
583
|
+
expect.objectContaining({
|
|
584
|
+
path: path.join(tempDir, 'src', 'generated', 'runtime-ssr.generated.ts'),
|
|
585
|
+
}),
|
|
586
|
+
]));
|
|
587
|
+
expect(runtimeBarrelWrite?.content).toContain("export * from './runtime-csr.generated';");
|
|
588
|
+
expect(runtimeBarrelWrite?.content).not.toContain("export * from './runtime-ssr.generated';");
|
|
589
|
+
expect(runtimeCsrWrite?.content).toContain("import { createStencilCSRComponent, createStencilClientSetup } from '@qwik-custom-elements/adapter-stencil/client';");
|
|
590
|
+
expect(runtimeCsrWrite?.content).toContain('export const GeneratedStencilCSRComponent = createStencilCSRComponent();');
|
|
591
|
+
expect(runtimeCsrWrite?.content).not.toContain('createStencilSSRComponent');
|
|
592
|
+
expect(wrapperWrite?.content).toContain("import { GeneratedStencilCSRComponent } from './runtime';");
|
|
593
|
+
expect(wrapperWrite?.content).not.toContain('useStencilClientSetup();');
|
|
594
|
+
expect(wrapperWrite?.content).toContain(' <GeneratedStencilCSRComponent');
|
|
595
|
+
expect(wrapperWrite?.content).toContain(' tagName="app-root"');
|
|
596
|
+
expect(wrapperWrite?.content).toContain(' props={elementProps}');
|
|
597
|
+
expect(wrapperWrite?.content).toContain(' events={mappedEvents}');
|
|
598
|
+
expect(wrapperWrite?.content).toContain(' slots={undefined}');
|
|
599
|
+
expect(wrapperWrite?.content).toContain(' {...passthroughEventProps}');
|
|
600
|
+
expect(wrapperWrite?.content).toContain(' </GeneratedStencilCSRComponent>');
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
it('generates a client runtime bootstrap from resolved PACKAGE_NAME loader imports', async () => {
|
|
604
|
+
await withTempDir(async (tempDir) => {
|
|
605
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
606
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
607
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
608
|
+
}), 'utf8');
|
|
609
|
+
await mkdir(path.join(fixturePackageRoot, 'loader'), { recursive: true });
|
|
610
|
+
await writeFile(path.join(fixturePackageRoot, 'loader', 'index.js'), 'export const defineCustomElements = () => undefined;\n', 'utf8');
|
|
611
|
+
const config = {
|
|
612
|
+
dryRun: true,
|
|
613
|
+
projects: [
|
|
614
|
+
{
|
|
615
|
+
id: 'demo',
|
|
616
|
+
adapter: 'stencil',
|
|
617
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
618
|
+
source: {
|
|
619
|
+
type: 'PACKAGE_NAME',
|
|
620
|
+
packageName: '@acme/stencil-lib',
|
|
621
|
+
},
|
|
622
|
+
outDir: './src/generated',
|
|
623
|
+
},
|
|
624
|
+
],
|
|
625
|
+
};
|
|
626
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
627
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-csr.generated.ts')));
|
|
628
|
+
expect(runtimeWrite).toBeDefined();
|
|
629
|
+
expect(runtimeWrite.content).toContain("import { defineCustomElements as runtimeDefineCustomElements } from '@acme/stencil-lib/loader';");
|
|
630
|
+
expect(runtimeWrite.content).toContain('export const useStencilClientSetup =');
|
|
631
|
+
expect(runtimeWrite.content).toContain('createStencilClientSetup(defineCustomElementsQrl);');
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
it('generates a server runtime bridge from resolved PACKAGE_NAME hydrate imports', async () => {
|
|
635
|
+
await withTempDir(async (tempDir) => {
|
|
636
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
637
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
638
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
639
|
+
}), 'utf8');
|
|
640
|
+
await mkdir(path.join(fixturePackageRoot, 'loader'), { recursive: true });
|
|
641
|
+
await writeFile(path.join(fixturePackageRoot, 'loader', 'index.js'), 'export const defineCustomElements = () => undefined;\n', 'utf8');
|
|
642
|
+
await mkdir(path.join(fixturePackageRoot, 'hydrate'), {
|
|
643
|
+
recursive: true,
|
|
644
|
+
});
|
|
645
|
+
await writeFile(path.join(fixturePackageRoot, 'hydrate', 'index.js'), 'export const renderToString = async () => ({ html: "<app-root></app-root>" });\n', 'utf8');
|
|
646
|
+
const config = {
|
|
647
|
+
dryRun: true,
|
|
648
|
+
projects: [
|
|
649
|
+
{
|
|
650
|
+
id: 'demo',
|
|
651
|
+
adapter: 'stencil',
|
|
652
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
653
|
+
source: {
|
|
654
|
+
type: 'PACKAGE_NAME',
|
|
655
|
+
packageName: '@acme/stencil-lib',
|
|
656
|
+
},
|
|
657
|
+
outDir: './src/generated',
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
};
|
|
661
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
662
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-ssr.generated.ts')));
|
|
663
|
+
expect(runtimeWrite).toBeDefined();
|
|
664
|
+
expect(runtimeWrite.content).toContain("const hydrateModuleId = '@acme/stencil-lib/hydrate';");
|
|
665
|
+
expect(runtimeWrite.content).toContain('export const renderToString: StencilRenderToString = async (input, options) => {');
|
|
666
|
+
expect(runtimeWrite.content).toContain('/* @vite-ignore */ hydrateModuleId');
|
|
667
|
+
expect(runtimeWrite.content).toContain('return runtimeRenderToString(input, options);');
|
|
668
|
+
expect(runtimeWrite.content).toContain('export const GeneratedStencilComponent = createStencilSSRBridgeComponent(');
|
|
669
|
+
});
|
|
670
|
+
});
|
|
671
|
+
it('renders Stencil wrappers through the generated SSR component when hydrate runtime is available', async () => {
|
|
672
|
+
await withTempDir(async (tempDir) => {
|
|
673
|
+
const hydrateModulePath = path.join(tempDir, 'runtime', 'acme-hydrate.mjs');
|
|
674
|
+
await mkdir(path.dirname(hydrateModulePath), { recursive: true });
|
|
675
|
+
await writeFile(hydrateModulePath, 'export async function renderToString() { return { html: "" }; }\n', 'utf8');
|
|
676
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
677
|
+
modules: [
|
|
678
|
+
{
|
|
679
|
+
declarations: [
|
|
680
|
+
{
|
|
681
|
+
tagName: 'a-button',
|
|
682
|
+
attributes: [
|
|
683
|
+
{
|
|
684
|
+
name: 'size',
|
|
685
|
+
fieldName: 'size',
|
|
686
|
+
type: { text: '"lg" | "md" | "sm"' },
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
events: [
|
|
690
|
+
{
|
|
691
|
+
name: 'tripleClick',
|
|
692
|
+
type: { text: 'CustomEvent<MouseEvent>' },
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
slots: [
|
|
696
|
+
{
|
|
697
|
+
name: 'footer',
|
|
698
|
+
},
|
|
699
|
+
],
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
}, null, 2), 'utf8');
|
|
705
|
+
const config = {
|
|
706
|
+
dryRun: true,
|
|
707
|
+
projects: [
|
|
708
|
+
{
|
|
709
|
+
id: 'demo',
|
|
710
|
+
adapter: 'stencil',
|
|
711
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
712
|
+
source: {
|
|
713
|
+
type: 'CEM',
|
|
714
|
+
path: './custom-elements.json',
|
|
715
|
+
},
|
|
716
|
+
outDir: './src/generated',
|
|
717
|
+
adapterOptions: {
|
|
718
|
+
runtime: {
|
|
719
|
+
loaderImport: './runtime/acme-loader',
|
|
720
|
+
hydrateImport: pathToFileURL(hydrateModulePath).href,
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
],
|
|
725
|
+
};
|
|
726
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
727
|
+
expect(result.projects[0].ssrCapabilities.available).toBe(true);
|
|
728
|
+
expect(result.projects[0].ssrCapabilities.supportsSsrProbe).toBe(true);
|
|
729
|
+
expect(result.projects[0].ssrCapabilities.ssrRuntimeSubpath).toBe('./ssr');
|
|
730
|
+
expect(result.projects[0].ssrCapabilities.clientOnlyMode).toBeUndefined();
|
|
731
|
+
const runtimeSsrWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-ssr.generated.ts')));
|
|
732
|
+
const buttonWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'a-button.tsx')));
|
|
733
|
+
expect(runtimeSsrWrite).toBeDefined();
|
|
734
|
+
expect(buttonWrite).toBeDefined();
|
|
735
|
+
expect(buttonWrite.content).toContain("import { GeneratedStencilComponent } from './runtime';");
|
|
736
|
+
expect(buttonWrite.content).not.toContain('useStencilClientSetup();');
|
|
737
|
+
expect(buttonWrite.content).toContain(' const events: Record<string, QRL<(...args: any[]) => void>> = {};');
|
|
738
|
+
expect(buttonWrite.content).toContain(' const passthroughEventProps = Object.fromEntries(');
|
|
739
|
+
expect(buttonWrite.content).toContain(' ([key]) => !mappedEventPropKeys.has(key),');
|
|
740
|
+
expect(buttonWrite.content).toContain(" if (props.onTripleClick$) { events['tripleClick'] = props.onTripleClick$; }");
|
|
741
|
+
expect(buttonWrite.content).toContain(' tagName="a-button"');
|
|
742
|
+
expect(buttonWrite.content).toContain(' events={mappedEvents}');
|
|
743
|
+
expect(buttonWrite.content).toContain(' slots={["footer"]}');
|
|
744
|
+
expect(buttonWrite.content).toContain(' {...passthroughEventProps}');
|
|
745
|
+
expect(buttonWrite.content).toContain(' </GeneratedStencilComponent>');
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
it('generates a runtime barrel for resolved Stencil runtime modules', async () => {
|
|
749
|
+
await withTempDir(async (tempDir) => {
|
|
750
|
+
const fixturePackageRoot = await createFixturePackage(tempDir, '@acme/stencil-lib');
|
|
751
|
+
await writeFile(path.join(fixturePackageRoot, 'custom-elements.json'), JSON.stringify({
|
|
752
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
753
|
+
}), 'utf8');
|
|
754
|
+
await mkdir(path.join(fixturePackageRoot, 'loader'), { recursive: true });
|
|
755
|
+
await writeFile(path.join(fixturePackageRoot, 'loader', 'index.js'), 'export const defineCustomElements = () => undefined;\n', 'utf8');
|
|
756
|
+
await mkdir(path.join(fixturePackageRoot, 'hydrate'), {
|
|
757
|
+
recursive: true,
|
|
758
|
+
});
|
|
759
|
+
await writeFile(path.join(fixturePackageRoot, 'hydrate', 'index.js'), 'export const renderToString = async () => ({ html: "<app-root></app-root>" });\n', 'utf8');
|
|
760
|
+
const config = {
|
|
761
|
+
dryRun: true,
|
|
762
|
+
projects: [
|
|
763
|
+
{
|
|
764
|
+
id: 'demo',
|
|
765
|
+
adapter: 'stencil',
|
|
766
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
767
|
+
source: {
|
|
768
|
+
type: 'PACKAGE_NAME',
|
|
769
|
+
packageName: '@acme/stencil-lib',
|
|
770
|
+
},
|
|
771
|
+
outDir: './src/generated',
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
};
|
|
775
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
776
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime.ts')));
|
|
777
|
+
expect(runtimeWrite).toBeDefined();
|
|
778
|
+
expect(runtimeWrite.content).toContain("export * from './runtime-csr.generated';");
|
|
779
|
+
expect(runtimeWrite.content).toContain("export * from './runtime-ssr.generated';");
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
it('generates a client runtime bootstrap from explicit CEM loader imports', async () => {
|
|
783
|
+
await withTempDir(async (tempDir) => {
|
|
784
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
785
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
786
|
+
}), 'utf8');
|
|
787
|
+
const config = {
|
|
788
|
+
dryRun: true,
|
|
789
|
+
projects: [
|
|
790
|
+
{
|
|
791
|
+
id: 'demo',
|
|
792
|
+
adapter: 'stencil',
|
|
793
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
794
|
+
source: {
|
|
795
|
+
type: 'CEM',
|
|
796
|
+
path: './custom-elements.json',
|
|
797
|
+
},
|
|
798
|
+
outDir: './src/generated',
|
|
799
|
+
adapterOptions: {
|
|
800
|
+
runtime: {
|
|
801
|
+
loaderImport: './runtime/acme-loader',
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
};
|
|
807
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
808
|
+
const runtimeWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'runtime-csr.generated.ts')));
|
|
809
|
+
expect(runtimeWrite).toBeDefined();
|
|
810
|
+
expect(runtimeWrite.content).toContain("import { defineCustomElements as runtimeDefineCustomElements } from './runtime/acme-loader';");
|
|
811
|
+
expect(runtimeWrite.content).toContain("import { createStencilCSRComponent, createStencilClientSetup } from '@qwik-custom-elements/adapter-stencil/client';");
|
|
812
|
+
expect(runtimeWrite.content).toContain('export const useStencilClientSetup =');
|
|
813
|
+
expect(runtimeWrite.content).toContain('export const GeneratedStencilCSRComponent = createStencilCSRComponent();');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
it('loads the lit adapter SSR subpath package without fallback warning', async () => {
|
|
817
|
+
await withTempDir(async (tempDir) => {
|
|
818
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
819
|
+
modules: [{ declarations: [{ tagName: 'lit-button' }] }],
|
|
820
|
+
}), 'utf8');
|
|
821
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
822
|
+
config.projects[0].adapter = 'lit';
|
|
823
|
+
config.projects[0].adapterPackage =
|
|
824
|
+
'@qwik-custom-elements/adapter-lit/ssr';
|
|
825
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
826
|
+
const litWrapperWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'lit-button.tsx')));
|
|
827
|
+
expect(result.projects[0].status).toBe('success');
|
|
828
|
+
expect(result.projects[0].componentTags).toEqual(['lit-button']);
|
|
829
|
+
expect(result.projects[0].ssrCapabilities).toEqual({
|
|
830
|
+
available: true,
|
|
831
|
+
supportsSsrProbe: true,
|
|
832
|
+
ssrRuntimeSubpath: './ssr',
|
|
833
|
+
});
|
|
834
|
+
expect(result.projects[0].observedErrorCodes).toEqual([]);
|
|
835
|
+
const indexWrite = result.projects[0].plannedWrites.find((plannedWrite) => plannedWrite.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
836
|
+
expect(litWrapperWrite?.content).toContain('export const QwikLitButton = component$<QwikLitButtonProps>((props) => {');
|
|
837
|
+
expect(litWrapperWrite?.content).toContain(' <GeneratedLitComponent');
|
|
838
|
+
expect(litWrapperWrite?.content).not.toContain('QwikLitButtonSsrHtml');
|
|
839
|
+
expect(indexWrite?.content).toContain("export { QwikLitButton } from './lit-button';");
|
|
840
|
+
expect(indexWrite?.content).toContain("export const generatedMode = 'ssr' as const;");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
it('generates generatedMode csr constant via real adapter-lit root entrypoint', async () => {
|
|
844
|
+
await withTempDir(async (tempDir) => {
|
|
845
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
846
|
+
modules: [{ declarations: [{ tagName: 'lit-button' }] }],
|
|
847
|
+
}), 'utf8');
|
|
848
|
+
const config = {
|
|
849
|
+
dryRun: true,
|
|
850
|
+
projects: [
|
|
851
|
+
{
|
|
852
|
+
id: 'demo-lit-csr',
|
|
853
|
+
adapter: 'lit',
|
|
854
|
+
adapterPackage: '@qwik-custom-elements/adapter-lit',
|
|
855
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
856
|
+
outDir: './src/generated',
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
};
|
|
860
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
861
|
+
expect(result.projects[0].status).toBe('success');
|
|
862
|
+
expect(result.projects[0].ssrCapabilities).toEqual({
|
|
863
|
+
available: false,
|
|
864
|
+
supportsSsrProbe: true,
|
|
865
|
+
ssrRuntimeSubpath: './ssr',
|
|
866
|
+
});
|
|
867
|
+
expect(result.projects[0].observedErrorCodes).toContain('QCE_SSR_UNSUPPORTED_FALLBACK');
|
|
868
|
+
const indexWrite = result.projects[0].plannedWrites.find((w) => w.path.endsWith(path.join('src', 'generated', 'index.ts')));
|
|
869
|
+
expect(indexWrite?.content).toContain("export const generatedMode = 'csr' as const;");
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
it('fails when adapter capabilities do not support the project source type', async () => {
|
|
873
|
+
await withTempDir(async (tempDir) => {
|
|
874
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
875
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
876
|
+
}), 'utf8');
|
|
877
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
878
|
+
'export const metadata = {',
|
|
879
|
+
" id: 'custom-adapter',",
|
|
880
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
881
|
+
'};',
|
|
882
|
+
'',
|
|
883
|
+
'export function createGeneratedOutput() {',
|
|
884
|
+
' return [];',
|
|
885
|
+
'}',
|
|
886
|
+
'',
|
|
887
|
+
].join('\n'), 'utf8');
|
|
888
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
889
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
890
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
891
|
+
code: 'QCE_ADAPTER_SOURCE_INCOMPATIBLE',
|
|
892
|
+
message: 'Project "demo" source type "CEM" is not supported by adapter "./adapter-package-name-only.mjs".',
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
it('discovers CEM for PACKAGE_NAME mode when one canonical candidate exists', async () => {
|
|
897
|
+
await withTempDir(async (tempDir) => {
|
|
898
|
+
const packageName = '@demo/components';
|
|
899
|
+
const packageRoot = await createFixturePackage(tempDir, packageName);
|
|
900
|
+
await writeFile(path.join(packageRoot, 'custom-elements.json'), JSON.stringify({
|
|
901
|
+
modules: [{ declarations: [{ tagName: 'pkg-button' }] }],
|
|
902
|
+
}), 'utf8');
|
|
903
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
904
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
905
|
+
config.projects[0].source = {
|
|
906
|
+
type: 'PACKAGE_NAME',
|
|
907
|
+
packageName,
|
|
908
|
+
};
|
|
909
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
910
|
+
'export const metadata = {',
|
|
911
|
+
" id: 'custom-adapter',",
|
|
912
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
913
|
+
'};',
|
|
914
|
+
'',
|
|
915
|
+
'export function createGeneratedOutput() {',
|
|
916
|
+
' return [];',
|
|
917
|
+
'}',
|
|
918
|
+
'',
|
|
919
|
+
].join('\n'), 'utf8');
|
|
920
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
921
|
+
expect(result.projects[0].componentTags).toEqual(['pkg-button']);
|
|
922
|
+
expect(result.projects[0].sourcePath).toBe(path.join(packageRoot, 'custom-elements.json'));
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
it('fails with remediation guidance when PACKAGE_NAME CEM discovery finds no candidates', async () => {
|
|
926
|
+
await withTempDir(async (tempDir) => {
|
|
927
|
+
const packageName = '@demo/components';
|
|
928
|
+
await createFixturePackage(tempDir, packageName);
|
|
929
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
930
|
+
'export const metadata = {',
|
|
931
|
+
" id: 'custom-adapter',",
|
|
932
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
933
|
+
'};',
|
|
934
|
+
'',
|
|
935
|
+
].join('\n'), 'utf8');
|
|
936
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
937
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
938
|
+
config.projects[0].source = {
|
|
939
|
+
type: 'PACKAGE_NAME',
|
|
940
|
+
packageName,
|
|
941
|
+
};
|
|
942
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
943
|
+
code: 'QCE_PACKAGE_NAME_CEM_NOT_FOUND',
|
|
944
|
+
message: expect.stringContaining('Set source.cemPath'),
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
it('fails deterministically when PACKAGE_NAME CEM discovery is ambiguous', async () => {
|
|
949
|
+
await withTempDir(async (tempDir) => {
|
|
950
|
+
const packageName = '@demo/components';
|
|
951
|
+
const packageRoot = await createFixturePackage(tempDir, packageName);
|
|
952
|
+
await writeFile(path.join(packageRoot, 'custom-elements.json'), JSON.stringify({ modules: [] }), 'utf8');
|
|
953
|
+
await mkdir(path.join(packageRoot, 'dist'), { recursive: true });
|
|
954
|
+
await writeFile(path.join(packageRoot, 'dist', 'custom-elements.json'), JSON.stringify({ modules: [] }), 'utf8');
|
|
955
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
956
|
+
'export const metadata = {',
|
|
957
|
+
" id: 'custom-adapter',",
|
|
958
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
959
|
+
'};',
|
|
960
|
+
'',
|
|
961
|
+
].join('\n'), 'utf8');
|
|
962
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
963
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
964
|
+
config.projects[0].source = {
|
|
965
|
+
type: 'PACKAGE_NAME',
|
|
966
|
+
packageName,
|
|
967
|
+
};
|
|
968
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
969
|
+
code: 'QCE_PACKAGE_NAME_CEM_AMBIGUOUS',
|
|
970
|
+
message: expect.stringContaining('Set source.cemPath'),
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
it('rejects absolute PACKAGE_NAME cemPath override values', async () => {
|
|
975
|
+
await withTempDir(async (tempDir) => {
|
|
976
|
+
const packageName = '@demo/components';
|
|
977
|
+
const packageRoot = await createFixturePackage(tempDir, packageName);
|
|
978
|
+
const absoluteCemPath = path.join(packageRoot, 'custom-elements.json');
|
|
979
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
980
|
+
'export const metadata = {',
|
|
981
|
+
" id: 'custom-adapter',",
|
|
982
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
983
|
+
'};',
|
|
984
|
+
'',
|
|
985
|
+
].join('\n'), 'utf8');
|
|
986
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
987
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
988
|
+
config.projects[0].source = {
|
|
989
|
+
type: 'PACKAGE_NAME',
|
|
990
|
+
packageName,
|
|
991
|
+
cemPath: absoluteCemPath,
|
|
992
|
+
};
|
|
993
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
994
|
+
code: 'QCE_PACKAGE_NAME_CEM_PATH_ABSOLUTE',
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
it('rejects PACKAGE_NAME cemPath overrides that escape package root', async () => {
|
|
999
|
+
await withTempDir(async (tempDir) => {
|
|
1000
|
+
const packageName = '@demo/components';
|
|
1001
|
+
await createFixturePackage(tempDir, packageName);
|
|
1002
|
+
await writeFile(path.join(tempDir, 'adapter-package-name-only.mjs'), [
|
|
1003
|
+
'export const metadata = {',
|
|
1004
|
+
" id: 'custom-adapter',",
|
|
1005
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
1006
|
+
'};',
|
|
1007
|
+
'',
|
|
1008
|
+
].join('\n'), 'utf8');
|
|
1009
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1010
|
+
config.projects[0].adapterPackage = './adapter-package-name-only.mjs';
|
|
1011
|
+
config.projects[0].source = {
|
|
1012
|
+
type: 'PACKAGE_NAME',
|
|
1013
|
+
packageName,
|
|
1014
|
+
cemPath: '../outside/custom-elements.json',
|
|
1015
|
+
};
|
|
1016
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1017
|
+
code: 'QCE_PACKAGE_NAME_CEM_PATH_OUTSIDE_PACKAGE',
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
it('fails with deterministic code when CEM shape is invalid', async () => {
|
|
1022
|
+
await withTempDir(async (tempDir) => {
|
|
1023
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({ schemaVersion: '1.0.0' }), 'utf8');
|
|
1024
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1025
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1026
|
+
code: 'QCE_CEM_INVALID_SHAPE',
|
|
1027
|
+
message: 'CEM at ' +
|
|
1028
|
+
path.join(tempDir, 'custom-elements.json') +
|
|
1029
|
+
' must include a "modules" array.',
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
it('fails when a module declarations field is not an array', async () => {
|
|
1034
|
+
await withTempDir(async (tempDir) => {
|
|
1035
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({ modules: [{ declarations: 'not-an-array' }] }), 'utf8');
|
|
1036
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1037
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1038
|
+
code: 'QCE_CEM_INVALID_SHAPE',
|
|
1039
|
+
message: 'CEM at ' +
|
|
1040
|
+
path.join(tempDir, 'custom-elements.json') +
|
|
1041
|
+
' has invalid shape: modules[0].declarations must be an array when provided.',
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
it('fails when declaration tagName is present but not a non-empty string', async () => {
|
|
1046
|
+
await withTempDir(async (tempDir) => {
|
|
1047
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({ modules: [{ declarations: [{ tagName: ' ' }] }] }), 'utf8');
|
|
1048
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1049
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1050
|
+
code: 'QCE_CEM_INVALID_SHAPE',
|
|
1051
|
+
message: 'CEM at ' +
|
|
1052
|
+
path.join(tempDir, 'custom-elements.json') +
|
|
1053
|
+
' has invalid shape: modules[0].declarations[0].tagName must be a non-empty string when provided.',
|
|
1054
|
+
});
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
it('fails with deterministic diagnostics when targeted project id does not exist', async () => {
|
|
1058
|
+
await withTempDir(async (tempDir) => {
|
|
1059
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
1060
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
1061
|
+
}), 'utf8');
|
|
1062
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1063
|
+
await expect(generateFromConfig(config, {
|
|
1064
|
+
cwd: tempDir,
|
|
1065
|
+
targetProjectIds: ['unknown-project'],
|
|
1066
|
+
})).rejects.toMatchObject({
|
|
1067
|
+
code: 'QCE_PROJECT_TARGET_UNKNOWN',
|
|
1068
|
+
message: 'Unknown project id "unknown-project" requested via CLI targeting.',
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
it('executes multiple projects sequentially in deterministic id order', async () => {
|
|
1073
|
+
await withTempDir(async (tempDir) => {
|
|
1074
|
+
await writeFile(path.join(tempDir, 'custom-elements-a.json'), JSON.stringify({
|
|
1075
|
+
modules: [{ declarations: [{ tagName: 'alpha-card' }] }],
|
|
1076
|
+
}), 'utf8');
|
|
1077
|
+
await writeFile(path.join(tempDir, 'custom-elements-z.json'), JSON.stringify({
|
|
1078
|
+
modules: [{ declarations: [{ tagName: 'zeta-card' }] }],
|
|
1079
|
+
}), 'utf8');
|
|
1080
|
+
const config = {
|
|
1081
|
+
dryRun: true,
|
|
1082
|
+
projects: [
|
|
1083
|
+
{
|
|
1084
|
+
id: 'z-project',
|
|
1085
|
+
adapter: 'stencil',
|
|
1086
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1087
|
+
source: { type: 'CEM', path: './custom-elements-z.json' },
|
|
1088
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1089
|
+
outDir: './src/generated/z',
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
id: 'a-project',
|
|
1093
|
+
adapter: 'stencil',
|
|
1094
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1095
|
+
source: { type: 'CEM', path: './custom-elements-a.json' },
|
|
1096
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1097
|
+
outDir: './src/generated/a',
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
1102
|
+
expect(result.projects.map((project) => project.projectId)).toEqual([
|
|
1103
|
+
'a-project',
|
|
1104
|
+
'z-project',
|
|
1105
|
+
]);
|
|
1106
|
+
expect(result.projects[0].componentTags).toEqual(['alpha-card']);
|
|
1107
|
+
expect(result.projects[1].componentTags).toEqual(['zeta-card']);
|
|
1108
|
+
});
|
|
1109
|
+
});
|
|
1110
|
+
it('executes multiple projects in parallel mode with deterministic id ordering', async () => {
|
|
1111
|
+
await withTempDir(async (tempDir) => {
|
|
1112
|
+
await writeFile(path.join(tempDir, 'custom-elements-a.json'), JSON.stringify({
|
|
1113
|
+
modules: [{ declarations: [{ tagName: 'alpha-card' }] }],
|
|
1114
|
+
}), 'utf8');
|
|
1115
|
+
await writeFile(path.join(tempDir, 'custom-elements-z.json'), JSON.stringify({
|
|
1116
|
+
modules: [{ declarations: [{ tagName: 'zeta-card' }] }],
|
|
1117
|
+
}), 'utf8');
|
|
1118
|
+
const config = {
|
|
1119
|
+
dryRun: true,
|
|
1120
|
+
parallel: true,
|
|
1121
|
+
projects: [
|
|
1122
|
+
{
|
|
1123
|
+
id: 'z-project',
|
|
1124
|
+
adapter: 'stencil',
|
|
1125
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1126
|
+
source: { type: 'CEM', path: './custom-elements-z.json' },
|
|
1127
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1128
|
+
outDir: './src/generated/z',
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
id: 'a-project',
|
|
1132
|
+
adapter: 'stencil',
|
|
1133
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1134
|
+
source: { type: 'CEM', path: './custom-elements-a.json' },
|
|
1135
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1136
|
+
outDir: './src/generated/a',
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
};
|
|
1140
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
1141
|
+
expect(result.projects.map((project) => project.projectId)).toEqual([
|
|
1142
|
+
'a-project',
|
|
1143
|
+
'z-project',
|
|
1144
|
+
]);
|
|
1145
|
+
expect(result.projects[0].componentTags).toEqual(['alpha-card']);
|
|
1146
|
+
expect(result.projects[1].componentTags).toEqual(['zeta-card']);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
it('aggregates all project failures in parallel mode', async () => {
|
|
1150
|
+
await withTempDir(async (tempDir) => {
|
|
1151
|
+
await writeFile(path.join(tempDir, 'custom-elements-valid.json'), JSON.stringify({
|
|
1152
|
+
modules: [{ declarations: [{ tagName: 'alpha-card' }] }],
|
|
1153
|
+
}), 'utf8');
|
|
1154
|
+
const config = {
|
|
1155
|
+
dryRun: true,
|
|
1156
|
+
parallel: true,
|
|
1157
|
+
projects: [
|
|
1158
|
+
{
|
|
1159
|
+
id: 'b-missing-project',
|
|
1160
|
+
adapter: 'stencil',
|
|
1161
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1162
|
+
source: { type: 'CEM', path: './custom-elements-missing-b.json' },
|
|
1163
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1164
|
+
outDir: './src/generated/b',
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
id: 'a-valid-project',
|
|
1168
|
+
adapter: 'stencil',
|
|
1169
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1170
|
+
source: { type: 'CEM', path: './custom-elements-valid.json' },
|
|
1171
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1172
|
+
outDir: './src/generated/a',
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
id: 'c-missing-project',
|
|
1176
|
+
adapter: 'stencil',
|
|
1177
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1178
|
+
source: { type: 'CEM', path: './custom-elements-missing-c.json' },
|
|
1179
|
+
adapterOptions: validStencilAdapterOptions,
|
|
1180
|
+
outDir: './src/generated/c',
|
|
1181
|
+
},
|
|
1182
|
+
],
|
|
1183
|
+
};
|
|
1184
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1185
|
+
code: 'QCE_PARALLEL_PROJECT_FAILURES',
|
|
1186
|
+
message: expect.stringContaining('b-missing-project'),
|
|
1187
|
+
});
|
|
1188
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1189
|
+
message: expect.stringContaining('c-missing-project'),
|
|
1190
|
+
});
|
|
1191
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1192
|
+
message: expect.stringContaining('QCE_CEM_READ_FAILED'),
|
|
1193
|
+
});
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
it('rejects output paths that resolve outside the workspace root', async () => {
|
|
1197
|
+
await withTempDir(async (tempDir) => {
|
|
1198
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
1199
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
1200
|
+
}), 'utf8');
|
|
1201
|
+
const config = createSingleProjectConfig(tempDir, true);
|
|
1202
|
+
config.projects[0].outDir = '../outside-generated';
|
|
1203
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1204
|
+
code: 'QCE_OUTPUT_OUTSIDE_WORKSPACE',
|
|
1205
|
+
message: 'Project "demo" output path resolves outside workspace root: ../outside-generated',
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
});
|
|
1209
|
+
it('fails when multiple projects resolve to the same output directory', async () => {
|
|
1210
|
+
await withTempDir(async (tempDir) => {
|
|
1211
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
1212
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
1213
|
+
}), 'utf8');
|
|
1214
|
+
const config = {
|
|
1215
|
+
dryRun: true,
|
|
1216
|
+
projects: [
|
|
1217
|
+
{
|
|
1218
|
+
id: 'a-project',
|
|
1219
|
+
adapter: 'stencil',
|
|
1220
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1221
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
1222
|
+
outDir: './src/generated',
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
id: 'b-project',
|
|
1226
|
+
adapter: 'stencil',
|
|
1227
|
+
adapterPackage: '@qwik-custom-elements/adapter-stencil',
|
|
1228
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
1229
|
+
outDir: './src/../src/generated',
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
};
|
|
1233
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
1234
|
+
code: 'QCE_OUTPUT_PATH_COLLISION',
|
|
1235
|
+
message: 'Projects "a-project" and "b-project" resolve to the same output directory: ' +
|
|
1236
|
+
path.resolve(tempDir, 'src/generated'),
|
|
1237
|
+
});
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
describe('augmentComponentDefinitions adapter hook', () => {
|
|
1241
|
+
it('calls augmentComponentDefinitions when exported and passes enriched slots to createGeneratedOutput', async () => {
|
|
1242
|
+
await withTempDir(async (tempDir) => {
|
|
1243
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
1244
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
1245
|
+
}), 'utf8');
|
|
1246
|
+
await writeFile(path.join(tempDir, 'adapter-with-augment.mjs'), [
|
|
1247
|
+
'export const metadata = {',
|
|
1248
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
1249
|
+
' supportsSsrProbe: false,',
|
|
1250
|
+
' ssrRuntimeSubpath: null,',
|
|
1251
|
+
'};',
|
|
1252
|
+
'',
|
|
1253
|
+
'export async function augmentComponentDefinitions({ componentDefinitions }) {',
|
|
1254
|
+
' return componentDefinitions.map((def) => ({',
|
|
1255
|
+
' ...def,',
|
|
1256
|
+
" slots: [...def.slots, { name: 'probe-injected' }],",
|
|
1257
|
+
' }));',
|
|
1258
|
+
'}',
|
|
1259
|
+
'',
|
|
1260
|
+
'export function createGeneratedOutput({ componentDefinitions }) {',
|
|
1261
|
+
' return componentDefinitions.map((def) => ({',
|
|
1262
|
+
' relativePath: `${def.tagName}.txt`,',
|
|
1263
|
+
' content: def.slots.map((s) => s.name).join(","),',
|
|
1264
|
+
' }));',
|
|
1265
|
+
'}',
|
|
1266
|
+
'',
|
|
1267
|
+
].join('\n'), 'utf8');
|
|
1268
|
+
const config = {
|
|
1269
|
+
dryRun: true,
|
|
1270
|
+
projects: [
|
|
1271
|
+
{
|
|
1272
|
+
id: 'demo',
|
|
1273
|
+
adapter: 'stencil',
|
|
1274
|
+
adapterPackage: `${pathToFileURL(path.join(tempDir, 'adapter-with-augment.mjs'))}`,
|
|
1275
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
1276
|
+
outDir: './src/generated',
|
|
1277
|
+
},
|
|
1278
|
+
],
|
|
1279
|
+
};
|
|
1280
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
1281
|
+
expect(result.projects[0].plannedWrites).toEqual([
|
|
1282
|
+
expect.objectContaining({
|
|
1283
|
+
path: path.join(tempDir, 'src', 'generated', 'app-root.txt'),
|
|
1284
|
+
content: 'probe-injected',
|
|
1285
|
+
}),
|
|
1286
|
+
]);
|
|
1287
|
+
});
|
|
1288
|
+
});
|
|
1289
|
+
it('skips augmentComponentDefinitions when adapter does not export it', async () => {
|
|
1290
|
+
await withTempDir(async (tempDir) => {
|
|
1291
|
+
await writeFile(path.join(tempDir, 'custom-elements.json'), JSON.stringify({
|
|
1292
|
+
modules: [{ declarations: [{ tagName: 'app-root' }] }],
|
|
1293
|
+
}), 'utf8');
|
|
1294
|
+
await writeFile(path.join(tempDir, 'adapter-without-augment.mjs'), [
|
|
1295
|
+
'export const metadata = {',
|
|
1296
|
+
" supportedSourceTypes: ['CEM', 'PACKAGE_NAME'],",
|
|
1297
|
+
' supportsSsrProbe: false,',
|
|
1298
|
+
' ssrRuntimeSubpath: null,',
|
|
1299
|
+
'};',
|
|
1300
|
+
'',
|
|
1301
|
+
'export function createGeneratedOutput({ componentDefinitions }) {',
|
|
1302
|
+
' return componentDefinitions.map((def) => ({',
|
|
1303
|
+
' relativePath: `${def.tagName}.txt`,',
|
|
1304
|
+
' content: def.slots.map((s) => s.name).join(","),',
|
|
1305
|
+
' }));',
|
|
1306
|
+
'}',
|
|
1307
|
+
'',
|
|
1308
|
+
].join('\n'), 'utf8');
|
|
1309
|
+
const config = {
|
|
1310
|
+
dryRun: true,
|
|
1311
|
+
projects: [
|
|
1312
|
+
{
|
|
1313
|
+
id: 'demo',
|
|
1314
|
+
adapter: 'stencil',
|
|
1315
|
+
adapterPackage: `${pathToFileURL(path.join(tempDir, 'adapter-without-augment.mjs'))}`,
|
|
1316
|
+
source: { type: 'CEM', path: './custom-elements.json' },
|
|
1317
|
+
outDir: './src/generated',
|
|
1318
|
+
},
|
|
1319
|
+
],
|
|
1320
|
+
};
|
|
1321
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
1322
|
+
expect(result.projects[0].plannedWrites).toEqual([
|
|
1323
|
+
expect.objectContaining({
|
|
1324
|
+
path: path.join(tempDir, 'src', 'generated', 'app-root.txt'),
|
|
1325
|
+
content: '',
|
|
1326
|
+
}),
|
|
1327
|
+
]);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
});
|