@qwik-custom-elements/core 1.0.0 → 1.0.2
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/README.md +17 -3
- package/dist/__tests__/config.test.js +25 -4
- package/dist/__tests__/generator.test.js +74 -0
- package/dist/generator.js +52 -8
- package/package.json +30 -1
package/README.md
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
# @qwik-custom-elements/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Qwik does not natively provide server-side rendering support for custom elements. Without additional tooling, Web Components render as empty tags on the server, lose SSR benefits, and require manual boilerplate to integrate typed props and events. `@qwik-custom-elements/core` is the CLI and orchestration layer that generates typed, SSR-capable Qwik wrapper components from a Custom Elements Manifest — giving you correct server output, full type safety, and automatic event wiring, with no per-component boilerplate.
|
|
4
|
+
|
|
5
|
+
`core` does not contain framework-specific logic. Instead, it delegates generated output to an adapter that understands your component library (Stencil, Lit, etc.). You configure a project once; the CLI does the rest.
|
|
6
|
+
|
|
7
|
+
## Ecosystem
|
|
8
|
+
|
|
9
|
+
This package is part of the `qwik-custom-elements` toolchain:
|
|
10
|
+
|
|
11
|
+
| Package | Role |
|
|
12
|
+
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
13
|
+
| **`@qwik-custom-elements/core`** _(this package)_ | CLI entry point and adapter-agnostic orchestration: config loading, CEM parsing, write planning, diagnostics |
|
|
14
|
+
| [**`@qwik-custom-elements/adapter-stencil`**](../adapter-stencil/README.md) | Stencil-specific generation: SSR/CSR bridge components, typed Qwik wrappers, hydrate runtime integration |
|
|
15
|
+
| [**`@qwik-custom-elements/adapter-lit`**](../adapter-lit/README.md) | Lit-specific generation: SSR/CSR bridge components, typed Qwik wrappers, Declarative Shadow DOM hydration |
|
|
16
|
+
|
|
17
|
+
Install `core` alongside whichever adapter matches your component library. You will interact with `core` only through the config file and the CLI — all framework-specific behavior lives in the adapter.
|
|
4
18
|
|
|
5
19
|
## Install
|
|
6
20
|
|
|
@@ -77,6 +91,6 @@ Adapters are responsible for returning the generated file set for their framewor
|
|
|
77
91
|
|
|
78
92
|
Changes that alter core or adapter ownership boundaries should be reflected in:
|
|
79
93
|
|
|
80
|
-
- `docs/
|
|
81
|
-
- `
|
|
94
|
+
- an ADR in `docs/adr/` (system-wide) or the affected package's `docs/adr/` (context-specific)
|
|
95
|
+
- the affected `CONTEXT.md` if new domain terms are introduced
|
|
82
96
|
- the package README files for any affected packages
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import { parseCliArgs, runCli } from '../cli.js';
|
|
6
7
|
import { ConfigValidationError, loadGeneratorConfig } from '../config.js';
|
|
8
|
+
async function readVersionFromPackageJson(packageJsonPath) {
|
|
9
|
+
try {
|
|
10
|
+
const packageJsonText = await readFile(packageJsonPath, 'utf8');
|
|
11
|
+
const packageJson = JSON.parse(packageJsonText);
|
|
12
|
+
if (packageJson != null &&
|
|
13
|
+
typeof packageJson.version === 'string' &&
|
|
14
|
+
packageJson.version.length > 0) {
|
|
15
|
+
return packageJson.version;
|
|
16
|
+
}
|
|
17
|
+
return 'unknown';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
async function withTempDir(run) {
|
|
8
24
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'qce-core-'));
|
|
9
25
|
try {
|
|
@@ -519,10 +535,15 @@ describe('runCli', () => {
|
|
|
519
535
|
supportsSsrProbe: true,
|
|
520
536
|
ssrRuntimeSubpath: './ssr',
|
|
521
537
|
});
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
538
|
+
// Read actual versions from package.json to make test resilient to version bumps
|
|
539
|
+
const corePackageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
540
|
+
const adapterPackageJsonPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'adapter-stencil', 'package.json');
|
|
541
|
+
const expectedCoreVersion = await readVersionFromPackageJson(corePackageJsonPath);
|
|
542
|
+
const expectedAdapterVersion = await readVersionFromPackageJson(adapterPackageJsonPath);
|
|
543
|
+
expect(summary.projects[0].resolvedCoreVersion).toBe(expectedCoreVersion);
|
|
544
|
+
expect(summary.projects[1].resolvedCoreVersion).toBe(expectedCoreVersion);
|
|
545
|
+
expect(summary.projects[0].resolvedAdapterVersion).toBe(expectedAdapterVersion);
|
|
546
|
+
expect(summary.projects[1].resolvedAdapterVersion).toBe(expectedAdapterVersion);
|
|
526
547
|
}
|
|
527
548
|
finally {
|
|
528
549
|
process.chdir(previousCwd);
|
|
@@ -945,6 +945,37 @@ describe('generateFromConfig', () => {
|
|
|
945
945
|
});
|
|
946
946
|
});
|
|
947
947
|
});
|
|
948
|
+
it('includes adapter-provided guidance in QCE_PACKAGE_NAME_CEM_NOT_FOUND when hook returns a hint', async () => {
|
|
949
|
+
await withTempDir(async (tempDir) => {
|
|
950
|
+
const packageName = '@demo/stencil-no-cem';
|
|
951
|
+
const _packageRoot = await createFixturePackage(tempDir, packageName);
|
|
952
|
+
// Mock adapter with the hint hook
|
|
953
|
+
await writeFile(path.join(tempDir, 'adapter-with-hint-hook.mjs'), [
|
|
954
|
+
'export const metadata = {',
|
|
955
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
956
|
+
'};',
|
|
957
|
+
'export function buildMissingCemHint({ packageRoot }) {',
|
|
958
|
+
' return " Hint from adapter! ";',
|
|
959
|
+
'}',
|
|
960
|
+
].join('\n'), 'utf8');
|
|
961
|
+
const config = {
|
|
962
|
+
dryRun: true,
|
|
963
|
+
projects: [
|
|
964
|
+
{
|
|
965
|
+
id: 'stencil-demo',
|
|
966
|
+
adapter: 'stencil',
|
|
967
|
+
adapterPackage: './adapter-with-hint-hook.mjs',
|
|
968
|
+
source: { type: 'PACKAGE_NAME', packageName },
|
|
969
|
+
outDir: './src/generated',
|
|
970
|
+
},
|
|
971
|
+
],
|
|
972
|
+
};
|
|
973
|
+
await expect(generateFromConfig(config, { cwd: tempDir })).rejects.toMatchObject({
|
|
974
|
+
code: 'QCE_PACKAGE_NAME_CEM_NOT_FOUND',
|
|
975
|
+
message: expect.stringContaining('Hint from adapter!'),
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
});
|
|
948
979
|
it('fails deterministically when PACKAGE_NAME CEM discovery is ambiguous', async () => {
|
|
949
980
|
await withTempDir(async (tempDir) => {
|
|
950
981
|
const packageName = '@demo/components';
|
|
@@ -1329,3 +1360,46 @@ describe('generateFromConfig', () => {
|
|
|
1329
1360
|
});
|
|
1330
1361
|
});
|
|
1331
1362
|
});
|
|
1363
|
+
it('resolves package root for PACKAGE_NAME source with strict exports map (no ./package.json)', async () => {
|
|
1364
|
+
await withTempDir(async (tempDir) => {
|
|
1365
|
+
const packageName = '@demo/strict-exports-lib';
|
|
1366
|
+
const packageRoot = path.join(tempDir, 'node_modules', '@demo', 'strict-exports-lib');
|
|
1367
|
+
await mkdir(path.join(packageRoot, 'dist'), { recursive: true });
|
|
1368
|
+
// package.json has strict exports — no ./package.json export
|
|
1369
|
+
await writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({
|
|
1370
|
+
name: packageName,
|
|
1371
|
+
version: '1.0.0',
|
|
1372
|
+
exports: {
|
|
1373
|
+
'.': './dist/index.js',
|
|
1374
|
+
},
|
|
1375
|
+
}), 'utf8');
|
|
1376
|
+
// A real CJS-style file so require.resolve can find it
|
|
1377
|
+
await writeFile(path.join(packageRoot, 'dist', 'index.js'), 'exports.noop = () => undefined;\n', 'utf8');
|
|
1378
|
+
await writeFile(path.join(packageRoot, 'custom-elements.json'), JSON.stringify({
|
|
1379
|
+
modules: [{ declarations: [{ tagName: 'strict-button' }] }],
|
|
1380
|
+
}), 'utf8');
|
|
1381
|
+
await writeFile(path.join(tempDir, 'adapter-minimal.mjs'), [
|
|
1382
|
+
'export const metadata = {',
|
|
1383
|
+
" supportedSourceTypes: ['PACKAGE_NAME'],",
|
|
1384
|
+
'};',
|
|
1385
|
+
'export function createGeneratedOutput() { return []; }',
|
|
1386
|
+
'',
|
|
1387
|
+
].join('\n'), 'utf8');
|
|
1388
|
+
const config = {
|
|
1389
|
+
dryRun: true,
|
|
1390
|
+
projects: [
|
|
1391
|
+
{
|
|
1392
|
+
id: 'demo',
|
|
1393
|
+
adapter: 'stencil',
|
|
1394
|
+
adapterPackage: './adapter-minimal.mjs',
|
|
1395
|
+
source: { type: 'PACKAGE_NAME', packageName },
|
|
1396
|
+
outDir: './src/generated',
|
|
1397
|
+
},
|
|
1398
|
+
],
|
|
1399
|
+
};
|
|
1400
|
+
const result = await generateFromConfig(config, { cwd: tempDir });
|
|
1401
|
+
expect(result.projects[0].status).toBe('success');
|
|
1402
|
+
expect(result.projects[0].componentTags).toEqual(['strict-button']);
|
|
1403
|
+
expect(result.projects[0].sourcePath).toBe(path.join(packageRoot, 'custom-elements.json'));
|
|
1404
|
+
});
|
|
1405
|
+
});
|
package/dist/generator.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -87,7 +87,7 @@ async function generateProject(project, cwd, dryRun) {
|
|
|
87
87
|
await validateAdapterProject(project, adapterModule);
|
|
88
88
|
const runtimeImportResult = await resolveAdapterRuntimeImports(project, adapterModule, cwd);
|
|
89
89
|
const { runtimeImports } = runtimeImportResult;
|
|
90
|
-
const sourcePath = resolveProjectSourcePath(project.id, project.source, cwd);
|
|
90
|
+
const sourcePath = resolveProjectSourcePath(project.id, project.source, cwd, adapterModule);
|
|
91
91
|
const outDirPath = path.resolve(cwd, project.outDir);
|
|
92
92
|
const componentDefinitions = await readComponentDefinitionsFromCem(sourcePath);
|
|
93
93
|
const augmentedComponentDefinitions = await augmentAdapterComponentDefinitions(componentDefinitions, adapterModule, runtimeImports, outDirPath, cwd);
|
|
@@ -423,7 +423,7 @@ function resolveSkippedProjectSourcePath(source, cwd) {
|
|
|
423
423
|
? `package:${source.packageName}`
|
|
424
424
|
: `package:${source.packageName}#${source.cemPath}`;
|
|
425
425
|
}
|
|
426
|
-
function resolveProjectSourcePath(projectId, source, cwd) {
|
|
426
|
+
function resolveProjectSourcePath(projectId, source, cwd, adapterModule) {
|
|
427
427
|
if (source.type === 'CEM') {
|
|
428
428
|
return path.resolve(cwd, source.path);
|
|
429
429
|
}
|
|
@@ -431,7 +431,7 @@ function resolveProjectSourcePath(projectId, source, cwd) {
|
|
|
431
431
|
if (source.cemPath != null) {
|
|
432
432
|
return resolvePackageNameOverrideCemPath(projectId, source, packageRoot);
|
|
433
433
|
}
|
|
434
|
-
return discoverPackageNameCemPath(projectId, source, packageRoot);
|
|
434
|
+
return discoverPackageNameCemPath(projectId, source, packageRoot, adapterModule);
|
|
435
435
|
}
|
|
436
436
|
function resolvePackageRootForProject(projectId, source, cwd) {
|
|
437
437
|
const packageSpecifier = `${source.packageName}/package.json`;
|
|
@@ -439,8 +439,37 @@ function resolvePackageRootForProject(projectId, source, cwd) {
|
|
|
439
439
|
const packageJsonPath = require.resolve(packageSpecifier, { paths: [cwd] });
|
|
440
440
|
return path.dirname(packageJsonPath);
|
|
441
441
|
}
|
|
442
|
-
catch (
|
|
443
|
-
|
|
442
|
+
catch (primaryError) {
|
|
443
|
+
// Fallback for packages with strict exports maps that don't expose ./package.json.
|
|
444
|
+
// Resolve via the main entry point, then walk up the directory tree to find
|
|
445
|
+
// the ancestor directory whose package.json `name` matches the package name.
|
|
446
|
+
// This approach is robust across npm, pnpm (virtual store), and Yarn PnP because
|
|
447
|
+
// it matches on the canonical `name` field rather than path segments.
|
|
448
|
+
try {
|
|
449
|
+
const mainEntry = require.resolve(source.packageName, { paths: [cwd] });
|
|
450
|
+
let dir = path.dirname(mainEntry);
|
|
451
|
+
while (true) {
|
|
452
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
453
|
+
if (existsSync(pkgJsonPath)) {
|
|
454
|
+
try {
|
|
455
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
456
|
+
if (pkg.name === source.packageName)
|
|
457
|
+
return dir;
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// Not a valid JSON package.json — keep walking up
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const parent = path.dirname(dir);
|
|
464
|
+
if (parent === dir)
|
|
465
|
+
break; // Reached filesystem root with no match
|
|
466
|
+
dir = parent;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
// Main entry also not resolvable — fall through to the original error
|
|
471
|
+
}
|
|
472
|
+
throw new GenerationError('QCE_PACKAGE_NAME_RESOLVE_FAILED', `Project "${projectId}" could not resolve source package "${source.packageName}" from ${cwd}: ${toErrorMessage(primaryError)}`);
|
|
444
473
|
}
|
|
445
474
|
}
|
|
446
475
|
function resolvePackageNameOverrideCemPath(projectId, source, packageRoot) {
|
|
@@ -458,7 +487,7 @@ function resolvePackageNameOverrideCemPath(projectId, source, packageRoot) {
|
|
|
458
487
|
}
|
|
459
488
|
return resolvedCemPath;
|
|
460
489
|
}
|
|
461
|
-
function discoverPackageNameCemPath(projectId, source, packageRoot) {
|
|
490
|
+
function discoverPackageNameCemPath(projectId, source, packageRoot, adapterModule) {
|
|
462
491
|
const candidatePaths = PACKAGE_NAME_CEM_DISCOVERY_CANDIDATES.map((candidate) => path.resolve(packageRoot, candidate));
|
|
463
492
|
const existingCandidates = [];
|
|
464
493
|
for (const candidatePath of candidatePaths) {
|
|
@@ -470,10 +499,25 @@ function discoverPackageNameCemPath(projectId, source, packageRoot) {
|
|
|
470
499
|
return existingCandidates[0];
|
|
471
500
|
}
|
|
472
501
|
if (existingCandidates.length === 0) {
|
|
473
|
-
|
|
502
|
+
const hint = tryAdapterMissingCemHintHook(adapterModule, packageRoot);
|
|
503
|
+
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.${hint}`);
|
|
474
504
|
}
|
|
475
505
|
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
506
|
}
|
|
507
|
+
function tryAdapterMissingCemHintHook(adapterModule, packageRoot) {
|
|
508
|
+
const hook = adapterModule.buildMissingCemHint;
|
|
509
|
+
if (typeof hook !== 'function') {
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const result = hook({ packageRoot });
|
|
514
|
+
return typeof result === 'string' ? result : '';
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
// Hook errors are non-fatal
|
|
518
|
+
return '';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
477
521
|
function resolveRuntimeImportSpecifier(specifier, cwd, packageRoot) {
|
|
478
522
|
const resolver = specifier.startsWith('.') && packageRoot != null
|
|
479
523
|
? createRequire(path.join(packageRoot, 'package.json'))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwik-custom-elements/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,6 +26,35 @@
|
|
|
26
26
|
"publishConfig": {
|
|
27
27
|
"access": "public"
|
|
28
28
|
},
|
|
29
|
+
"description": "Generate typed, SSR-capable Qwik components from Web Component libraries (CLI + orchestration)",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"qwik",
|
|
32
|
+
"web-components",
|
|
33
|
+
"custom-elements",
|
|
34
|
+
"ssr",
|
|
35
|
+
"server-side-rendering",
|
|
36
|
+
"wrapper",
|
|
37
|
+
"manifest",
|
|
38
|
+
"cli",
|
|
39
|
+
"generator",
|
|
40
|
+
"code-generation"
|
|
41
|
+
],
|
|
42
|
+
"author": "Dmitry A. Efimenko (https://github.com/DmitryEfimenko)",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/DmitryEfimenko/qwik-custom-elements.git",
|
|
47
|
+
"directory": "packages/core"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/DmitryEfimenko/qwik-custom-elements#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/DmitryEfimenko/qwik-custom-elements/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
},
|
|
56
|
+
"funding": "https://github.com/sponsors/DmitryEfimenko",
|
|
57
|
+
"sideEffects": false,
|
|
29
58
|
"scripts": {
|
|
30
59
|
"build": "tsc -p tsconfig.json",
|
|
31
60
|
"check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|