@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,720 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
export class GenerationError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'GenerationError';
|
|
11
|
+
this.code = code;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const SSR_UNSUPPORTED_FALLBACK_CODE = 'QCE_SSR_UNSUPPORTED_FALLBACK';
|
|
15
|
+
const PACKAGE_NAME_CEM_DISCOVERY_CANDIDATES = [
|
|
16
|
+
'custom-elements.json',
|
|
17
|
+
path.join('dist', 'custom-elements.json'),
|
|
18
|
+
];
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
export async function generateFromConfig(config, options = {}) {
|
|
21
|
+
const cwd = options.cwd ?? process.cwd();
|
|
22
|
+
const requestedProjectIds = options.targetProjectIds ?? [];
|
|
23
|
+
const requestedProjectIdSet = new Set(requestedProjectIds);
|
|
24
|
+
const sortedProjects = [...config.projects].sort((a, b) => a.id.localeCompare(b.id));
|
|
25
|
+
for (const requestedProjectId of requestedProjectIds) {
|
|
26
|
+
if (!config.projects.some((project) => project.id === requestedProjectId)) {
|
|
27
|
+
throw new GenerationError('QCE_PROJECT_TARGET_UNKNOWN', `Unknown project id "${requestedProjectId}" requested via CLI targeting.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const filteredProjects = requestedProjectIdSet.size === 0
|
|
31
|
+
? sortedProjects
|
|
32
|
+
: sortedProjects.filter((project) => requestedProjectIdSet.has(project.id));
|
|
33
|
+
validateProjectOutputSafety(filteredProjects, cwd);
|
|
34
|
+
const projectResults = config.parallel === true
|
|
35
|
+
? await generateProjectsInParallel(filteredProjects, cwd, config.dryRun === true)
|
|
36
|
+
: await generateProjectsSequentially(filteredProjects, cwd, config.dryRun === true);
|
|
37
|
+
const skippedProjectResults = requestedProjectIdSet.size === 0
|
|
38
|
+
? []
|
|
39
|
+
: sortedProjects
|
|
40
|
+
.filter((project) => !requestedProjectIdSet.has(project.id))
|
|
41
|
+
.map((project) => createSkippedProjectResult(project, cwd));
|
|
42
|
+
return {
|
|
43
|
+
dryRun: config.dryRun === true,
|
|
44
|
+
projects: [...projectResults, ...skippedProjectResults].sort((a, b) => a.projectId.localeCompare(b.projectId)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function generateProjectsSequentially(projects, cwd, dryRun) {
|
|
48
|
+
const projectResults = [];
|
|
49
|
+
for (const project of projects) {
|
|
50
|
+
const result = await generateProject(project, cwd, dryRun);
|
|
51
|
+
projectResults.push(result);
|
|
52
|
+
}
|
|
53
|
+
return projectResults;
|
|
54
|
+
}
|
|
55
|
+
async function generateProjectsInParallel(projects, cwd, dryRun) {
|
|
56
|
+
const settledResults = await Promise.allSettled(projects.map((project) => generateProject(project, cwd, dryRun)));
|
|
57
|
+
const projectResults = [];
|
|
58
|
+
const failures = [];
|
|
59
|
+
for (let index = 0; index < settledResults.length; index += 1) {
|
|
60
|
+
const settledResult = settledResults[index];
|
|
61
|
+
const project = projects[index];
|
|
62
|
+
if (settledResult.status === 'fulfilled') {
|
|
63
|
+
projectResults.push(settledResult.value);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
failures.push({
|
|
67
|
+
projectId: project.id,
|
|
68
|
+
error: settledResult.reason,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (failures.length > 0) {
|
|
72
|
+
const details = failures
|
|
73
|
+
.map(({ projectId, error }) => {
|
|
74
|
+
const errorCode = error instanceof GenerationError ? error.code : 'QCE_UNEXPECTED';
|
|
75
|
+
const errorMessage = toErrorMessage(error);
|
|
76
|
+
return `${projectId} (${errorCode}): ${errorMessage}`;
|
|
77
|
+
})
|
|
78
|
+
.join(' | ');
|
|
79
|
+
throw new GenerationError('QCE_PARALLEL_PROJECT_FAILURES', `Parallel generation failed for ${failures.length} project(s): ${details}`);
|
|
80
|
+
}
|
|
81
|
+
return projectResults;
|
|
82
|
+
}
|
|
83
|
+
async function generateProject(project, cwd, dryRun) {
|
|
84
|
+
const startedAtMs = Date.now();
|
|
85
|
+
const adapterModule = await loadAdapterModule(project.adapterPackage, cwd);
|
|
86
|
+
validateAdapterSourceCompatibility(project, adapterModule);
|
|
87
|
+
await validateAdapterProject(project, adapterModule);
|
|
88
|
+
const runtimeImportResult = await resolveAdapterRuntimeImports(project, adapterModule, cwd);
|
|
89
|
+
const { runtimeImports } = runtimeImportResult;
|
|
90
|
+
const sourcePath = resolveProjectSourcePath(project.id, project.source, cwd);
|
|
91
|
+
const outDirPath = path.resolve(cwd, project.outDir);
|
|
92
|
+
const componentDefinitions = await readComponentDefinitionsFromCem(sourcePath);
|
|
93
|
+
const augmentedComponentDefinitions = await augmentAdapterComponentDefinitions(componentDefinitions, adapterModule, runtimeImports, outDirPath, cwd);
|
|
94
|
+
const componentTags = augmentedComponentDefinitions.map((componentDefinition) => componentDefinition.tagName);
|
|
95
|
+
const adapterSsrCapabilities = resolveAdapterSsrCapabilities(adapterModule);
|
|
96
|
+
const ssrProbe = await probeProjectSsrAvailability(project, adapterModule, runtimeImports);
|
|
97
|
+
const adapterPlannedWrites = await createAdapterPlannedWrites(project, outDirPath, augmentedComponentDefinitions, adapterModule, runtimeImports, ssrProbe.available);
|
|
98
|
+
const plannedWrites = createPlannedWrites(project.id, outDirPath, augmentedComponentDefinitions, adapterModule, ssrProbe.available, adapterPlannedWrites);
|
|
99
|
+
const observedErrorCodes = [...runtimeImportResult.observedErrorCodes];
|
|
100
|
+
if (!ssrProbe.available) {
|
|
101
|
+
observedErrorCodes.push(SSR_UNSUPPORTED_FALLBACK_CODE);
|
|
102
|
+
}
|
|
103
|
+
observedErrorCodes.sort((a, b) => a.localeCompare(b));
|
|
104
|
+
if (!dryRun) {
|
|
105
|
+
await mkdir(outDirPath, { recursive: true });
|
|
106
|
+
for (const plannedWrite of plannedWrites) {
|
|
107
|
+
await writeFile(plannedWrite.path, plannedWrite.content, 'utf8');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const hasClientOnlySuccessSignal = runtimeImportResult.clientOnlyMode === true &&
|
|
111
|
+
!ssrProbe.available &&
|
|
112
|
+
plannedWrites.length > 0;
|
|
113
|
+
const generatedIndexPath = path.join(outDirPath, 'index.ts');
|
|
114
|
+
const durationMs = Date.now() - startedAtMs;
|
|
115
|
+
return {
|
|
116
|
+
projectId: project.id,
|
|
117
|
+
status: 'success',
|
|
118
|
+
durationMs,
|
|
119
|
+
adapterPackage: project.adapterPackage,
|
|
120
|
+
sourcePath,
|
|
121
|
+
outDirPath,
|
|
122
|
+
generatedIndexPath,
|
|
123
|
+
componentTags,
|
|
124
|
+
plannedWrites,
|
|
125
|
+
wroteFiles: !dryRun,
|
|
126
|
+
ssrCapabilities: {
|
|
127
|
+
available: ssrProbe.available,
|
|
128
|
+
supportsSsrProbe: adapterSsrCapabilities.supportsSsrProbe,
|
|
129
|
+
ssrRuntimeSubpath: adapterSsrCapabilities.ssrRuntimeSubpath,
|
|
130
|
+
...(hasClientOnlySuccessSignal ? { clientOnlyMode: true } : {}),
|
|
131
|
+
},
|
|
132
|
+
observedErrorCodes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function resolveAdapterSsrCapabilities(adapterModule) {
|
|
136
|
+
const metadata = adapterModule != null && typeof adapterModule.metadata === 'object'
|
|
137
|
+
? adapterModule.metadata
|
|
138
|
+
: undefined;
|
|
139
|
+
const supportsSsrProbe = metadata?.supportsSsrProbe === true;
|
|
140
|
+
const ssrRuntimeSubpath = typeof metadata?.ssrRuntimeSubpath === 'string'
|
|
141
|
+
? metadata.ssrRuntimeSubpath
|
|
142
|
+
: null;
|
|
143
|
+
return {
|
|
144
|
+
supportsSsrProbe,
|
|
145
|
+
ssrRuntimeSubpath,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function augmentAdapterComponentDefinitions(componentDefinitions, adapterModule, runtimeImports, outDirPath, cwd) {
|
|
149
|
+
const augmentFn = adapterModule != null &&
|
|
150
|
+
typeof adapterModule.augmentComponentDefinitions === 'function'
|
|
151
|
+
? adapterModule.augmentComponentDefinitions
|
|
152
|
+
: undefined;
|
|
153
|
+
if (augmentFn == null) {
|
|
154
|
+
return componentDefinitions;
|
|
155
|
+
}
|
|
156
|
+
const resolvedRuntimeImports = resolveProbeRuntimeImports(runtimeImports, outDirPath, cwd);
|
|
157
|
+
const result = (await augmentFn({
|
|
158
|
+
componentDefinitions,
|
|
159
|
+
runtimeImports: resolvedRuntimeImports,
|
|
160
|
+
}));
|
|
161
|
+
if (!Array.isArray(result)) {
|
|
162
|
+
return componentDefinitions;
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function resolveProbeRuntimeImports(runtimeImports, outDirPath, cwd) {
|
|
167
|
+
if (runtimeImports == null)
|
|
168
|
+
return undefined;
|
|
169
|
+
const hydrateImport = runtimeImports.hydrateImport;
|
|
170
|
+
if (typeof hydrateImport !== 'string' || hydrateImport.trim() === '') {
|
|
171
|
+
return runtimeImports;
|
|
172
|
+
}
|
|
173
|
+
const resolved = tryResolveSpecifier(hydrateImport, outDirPath, cwd);
|
|
174
|
+
if (resolved === hydrateImport)
|
|
175
|
+
return runtimeImports;
|
|
176
|
+
return { ...runtimeImports, hydrateImport: resolved };
|
|
177
|
+
}
|
|
178
|
+
function tryResolveSpecifier(specifier, outDirPath, cwd) {
|
|
179
|
+
if (specifier.startsWith('file://') ||
|
|
180
|
+
specifier.startsWith('.') ||
|
|
181
|
+
path.isAbsolute(specifier)) {
|
|
182
|
+
return specifier;
|
|
183
|
+
}
|
|
184
|
+
for (const dir of [outDirPath, cwd]) {
|
|
185
|
+
try {
|
|
186
|
+
const resolved = createRequire(path.join(dir, '__qce_probe__.cjs')).resolve(specifier);
|
|
187
|
+
return pathToFileURL(resolved).href;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// try next candidate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return specifier;
|
|
194
|
+
}
|
|
195
|
+
async function probeProjectSsrAvailability(project, adapterModule, runtimeImports) {
|
|
196
|
+
const probeSSR = adapterModule != null && typeof adapterModule.probeSSR === 'function'
|
|
197
|
+
? adapterModule.probeSSR
|
|
198
|
+
: undefined;
|
|
199
|
+
if (probeSSR == null) {
|
|
200
|
+
return { available: false };
|
|
201
|
+
}
|
|
202
|
+
const probeResult = (await probeSSR({
|
|
203
|
+
projectId: project.id,
|
|
204
|
+
source: project.source,
|
|
205
|
+
adapterOptions: project.adapterOptions ?? {},
|
|
206
|
+
runtimeImports,
|
|
207
|
+
}));
|
|
208
|
+
if (probeResult != null && probeResult.available === true) {
|
|
209
|
+
return { available: true };
|
|
210
|
+
}
|
|
211
|
+
return { available: false };
|
|
212
|
+
}
|
|
213
|
+
async function resolveAdapterRuntimeImports(project, adapterModule, cwd) {
|
|
214
|
+
const resolveRuntimeImports = adapterModule != null &&
|
|
215
|
+
typeof adapterModule.resolveRuntimeImports === 'function'
|
|
216
|
+
? adapterModule.resolveRuntimeImports
|
|
217
|
+
: undefined;
|
|
218
|
+
if (resolveRuntimeImports == null) {
|
|
219
|
+
return {
|
|
220
|
+
runtimeImports: undefined,
|
|
221
|
+
observedErrorCodes: [],
|
|
222
|
+
clientOnlyMode: false,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const resolvedRuntimeImports = await resolveRuntimeImports({
|
|
227
|
+
projectId: project.id,
|
|
228
|
+
cwd,
|
|
229
|
+
runtimeResolution: {
|
|
230
|
+
resolveSourcePackageRoot: (packageName) => resolvePackageRootForProject(project.id, { type: 'PACKAGE_NAME', packageName }, cwd),
|
|
231
|
+
resolveImportSpecifier: (specifier, packageRoot) => resolveRuntimeImportSpecifier(specifier, cwd, packageRoot),
|
|
232
|
+
},
|
|
233
|
+
source: project.source,
|
|
234
|
+
adapterOptions: project.adapterOptions ?? {},
|
|
235
|
+
});
|
|
236
|
+
const observedErrorCodes = extractObservedRuntimeImportErrorCodes(resolvedRuntimeImports);
|
|
237
|
+
const runtimeImports = extractResolvedRuntimeImports(resolvedRuntimeImports);
|
|
238
|
+
const clientOnlyMode = extractClientOnlyModeSignal(resolvedRuntimeImports);
|
|
239
|
+
return {
|
|
240
|
+
runtimeImports,
|
|
241
|
+
observedErrorCodes,
|
|
242
|
+
clientOnlyMode,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
if (error instanceof GenerationError) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
const errorCode = typeof error === 'object' &&
|
|
250
|
+
error !== null &&
|
|
251
|
+
'code' in error &&
|
|
252
|
+
typeof error.code === 'string'
|
|
253
|
+
? error.code
|
|
254
|
+
: 'QCE_ADAPTER_RUNTIME_IMPORTS_INVALID';
|
|
255
|
+
throw new GenerationError(errorCode, toErrorMessage(error));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function extractObservedRuntimeImportErrorCodes(resolvedRuntimeImports) {
|
|
259
|
+
if (resolvedRuntimeImports == null ||
|
|
260
|
+
typeof resolvedRuntimeImports !== 'object' ||
|
|
261
|
+
!('observedErrorCodes' in resolvedRuntimeImports) ||
|
|
262
|
+
!Array.isArray(resolvedRuntimeImports.observedErrorCodes)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
return resolvedRuntimeImports.observedErrorCodes.filter((errorCode) => typeof errorCode === 'string');
|
|
266
|
+
}
|
|
267
|
+
function extractResolvedRuntimeImports(resolvedRuntimeImports) {
|
|
268
|
+
if (resolvedRuntimeImports == null ||
|
|
269
|
+
typeof resolvedRuntimeImports !== 'object') {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if ('runtimeImports' in resolvedRuntimeImports &&
|
|
273
|
+
resolvedRuntimeImports.runtimeImports != null &&
|
|
274
|
+
typeof resolvedRuntimeImports.runtimeImports === 'object') {
|
|
275
|
+
return resolvedRuntimeImports.runtimeImports;
|
|
276
|
+
}
|
|
277
|
+
return resolvedRuntimeImports;
|
|
278
|
+
}
|
|
279
|
+
function extractClientOnlyModeSignal(resolvedRuntimeImports) {
|
|
280
|
+
if (resolvedRuntimeImports == null ||
|
|
281
|
+
typeof resolvedRuntimeImports !== 'object' ||
|
|
282
|
+
!('clientOnlyMode' in resolvedRuntimeImports)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
return resolvedRuntimeImports.clientOnlyMode === true;
|
|
286
|
+
}
|
|
287
|
+
function validateAdapterSourceCompatibility(project, adapterModule) {
|
|
288
|
+
const metadata = adapterModule != null && typeof adapterModule.metadata === 'object'
|
|
289
|
+
? adapterModule.metadata
|
|
290
|
+
: undefined;
|
|
291
|
+
const supportedSourceTypes = metadata != null && Array.isArray(metadata.supportedSourceTypes)
|
|
292
|
+
? metadata.supportedSourceTypes
|
|
293
|
+
: ['CEM'];
|
|
294
|
+
const isSourceSupported = supportedSourceTypes.some((supportedSourceType) => supportedSourceType === project.source.type);
|
|
295
|
+
if (!isSourceSupported) {
|
|
296
|
+
throw new GenerationError('QCE_ADAPTER_SOURCE_INCOMPATIBLE', `Project "${project.id}" source type "${project.source.type}" is not supported by adapter "${project.adapterPackage}".`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function validateAdapterProject(project, adapterModule) {
|
|
300
|
+
const validateProject = adapterModule != null && typeof adapterModule.validateProject === 'function'
|
|
301
|
+
? adapterModule.validateProject
|
|
302
|
+
: undefined;
|
|
303
|
+
if (validateProject == null) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
await validateProject({
|
|
308
|
+
projectId: project.id,
|
|
309
|
+
source: project.source,
|
|
310
|
+
adapterOptions: project.adapterOptions ?? {},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (error instanceof GenerationError) {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
const errorCode = typeof error === 'object' &&
|
|
318
|
+
error !== null &&
|
|
319
|
+
'code' in error &&
|
|
320
|
+
typeof error.code === 'string'
|
|
321
|
+
? error.code
|
|
322
|
+
: 'QCE_ADAPTER_PROJECT_INVALID';
|
|
323
|
+
throw new GenerationError(errorCode, toErrorMessage(error));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function loadAdapterModule(adapterPackage, cwd) {
|
|
327
|
+
if (isRelativeOrAbsoluteSpecifier(adapterPackage)) {
|
|
328
|
+
const resolvedPath = path.isAbsolute(adapterPackage)
|
|
329
|
+
? adapterPackage
|
|
330
|
+
: path.resolve(cwd, adapterPackage);
|
|
331
|
+
return (await import(pathToFileURL(resolvedPath).href));
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
return (await import(adapterPackage));
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
const workspaceLocalPath = resolveWorkspaceLocalAdapterPath(adapterPackage);
|
|
338
|
+
if (workspaceLocalPath == null) {
|
|
339
|
+
throw new GenerationError('QCE_ADAPTER_LOAD_FAILED', `Could not load adapter package "${adapterPackage}": ${toErrorMessage(error)}`);
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
return (await import(pathToFileURL(workspaceLocalPath).href));
|
|
343
|
+
}
|
|
344
|
+
catch (workspaceError) {
|
|
345
|
+
throw new GenerationError('QCE_ADAPTER_LOAD_FAILED', `Could not load adapter package "${adapterPackage}": ${toErrorMessage(workspaceError)}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function isRelativeOrAbsoluteSpecifier(specifier) {
|
|
350
|
+
return (specifier.startsWith('.') ||
|
|
351
|
+
specifier.startsWith('/') ||
|
|
352
|
+
path.isAbsolute(specifier));
|
|
353
|
+
}
|
|
354
|
+
function resolveWorkspaceLocalAdapterPath(adapterPackage) {
|
|
355
|
+
if (!adapterPackage.startsWith('@qwik-custom-elements/')) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
const [scope, packageDirectoryName, ...subpathSegments] = adapterPackage.split('/');
|
|
359
|
+
if (scope !== '@qwik-custom-elements' || !packageDirectoryName) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', packageDirectoryName);
|
|
363
|
+
const packageJsonPath = path.join(packageRoot, 'package.json');
|
|
364
|
+
const exportKey = subpathSegments.length === 0 ? '.' : `./${subpathSegments.join('/')}`;
|
|
365
|
+
try {
|
|
366
|
+
const packageJson = JSON.parse(require('node:fs').readFileSync(packageJsonPath, 'utf8'));
|
|
367
|
+
const exportTarget = resolvePackageExportImportTarget(packageJson.exports?.[exportKey]);
|
|
368
|
+
if (typeof exportTarget === 'string') {
|
|
369
|
+
return path.resolve(packageRoot, exportTarget);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// Fall through to legacy dist-path heuristics.
|
|
374
|
+
}
|
|
375
|
+
const subpath = subpathSegments.length === 0
|
|
376
|
+
? path.join('dist', 'index.js')
|
|
377
|
+
: path.join('dist', subpathSegments.join('/'));
|
|
378
|
+
const subpathWithExtension = path.extname(subpath) === '' ? `${subpath}.js` : subpath;
|
|
379
|
+
return path.resolve(packageRoot, subpathWithExtension);
|
|
380
|
+
}
|
|
381
|
+
function resolvePackageExportImportTarget(exportValue) {
|
|
382
|
+
if (typeof exportValue === 'string') {
|
|
383
|
+
return exportValue;
|
|
384
|
+
}
|
|
385
|
+
if (exportValue == null || typeof exportValue !== 'object') {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
const exportRecord = exportValue;
|
|
389
|
+
if (typeof exportRecord.import === 'string') {
|
|
390
|
+
return exportRecord.import;
|
|
391
|
+
}
|
|
392
|
+
if (typeof exportRecord.default === 'string') {
|
|
393
|
+
return exportRecord.default;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
function createSkippedProjectResult(project, cwd) {
|
|
398
|
+
const outDirPath = path.resolve(cwd, project.outDir);
|
|
399
|
+
return {
|
|
400
|
+
projectId: project.id,
|
|
401
|
+
status: 'skipped',
|
|
402
|
+
durationMs: 0,
|
|
403
|
+
adapterPackage: project.adapterPackage,
|
|
404
|
+
sourcePath: resolveSkippedProjectSourcePath(project.source, cwd),
|
|
405
|
+
outDirPath,
|
|
406
|
+
generatedIndexPath: path.join(outDirPath, 'index.ts'),
|
|
407
|
+
componentTags: [],
|
|
408
|
+
plannedWrites: [],
|
|
409
|
+
wroteFiles: false,
|
|
410
|
+
ssrCapabilities: {
|
|
411
|
+
available: false,
|
|
412
|
+
supportsSsrProbe: false,
|
|
413
|
+
ssrRuntimeSubpath: null,
|
|
414
|
+
},
|
|
415
|
+
observedErrorCodes: [],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function resolveSkippedProjectSourcePath(source, cwd) {
|
|
419
|
+
if (source.type === 'CEM') {
|
|
420
|
+
return path.resolve(cwd, source.path);
|
|
421
|
+
}
|
|
422
|
+
return source.cemPath == null
|
|
423
|
+
? `package:${source.packageName}`
|
|
424
|
+
: `package:${source.packageName}#${source.cemPath}`;
|
|
425
|
+
}
|
|
426
|
+
function resolveProjectSourcePath(projectId, source, cwd) {
|
|
427
|
+
if (source.type === 'CEM') {
|
|
428
|
+
return path.resolve(cwd, source.path);
|
|
429
|
+
}
|
|
430
|
+
const packageRoot = resolvePackageRootForProject(projectId, source, cwd);
|
|
431
|
+
if (source.cemPath != null) {
|
|
432
|
+
return resolvePackageNameOverrideCemPath(projectId, source, packageRoot);
|
|
433
|
+
}
|
|
434
|
+
return discoverPackageNameCemPath(projectId, source, packageRoot);
|
|
435
|
+
}
|
|
436
|
+
function resolvePackageRootForProject(projectId, source, cwd) {
|
|
437
|
+
const packageSpecifier = `${source.packageName}/package.json`;
|
|
438
|
+
try {
|
|
439
|
+
const packageJsonPath = require.resolve(packageSpecifier, { paths: [cwd] });
|
|
440
|
+
return path.dirname(packageJsonPath);
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
throw new GenerationError('QCE_PACKAGE_NAME_RESOLVE_FAILED', `Project "${projectId}" could not resolve source package "${source.packageName}" from ${cwd}: ${toErrorMessage(error)}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function resolvePackageNameOverrideCemPath(projectId, source, packageRoot) {
|
|
447
|
+
const cemPath = source.cemPath;
|
|
448
|
+
if (path.isAbsolute(cemPath)) {
|
|
449
|
+
throw new GenerationError('QCE_PACKAGE_NAME_CEM_PATH_ABSOLUTE', `Project "${projectId}" PACKAGE_NAME source.cemPath must be relative to package root; received absolute path "${cemPath}".`);
|
|
450
|
+
}
|
|
451
|
+
const resolvedCemPath = path.resolve(packageRoot, cemPath);
|
|
452
|
+
const relativeToPackageRoot = path.relative(packageRoot, resolvedCemPath);
|
|
453
|
+
const resolvesOutsidePackageRoot = relativeToPackageRoot === '..' ||
|
|
454
|
+
relativeToPackageRoot.startsWith(`..${path.sep}`) ||
|
|
455
|
+
path.isAbsolute(relativeToPackageRoot);
|
|
456
|
+
if (resolvesOutsidePackageRoot) {
|
|
457
|
+
throw new GenerationError('QCE_PACKAGE_NAME_CEM_PATH_OUTSIDE_PACKAGE', `Project "${projectId}" PACKAGE_NAME source.cemPath resolves outside package root: "${cemPath}".`);
|
|
458
|
+
}
|
|
459
|
+
return resolvedCemPath;
|
|
460
|
+
}
|
|
461
|
+
function discoverPackageNameCemPath(projectId, source, packageRoot) {
|
|
462
|
+
const candidatePaths = PACKAGE_NAME_CEM_DISCOVERY_CANDIDATES.map((candidate) => path.resolve(packageRoot, candidate));
|
|
463
|
+
const existingCandidates = [];
|
|
464
|
+
for (const candidatePath of candidatePaths) {
|
|
465
|
+
if (existsSync(candidatePath)) {
|
|
466
|
+
existingCandidates.push(candidatePath);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (existingCandidates.length === 1) {
|
|
470
|
+
return existingCandidates[0];
|
|
471
|
+
}
|
|
472
|
+
if (existingCandidates.length === 0) {
|
|
473
|
+
throw new GenerationError('QCE_PACKAGE_NAME_CEM_NOT_FOUND', `Project "${projectId}" could not discover a CEM file for source package "${source.packageName}". Checked: ${PACKAGE_NAME_CEM_DISCOVERY_CANDIDATES.join(', ')}. Set source.cemPath to the manifest path relative to the package root.`);
|
|
474
|
+
}
|
|
475
|
+
throw new GenerationError('QCE_PACKAGE_NAME_CEM_AMBIGUOUS', `Project "${projectId}" discovered multiple CEM candidates for source package "${source.packageName}": ${existingCandidates.join(', ')}. Set source.cemPath to disambiguate.`);
|
|
476
|
+
}
|
|
477
|
+
function resolveRuntimeImportSpecifier(specifier, cwd, packageRoot) {
|
|
478
|
+
const resolver = specifier.startsWith('.') && packageRoot != null
|
|
479
|
+
? createRequire(path.join(packageRoot, 'package.json'))
|
|
480
|
+
: createRequire(path.resolve(cwd, '__qce_runtime_resolution__.cjs'));
|
|
481
|
+
return resolver.resolve(specifier);
|
|
482
|
+
}
|
|
483
|
+
function validateProjectOutputSafety(projects, workspaceRoot) {
|
|
484
|
+
const outputDirOwners = new Map();
|
|
485
|
+
for (const project of projects) {
|
|
486
|
+
const resolvedOutDir = path.resolve(workspaceRoot, project.outDir);
|
|
487
|
+
validateResolvedOutDirWithinWorkspace(project, workspaceRoot, resolvedOutDir);
|
|
488
|
+
const existingOwner = outputDirOwners.get(resolvedOutDir);
|
|
489
|
+
if (existingOwner != null) {
|
|
490
|
+
throw new GenerationError('QCE_OUTPUT_PATH_COLLISION', `Projects "${existingOwner}" and "${project.id}" resolve to the same output directory: ${resolvedOutDir}`);
|
|
491
|
+
}
|
|
492
|
+
outputDirOwners.set(resolvedOutDir, project.id);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function validateResolvedOutDirWithinWorkspace(project, workspaceRoot, resolvedOutDir) {
|
|
496
|
+
const relativeToWorkspace = path.relative(workspaceRoot, resolvedOutDir);
|
|
497
|
+
const resolvesOutsideWorkspace = relativeToWorkspace === '..' ||
|
|
498
|
+
relativeToWorkspace.startsWith(`..${path.sep}`) ||
|
|
499
|
+
path.isAbsolute(relativeToWorkspace);
|
|
500
|
+
if (resolvesOutsideWorkspace) {
|
|
501
|
+
throw new GenerationError('QCE_OUTPUT_OUTSIDE_WORKSPACE', `Project "${project.id}" output path resolves outside workspace root: ${project.outDir}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function createPlannedWrites(_projectId, _outDirPath, _componentDefinitions, _adapterModule, _ssrAvailable, adapterPlannedWrites = []) {
|
|
505
|
+
return adapterPlannedWrites;
|
|
506
|
+
}
|
|
507
|
+
async function createAdapterPlannedWrites(project, outDirPath, componentDefinitions, adapterModule, runtimeImports, ssrAvailable) {
|
|
508
|
+
const createGeneratedOutput = adapterModule != null &&
|
|
509
|
+
typeof adapterModule.createGeneratedOutput === 'function'
|
|
510
|
+
? adapterModule.createGeneratedOutput
|
|
511
|
+
: undefined;
|
|
512
|
+
if (createGeneratedOutput == null) {
|
|
513
|
+
throw new GenerationError('QCE_ADAPTER_GENERATION_CONTRACT_REQUIRED', `Project "${project.id}" adapter "${project.adapterPackage}" must export createGeneratedOutput() as its primary generated-output contract.`);
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
const plannedWrites = (await createGeneratedOutput({
|
|
517
|
+
projectId: project.id,
|
|
518
|
+
libraryName: project.libraryName,
|
|
519
|
+
source: project.source,
|
|
520
|
+
adapterOptions: project.adapterOptions ?? {},
|
|
521
|
+
runtimeImports,
|
|
522
|
+
componentDefinitions,
|
|
523
|
+
ssrAvailable,
|
|
524
|
+
}));
|
|
525
|
+
if (!Array.isArray(plannedWrites)) {
|
|
526
|
+
return [];
|
|
527
|
+
}
|
|
528
|
+
return plannedWrites
|
|
529
|
+
.filter((plannedWrite) => plannedWrite != null &&
|
|
530
|
+
typeof plannedWrite === 'object' &&
|
|
531
|
+
'relativePath' in plannedWrite &&
|
|
532
|
+
typeof plannedWrite.relativePath === 'string' &&
|
|
533
|
+
'content' in plannedWrite &&
|
|
534
|
+
typeof plannedWrite.content === 'string')
|
|
535
|
+
.map((plannedWrite) => ({
|
|
536
|
+
path: path.join(outDirPath, plannedWrite.relativePath),
|
|
537
|
+
content: plannedWrite.content,
|
|
538
|
+
}));
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
const errorCode = typeof error === 'object' &&
|
|
542
|
+
error !== null &&
|
|
543
|
+
'code' in error &&
|
|
544
|
+
typeof error.code === 'string'
|
|
545
|
+
? error.code
|
|
546
|
+
: 'QCE_ADAPTER_PLANNED_WRITES_INVALID';
|
|
547
|
+
throw new GenerationError(errorCode, toErrorMessage(error));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function readComponentDefinitionsFromCem(sourcePath) {
|
|
551
|
+
let parsed;
|
|
552
|
+
try {
|
|
553
|
+
const rawText = await readFile(sourcePath, 'utf8');
|
|
554
|
+
parsed = JSON.parse(rawText);
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
throw new GenerationError('QCE_CEM_READ_FAILED', `Could not read CEM source at ${sourcePath}: ${toErrorMessage(error)}`);
|
|
558
|
+
}
|
|
559
|
+
const manifest = parsed;
|
|
560
|
+
if (!Array.isArray(manifest.modules)) {
|
|
561
|
+
throw new GenerationError('QCE_CEM_INVALID_SHAPE', `CEM at ${sourcePath} must include a "modules" array.`);
|
|
562
|
+
}
|
|
563
|
+
const definitions = new Map();
|
|
564
|
+
for (let moduleIndex = 0; moduleIndex < manifest.modules.length; moduleIndex += 1) {
|
|
565
|
+
const module = manifest.modules[moduleIndex];
|
|
566
|
+
if (module.declarations != null && !Array.isArray(module.declarations)) {
|
|
567
|
+
throw new GenerationError('QCE_CEM_INVALID_SHAPE', `CEM at ${sourcePath} has invalid shape: modules[${moduleIndex}].declarations must be an array when provided.`);
|
|
568
|
+
}
|
|
569
|
+
const declarations = module.declarations ?? [];
|
|
570
|
+
for (let declarationIndex = 0; declarationIndex < declarations.length; declarationIndex += 1) {
|
|
571
|
+
const declaration = declarations[declarationIndex];
|
|
572
|
+
if (!Object.prototype.hasOwnProperty.call(declaration, 'tagName')) {
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (typeof declaration.tagName !== 'string' ||
|
|
576
|
+
declaration.tagName.trim() === '') {
|
|
577
|
+
throw new GenerationError('QCE_CEM_INVALID_SHAPE', `CEM at ${sourcePath} has invalid shape: modules[${moduleIndex}].declarations[${declarationIndex}].tagName must be a non-empty string when provided.`);
|
|
578
|
+
}
|
|
579
|
+
const tagName = declaration.tagName.trim();
|
|
580
|
+
const existingDefinition = definitions.get(tagName);
|
|
581
|
+
const definition = existingDefinition ?? {
|
|
582
|
+
tagName,
|
|
583
|
+
props: [],
|
|
584
|
+
events: [],
|
|
585
|
+
slots: [],
|
|
586
|
+
};
|
|
587
|
+
const propsByName = new Map(definition.props.map((prop) => [prop.name, prop]));
|
|
588
|
+
const eventsByName = new Map(definition.events.map((event) => [event.name, event]));
|
|
589
|
+
const slotsByName = new Map(definition.slots.map((slot) => [slot.name, slot]));
|
|
590
|
+
const declarationRecord = declaration;
|
|
591
|
+
for (const prop of readComponentPropsFromAttributes(declarationRecord.attributes)) {
|
|
592
|
+
propsByName.set(prop.name, prop);
|
|
593
|
+
}
|
|
594
|
+
for (const prop of readComponentPropsFromMembers(declarationRecord.members)) {
|
|
595
|
+
const existingProp = propsByName.get(prop.name);
|
|
596
|
+
if (existingProp == null ||
|
|
597
|
+
existingProp.type === 'unknown' ||
|
|
598
|
+
existingProp.type.trim() === '') {
|
|
599
|
+
propsByName.set(prop.name, prop);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
for (const event of readComponentEventsFromDeclaration(declarationRecord.events)) {
|
|
603
|
+
if (!eventsByName.has(event.name)) {
|
|
604
|
+
eventsByName.set(event.name, event);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const slot of readComponentSlotsFromDeclaration(declarationRecord.slots)) {
|
|
608
|
+
if (!slotsByName.has(slot.name)) {
|
|
609
|
+
slotsByName.set(slot.name, slot);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
definition.props = Array.from(propsByName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
613
|
+
definition.events = Array.from(eventsByName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
614
|
+
definition.slots = Array.from(slotsByName.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
615
|
+
definitions.set(tagName, definition);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return Array.from(definitions.values()).sort((a, b) => a.tagName.localeCompare(b.tagName));
|
|
619
|
+
}
|
|
620
|
+
function readComponentPropsFromAttributes(attributes) {
|
|
621
|
+
if (!Array.isArray(attributes)) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
const props = [];
|
|
625
|
+
for (const attribute of attributes) {
|
|
626
|
+
if (typeof attribute !== 'object' || attribute === null) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const attributeRecord = attribute;
|
|
630
|
+
const rawName = typeof attributeRecord.fieldName === 'string' &&
|
|
631
|
+
attributeRecord.fieldName.trim() !== ''
|
|
632
|
+
? attributeRecord.fieldName
|
|
633
|
+
: attributeRecord.name;
|
|
634
|
+
if (typeof rawName !== 'string' || rawName.trim() === '') {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
props.push({
|
|
638
|
+
name: rawName.trim(),
|
|
639
|
+
type: normalizeCemTypeText(attributeRecord.type?.text),
|
|
640
|
+
required: false,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return props;
|
|
644
|
+
}
|
|
645
|
+
function readComponentPropsFromMembers(members) {
|
|
646
|
+
if (!Array.isArray(members)) {
|
|
647
|
+
return [];
|
|
648
|
+
}
|
|
649
|
+
const props = [];
|
|
650
|
+
for (const member of members) {
|
|
651
|
+
if (typeof member !== 'object' || member === null) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const memberRecord = member;
|
|
655
|
+
if (memberRecord.kind !== 'field') {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (memberRecord.privacy === 'private') {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (typeof memberRecord.name !== 'string' ||
|
|
662
|
+
memberRecord.name.trim() === '') {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
props.push({
|
|
666
|
+
name: memberRecord.name.trim(),
|
|
667
|
+
type: normalizeCemTypeText(memberRecord.type?.text),
|
|
668
|
+
required: false,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return props;
|
|
672
|
+
}
|
|
673
|
+
function readComponentEventsFromDeclaration(events) {
|
|
674
|
+
if (!Array.isArray(events)) {
|
|
675
|
+
return [];
|
|
676
|
+
}
|
|
677
|
+
const componentEvents = [];
|
|
678
|
+
for (const event of events) {
|
|
679
|
+
if (typeof event !== 'object' || event === null) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const eventRecord = event;
|
|
683
|
+
if (typeof eventRecord.name !== 'string' ||
|
|
684
|
+
eventRecord.name.trim() === '') {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
componentEvents.push({
|
|
688
|
+
name: eventRecord.name.trim(),
|
|
689
|
+
type: normalizeCemTypeText(eventRecord.type?.text),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
return componentEvents;
|
|
693
|
+
}
|
|
694
|
+
function readComponentSlotsFromDeclaration(slots) {
|
|
695
|
+
if (!Array.isArray(slots)) {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
const componentSlots = [];
|
|
699
|
+
for (const slot of slots) {
|
|
700
|
+
if (typeof slot !== 'object' || slot === null) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const slotRecord = slot;
|
|
704
|
+
if (typeof slotRecord.name !== 'string' || slotRecord.name.trim() === '') {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
componentSlots.push({
|
|
708
|
+
name: slotRecord.name.trim(),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return componentSlots;
|
|
712
|
+
}
|
|
713
|
+
function normalizeCemTypeText(typeText) {
|
|
714
|
+
return typeof typeText === 'string' && typeText.trim() !== ''
|
|
715
|
+
? typeText.trim()
|
|
716
|
+
: 'unknown';
|
|
717
|
+
}
|
|
718
|
+
function toErrorMessage(error) {
|
|
719
|
+
return error instanceof Error ? error.message : String(error);
|
|
720
|
+
}
|