create-wirejs-app 1.0.3 → 2.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/bin.js +4 -6
- package/package.json +21 -20
- package/templates/default/api/index.js +99 -0
- package/templates/default/api/package.json +18 -0
- package/templates/default/api/prebuild.js +58 -0
- package/templates/default/gitignore +5 -0
- package/templates/default/package.json +25 -0
- package/templates/default/src/build_id.json +1 -0
- package/templates/default/src/components/account-menu.js +123 -0
- package/templates/default/src/components/authenticator.js +149 -0
- package/templates/default/src/components/countdown.js +36 -0
- package/templates/default/src/layouts/default.js +3 -0
- package/templates/default/src/lib/sample-lib.js +8 -0
- package/templates/default/src/package.json +10 -0
- package/templates/default/src/ssg/index.js +27 -0
- package/templates/default/src/ssg/todo-app.js +83 -0
- package/template/_gitignore +0 -4
- package/template/package.json +0 -16
- package/template/src/api/sample.js +0 -10
- package/template/src/api/todo.js +0 -76
- package/template/src/build_id.json +0 -1
- package/template/src/components/countdown.js +0 -33
- package/template/src/layouts/default.js +0 -10
- package/template/src/lib/sample-lib.js +0 -8
- package/template/src/routes/html.html +0 -8
- package/template/src/routes/index.md +0 -30
- /package/{template → templates/default}/src/layouts/bare.html +0 -0
- /package/{template → templates/default}/src/layouts/core.css +0 -0
- /package/{template → templates/default}/src/layouts/default.css +0 -0
- /package/{template → templates/default}/src/layouts/default.html +0 -0
- /package/{template → templates/default}/static/images/wirejs.svg +0 -0
package/bin.js
CHANGED
|
@@ -15,11 +15,9 @@ const [
|
|
|
15
15
|
fs.mkdirSync(projectName);
|
|
16
16
|
|
|
17
17
|
console.log("Writing base package files ...");
|
|
18
|
-
await copy(`${__dirname}/
|
|
19
|
-
await copy(
|
|
20
|
-
|
|
21
|
-
`./${projectName}/.gitignore`
|
|
22
|
-
);
|
|
18
|
+
await copy(`${__dirname}/templates/default`, `./${projectName}`);
|
|
19
|
+
await copy(`${__dirname}/templates/default/gitignore`, `./${projectName}/.gitignore`);
|
|
20
|
+
await fs.promises.unlink(`./${projectName}/gitignore`);
|
|
23
21
|
|
|
24
22
|
const packageJson = await fs.readFileSync(`./${projectName}/package.json`);
|
|
25
23
|
fs.writeFileSync(
|
|
@@ -31,7 +29,7 @@ const [
|
|
|
31
29
|
|
|
32
30
|
console.log("Fetching dependencies ...");
|
|
33
31
|
process.chdir(projectName);
|
|
34
|
-
execSync('npm install'
|
|
32
|
+
execSync('npm install');
|
|
35
33
|
|
|
36
34
|
console.log(`
|
|
37
35
|
Done creating ${projectName}!
|
package/package.json
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
"name": "create-wirejs-app",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Initializes a wirejs package.",
|
|
5
|
+
"author": "Jon Wire",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-wirejs-app": "./bin.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"recursive-copy": "^2.0.14"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "echo \"Nothing to build\"",
|
|
15
|
+
"clean": "echo \"Nothing to clean\""
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin.js",
|
|
19
|
+
"package.json",
|
|
20
|
+
"templates/*"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
|
|
2
|
+
import { defaultGreeting } from '../src/lib/sample-lib.js';
|
|
3
|
+
|
|
4
|
+
const userTodos = new FileService('userTodoApp');
|
|
5
|
+
const wikiPages = new FileService('wikiPages');
|
|
6
|
+
const authService = new AuthenticationService('core-users');
|
|
7
|
+
|
|
8
|
+
export const auth = authService.buildApi();
|
|
9
|
+
|
|
10
|
+
async function currentUser(context) {
|
|
11
|
+
const { user } = await authService.getBaseState(context.cookies);
|
|
12
|
+
return user;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Given a name, this will return a friendly, personalized greeting.
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @returns {Promise<string>} A friendly greeting.
|
|
19
|
+
*/
|
|
20
|
+
export const hello = withContext(context => async (name) => {
|
|
21
|
+
const user = await currentUser();
|
|
22
|
+
return `${defaultGreeting()}, ${user ? `<b>${user}</b>` : '<i>Anonymous</i>'}.`;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const todos = withContext(context => ({
|
|
26
|
+
async read() {
|
|
27
|
+
const user = await currentUser(context);
|
|
28
|
+
|
|
29
|
+
console.log('current user', user);
|
|
30
|
+
|
|
31
|
+
if (!user) {
|
|
32
|
+
throw new Error("Unauthorized");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const todos = await userTodos.read(`${user}/todos.json`);
|
|
37
|
+
return todos ? JSON.parse(todos) : [];
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* @param {string[]} todos
|
|
44
|
+
*/
|
|
45
|
+
async write(todos) {
|
|
46
|
+
const user = await currentUser(context);
|
|
47
|
+
|
|
48
|
+
if (!user) {
|
|
49
|
+
throw new Error("Unauthorized");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!Array.isArray(todos)) {
|
|
53
|
+
throw new Error("Invalid todos!");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!todos.every(todo =>
|
|
57
|
+
typeof todo.id === 'string'
|
|
58
|
+
&& typeof todo.text === 'string')
|
|
59
|
+
) {
|
|
60
|
+
throw new Error("Invalid todos!");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
|
|
64
|
+
await userTodos.write(`${user}/todos.json`, JSON.stringify(finalTodos));
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
function normalizeWikiPageFilename(page) {
|
|
71
|
+
return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const wiki = withContext(context => ({
|
|
75
|
+
async read(page) {
|
|
76
|
+
const filename = normalizeWikiPageFilename(page);
|
|
77
|
+
try {
|
|
78
|
+
return await wikiPages.read(filename);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.log("returning empty content");
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* @param {string[]} todos
|
|
86
|
+
*/
|
|
87
|
+
async write(page, content) {
|
|
88
|
+
const user = await currentUser(context);
|
|
89
|
+
|
|
90
|
+
if (!user) {
|
|
91
|
+
throw new Error("Unauthorized");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const filename = normalizeWikiPageFilename(page);
|
|
95
|
+
await wikiPages.write(filename, content);
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-api",
|
|
3
|
+
"private": "true",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prestart": "node prebuild.js",
|
|
8
|
+
"start": "",
|
|
9
|
+
"prebuild": "node prebuild.js"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"rimraf": "^6.0.1"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"wirejs:client": "./index.client.js",
|
|
16
|
+
"default": "./index.js"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
const indexModule = await import('./index.js');
|
|
4
|
+
|
|
5
|
+
function dedent(tabs, text) {
|
|
6
|
+
const tabString = new Array(tabs).fill('\t').join('');
|
|
7
|
+
return text.trim().replace(new RegExp(`^${tabString}`, 'gm'), '');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const apiCode = Object.keys(indexModule)
|
|
11
|
+
.map(name => `export const ${name} = apiTree(${JSON.stringify([name])});`)
|
|
12
|
+
.join('\n')
|
|
13
|
+
;
|
|
14
|
+
|
|
15
|
+
const baseClient = dedent(1, /* js */ `
|
|
16
|
+
async function wirejsCallApi(method, ...args) {
|
|
17
|
+
let cookieHeader = {};
|
|
18
|
+
if (typeof args[0]?.cookies?.getAll === 'function') {
|
|
19
|
+
const cookies = args[0]?.cookies?.getAll();
|
|
20
|
+
cookieHeader = typeof cookies === 'object'
|
|
21
|
+
? {
|
|
22
|
+
Cookie: Object.entries(cookies).map(kv => kv.join('=')).join('; ')
|
|
23
|
+
}
|
|
24
|
+
: {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const response = await fetch("/api", {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: {
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
...cookieHeader
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify([{method, args:[...args]}]),
|
|
34
|
+
});
|
|
35
|
+
const body = await response.json();
|
|
36
|
+
|
|
37
|
+
const error = body[0].error;
|
|
38
|
+
if (error) {
|
|
39
|
+
throw new Error(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const value = body[0].data;
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function apiTree(path = []) {
|
|
47
|
+
return new Proxy(function() {}, {
|
|
48
|
+
apply(_target, _thisArg, args) {
|
|
49
|
+
return wirejsCallApi(path, ...args);
|
|
50
|
+
},
|
|
51
|
+
get(_target, prop) {
|
|
52
|
+
return apiTree([...path, prop]);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
writeFileSync('index.client.js', [baseClient, apiCode].join('\n\n'));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sample-app",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"workspaces": [
|
|
7
|
+
"src",
|
|
8
|
+
"api"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"wirejs-dom": "^1.0.34",
|
|
12
|
+
"wirejs-resources": "^0.1.1-alpha",
|
|
13
|
+
"dompurify": "^3.2.3",
|
|
14
|
+
"marked": "^15.0.6"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"wirejs-scripts": "^3.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"prebuild": "npm run prebuild --workspaces --if-present",
|
|
21
|
+
"prestart": "npm run prestart --workspaces --if-present",
|
|
22
|
+
"start": "wirejs-scripts start",
|
|
23
|
+
"build": "wirejs-scripts build"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"1731789421822"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { attribute, html, node, text, id } from 'wirejs-dom/v2';
|
|
2
|
+
import { authenticator } from './authenticator.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('wirejs-services').AuthenticationService} AuthenticationService
|
|
6
|
+
* @typedef {ReturnType<AuthenticationService['buildApi']>} AuthStateApi
|
|
7
|
+
* @typedef {Awaited<ReturnType<AuthStateApi['getState']>>} AuthState
|
|
8
|
+
* @typedef {AuthState['actions'][string]} AuthStateAction
|
|
9
|
+
* @typedef {Parameters<AuthStateApi['setState']>[0]} AuthStateActionInput
|
|
10
|
+
* @typedef {import('wirejs-services').Context} Context
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {AuthStateApi} api
|
|
15
|
+
*/
|
|
16
|
+
export const accountMenu = (api) => {
|
|
17
|
+
const uiState = {
|
|
18
|
+
expanded: false
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @type {Set<(state: AuthState) => any>}
|
|
23
|
+
*/
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
|
|
26
|
+
const listenForClose = event => {
|
|
27
|
+
if (
|
|
28
|
+
(event.type === 'click' && !self.data.menu.contains(event.target))
|
|
29
|
+
|| (event.type === 'keyup' && event.key === 'Escape')
|
|
30
|
+
) {
|
|
31
|
+
close()
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const close = () => {
|
|
36
|
+
uiState.expanded = false;
|
|
37
|
+
updateStyleToMatchState();
|
|
38
|
+
document.removeEventListener('click', listenForClose);
|
|
39
|
+
document.removeEventListener('keyup', listenForClose);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const removeListenForClose = () => {
|
|
43
|
+
document.removeEventListener('click', listenForClose);
|
|
44
|
+
document.removeEventListener('keyup', listenForClose);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const updateStyleToMatchState = () => {
|
|
48
|
+
self.data.menu.style.display = uiState.expanded ? '' : 'none';
|
|
49
|
+
const position = self.getBoundingClientRect();
|
|
50
|
+
self.data.menu.style.top = `${position.bottom + 1}px`;
|
|
51
|
+
self.data.menu.style.right = `${document.body.clientWidth - position.right + 16}px`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const authenticatorNode = authenticator(api);
|
|
55
|
+
authenticatorNode.data.onchange(state => {
|
|
56
|
+
self.data.user = state.state.user || '';
|
|
57
|
+
close();
|
|
58
|
+
for (const listener of listeners) {
|
|
59
|
+
try {
|
|
60
|
+
listener(state);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Error calling auth state listener.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const self = html`<accountmenu style='display: inline-block;'>
|
|
68
|
+
<div
|
|
69
|
+
style='display: inline-block;'
|
|
70
|
+
>${node('user', name => name ? html`<b>${name}</b>` : html`<i>Anonymous</i>`)}</div>
|
|
71
|
+
<div style='
|
|
72
|
+
display: inline-block;
|
|
73
|
+
border: 1px solid silver;
|
|
74
|
+
border-radius: 0.25rem;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
padding: 0 0.25em;
|
|
77
|
+
'
|
|
78
|
+
onclick=${() => {
|
|
79
|
+
uiState.expanded = !uiState.expanded;
|
|
80
|
+
updateStyleToMatchState();
|
|
81
|
+
if (uiState.expanded) {
|
|
82
|
+
authenticatorNode.data.focus();
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
document.addEventListener('click', listenForClose);
|
|
85
|
+
document.addEventListener('keyup', listenForClose);
|
|
86
|
+
}, 1);
|
|
87
|
+
} else {
|
|
88
|
+
removeListenForClose()
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
>☰</div>
|
|
92
|
+
<div ${id('menu')} style='
|
|
93
|
+
display: none;
|
|
94
|
+
position: absolute;
|
|
95
|
+
border: 1px solid gray;
|
|
96
|
+
border-radius: 0.25rem;
|
|
97
|
+
background-color: white;
|
|
98
|
+
padding: 0.5rem;
|
|
99
|
+
box-shadow: -0.125rem 0.125rem 0.25rem lightgray;
|
|
100
|
+
'>${node('authenticator', authenticatorNode)}</div>
|
|
101
|
+
</accountmenu>`.onadd(async self => {
|
|
102
|
+
const state = await api.getState(true);
|
|
103
|
+
self.data.user = state.state.user || '';
|
|
104
|
+
}).extend(self => ({
|
|
105
|
+
data: {
|
|
106
|
+
/**
|
|
107
|
+
* @param {(state: AuthState) => any} callback
|
|
108
|
+
*/
|
|
109
|
+
onchange: (callback) => {
|
|
110
|
+
listeners.add(callback);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {(state: AuthState) => any} callback
|
|
115
|
+
*/
|
|
116
|
+
removeonchange: (callback) => {
|
|
117
|
+
listeners.delete(callback);
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return self;
|
|
123
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { attribute, html, node } from 'wirejs-dom/v2';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('wirejs-services').AuthenticationService} AuthenticationService
|
|
5
|
+
* @typedef {ReturnType<AuthenticationService['buildApi']>} AuthStateApi
|
|
6
|
+
* @typedef {Awaited<ReturnType<AuthStateApi['getState']>>} AuthState
|
|
7
|
+
* @typedef {AuthState['actions'][string]} AuthStateAction
|
|
8
|
+
* @typedef {Parameters<AuthStateApi['setState']>[0]} AuthStateActionInput
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {AuthStateAction} action
|
|
13
|
+
* @param {(act: AuthStateActionInput) => void} act
|
|
14
|
+
*/
|
|
15
|
+
export const authenticatoraction = (action, act) => {
|
|
16
|
+
const inputs = Object.entries(action.inputs || []).map(([name, { label, type }]) => {
|
|
17
|
+
const id = `input_${Math.floor(Math.random() * 1_000_000)}`;
|
|
18
|
+
const input = html`<div>
|
|
19
|
+
<label for=${id}>${label}</label>
|
|
20
|
+
<br />
|
|
21
|
+
<input
|
|
22
|
+
id=${id}
|
|
23
|
+
name=${name}
|
|
24
|
+
type=${type}
|
|
25
|
+
value=${attribute('value', '')}
|
|
26
|
+
style='width: calc(100% - 1rem); margin-bottom: 0.5rem;'
|
|
27
|
+
/>
|
|
28
|
+
</div>`.extend(self => ({
|
|
29
|
+
data: { name }
|
|
30
|
+
}));
|
|
31
|
+
return input;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const buttons = action.buttons?.map(b => html`<p>
|
|
35
|
+
<button type='submit' value='${b}'>${b}</button>
|
|
36
|
+
</p>`);
|
|
37
|
+
|
|
38
|
+
const link = buttons ? undefined : [
|
|
39
|
+
html`<p><a
|
|
40
|
+
style='cursor: pointer; font-weight: bold;'
|
|
41
|
+
onclick=${() => act({ key: action.key })}
|
|
42
|
+
>${action.name}</a></p>`
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const actors = link ?? buttons;
|
|
46
|
+
|
|
47
|
+
if (action.inputs && Object.keys(action.inputs).length > 0) {
|
|
48
|
+
return html`<authenticatoraction>
|
|
49
|
+
<div>
|
|
50
|
+
<h4 style='margin-top: 1rem; margin-bottom: 0.5rem;'>${action.name}</h4>
|
|
51
|
+
<form
|
|
52
|
+
onsubmit=${evt => {
|
|
53
|
+
evt.preventDefault();
|
|
54
|
+
act({
|
|
55
|
+
key: action.key,
|
|
56
|
+
verb: evt.submitter?.value,
|
|
57
|
+
inputs: Object.fromEntries(inputs.map(input => ([
|
|
58
|
+
input.data.name,
|
|
59
|
+
input.data.value
|
|
60
|
+
])))
|
|
61
|
+
});
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
${inputs}
|
|
65
|
+
${actors}
|
|
66
|
+
</form>
|
|
67
|
+
<hr style='width: 33%; height: 1px; border: none; background: silver;' />
|
|
68
|
+
</div>
|
|
69
|
+
</authenticatoraction>`;
|
|
70
|
+
} else {
|
|
71
|
+
return html`<authenticatoraction>
|
|
72
|
+
${actors}
|
|
73
|
+
</authenticatoraction>`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {AuthStateApi} stateManager
|
|
79
|
+
* @returns
|
|
80
|
+
*/
|
|
81
|
+
export const authenticator = (stateManager) => {
|
|
82
|
+
/**
|
|
83
|
+
* @type {Set<(state: AuthState) => any>}
|
|
84
|
+
*/
|
|
85
|
+
const listeners = new Set();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @type {AuthState}
|
|
89
|
+
*/
|
|
90
|
+
let lastKnownState = undefined;
|
|
91
|
+
|
|
92
|
+
const self = html`<authenticator style='display: block; min-width: 15em;'>
|
|
93
|
+
${node('state', html`<span>Loading ...</span>`)}
|
|
94
|
+
</authenticator>`.extend(self => ({
|
|
95
|
+
/**
|
|
96
|
+
* @param {AuthState} state
|
|
97
|
+
*/
|
|
98
|
+
renderState(state) {
|
|
99
|
+
lastKnownState = state;
|
|
100
|
+
if (state.errors) {
|
|
101
|
+
alert(state.errors.map(e => e.message).join("\n\n"));
|
|
102
|
+
} else {
|
|
103
|
+
self.data.state = html`<div>
|
|
104
|
+
<div>${state.message || ''}</div>
|
|
105
|
+
<div>${Object.entries(state.actions).map(([key, action]) => {
|
|
106
|
+
return authenticatoraction({key, ...action}, async act => {
|
|
107
|
+
self.renderState(await stateManager.setState(true, act));
|
|
108
|
+
});
|
|
109
|
+
})}</div>
|
|
110
|
+
</div>`;
|
|
111
|
+
}
|
|
112
|
+
for (const listener of listeners) {
|
|
113
|
+
try {
|
|
114
|
+
listener(state);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error("Error calling auth state listener.");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
})).onadd(async (self) => {
|
|
121
|
+
self.renderState(await stateManager.getState(true))
|
|
122
|
+
}).extend(self => ({
|
|
123
|
+
data: {
|
|
124
|
+
/**
|
|
125
|
+
* @param {(state: AuthState) => any} callback
|
|
126
|
+
*/
|
|
127
|
+
onchange: (callback) => {
|
|
128
|
+
listeners.add(callback);
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {(state: AuthState) => any} callback
|
|
133
|
+
*/
|
|
134
|
+
removeonchange: (callback) => {
|
|
135
|
+
listeners.delete(callback);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
focus: () => {
|
|
139
|
+
[...self.getElementsByTagName('input')].shift()?.focus();
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
get lastKnownState() {
|
|
143
|
+
return lastKnownState;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
return self;
|
|
149
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hello } from 'my-api';
|
|
2
|
+
import { html, text, node } from 'wirejs-dom/v2';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Counts down from a given time.
|
|
6
|
+
*
|
|
7
|
+
* @param {number} from - The time to count down "from".
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export async function Countdown(T = 10) {
|
|
11
|
+
return html`<div>
|
|
12
|
+
${node('remaining', T, timeOrGreeting => {
|
|
13
|
+
if (typeof timeOrGreeting === 'string') {
|
|
14
|
+
return html`<div>${timeOrGreeting}</div>`;
|
|
15
|
+
} else if (timeOrGreeting === 0) {
|
|
16
|
+
return html`<div><b>ALL DONE!</b></div>`;
|
|
17
|
+
} else if (timeOrGreeting === 1) {
|
|
18
|
+
return html`<div><i>ONE second left!!!</i></div>`;
|
|
19
|
+
} else {
|
|
20
|
+
return html`<div>${timeOrGreeting} seconds remaining ...</div>`;
|
|
21
|
+
}
|
|
22
|
+
})}
|
|
23
|
+
</div>`.onadd(self => {
|
|
24
|
+
function tick() {
|
|
25
|
+
self.data.remaining = self.data.remaining - 1;
|
|
26
|
+
if (self.data.remaining > 0) {
|
|
27
|
+
setTimeout(() => tick(), 1000);
|
|
28
|
+
} else {
|
|
29
|
+
hello("So and so").then(r => self.data.remaining = r);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
tick();
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default Countdown;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { html, hydrate } from 'wirejs-dom/v2';
|
|
2
|
+
|
|
3
|
+
async function App() {
|
|
4
|
+
return html`<div id='app'>
|
|
5
|
+
</div>`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function generate() {
|
|
9
|
+
const page = html`
|
|
10
|
+
<!doctype html>
|
|
11
|
+
<html>
|
|
12
|
+
<head>
|
|
13
|
+
<title>Welcome!</title>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<h1>Welcome!</h1>
|
|
17
|
+
<p>This is your wirejs app!</p>
|
|
18
|
+
<p>It comes with some sample API methods and pages.</p>
|
|
19
|
+
<ul>
|
|
20
|
+
<li><a href='/todo-app.html'>Todo App</a></li>
|
|
21
|
+
<li><a href='/simple-wiki/index.html'>Simple Wiki</a></li>
|
|
22
|
+
</ul>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
25
|
+
`;
|
|
26
|
+
return page;
|
|
27
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { html, node, list, attribute, hydrate } from 'wirejs-dom/v2';
|
|
2
|
+
import { accountMenu } from '../components/account-menu.js';
|
|
3
|
+
import { auth, todos } from 'my-api';
|
|
4
|
+
|
|
5
|
+
function Todos() {
|
|
6
|
+
const save = async () => {
|
|
7
|
+
try {
|
|
8
|
+
await todos.write(true, self.data.todos);
|
|
9
|
+
} catch (error) {
|
|
10
|
+
alert(error);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const remove = todo => {
|
|
15
|
+
self.data.todos = self.data.todos.filter(t => t.id !== todo.id);
|
|
16
|
+
save();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const newid = () => crypto.randomUUID();
|
|
20
|
+
|
|
21
|
+
const self = html`<div>
|
|
22
|
+
<h4>Your Todos</h4>
|
|
23
|
+
<ol>${list('todos', todo => html`<li>
|
|
24
|
+
${todo.text} : <span
|
|
25
|
+
style='color: darkred; font-weight: bold; cursor: pointer;'
|
|
26
|
+
onclick=${() => remove(todo)}
|
|
27
|
+
>X</span>
|
|
28
|
+
</li>`)}</ol>
|
|
29
|
+
<div>
|
|
30
|
+
<form onsubmit=${event => {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
self.data.todos.push({ id: newid(), text: self.data.newTodo });
|
|
33
|
+
self.data.newTodo = '';
|
|
34
|
+
save();
|
|
35
|
+
}}>
|
|
36
|
+
<input type='text' value=${attribute('newTodo', '')} />
|
|
37
|
+
<input type='submit' value='Add' />
|
|
38
|
+
</form>
|
|
39
|
+
</div>
|
|
40
|
+
<div>`.onadd(async self => {
|
|
41
|
+
self.data.todos = await todos.read(true);
|
|
42
|
+
});
|
|
43
|
+
return self;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function App() {
|
|
47
|
+
const accountMenuNode = accountMenu(auth);
|
|
48
|
+
|
|
49
|
+
accountMenuNode.data.onchange(async state => {
|
|
50
|
+
if (state.state.user) {
|
|
51
|
+
self.data.content = Todos();
|
|
52
|
+
} else {
|
|
53
|
+
self.data.content = html`<div>You need to sign in to add your todo list.</div>`;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const self = html`<div id='app'>
|
|
58
|
+
<div style='float: right;'>${accountMenuNode}</div>
|
|
59
|
+
${node('content', html`<div>Loading ...</div>`)}
|
|
60
|
+
</div>`;
|
|
61
|
+
|
|
62
|
+
return self;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function generate() {
|
|
66
|
+
const page = html`
|
|
67
|
+
<!doctype html>
|
|
68
|
+
<html>
|
|
69
|
+
<head>
|
|
70
|
+
<title>Todo App</title>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<p><a href='/'>Home</a></p>
|
|
74
|
+
<h1>Todo App</h1>
|
|
75
|
+
${await App()}
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
return page;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
hydrate('app', App);
|
package/template/_gitignore
DELETED
package/template/package.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "project-name",
|
|
3
|
-
"version": "2.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"dependencies": {
|
|
6
|
-
"highlight.js": "^11.5.1",
|
|
7
|
-
"wirejs-dom": "^1.0.7"
|
|
8
|
-
},
|
|
9
|
-
"devDependencies": {
|
|
10
|
-
"wirejs-scripts": "^2.0.0"
|
|
11
|
-
},
|
|
12
|
-
"scripts": {
|
|
13
|
-
"start": "wirejs-scripts start",
|
|
14
|
-
"build": "wirejs-scripts build"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const { defaultGreeting } = require('../lib/sample-lib');
|
|
2
|
-
|
|
3
|
-
module.exports = {
|
|
4
|
-
/**
|
|
5
|
-
* Given a name, this will return a friendly, personalized greeting.
|
|
6
|
-
* @param {string} name
|
|
7
|
-
* @returns {string} A friendly greeting.
|
|
8
|
-
*/
|
|
9
|
-
hello: async (name) => `${defaultGreeting()}, ${name}.`
|
|
10
|
-
};
|
package/template/src/api/todo.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// included from local template during development.
|
|
2
|
-
// long term, maybe this goes into the wirejs-scripts repo, or possibly as a
|
|
3
|
-
// dep thereof. but, the collection class provided may eventually depend on
|
|
4
|
-
// which deployment target is specified.
|
|
5
|
-
const { Collection } = require('../api-lib/collection');
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Simple API example demonstrating interaction with a collection without auth.
|
|
9
|
-
*
|
|
10
|
-
* The `Note` class used shows how you might perform simple validation on data
|
|
11
|
-
* as it enters the API boundary.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* A simple sample class we'll be saving and loading from the API.
|
|
17
|
-
*
|
|
18
|
-
* We could just as easily *not* use a class at all. (Maybe that would actually
|
|
19
|
-
* be better for this example ...)
|
|
20
|
-
*/
|
|
21
|
-
class Note {
|
|
22
|
-
/**
|
|
23
|
-
* Construct a new note with an id (optional), title, and body.
|
|
24
|
-
*
|
|
25
|
-
* If an id is not given, one will be generated.
|
|
26
|
-
*/
|
|
27
|
-
constructor({id, title, body}) {
|
|
28
|
-
this.id = id || this.makeId();
|
|
29
|
-
this.title = title;
|
|
30
|
-
this.body = body;
|
|
31
|
-
this.validate();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Semi-random, semi-increasing string. Date + rand.
|
|
36
|
-
*/
|
|
37
|
-
makeId() {
|
|
38
|
-
return Date.now() + '-' + String(Math.random()).substring(2, 10);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Throw an error if the note doesn't contain all required fields with the
|
|
43
|
-
* required constraints. In this case, id, title, and body must all be
|
|
44
|
-
* "truthy" values.
|
|
45
|
-
*/
|
|
46
|
-
validate() {
|
|
47
|
-
if (!(this.id && this.title && this.body)) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
"A note must contain a truthy id, title, and body!"
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The actual API.
|
|
57
|
-
*/
|
|
58
|
-
module.exports = {
|
|
59
|
-
/**
|
|
60
|
-
* Adds a new note.
|
|
61
|
-
*/
|
|
62
|
-
add: async (note) => {
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
/*
|
|
66
|
-
*
|
|
67
|
-
*/
|
|
68
|
-
remove: async (id) => {
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
/*
|
|
72
|
-
*
|
|
73
|
-
*/
|
|
74
|
-
list: async () => {
|
|
75
|
-
}
|
|
76
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"1677445565514"
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const { DomClass } = require('wirejs-dom');
|
|
2
|
-
|
|
3
|
-
const markup = `<sample:countdown>
|
|
4
|
-
<h3>Limited time offer!</h3>
|
|
5
|
-
<div data-id='countdown'>
|
|
6
|
-
<b>
|
|
7
|
-
<span data-id='remaining'></span>
|
|
8
|
-
<span data-id='label'>seconds</span>
|
|
9
|
-
</b> left!
|
|
10
|
-
</div>
|
|
11
|
-
</sample:countdown>`;
|
|
12
|
-
|
|
13
|
-
const Countdown = DomClass(markup, function() {
|
|
14
|
-
this.remaining = this.from || 60;
|
|
15
|
-
|
|
16
|
-
const tick = () => {
|
|
17
|
-
this.remaining = this.remaining - 1;
|
|
18
|
-
|
|
19
|
-
if (this.remaining == 1) {
|
|
20
|
-
this.label = 'second';
|
|
21
|
-
} else if (this.remaining == 0) {
|
|
22
|
-
this.countdown = '<b>ALL DONE! <i>You missed it!!!</i></b>';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (this.remaining > 0) {
|
|
26
|
-
setTimeout(() => tick(), 1000);
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
tick();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
module.exports = Countdown;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// core deps
|
|
2
|
-
const wirejs = require('wirejs-dom');
|
|
3
|
-
require('highlight.js/styles/github.css');
|
|
4
|
-
require('./default.css');
|
|
5
|
-
|
|
6
|
-
// expose wirejs to inline scripts
|
|
7
|
-
Object.assign(window, wirejs);
|
|
8
|
-
|
|
9
|
-
// common components
|
|
10
|
-
require('../components/countdown');
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
${meta({
|
|
2
|
-
title: "an html page"
|
|
3
|
-
})}
|
|
4
|
-
<p><code>html</code> pages use the default template by default too.</p>
|
|
5
|
-
<p>And, it's important to remember that pages are parsed like
|
|
6
|
-
<a href='https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals' target='_blank'>template literals</a>
|
|
7
|
-
<b>at build time</b>.
|
|
8
|
-
</p>
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
${meta({
|
|
2
|
-
title: "First Page"
|
|
3
|
-
})}
|
|
4
|
-
|
|
5
|
-
# markdown!
|
|
6
|
-
|
|
7
|
-
If all you need is markdown, you can do that!
|
|
8
|
-
|
|
9
|
-
We can even do code blocks with syntax highlighting.
|
|
10
|
-
|
|
11
|
-
```js
|
|
12
|
-
const result = getResult();
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Or mermaid diagrams:
|
|
16
|
-
|
|
17
|
-
```mermaid
|
|
18
|
-
graph LR;
|
|
19
|
-
x --> y(probably y)
|
|
20
|
-
y --> z(definitely z)
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Of course, you can also build [normal html pages](html.html).
|
|
24
|
-
|
|
25
|
-
And of course, you can also embed HTML with custom components directly in your
|
|
26
|
-
markdown:
|
|
27
|
-
|
|
28
|
-
<div>
|
|
29
|
-
<sample:countdown from=10></sample:countdown>
|
|
30
|
-
</div>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|