create-template-project 1.5.24 → 1.6.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/README.md +39 -5
- package/dist/config/dependencies.json +21 -1
- package/dist/index.js +733 -142
- package/dist/templates/base/files/README.md +1 -1
- package/dist/templates/base/files/_oxc.config.ts +17 -35
- package/dist/templates/web-widget/files/.github/workflows/pages.yml +36 -0
- package/dist/templates/web-widget/files/index.html +21 -0
- package/dist/templates/web-widget/files/package.json +53 -0
- package/dist/templates/web-widget/files/playwright.config.ts +29 -0
- package/dist/templates/web-widget/files/scripts/screenshot.mjs +35 -0
- package/dist/templates/web-widget/files/src/demo/app.ts +22 -0
- package/dist/templates/web-widget/files/src/demo/demo-shell.css +50 -0
- package/dist/templates/web-widget/files/src/lib/index.ts +2 -0
- package/dist/templates/web-widget/files/src/lib/react.tsx +54 -0
- package/dist/templates/web-widget/files/src/lib/test-setup.ts +7 -0
- package/dist/templates/web-widget/files/src/lib/widget.test.ts +33 -0
- package/dist/templates/web-widget/files/src/lib/widget.ts +83 -0
- package/dist/templates/web-widget/files/src/styles/index.css +42 -0
- package/dist/templates/web-widget/files/stylelint.base.config.js +16 -0
- package/dist/templates/web-widget/files/stylelint.config.js +15 -0
- package/dist/templates/web-widget/files/tests/e2e/widget.e2e-test.ts +16 -0
- package/dist/templates/web-widget/files/tsdown.config.ts +20 -0
- package/dist/templates/web-widget/files/typedoc.json +12 -0
- package/dist/templates/web-widget/files/vite.config.ts +37 -0
- package/package.json +6 -4
- /package/dist/templates/base/files/.github/workflows/{node.js.yml → ci.yml} +0 -0
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/{{projectName}})
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://github.com/{{githubUsername}}/{{projectName}}/actions/workflows/ci.yml)
|
|
8
8
|
[](https://coveralls.io/github/{{githubUsername}}/{{projectName}}?branch=main)
|
|
9
9
|
|
|
10
10
|
## Development Workflow
|
|
@@ -20,32 +20,14 @@ import {configs as regexpConfigs} from 'eslint-plugin-regexp';
|
|
|
20
20
|
/** Filter out core ESLint rules bundled into eslint-plugin-regexp recommended config */
|
|
21
21
|
const regexpPluginRules = Object.fromEntries(Object.entries(regexpConfigs.recommended.rules).filter(([key]) => key.startsWith('regexp/')));
|
|
22
22
|
|
|
23
|
-
const commonIgnore = [
|
|
24
|
-
'**/.*',
|
|
25
|
-
'node_modules/**',
|
|
26
|
-
'dist/**',
|
|
27
|
-
'build/**',
|
|
28
|
-
'coverage/**',
|
|
29
|
-
'temp/**',
|
|
30
|
-
'public/**',
|
|
31
|
-
'**/*.md',
|
|
32
|
-
];
|
|
23
|
+
const commonIgnore = ['**/.*', 'node_modules/**', 'dist/**', 'build/**', 'coverage/**', 'temp/**', 'public/**', '**/*.md'];
|
|
33
24
|
|
|
34
25
|
export const linter = defineConfig({
|
|
35
26
|
options: {
|
|
36
27
|
typeAware: true,
|
|
37
28
|
typeCheck: true,
|
|
38
29
|
},
|
|
39
|
-
plugins: [
|
|
40
|
-
'unicorn',
|
|
41
|
-
'typescript',
|
|
42
|
-
'oxc',
|
|
43
|
-
'import',
|
|
44
|
-
'react',
|
|
45
|
-
'jsdoc',
|
|
46
|
-
'promise',
|
|
47
|
-
'vitest',
|
|
48
|
-
],
|
|
30
|
+
plugins: ['unicorn', 'typescript', 'oxc', 'import', 'react', 'jsdoc', 'promise', 'vitest'],
|
|
49
31
|
jsPlugins: ['eslint-plugin-regexp'],
|
|
50
32
|
categories: {
|
|
51
33
|
correctness: 'error',
|
|
@@ -100,7 +82,7 @@ export const linter = defineConfig({
|
|
|
100
82
|
'import/no-named-export': 'off',
|
|
101
83
|
'import/no-namespace': 'off', // TODO: consider enabling
|
|
102
84
|
'import/no-nodejs-modules': 'off',
|
|
103
|
-
'import/no-unassigned-import': ['error', {
|
|
85
|
+
'import/no-unassigned-import': ['error', {allow: ['**/*.css']}],
|
|
104
86
|
'import/prefer-default-export': 'off',
|
|
105
87
|
'import/no-default-export': 'off',
|
|
106
88
|
'jest/consistent-test-it': 'off',
|
|
@@ -169,7 +151,7 @@ export const linter = defineConfig({
|
|
|
169
151
|
'oxc/no-rest-spread-properties': 'off',
|
|
170
152
|
'unicorn/escape-case': 'off',
|
|
171
153
|
'unicorn/filename-case': 'off', // TODO: consider enabling
|
|
172
|
-
'unicorn/max-nested-calls': ['warn', {
|
|
154
|
+
'unicorn/max-nested-calls': ['warn', {max: 5}],
|
|
173
155
|
'unicorn/no-array-reduce': 'off', // TODO: consider enabling
|
|
174
156
|
'unicorn/no-array-sort': 'off', // TODO: consider enabling
|
|
175
157
|
'unicorn/no-hex-escape': 'off',
|
|
@@ -231,19 +213,19 @@ export const linter = defineConfig({
|
|
|
231
213
|
linkComponents: [],
|
|
232
214
|
version: undefined,
|
|
233
215
|
},
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
216
|
+
jsdoc: {
|
|
217
|
+
ignorePrivate: false,
|
|
218
|
+
ignoreInternal: false,
|
|
219
|
+
ignoreReplacesDocs: true,
|
|
220
|
+
overrideReplacesDocs: true,
|
|
221
|
+
augmentsExtendsReplacesDocs: false,
|
|
222
|
+
implementsReplacesDocs: false,
|
|
223
|
+
exemptDestructuredRootsFromChecks: false,
|
|
224
|
+
tagNamePreference: {},
|
|
225
|
+
},
|
|
226
|
+
vitest: {
|
|
227
|
+
typecheck: false,
|
|
228
|
+
},
|
|
247
229
|
},
|
|
248
230
|
env: {
|
|
249
231
|
builtin: true,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: GitHub Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
deploy:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
pages: write
|
|
13
|
+
id-token: write
|
|
14
|
+
environment:
|
|
15
|
+
name: github-pages
|
|
16
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v7
|
|
21
|
+
with:
|
|
22
|
+
node-version: 22.x
|
|
23
|
+
cache: "{{packageManager}}"
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: "{{installCommand}}"
|
|
26
|
+
- name: Build demo
|
|
27
|
+
run: "{{packageManager}} run build -- --base=/{{projectName}}/"
|
|
28
|
+
- name: Build API docs
|
|
29
|
+
run: "{{packageManager}} run docs:api"
|
|
30
|
+
- name: Stage API docs
|
|
31
|
+
run: mkdir -p dist-demo/docs && mv docs/api dist-demo/docs/api
|
|
32
|
+
- uses: actions/upload-pages-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
path: dist-demo
|
|
35
|
+
- id: deployment
|
|
36
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{projectName}}</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles/index.css" />
|
|
8
|
+
<link rel="stylesheet" href="/src/demo/demo-shell.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<main class="demo-shell">
|
|
12
|
+
<section class="demo-hero" aria-labelledby="demo-title">
|
|
13
|
+
<p class="demo-eyebrow">Native TypeScript widget</p>
|
|
14
|
+
<h1 id="demo-title">{{projectName}}</h1>
|
|
15
|
+
<p class="demo-copy">A dependency-free widget package with a Vite demo, API docs, and npm-ready builds.</p>
|
|
16
|
+
<div id="widget-host" class="demo-card"></div>
|
|
17
|
+
</section>
|
|
18
|
+
</main>
|
|
19
|
+
<script type="module" src="/src/demo/app.ts"></script>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": false,
|
|
3
|
+
"files": ["dist", "CHANGELOG.md"],
|
|
4
|
+
"main": "./dist/index.mjs",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.mts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./react": {
|
|
14
|
+
"types": "./dist/react.d.mts",
|
|
15
|
+
"import": "./dist/react.mjs",
|
|
16
|
+
"default": "./dist/react.mjs"
|
|
17
|
+
},
|
|
18
|
+
"./styles.css": "./dist/styles/index.css"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"lint": "oxlint",
|
|
23
|
+
"lint:css": "stylelint 'src/{demo,styles}/**/*.css'",
|
|
24
|
+
"lint:css:fix": "stylelint --fix 'src/{demo,styles}/**/*.css'",
|
|
25
|
+
"format": "oxfmt --write && stylelint --fix 'src/{demo,styles}/**/*.css'",
|
|
26
|
+
"format:check": "oxfmt --check",
|
|
27
|
+
"ci": "{{packageManager}} run typecheck && {{packageManager}} run lint && {{packageManager}} run lint:css && {{packageManager}} run format:check && {{packageManager}} run build:lib && {{packageManager}} run build && {{packageManager}} run docs:api && {{packageManager}} run test",
|
|
28
|
+
"build:lib": "tsdown",
|
|
29
|
+
"build:demo": "vite build",
|
|
30
|
+
"docs:api": "typedoc",
|
|
31
|
+
"screenshot": "node scripts/screenshot.mjs"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"vite": "",
|
|
35
|
+
"vitest": "",
|
|
36
|
+
"@vitest/browser": "",
|
|
37
|
+
"@vitest/browser-playwright": "",
|
|
38
|
+
"playwright": "",
|
|
39
|
+
"@playwright/test": "",
|
|
40
|
+
"tsdown": "",
|
|
41
|
+
"typedoc": "",
|
|
42
|
+
"stylelint": "",
|
|
43
|
+
"stylelint-config-standard": "",
|
|
44
|
+
"typescript": "6.0.3",
|
|
45
|
+
"react": "",
|
|
46
|
+
"react-dom": "",
|
|
47
|
+
"@types/react": "",
|
|
48
|
+
"@types/react-dom": ""
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": ">=18.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {defineConfig, devices} from '@playwright/test';
|
|
2
|
+
import {env} from 'node:process';
|
|
3
|
+
|
|
4
|
+
const isCi = env.CI !== undefined;
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: './tests/e2e',
|
|
8
|
+
testMatch: '**/*.e2e-test.ts',
|
|
9
|
+
fullyParallel: true,
|
|
10
|
+
forbidOnly: isCi,
|
|
11
|
+
retries: isCi ? 2 : 0,
|
|
12
|
+
workers: isCi ? 1 : undefined,
|
|
13
|
+
reporter: 'html',
|
|
14
|
+
use: {
|
|
15
|
+
baseURL: 'http://localhost:4173',
|
|
16
|
+
trace: 'on-first-retry',
|
|
17
|
+
},
|
|
18
|
+
projects: [
|
|
19
|
+
{
|
|
20
|
+
name: 'chromium',
|
|
21
|
+
use: {...devices['Desktop Chrome']},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
webServer: {
|
|
25
|
+
command: '{{packageManager}} run build && {{packageManager}} run start',
|
|
26
|
+
url: 'http://localhost:4173',
|
|
27
|
+
reuseExistingServer: !isCi,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {mkdir} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {cwd} from 'node:process';
|
|
4
|
+
|
|
5
|
+
import {chromium} from 'playwright';
|
|
6
|
+
import {createServer} from 'vite';
|
|
7
|
+
|
|
8
|
+
const PORT = 4199;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const server = await createServer({
|
|
12
|
+
root: cwd(),
|
|
13
|
+
server: {port: PORT, strictPort: true},
|
|
14
|
+
});
|
|
15
|
+
await server.listen();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const browser = await chromium.launch();
|
|
19
|
+
const page = await browser.newPage({viewport: {width: 1280, height: 800}});
|
|
20
|
+
|
|
21
|
+
await page.goto(`http://localhost:${PORT}`);
|
|
22
|
+
await page.waitForSelector('.widget');
|
|
23
|
+
const outputPath = path.resolve(cwd(), 'docs/images/widget-demo.png');
|
|
24
|
+
await mkdir(path.dirname(outputPath), {recursive: true});
|
|
25
|
+
await page.locator('.demo-hero').screenshot({path: outputPath, type: 'png'});
|
|
26
|
+
|
|
27
|
+
console.log(`Screenshot saved to ${outputPath}`);
|
|
28
|
+
await browser.close();
|
|
29
|
+
} finally {
|
|
30
|
+
await server.close();
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(String(error));
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {createWidget} from '{{projectName}}';
|
|
2
|
+
|
|
3
|
+
const init = (): void => {
|
|
4
|
+
const host = document.querySelector<HTMLElement>('#widget-host');
|
|
5
|
+
if (host === null) {
|
|
6
|
+
throw new Error('Missing #widget-host element');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
createWidget(host, {
|
|
10
|
+
title: '{{projectName}}',
|
|
11
|
+
message: 'Built with TypeScript, tsdown, Vite, TypeDoc, and Playwright.',
|
|
12
|
+
onAction: (count) => {
|
|
13
|
+
console.info(`Widget action ${count}`);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (document.readyState === 'loading') {
|
|
19
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
20
|
+
} else {
|
|
21
|
+
init();
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
body {
|
|
2
|
+
min-width: 320px;
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
margin: 0;
|
|
5
|
+
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
6
|
+
color: #0f172a;
|
|
7
|
+
background: #f8fafc;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.demo-shell {
|
|
11
|
+
display: grid;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
place-items: center;
|
|
14
|
+
padding: 2rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.demo-hero {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: 1rem;
|
|
20
|
+
width: min(100%, 52rem);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.demo-eyebrow {
|
|
24
|
+
margin: 0;
|
|
25
|
+
font-size: 0.8rem;
|
|
26
|
+
font-weight: 800;
|
|
27
|
+
letter-spacing: 0.12em;
|
|
28
|
+
color: #7c3aed;
|
|
29
|
+
text-transform: uppercase;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.demo-hero h1 {
|
|
33
|
+
max-width: 11ch;
|
|
34
|
+
margin: 0;
|
|
35
|
+
font-size: clamp(3rem, 12vw, 6.5rem);
|
|
36
|
+
line-height: 0.9;
|
|
37
|
+
letter-spacing: -0.08em;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.demo-copy {
|
|
41
|
+
max-width: 42rem;
|
|
42
|
+
margin: 0;
|
|
43
|
+
font-size: 1.1rem;
|
|
44
|
+
line-height: 1.6;
|
|
45
|
+
color: #475569;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.demo-card {
|
|
49
|
+
margin-top: 1.5rem;
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {forwardRef, useEffect, useRef, type ForwardedRef, type ReactElement} from 'react';
|
|
2
|
+
|
|
3
|
+
import {DemoWidget as NativeDemoWidget, type DemoWidgetInstance, type DemoWidgetOptions} from './widget.ts';
|
|
4
|
+
|
|
5
|
+
export type DemoWidgetReactProps = DemoWidgetOptions & {
|
|
6
|
+
readonly message?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const setForwardedRef = (ref: ForwardedRef<DemoWidgetInstance>, value: DemoWidgetInstance | null): void => {
|
|
10
|
+
if (typeof ref === 'function') {
|
|
11
|
+
ref(value);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (ref !== null) {
|
|
15
|
+
ref.current = value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const DemoWidget = forwardRef<DemoWidgetInstance, DemoWidgetReactProps>((props, ref): ReactElement => {
|
|
20
|
+
const {message, ...mountOptions} = props;
|
|
21
|
+
const hostRef = useRef<HTMLDivElement | null>(null);
|
|
22
|
+
const instanceRef = useRef<DemoWidgetInstance | null>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const host = hostRef.current;
|
|
26
|
+
if (host === null) {
|
|
27
|
+
return (): void => {
|
|
28
|
+
setForwardedRef(ref, null);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const instance = new NativeDemoWidget(host, {...mountOptions, ...(message === undefined ? {} : {message})});
|
|
33
|
+
instanceRef.current = instance;
|
|
34
|
+
setForwardedRef(ref, instance);
|
|
35
|
+
|
|
36
|
+
return (): void => {
|
|
37
|
+
instanceRef.current = null;
|
|
38
|
+
setForwardedRef(ref, null);
|
|
39
|
+
instance.destroy();
|
|
40
|
+
};
|
|
41
|
+
// Mount-only options are intentionally captured once. Mutable props are synced below through native setters.
|
|
42
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (message !== undefined) {
|
|
47
|
+
instanceRef.current?.setMessage(message);
|
|
48
|
+
}
|
|
49
|
+
}, [message]);
|
|
50
|
+
|
|
51
|
+
return <div ref={hostRef} />;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
DemoWidget.displayName = 'DemoWidget';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {createWidget} from './widget.ts';
|
|
3
|
+
|
|
4
|
+
describe('DemoWidget', () => {
|
|
5
|
+
it('renders and handles actions', async () => {
|
|
6
|
+
const host = document.createElement('div');
|
|
7
|
+
const actions: number[] = [];
|
|
8
|
+
const widget = createWidget(host, {
|
|
9
|
+
title: 'Test Widget',
|
|
10
|
+
onAction: (count) => {
|
|
11
|
+
actions.push(count);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(host.querySelector('.widget__title')?.textContent).toBe('Test Widget');
|
|
16
|
+
await widget.increment();
|
|
17
|
+
|
|
18
|
+
expect(host.querySelector('.widget__message')?.textContent).toBe('Clicked 1 time.');
|
|
19
|
+
expect(actions).toStrictEqual([1]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('throws after destroy', () => {
|
|
23
|
+
const host = document.createElement('div');
|
|
24
|
+
const widget = createWidget(host);
|
|
25
|
+
|
|
26
|
+
widget.destroy();
|
|
27
|
+
|
|
28
|
+
expect(() => {
|
|
29
|
+
widget.setMessage('late update');
|
|
30
|
+
}).toThrow('Widget has been destroyed.');
|
|
31
|
+
expect(host.children).toHaveLength(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type DemoWidgetOptions = {
|
|
2
|
+
readonly title?: string;
|
|
3
|
+
readonly message?: string;
|
|
4
|
+
readonly actionLabel?: string;
|
|
5
|
+
readonly onAction?: (count: number) => void | Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DemoWidgetInstance = {
|
|
9
|
+
readonly increment: () => Promise<void>;
|
|
10
|
+
readonly setMessage: (message: string) => void;
|
|
11
|
+
readonly destroy: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TITLE = 'Native Widget';
|
|
15
|
+
const DEFAULT_MESSAGE = 'Ready to publish on npm.';
|
|
16
|
+
const DEFAULT_ACTION_LABEL = 'Increment';
|
|
17
|
+
|
|
18
|
+
export class DemoWidget implements DemoWidgetInstance {
|
|
19
|
+
readonly #host: HTMLElement;
|
|
20
|
+
readonly #root: HTMLDivElement;
|
|
21
|
+
readonly #message: HTMLParagraphElement;
|
|
22
|
+
readonly #button: HTMLButtonElement;
|
|
23
|
+
readonly #onAction: ((count: number) => void | Promise<void>) | undefined;
|
|
24
|
+
#count = 0;
|
|
25
|
+
#destroyed = false;
|
|
26
|
+
|
|
27
|
+
public constructor(host: HTMLElement, options: DemoWidgetOptions = {}) {
|
|
28
|
+
this.#host = host;
|
|
29
|
+
this.#onAction = options.onAction;
|
|
30
|
+
this.#root = document.createElement('div');
|
|
31
|
+
this.#root.className = 'widget';
|
|
32
|
+
|
|
33
|
+
const title = document.createElement('h2');
|
|
34
|
+
title.className = 'widget__title';
|
|
35
|
+
title.textContent = options.title ?? DEFAULT_TITLE;
|
|
36
|
+
|
|
37
|
+
this.#message = document.createElement('p');
|
|
38
|
+
this.#message.className = 'widget__message';
|
|
39
|
+
this.#message.textContent = options.message ?? DEFAULT_MESSAGE;
|
|
40
|
+
|
|
41
|
+
this.#button = document.createElement('button');
|
|
42
|
+
this.#button.className = 'widget__button';
|
|
43
|
+
this.#button.type = 'button';
|
|
44
|
+
this.#button.textContent = options.actionLabel ?? DEFAULT_ACTION_LABEL;
|
|
45
|
+
this.#button.addEventListener('click', this.#handleClick);
|
|
46
|
+
|
|
47
|
+
this.#root.append(title, this.#message, this.#button);
|
|
48
|
+
this.#host.append(this.#root);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public readonly increment = async (): Promise<void> => {
|
|
52
|
+
this.#assertActive();
|
|
53
|
+
this.#count += 1;
|
|
54
|
+
this.#message.textContent = `Clicked ${this.#count} time${this.#count === 1 ? '' : 's'}.`;
|
|
55
|
+
await this.#onAction?.(this.#count);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
public readonly setMessage = (message: string): void => {
|
|
59
|
+
this.#assertActive();
|
|
60
|
+
this.#message.textContent = message;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
public readonly destroy = (): void => {
|
|
64
|
+
if (this.#destroyed) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.#button.removeEventListener('click', this.#handleClick);
|
|
68
|
+
this.#root.remove();
|
|
69
|
+
this.#destroyed = true;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
readonly #handleClick = (): void => {
|
|
73
|
+
void this.increment();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
#assertActive(): void {
|
|
77
|
+
if (this.#destroyed) {
|
|
78
|
+
throw new Error('Widget has been destroyed.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const createWidget = (host: HTMLElement, options?: DemoWidgetOptions): DemoWidgetInstance => new DemoWidget(host, options);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.widget {
|
|
2
|
+
--widget-bg: #121826;
|
|
3
|
+
--widget-fg: #f8fafc;
|
|
4
|
+
--widget-muted: #a5b4fc;
|
|
5
|
+
--widget-accent: #8b5cf6;
|
|
6
|
+
|
|
7
|
+
display: grid;
|
|
8
|
+
gap: 0.85rem;
|
|
9
|
+
max-width: 28rem;
|
|
10
|
+
padding: 1.25rem;
|
|
11
|
+
color: var(--widget-fg);
|
|
12
|
+
background: linear-gradient(135deg, var(--widget-bg), #312e81);
|
|
13
|
+
border-radius: 1.25rem;
|
|
14
|
+
box-shadow: 0 1.25rem 3rem rgba(15, 23, 42, 0.24);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.widget__title {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font-size: 1.5rem;
|
|
20
|
+
line-height: 1.1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.widget__message {
|
|
24
|
+
margin: 0;
|
|
25
|
+
color: var(--widget-muted);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.widget__button {
|
|
29
|
+
justify-self: start;
|
|
30
|
+
padding: 0.7rem 1rem;
|
|
31
|
+
font: inherit;
|
|
32
|
+
font-weight: 700;
|
|
33
|
+
color: var(--widget-fg);
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
background: var(--widget-accent);
|
|
36
|
+
border: 0;
|
|
37
|
+
border-radius: 999px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.widget__button:hover {
|
|
41
|
+
background: #7c3aed;
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** @type {import('stylelint').Config} */
|
|
2
|
+
const config = {
|
|
3
|
+
extends: ['stylelint-config-standard'],
|
|
4
|
+
rules: {
|
|
5
|
+
'alpha-value-notation': 'number',
|
|
6
|
+
'color-function-notation': 'legacy',
|
|
7
|
+
'declaration-empty-line-before': null,
|
|
8
|
+
'font-family-name-quotes': null,
|
|
9
|
+
'media-feature-range-notation': 'context',
|
|
10
|
+
'number-max-precision': 4,
|
|
11
|
+
'rule-empty-line-before': null,
|
|
12
|
+
'selector-not-notation': 'simple',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import baseConfig from './stylelint.base.config.js';
|
|
2
|
+
|
|
3
|
+
const widgetCssPrefix = 'widget';
|
|
4
|
+
|
|
5
|
+
/** @type {import('stylelint').Config} */
|
|
6
|
+
const config = {
|
|
7
|
+
...baseConfig,
|
|
8
|
+
rules: {
|
|
9
|
+
...baseConfig.rules,
|
|
10
|
+
'custom-property-pattern': `^(${widgetCssPrefix}|demo)-[a-z][a-z0-9-]*$`,
|
|
11
|
+
'selector-class-pattern': [`^(${widgetCssPrefix}(?:[a-zA-Z0-9_-]+)?|demo-[a-zA-Z0-9_-]+)$`, {resolveNestedSelectors: true}],
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default config;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {expect, test} from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
test.describe('widget demo', () => {
|
|
4
|
+
test.beforeEach(async ({page}) => {
|
|
5
|
+
await page.goto('/');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('renders the widget and increments from the demo', async ({page}) => {
|
|
9
|
+
await expect(page.locator('.widget')).toBeVisible();
|
|
10
|
+
await expect(page.locator('.widget__title')).toHaveText('{{projectName}}');
|
|
11
|
+
|
|
12
|
+
await page.locator('.widget__button').click();
|
|
13
|
+
|
|
14
|
+
await expect(page.locator('.widget__message')).toHaveText('Clicked 1 time.');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {defineConfig} from 'tsdown';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/lib/index.ts',
|
|
6
|
+
react: 'src/lib/react.tsx',
|
|
7
|
+
},
|
|
8
|
+
deps: {
|
|
9
|
+
neverBundle: ['react', 'react/jsx-runtime'],
|
|
10
|
+
},
|
|
11
|
+
format: ['esm'],
|
|
12
|
+
dts: true,
|
|
13
|
+
fixedExtension: true,
|
|
14
|
+
outDir: 'dist',
|
|
15
|
+
clean: true,
|
|
16
|
+
target: 'es2022',
|
|
17
|
+
platform: 'neutral',
|
|
18
|
+
sourcemap: true,
|
|
19
|
+
copy: [{from: 'src/styles/index.css', to: 'dist/styles', flatten: true}],
|
|
20
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entryPoints": ["src/lib/index.ts", "src/lib/react.tsx"],
|
|
3
|
+
"out": "docs/api",
|
|
4
|
+
"exclude": ["**/*.test.ts", "**/*.test.tsx", "tests/**", "src/demo/**"],
|
|
5
|
+
"excludeInternal": true,
|
|
6
|
+
"excludePrivate": true,
|
|
7
|
+
"excludeProtected": true,
|
|
8
|
+
"readme": "none",
|
|
9
|
+
"plugin": [],
|
|
10
|
+
"tsconfig": "tsconfig.json",
|
|
11
|
+
"logLevel": "Warn"
|
|
12
|
+
}
|