@design.estate/wcctools 5.1.0 → 6.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/.smartconfig.json +7 -3
- package/changelog.md +15 -0
- package/cli.js +3 -0
- package/dist_shell/bundle.js +2970 -0
- package/dist_shell/bundle.js.map +1 -0
- package/dist_shell/bundle.js.third-party-notices.json +1722 -0
- package/dist_shell/bundle.js.third-party-notices.txt +4195 -0
- package/dist_shell/index.html +21 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.devapi.d.ts +11 -0
- package/dist_ts/classes.devapi.js +21 -0
- package/dist_ts/classes.devserver.d.ts +49 -0
- package/dist_ts/classes.devserver.js +142 -0
- package/dist_ts/cli.d.ts +6 -0
- package/dist_ts/cli.js +45 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/paths.d.ts +4 -0
- package/dist_ts/paths.js +6 -0
- package/dist_ts/plugins.d.ts +15 -0
- package/dist_ts/plugins.js +21 -0
- package/dist_ts/shellcontent.d.ts +9 -0
- package/dist_ts/shellcontent.js +42 -0
- package/dist_ts_interfaces/bridge.d.ts +88 -0
- package/dist_ts_interfaces/bridge.js +2 -0
- package/dist_ts_interfaces/catalog.d.ts +32 -0
- package/dist_ts_interfaces/catalog.js +6 -0
- package/dist_ts_interfaces/index.d.ts +3 -0
- package/dist_ts_interfaces/index.js +4 -0
- package/dist_ts_interfaces/plugins.d.ts +2 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +15 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_shared/index.d.ts +1 -0
- package/dist_ts_shared/index.js +2 -0
- package/dist_ts_shared/plugins.d.ts +2 -0
- package/dist_ts_shared/plugins.js +3 -0
- package/dist_ts_shared/previewroute.d.ts +18 -0
- package/dist_ts_shared/previewroute.js +47 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.js +1 -1
- package/dist_ts_shell/elements/wcc-preview-frame.d.ts +23 -0
- package/dist_ts_shell/elements/wcc-preview-frame.js +197 -0
- package/dist_ts_shell/elements/wcc-properties.d.ts +49 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-properties.js +149 -216
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.js +1 -1
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.d.ts +2 -2
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.js +12 -16
- package/dist_ts_shell/elements/wcc-shell.d.ts +87 -0
- package/dist_ts_shell/elements/wcc-shell.js +524 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.d.ts +12 -10
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.js +137 -183
- package/dist_ts_shell/index.d.ts +4 -0
- package/dist_ts_shell/index.js +5 -0
- package/dist_ts_shell/plugins.d.ts +4 -0
- package/dist_ts_shell/plugins.js +5 -0
- package/dist_ts_shell/previewconnection.d.ts +30 -0
- package/dist_ts_shell/previewconnection.js +86 -0
- package/dist_ts_shell/services/recorder.service.js +353 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/catalog.d.ts +45 -0
- package/dist_ts_web/catalog.js +153 -0
- package/dist_ts_web/elementproperties.d.ts +15 -0
- package/dist_ts_web/elementproperties.js +89 -0
- package/dist_ts_web/elements/wcc-preview.d.ts +19 -0
- package/dist_ts_web/elements/wcc-preview.js +164 -0
- package/dist_ts_web/elements/wcc-standalone-notice.d.ts +14 -0
- package/dist_ts_web/elements/wcc-standalone-notice.js +110 -0
- package/dist_ts_web/elements/wcctools.helpers.d.ts +2 -6
- package/dist_ts_web/elements/wcctools.helpers.js +1 -7
- package/dist_ts_web/index.d.ts +2 -5
- package/dist_ts_web/index.js +38 -17
- package/dist_ts_web/previewbridge.d.ts +39 -0
- package/dist_ts_web/previewbridge.js +153 -0
- package/dist_ts_web/renderedtree.d.ts +5 -0
- package/dist_ts_web/renderedtree.js +37 -0
- package/dist_ts_web/wcctools.interfaces.d.ts +3 -6
- package/dist_ts_web/wcctools.interfaces.js +1 -1
- package/dist_ts_web/wcctools.plugins.d.ts +3 -1
- package/dist_ts_web/wcctools.plugins.js +4 -2
- package/package.json +16 -8
- package/readme.md +60 -28
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.devapi.ts +25 -0
- package/ts/classes.devserver.ts +161 -0
- package/ts/cli.ts +46 -0
- package/ts/index.ts +3 -0
- package/ts/paths.ts +7 -0
- package/ts/plugins.ts +29 -0
- package/ts/shellcontent.ts +42 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/catalog.ts +194 -0
- package/ts_web/elementproperties.ts +96 -0
- package/ts_web/elements/wcc-preview.ts +111 -0
- package/ts_web/elements/wcc-standalone-notice.ts +65 -0
- package/ts_web/elements/wcctools.helpers.ts +2 -10
- package/ts_web/index.ts +38 -19
- package/ts_web/previewbridge.ts +189 -0
- package/ts_web/readme.md +26 -85
- package/ts_web/renderedtree.ts +43 -0
- package/ts_web/wcctools.interfaces.ts +4 -7
- package/ts_web/wcctools.plugins.ts +6 -1
- package/dist_bundle/bundle.js +0 -92419
- package/dist_bundle/bundle.js.map +0 -1
- package/dist_ts_web/elements/wcc-dashboard.d.ts +0 -119
- package/dist_ts_web/elements/wcc-dashboard.js +0 -700
- package/dist_ts_web/elements/wcc-frame.d.ts +0 -22
- package/dist_ts_web/elements/wcc-frame.js +0 -256
- package/dist_ts_web/elements/wcc-properties.d.ts +0 -43
- package/dist_ts_web/pages/index.d.ts +0 -1
- package/dist_ts_web/pages/index.js +0 -2
- package/dist_ts_web/services/recorder.service.js +0 -353
- package/ts_web/elements/wcc-contextmenu.ts +0 -291
- package/ts_web/elements/wcc-dashboard.ts +0 -672
- package/ts_web/elements/wcc-frame.ts +0 -189
- package/ts_web/elements/wcc-properties.ts +0 -1089
- package/ts_web/elements/wcc-record-button.ts +0 -108
- package/ts_web/elements/wcc-recording-panel.ts +0 -1010
- package/ts_web/elements/wcc-sidebar.ts +0 -1470
- package/ts_web/pages/index.ts +0 -1
- package/ts_web/services/recorder.service.ts +0 -451
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +0 -12
- package/ts_web/types/dom-mediacapture-stub/package.json +0 -6
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +0 -2
- package/ts_web/types/dom-webcodecs-stub/package.json +0 -6
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/services/recorder.service.d.ts +0 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The typed API the shell reaches on `/wcctools/typedrequest`. Later features register their
|
|
5
|
+
* handlers on `typedrouter`.
|
|
6
|
+
*/
|
|
7
|
+
export class WccDevApi {
|
|
8
|
+
public readonly typedrouter = new plugins.typedrequest.TypedRouter();
|
|
9
|
+
private serverInfo: plugins.interfaces.IWccDevServerInfo | null = null;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
this.typedrouter.addTypedHandler(
|
|
13
|
+
new plugins.typedrequest.TypedHandler<plugins.interfaces.IReq_GetDevServerInfo>('getDevServerInfo', async () => {
|
|
14
|
+
if (!this.serverInfo) {
|
|
15
|
+
throw new Error('The dev server has not started.');
|
|
16
|
+
}
|
|
17
|
+
return { ...this.serverInfo };
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public setServerInfo(infoArg: plugins.interfaces.IWccDevServerInfo) {
|
|
23
|
+
this.serverInfo = { ...infoArg };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as paths from './paths.js';
|
|
3
|
+
import { commitinfo } from './00_commitinfo_data.js';
|
|
4
|
+
import { WccDevApi } from './classes.devapi.js';
|
|
5
|
+
import { loadShellContent, shellPathPrefix } from './shellcontent.js';
|
|
6
|
+
|
|
7
|
+
export interface IWccDevServerOptions {
|
|
8
|
+
/** Port to listen on; overrides the tswatch configuration. 0 picks a free port. */
|
|
9
|
+
port?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IWccDevServerAddress {
|
|
13
|
+
port: number;
|
|
14
|
+
/** The shell's address. */
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** tswatch's default port and serve directory, used when the configuration names none. */
|
|
19
|
+
const defaultPort = 3002;
|
|
20
|
+
const defaultServeDir = './dist_watch/';
|
|
21
|
+
|
|
22
|
+
/** The development headers tswatch serves catalogs with: a cross-origin isolated document. */
|
|
23
|
+
const securityHeaders: plugins.typedserver.ISecurityHeaders = {
|
|
24
|
+
crossOriginOpenerPolicy: 'same-origin',
|
|
25
|
+
crossOriginEmbedderPolicy: 'require-corp',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const acceptAnyHostname = () => true;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* `wcctools dev`: bundles the catalog of the current project with tswatch, as its
|
|
32
|
+
* `@git.zone/tswatch` configuration (or the `element` preset) describes, and serves on one port:
|
|
33
|
+
*
|
|
34
|
+
* - the shell on `/wcctools/` (`/` redirects there) and its routes `/wcctools-route/...`,
|
|
35
|
+
* - the typed API on `/wcctools/typedrequest`,
|
|
36
|
+
* - the catalog's preview document and bundle from the tswatch serve directory on every other
|
|
37
|
+
* path, with live reload that reloads only the preview document.
|
|
38
|
+
*
|
|
39
|
+
* tswatch resolves its paths against the process's working directory, so the server serves the
|
|
40
|
+
* project the process runs in.
|
|
41
|
+
*/
|
|
42
|
+
export class WccDevServer {
|
|
43
|
+
/** The typed API on `/wcctools/typedrequest`; register further handlers before `start()`. */
|
|
44
|
+
public readonly api = new WccDevApi();
|
|
45
|
+
private tsWatch: plugins.tswatch.TsWatch | null = null;
|
|
46
|
+
private typedServer: plugins.typedserver.TypedServer | null = null;
|
|
47
|
+
private readonly smartExit = new plugins.smartexit.SmartExit({ silent: true });
|
|
48
|
+
|
|
49
|
+
constructor(private readonly options: IWccDevServerOptions = {}) {
|
|
50
|
+
this.smartExit.addCleanupFunction(() => this.stop());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async start(): Promise<IWccDevServerAddress> {
|
|
54
|
+
if (this.typedServer) {
|
|
55
|
+
throw new Error('wcctools: the dev server is already started.');
|
|
56
|
+
}
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
const configHandler = new plugins.tswatch.ConfigHandler(cwd);
|
|
59
|
+
const config = configHandler.loadConfig() ?? configHandler.getPreset('element');
|
|
60
|
+
const serveDir = plugins.path.resolve(cwd, config.server?.serveDir ?? defaultServeDir);
|
|
61
|
+
const port = this.options.port ?? config.server?.port ?? defaultPort;
|
|
62
|
+
|
|
63
|
+
// The preview surface watches the serve directory, which must exist before the first bundle
|
|
64
|
+
await plugins.fs.mkdir(serveDir, { recursive: true });
|
|
65
|
+
this.api.setServerInfo({
|
|
66
|
+
wcctoolsVersion: commitinfo.version,
|
|
67
|
+
projectName: await this.readProjectName(cwd),
|
|
68
|
+
});
|
|
69
|
+
const typedServer = new plugins.typedserver.TypedServer({
|
|
70
|
+
cors: false,
|
|
71
|
+
port,
|
|
72
|
+
surfaces: [
|
|
73
|
+
await this.createShellSurface(),
|
|
74
|
+
this.createPreviewSurface(serveDir),
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
this.typedServer = typedServer;
|
|
78
|
+
|
|
79
|
+
// Bundling and watching stay tswatch's; its own server stays off
|
|
80
|
+
this.tsWatch = new plugins.tswatch.TsWatch({ ...config, server: { enabled: false } });
|
|
81
|
+
try {
|
|
82
|
+
await this.tsWatch.start();
|
|
83
|
+
await typedServer.start();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
await this.stop();
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
const boundPort = typedServer.listeningPort ?? port;
|
|
89
|
+
return { port: boundPort, url: `http://localhost:${boundPort}${shellPathPrefix}/` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Stops the server, then tswatch's bundling and watching. */
|
|
93
|
+
public async stop(): Promise<void> {
|
|
94
|
+
const typedServer = this.typedServer;
|
|
95
|
+
const tsWatch = this.tsWatch;
|
|
96
|
+
this.typedServer = null;
|
|
97
|
+
this.tsWatch = null;
|
|
98
|
+
this.smartExit.deregister();
|
|
99
|
+
await typedServer?.stop();
|
|
100
|
+
await tsWatch?.stop();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The shell: the prebuilt shell bundle and the typed API.
|
|
105
|
+
*
|
|
106
|
+
* Seam for side-effecting APIs (assistant, capture, standards fixes): before any handler with
|
|
107
|
+
* side effects is registered here, add a `websocketAdmission`/`requestAdmission` that accepts
|
|
108
|
+
* only the shell's own origin, and keep surface mode's strict authority validation.
|
|
109
|
+
*/
|
|
110
|
+
private async createShellSurface(): Promise<plugins.typedserver.ITypedServerSurface> {
|
|
111
|
+
return {
|
|
112
|
+
name: 'shell',
|
|
113
|
+
match: {
|
|
114
|
+
hostnameResolver: acceptAnyHostname,
|
|
115
|
+
pathPrefixes: [shellPathPrefix, '/wcctools-route'],
|
|
116
|
+
},
|
|
117
|
+
bundledContent: await loadShellContent(paths.distShellDir),
|
|
118
|
+
spaFallback: true,
|
|
119
|
+
httpTypedRouter: this.api.typedrouter,
|
|
120
|
+
typedRequestPath: `${shellPathPrefix}/typedrequest`,
|
|
121
|
+
noCache: true,
|
|
122
|
+
securityHeaders,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The preview: the catalog's own document and bundle from the tswatch serve directory, on
|
|
128
|
+
* the root paths it was written for. Its live reload client is injected into its document
|
|
129
|
+
* only, so a rebuilt bundle reloads the preview frame and never the shell.
|
|
130
|
+
*/
|
|
131
|
+
private createPreviewSurface(serveDirArg: string): plugins.typedserver.ITypedServerSurface {
|
|
132
|
+
return {
|
|
133
|
+
name: 'preview',
|
|
134
|
+
match: {
|
|
135
|
+
hostnameResolver: acceptAnyHostname,
|
|
136
|
+
pathPrefixes: ['/'],
|
|
137
|
+
},
|
|
138
|
+
serveDir: serveDirArg,
|
|
139
|
+
spaFallback: true,
|
|
140
|
+
injectReload: true,
|
|
141
|
+
watch: true,
|
|
142
|
+
// Lets the reload client get pushed changes instead of polling
|
|
143
|
+
websocketTypedRouter: new plugins.typedrequest.TypedRouter(),
|
|
144
|
+
// As tswatch serves catalogs
|
|
145
|
+
cors: true,
|
|
146
|
+
httpHandler: async (contextArg) => {
|
|
147
|
+
if (contextArg.path === '/' && (contextArg.method === 'GET' || contextArg.method === 'HEAD')) {
|
|
148
|
+
return new Response(null, { status: 302, headers: { Location: `${shellPathPrefix}/` } });
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
},
|
|
152
|
+
noCache: true,
|
|
153
|
+
securityHeaders,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async readProjectName(cwdArg: string): Promise<string> {
|
|
158
|
+
const packageJson = JSON.parse(await plugins.fs.readFile(plugins.path.join(cwdArg, 'package.json'), 'utf8')) as { name?: string };
|
|
159
|
+
return packageJson.name ?? plugins.path.basename(cwdArg);
|
|
160
|
+
}
|
|
161
|
+
}
|
package/ts/cli.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import { commitinfo } from './00_commitinfo_data.js';
|
|
3
|
+
import { WccDevServer } from './classes.devserver.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registers the wcctools commands on the given console.
|
|
7
|
+
*/
|
|
8
|
+
export const registerCommands = (out: plugins.smartconsole.SmartConsole): void => {
|
|
9
|
+
out.cli.configure({
|
|
10
|
+
name: 'wcctools',
|
|
11
|
+
version: commitinfo.version,
|
|
12
|
+
description: 'Develop a web component catalog in the wcctools shell',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
out.cli.command({
|
|
16
|
+
name: 'dev',
|
|
17
|
+
description: 'Bundle the catalog with live reload and serve it in the wcctools shell',
|
|
18
|
+
options: {
|
|
19
|
+
port: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Port to listen on (0 picks a free port); overrides the tswatch configuration',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}, async ({ options }) => {
|
|
25
|
+
const devServer = new WccDevServer({ port: options.port });
|
|
26
|
+
const address = await devServer.start();
|
|
27
|
+
await out.log(`wcctools dev: ${address.url}`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
out.cli.default({}, async () => {
|
|
31
|
+
await out.log(out.cli.renderHelp());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const runCli = async () => {
|
|
36
|
+
const out = new plugins.smartconsole.SmartConsole();
|
|
37
|
+
try {
|
|
38
|
+
registerCommands(out);
|
|
39
|
+
await out.cli.run();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await out.error(error instanceof Error ? error.message : String(error));
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
} finally {
|
|
44
|
+
await out.dispose();
|
|
45
|
+
}
|
|
46
|
+
};
|
package/ts/index.ts
ADDED
package/ts/paths.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** The root of the installed wcctools package. */
|
|
4
|
+
export const packageDir = plugins.path.resolve(plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)), '..');
|
|
5
|
+
|
|
6
|
+
/** The prebuilt shell bundle the package ships. */
|
|
7
|
+
export const distShellDir = plugins.path.join(packageDir, 'dist_shell');
|
package/ts/plugins.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// node native scope
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as url from 'node:url';
|
|
5
|
+
|
|
6
|
+
export { fs, path, url };
|
|
7
|
+
|
|
8
|
+
// @api.global scope
|
|
9
|
+
import * as typedrequest from '@api.global/typedrequest';
|
|
10
|
+
import * as typedserver from '@api.global/typedserver';
|
|
11
|
+
|
|
12
|
+
export { typedrequest, typedserver };
|
|
13
|
+
|
|
14
|
+
// @git.zone scope
|
|
15
|
+
import * as tswatch from '@git.zone/tswatch';
|
|
16
|
+
|
|
17
|
+
export { tswatch };
|
|
18
|
+
|
|
19
|
+
// @push.rocks scope
|
|
20
|
+
import * as smartconsole from '@push.rocks/smartconsole';
|
|
21
|
+
import * as smartexit from '@push.rocks/smartexit';
|
|
22
|
+
|
|
23
|
+
export { smartconsole, smartexit };
|
|
24
|
+
|
|
25
|
+
// project scope
|
|
26
|
+
import * as interfaces from '../ts_interfaces/index.js';
|
|
27
|
+
import * as shared from '../ts_shared/index.js';
|
|
28
|
+
|
|
29
|
+
export { interfaces, shared };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** The path below which the shell surface serves the shell bundle. */
|
|
4
|
+
export const shellPathPrefix = '/wcctools';
|
|
5
|
+
|
|
6
|
+
const collectFiles = async (dirArg: string, relativeDirArg = ''): Promise<string[]> => {
|
|
7
|
+
const entries = await plugins.fs.readdir(plugins.path.join(dirArg, relativeDirArg), { withFileTypes: true });
|
|
8
|
+
const files: string[] = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const relativePath = relativeDirArg ? `${relativeDirArg}/${entry.name}` : entry.name;
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
files.push(...await collectFiles(dirArg, relativePath));
|
|
13
|
+
} else if (entry.isFile()) {
|
|
14
|
+
files.push(relativePath);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return files;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The prebuilt shell bundle as in-memory content of the shell surface. A surface serves files
|
|
22
|
+
* by their full request path, so every file is keyed below `wcctools/`; the shell page is
|
|
23
|
+
* also keyed as `index.html`, which the surface's SPA fallback answers `/wcctools-route/...` with.
|
|
24
|
+
*/
|
|
25
|
+
export const loadShellContent = async (distShellDirArg: string): Promise<plugins.typedserver.IBundledContentItem[]> => {
|
|
26
|
+
const indexPath = plugins.path.join(distShellDirArg, 'index.html');
|
|
27
|
+
try {
|
|
28
|
+
await plugins.fs.access(indexPath);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`wcctools: the shell bundle is missing (${indexPath}); build the package with "pnpm build".`);
|
|
31
|
+
}
|
|
32
|
+
const prefix = shellPathPrefix.slice(1);
|
|
33
|
+
const content: plugins.typedserver.IBundledContentItem[] = [];
|
|
34
|
+
for (const relativePath of await collectFiles(distShellDirArg)) {
|
|
35
|
+
const contentBase64 = (await plugins.fs.readFile(plugins.path.join(distShellDirArg, relativePath))).toString('base64');
|
|
36
|
+
content.push({ path: `${prefix}/${relativePath}`, contentBase64 });
|
|
37
|
+
if (relativePath === 'index.html') {
|
|
38
|
+
content.push({ path: 'index.html', contentBase64 });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return content;
|
|
42
|
+
};
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@design.estate/wcctools',
|
|
6
|
-
version: '
|
|
6
|
+
version: '6.0.0',
|
|
7
7
|
description: 'A set of web component tools for creating element catalogues, enabling the structured development and documentation of custom elements and pages.'
|
|
8
8
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as plugins from './wcctools.plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
classifyCatalogEntry,
|
|
4
|
+
getDemoAtIndex,
|
|
5
|
+
getDemoCount,
|
|
6
|
+
getElementDemoGroups,
|
|
7
|
+
getElementTagName,
|
|
8
|
+
type ICatalogElementConstructor,
|
|
9
|
+
type TDemoDefinition,
|
|
10
|
+
type TTemplateFactory,
|
|
11
|
+
} from './elements/wcctools.helpers.js';
|
|
12
|
+
import type { IWccConfig, IWccSection } from './wcctools.interfaces.js';
|
|
13
|
+
|
|
14
|
+
type TSectionEntry = [string, any];
|
|
15
|
+
|
|
16
|
+
const applySectionFilterAndSort = (
|
|
17
|
+
section: IWccSection,
|
|
18
|
+
entries: TSectionEntry[],
|
|
19
|
+
fallbackSort?: (a: TSectionEntry, b: TSectionEntry) => number
|
|
20
|
+
): TSectionEntry[] => {
|
|
21
|
+
let result = entries;
|
|
22
|
+
if (section.filter) {
|
|
23
|
+
const filterFn = section.filter;
|
|
24
|
+
result = result.filter(([name, item]) => filterFn(name, item));
|
|
25
|
+
}
|
|
26
|
+
const sortFn = section.sort ?? fallbackSort;
|
|
27
|
+
if (sortFn) {
|
|
28
|
+
result = [...result].sort(sortFn);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The navigable entries of a section, after classification and the section's own filter and sort.
|
|
35
|
+
* Elements sections keep only custom element classes with a usable static `demo`;
|
|
36
|
+
* pages sections keep only template factories.
|
|
37
|
+
*/
|
|
38
|
+
export const getSectionItems = (section: IWccSection): TSectionEntry[] => {
|
|
39
|
+
const entries = Object.entries(section.items).filter(([, item]) =>
|
|
40
|
+
section.type === 'elements'
|
|
41
|
+
? classifyCatalogEntry(item) === 'demo'
|
|
42
|
+
: typeof item === 'function'
|
|
43
|
+
);
|
|
44
|
+
return applySectionFilterAndSort(section, entries);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Registered custom elements of an elements section that have no usable demo.
|
|
49
|
+
* They are listed (not navigable) in the sidebar's "Without demo" group.
|
|
50
|
+
*/
|
|
51
|
+
export const getSectionItemsWithoutDemo = (section: IWccSection): TSectionEntry[] => {
|
|
52
|
+
if (section.type !== 'elements') {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const entries = Object.entries(section.items).filter(([, item]) => classifyCatalogEntry(item) === 'no-demo');
|
|
56
|
+
return applySectionFilterAndSort(section, entries, ([a], [b]) => a.localeCompare(b));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** What a selection resolves to: something to render, or the reason there is nothing. */
|
|
60
|
+
export type TWccCatalogTarget =
|
|
61
|
+
| { kind: 'element'; factory: TTemplateFactory; elementClass: ICatalogElementConstructor }
|
|
62
|
+
| { kind: 'page'; factory: TTemplateFactory }
|
|
63
|
+
| { kind: 'empty'; reason: plugins.interfaces.TWccEmptyReason; emptyState: plugins.interfaces.IWccEmptyState };
|
|
64
|
+
|
|
65
|
+
type TResolvedEntry =
|
|
66
|
+
| { kind: 'listed'; section: IWccSection; item: unknown }
|
|
67
|
+
| { kind: 'no-demo' | 'ignored' | 'missing'; section: IWccSection | null };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The catalog of one `setupWccTools` call: classifies its sections, describes them as a
|
|
71
|
+
* data-only manifest and resolves selections to template factories.
|
|
72
|
+
*/
|
|
73
|
+
export class WccCatalog {
|
|
74
|
+
private readonly sections: IWccSection[];
|
|
75
|
+
|
|
76
|
+
constructor(configArg: IWccConfig) {
|
|
77
|
+
this.sections = configArg.sections;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public getManifest(): plugins.interfaces.IWccCatalogManifest {
|
|
81
|
+
return {
|
|
82
|
+
sections: this.sections.map((section) => ({
|
|
83
|
+
name: section.name,
|
|
84
|
+
type: section.type,
|
|
85
|
+
...(section.icon ? { icon: section.icon } : {}),
|
|
86
|
+
...(section.collapsed ? { collapsed: true } : {}),
|
|
87
|
+
entries: getSectionItems(section).map(([name, item]) => ({
|
|
88
|
+
name,
|
|
89
|
+
tagName: getElementTagName(item),
|
|
90
|
+
demoCount: section.type === 'elements' ? getDemoCount(item.demo as TDemoDefinition) : 1,
|
|
91
|
+
demoGroups: getElementDemoGroups(item),
|
|
92
|
+
})),
|
|
93
|
+
entriesWithoutDemo: getSectionItemsWithoutDemo(section).map(([name, item]) => ({
|
|
94
|
+
name,
|
|
95
|
+
tagName: getElementTagName(item),
|
|
96
|
+
})),
|
|
97
|
+
})),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public resolve(selectionArg: plugins.interfaces.IWccSelection | null): TWccCatalogTarget {
|
|
102
|
+
if (!selectionArg) {
|
|
103
|
+
return {
|
|
104
|
+
kind: 'empty',
|
|
105
|
+
reason: 'nothing-selected',
|
|
106
|
+
emptyState: {
|
|
107
|
+
title: 'Nothing selected',
|
|
108
|
+
detail: 'Choose an element or page in the sidebar to preview it here.',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { sectionName, itemName, demoIndex } = selectionArg;
|
|
113
|
+
const resolved = this.resolveEntry(sectionName, itemName);
|
|
114
|
+
if (resolved.kind !== 'listed') {
|
|
115
|
+
return { kind: 'empty', reason: resolved.kind, emptyState: this.getUnavailableEmptyState(resolved, sectionName, itemName) };
|
|
116
|
+
}
|
|
117
|
+
if (resolved.section.type === 'pages') {
|
|
118
|
+
return { kind: 'page', factory: resolved.item as TTemplateFactory };
|
|
119
|
+
}
|
|
120
|
+
const elementClass = resolved.item as ICatalogElementConstructor;
|
|
121
|
+
const demo = elementClass.demo as TDemoDefinition;
|
|
122
|
+
const factory = getDemoAtIndex(demo, demoIndex);
|
|
123
|
+
if (!factory) {
|
|
124
|
+
const count = getDemoCount(demo);
|
|
125
|
+
return {
|
|
126
|
+
kind: 'empty',
|
|
127
|
+
reason: 'no-such-demo',
|
|
128
|
+
emptyState: {
|
|
129
|
+
title: `${itemName} has no demo ${demoIndex + 1}`,
|
|
130
|
+
detail: `This element defines ${count} ${count === 1 ? 'demo' : 'demos'}.`,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { kind: 'element', factory, elementClass };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a section/item pair to a listed entry, or to the reason it cannot be previewed.
|
|
139
|
+
* The section names `element` and `page` of links from before sections existed search every section.
|
|
140
|
+
*/
|
|
141
|
+
private resolveEntry(sectionNameArg: string, itemNameArg: string): TResolvedEntry {
|
|
142
|
+
const section = this.sections.find((sectionArg) => sectionArg.name === sectionNameArg) ?? null;
|
|
143
|
+
const isLegacyName = !section && (sectionNameArg === 'element' || sectionNameArg === 'page');
|
|
144
|
+
const candidates = section ? [section] : isLegacyName ? this.sections : [];
|
|
145
|
+
|
|
146
|
+
for (const candidate of candidates) {
|
|
147
|
+
const listed = getSectionItems(candidate).find(([name]) => name === itemNameArg);
|
|
148
|
+
if (listed) {
|
|
149
|
+
return { kind: 'listed', section: candidate, item: listed[1] };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const candidate of candidates) {
|
|
153
|
+
if (getSectionItemsWithoutDemo(candidate).some(([name]) => name === itemNameArg)) {
|
|
154
|
+
return { kind: 'no-demo', section: candidate };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const candidate of candidates) {
|
|
158
|
+
if (Object.prototype.hasOwnProperty.call(candidate.items, itemNameArg)) {
|
|
159
|
+
const item = candidate.items[itemNameArg];
|
|
160
|
+
const notDemoable = candidate.type === 'elements'
|
|
161
|
+
? classifyCatalogEntry(item) === 'ignored'
|
|
162
|
+
: typeof item !== 'function';
|
|
163
|
+
return { kind: notDemoable ? 'ignored' : 'missing', section: candidate };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return { kind: 'missing', section };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private getUnavailableEmptyState(
|
|
170
|
+
resolvedArg: Exclude<TResolvedEntry, { kind: 'listed' }>,
|
|
171
|
+
sectionNameArg: string,
|
|
172
|
+
itemNameArg: string,
|
|
173
|
+
): plugins.interfaces.IWccEmptyState {
|
|
174
|
+
switch (resolvedArg.kind) {
|
|
175
|
+
case 'no-demo': {
|
|
176
|
+
const tagName = getElementTagName(resolvedArg.section?.items[itemNameArg]);
|
|
177
|
+
return {
|
|
178
|
+
title: `${itemNameArg} has no demo`,
|
|
179
|
+
detail: `${tagName ? `<${tagName}>` : 'This element'} is a registered custom element without a static demo. It is listed under "Without demo".`,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
case 'ignored':
|
|
183
|
+
return {
|
|
184
|
+
title: `${itemNameArg} cannot be previewed`,
|
|
185
|
+
detail: 'This export is not a custom element with a demo, so the catalog does not list it.',
|
|
186
|
+
};
|
|
187
|
+
case 'missing':
|
|
188
|
+
return {
|
|
189
|
+
title: `${itemNameArg} was not found`,
|
|
190
|
+
detail: `No entry with this name exists in "${sectionNameArg}".`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as plugins from './wcctools.plugins.js';
|
|
2
|
+
import { determinePropertyType, type IReactivePropertyDeclaration } from './elements/wcctools.helpers.js';
|
|
3
|
+
|
|
4
|
+
/** The reactive property declarations Lit keeps on an element class. */
|
|
5
|
+
type TPropertyDeclarations = Map<PropertyKey, IReactivePropertyDeclaration>;
|
|
6
|
+
|
|
7
|
+
const skippedPropertyNames = new Set(['goBright', 'domtools']);
|
|
8
|
+
|
|
9
|
+
const scalarKinds = new Set<plugins.interfaces.TWccPropertyKind>(['String', 'Number', 'Boolean', 'Enum']);
|
|
10
|
+
|
|
11
|
+
const getDeclarations = (elementClassArg: unknown): TPropertyDeclarations | null => {
|
|
12
|
+
const declarations = (elementClassArg as { elementProperties?: unknown }).elementProperties;
|
|
13
|
+
return declarations instanceof Map ? declarations as TPropertyDeclarations : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const toScalar = (valueArg: unknown): plugins.interfaces.TWccPropertyScalar => {
|
|
17
|
+
if (typeof valueArg === 'string' || typeof valueArg === 'number' || typeof valueArg === 'boolean') {
|
|
18
|
+
return valueArg;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toJson = (valueArg: unknown): string | undefined => {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.stringify(valueArg ?? null, null, 2);
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The editable properties an element class declares and the values its instance holds.
|
|
33
|
+
* It runs in the realm that defined the class, so Lit's `type: String` declarations compare
|
|
34
|
+
* against the same `String` and the values never cross into another document's realm.
|
|
35
|
+
*/
|
|
36
|
+
export class WccElementProperties {
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly elementClass: unknown,
|
|
39
|
+
private readonly instance: HTMLElement,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
public describe(): plugins.interfaces.IWccPropertyDescriptor[] | null {
|
|
43
|
+
const declarations = getDeclarations(this.elementClass);
|
|
44
|
+
if (!declarations) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const descriptors: plugins.interfaces.IWccPropertyDescriptor[] = [];
|
|
48
|
+
for (const [key, declaration] of declarations) {
|
|
49
|
+
if (typeof key !== 'string' || key.startsWith('#') || skippedPropertyNames.has(key)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const kind = determinePropertyType(declaration);
|
|
53
|
+
if (!kind) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const value = (this.instance as unknown as Record<string, unknown>)[key];
|
|
57
|
+
if (scalarKinds.has(kind)) {
|
|
58
|
+
descriptors.push(kind === 'Enum'
|
|
59
|
+
? { name: key, kind, value: toScalar(value), enumValues: Object.keys(declaration.type as object) }
|
|
60
|
+
: { name: key, kind, value: toScalar(value) });
|
|
61
|
+
} else {
|
|
62
|
+
descriptors.push({ name: key, kind, json: toJson(value) });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return descriptors;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public set(nameArg: string, valueArg: plugins.interfaces.TWccPropertyScalar): plugins.interfaces.TWccPropertyEditResult {
|
|
69
|
+
const kind = this.getKind(nameArg);
|
|
70
|
+
if (!kind || !scalarKinds.has(kind)) {
|
|
71
|
+
return { ok: false, error: `${nameArg} is not an editable scalar property` };
|
|
72
|
+
}
|
|
73
|
+
(this.instance as unknown as Record<string, unknown>)[nameArg] = valueArg;
|
|
74
|
+
return { ok: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public setJson(nameArg: string, jsonArg: string): plugins.interfaces.TWccPropertyEditResult {
|
|
78
|
+
const kind = this.getKind(nameArg);
|
|
79
|
+
if (kind !== 'Object' && kind !== 'Array') {
|
|
80
|
+
return { ok: false, error: `${nameArg} is not an Object or Array property` };
|
|
81
|
+
}
|
|
82
|
+
let value: unknown;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(jsonArg);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return { ok: false, error: `Invalid JSON: ${(error as Error).message}` };
|
|
87
|
+
}
|
|
88
|
+
(this.instance as unknown as Record<string, unknown>)[nameArg] = value;
|
|
89
|
+
return { ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getKind(nameArg: string): plugins.interfaces.TWccPropertyKind | null {
|
|
93
|
+
const declaration = getDeclarations(this.elementClass)?.get(nameArg);
|
|
94
|
+
return declaration && !skippedPropertyNames.has(nameArg) ? determinePropertyType(declaration) : null;
|
|
95
|
+
}
|
|
96
|
+
}
|