create-wirejs-app 2.0.9 → 2.0.10

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 CHANGED
@@ -18,11 +18,7 @@ const [
18
18
  await copy(`${__dirname}/templates/default`, `./${projectName}`);
19
19
  await copy(`${__dirname}/templates/default/gitignore`, `./${projectName}/.gitignore`);
20
20
  await fs.promises.unlink(`./${projectName}/gitignore`);
21
- await fs.promises.rename(
22
- `./${projectName}/src/ssr/simple-wiki/STAR.js`,
23
- `./${projectName}/src/ssr/simple-wiki/*.js`
24
- )
25
-
21
+
26
22
  const packageJson = await fs.readFileSync(`./${projectName}/package.json`);
27
23
  fs.writeFileSync(
28
24
  `./${projectName}/package.json`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-wirejs-app",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "Initializes a wirejs package.",
5
5
  "author": "Jon Wire",
6
6
  "license": "MIT",
@@ -0,0 +1,63 @@
1
+ import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
2
+
3
+ const userTodos = new FileService('app', 'userTodoApp');
4
+ const wikiPages = new FileService('app', 'wikiPages');
5
+ const authService = new AuthenticationService('app', 'core-users');
6
+
7
+ export const auth = authService.buildApi();
8
+
9
+ export type Todo = {
10
+ id: string;
11
+ text: string;
12
+ };
13
+
14
+ export const todos = withContext(context => ({
15
+ async read(): Promise<Todo[]> {
16
+ const user = await auth.requireCurrentUser(context);
17
+
18
+ try {
19
+ const todos = await userTodos.read(`${user.id}/todos.json`);
20
+ return todos ? JSON.parse(todos) : [];
21
+ } catch (error) {
22
+ return [];
23
+ }
24
+ },
25
+ async write(todos: Todo[]) {
26
+ const user = await auth.requireCurrentUser(context);
27
+
28
+ if (!Array.isArray(todos) || !todos.every(todo =>
29
+ typeof todo.id === 'string'
30
+ && typeof todo.text === 'string')
31
+ ) {
32
+ throw new Error("Invalid todos!");
33
+ }
34
+
35
+ const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
36
+ await userTodos.write(`${user.id}/todos.json`, JSON.stringify(finalTodos));
37
+
38
+ return true;
39
+ }
40
+ }));
41
+
42
+ function normalizeWikiPageFilename(page: string) {
43
+ return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
44
+ }
45
+
46
+ export const wiki = withContext(context => ({
47
+ async read(page: string) {
48
+ const filename = normalizeWikiPageFilename(page);
49
+ try {
50
+ return await wikiPages.read(filename);
51
+ } catch (error) {
52
+ return null;
53
+ }
54
+ },
55
+ async write(page: string, content: string) {
56
+ await auth.requireCurrentUser(context);
57
+
58
+ const filename = normalizeWikiPageFilename(page);
59
+ await wikiPages.write(filename, content);
60
+
61
+ return true;
62
+ }
63
+ }));
@@ -1,5 +1,7 @@
1
1
  node_modules
2
2
  dist
3
+ pre-dist
3
4
  temp
4
5
  src/build_id.json
5
6
  api/index.client.js
7
+ api/index.js
@@ -0,0 +1,63 @@
1
+ import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
2
+
3
+ const userTodos = new FileService('app', 'userTodoApp');
4
+ const wikiPages = new FileService('app', 'wikiPages');
5
+ const authService = new AuthenticationService('app', 'core-users');
6
+
7
+ export const auth = authService.buildApi();
8
+
9
+ export type Todo = {
10
+ id: string;
11
+ text: string;
12
+ };
13
+
14
+ export const todos = withContext(context => ({
15
+ async read(): Promise<Todo[]> {
16
+ const user = await auth.requireCurrentUser(context);
17
+
18
+ try {
19
+ const todos = await userTodos.read(`${user.id}/todos.json`);
20
+ return todos ? JSON.parse(todos) : [];
21
+ } catch (error) {
22
+ return [];
23
+ }
24
+ },
25
+ async write(todos: Todo[]) {
26
+ const user = await auth.requireCurrentUser(context);
27
+
28
+ if (!Array.isArray(todos) || !todos.every(todo =>
29
+ typeof todo.id === 'string'
30
+ && typeof todo.text === 'string')
31
+ ) {
32
+ throw new Error("Invalid todos!");
33
+ }
34
+
35
+ const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
36
+ await userTodos.write(`${user.id}/todos.json`, JSON.stringify(finalTodos));
37
+
38
+ return true;
39
+ }
40
+ }));
41
+
42
+ function normalizeWikiPageFilename(page: string) {
43
+ return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
44
+ }
45
+
46
+ export const wiki = withContext(context => ({
47
+ async read(page: string) {
48
+ const filename = normalizeWikiPageFilename(page);
49
+ try {
50
+ return await wikiPages.read(filename);
51
+ } catch (error) {
52
+ return null;
53
+ }
54
+ },
55
+ async write(page: string, content: string) {
56
+ await auth.requireCurrentUser(context);
57
+
58
+ const filename = normalizeWikiPageFilename(page);
59
+ await wikiPages.write(filename, content);
60
+
61
+ return true;
62
+ }
63
+ }));
@@ -8,13 +8,14 @@
8
8
  "api"
9
9
  ],
10
10
  "dependencies": {
11
- "wirejs-dom": "^1.0.35",
12
- "wirejs-resources": "^0.1.9-alpha",
13
11
  "dompurify": "^3.2.3",
14
- "marked": "^15.0.6"
12
+ "marked": "^15.0.6",
13
+ "wirejs-dom": "^1.0.38",
14
+ "wirejs-resources": "^0.1.9-alpha"
15
15
  },
16
16
  "devDependencies": {
17
- "wirejs-scripts": "^3.0.2"
17
+ "wirejs-scripts": "^3.0.7",
18
+ "typescript": "^5.7.3"
18
19
  },
19
20
  "scripts": {
20
21
  "prebuild": "npm run prebuild --workspaces --if-present",
@@ -1,32 +1,28 @@
1
1
  import { attribute, html, node, text, id } from 'wirejs-dom/v2';
2
2
  import { authenticator } from './authenticator.js';
3
+ import type {
4
+ AuthenticationApi,
5
+ AuthenticationMachineState,
6
+ } from 'wirejs-resources';
3
7
 
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
- */
8
+ type Callback = (state: AuthenticationMachineState) => any;
12
9
 
13
- /**
14
- * @param {AuthStateApi} api
15
- */
16
- export const accountMenu = (api) => {
10
+ export const accountMenu = ({ api, initialState }: {
11
+ api: AuthenticationApi,
12
+ initialState?: AuthenticationMachineState
13
+ }) => {
17
14
  const uiState = {
18
15
  expanded: false
19
16
  };
20
17
 
21
- /**
22
- * @type {Set<(state: AuthState) => any>}
23
- */
24
- const listeners = new Set();
18
+ const listeners = new Set<Callback>();
25
19
 
26
- const listenForClose = event => {
20
+ const listenForClose = (
21
+ event: (MouseEvent | KeyboardEvent)
22
+ ) => {
27
23
  if (
28
- (event.type === 'click' && !self.data.menu.contains(event.target))
29
- || (event.type === 'keyup' && event.key === 'Escape')
24
+ (event.type === 'click' && !self.data.menu.contains(event.target as any))
25
+ || (event.type === 'keyup' && (event as any).key === 'Escape')
30
26
  ) {
31
27
  close()
32
28
  }
@@ -51,9 +47,9 @@ export const accountMenu = (api) => {
51
47
  self.data.menu.style.right = `${document.body.clientWidth - position.right + 16}px`;
52
48
  };
53
49
 
54
- const authenticatorNode = authenticator(api);
50
+ const authenticatorNode = authenticator(api, initialState);
55
51
  authenticatorNode.data.onchange(state => {
56
- self.data.user = state.state.user || '';
52
+ self.data.user = state.user?.username || '';
57
53
  close();
58
54
  for (const listener of listeners) {
59
55
  try {
@@ -67,7 +63,10 @@ export const accountMenu = (api) => {
67
63
  const self = html`<accountmenu style='display: inline-block;'>
68
64
  <div
69
65
  style='display: inline-block;'
70
- >${node('user', name => name ? html`<b>${name}</b>` : html`<i>Anonymous</i>`)}</div>
66
+ >${node(
67
+ 'user',
68
+ initialState?.user?.username || '',
69
+ name => name ? html`<b>${name}</b>` : html`<i>Anonymous</i>`)}</div>
71
70
  <div style='
72
71
  display: inline-block;
73
72
  border: 1px solid silver;
@@ -89,7 +88,7 @@ export const accountMenu = (api) => {
89
88
  }
90
89
  }}
91
90
  >☰</div>
92
- <div ${id('menu')} style='
91
+ <div ${id('menu', HTMLDivElement)} style='
93
92
  display: none;
94
93
  position: absolute;
95
94
  border: 1px solid gray;
@@ -99,21 +98,18 @@ export const accountMenu = (api) => {
99
98
  box-shadow: -0.125rem 0.125rem 0.25rem lightgray;
100
99
  '>${node('authenticator', authenticatorNode)}</div>
101
100
  </accountmenu>`.onadd(async self => {
102
- const state = await api.getState(true);
103
- self.data.user = state.state.user || '';
101
+ if (!initialState) {
102
+ const state = await api.getState(null);
103
+ authenticatorNode.data.setState(state);
104
+ self.data.user = state.user?.username || '';
105
+ }
104
106
  }).extend(self => ({
105
107
  data: {
106
- /**
107
- * @param {(state: AuthState) => any} callback
108
- */
109
- onchange: (callback) => {
108
+ onchange: (callback: Callback) => {
110
109
  listeners.add(callback);
111
110
  },
112
111
 
113
- /**
114
- * @param {(state: AuthState) => any} callback
115
- */
116
- removeonchange: (callback) => {
112
+ removeonchange: (callback: Callback) => {
117
113
  listeners.delete(callback);
118
114
  },
119
115
  }
@@ -1,19 +1,19 @@
1
1
  import { attribute, html, node } from 'wirejs-dom/v2';
2
+ import type {
3
+ AuthenticationApi,
4
+ AuthenticationState,
5
+ AuthenticationMachineState,
6
+ AuthenticationMachineAction,
7
+ AuthenticationMachineInput,
8
+ } from 'wirejs-resources';
2
9
 
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
+ type AuthCallback = (state: AuthenticationMachineState) => any;
10
11
 
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 }]) => {
12
+ export const authenticatoraction = (
13
+ action: AuthenticationMachineAction,
14
+ act: (input: AuthenticationMachineInput) => void
15
+ ) => {
16
+ const inputs = Object.entries(action.fields || []).map(([name, { label, type }]) => {
17
17
  const id = `input_${Math.floor(Math.random() * 1_000_000)}`;
18
18
  const input = html`<div>
19
19
  <label for=${id}>${label}</label>
@@ -44,16 +44,16 @@ export const authenticatoraction = (action, act) => {
44
44
 
45
45
  const actors = link ?? buttons;
46
46
 
47
- if (action.inputs && Object.keys(action.inputs).length > 0) {
47
+ if (action.fields && Object.keys(action.fields).length > 0) {
48
48
  return html`<authenticatoraction>
49
49
  <div>
50
50
  <h4 style='margin-top: 1rem; margin-bottom: 0.5rem;'>${action.name}</h4>
51
51
  <form
52
- onsubmit=${evt => {
52
+ onsubmit=${(evt: SubmitEvent) => {
53
53
  evt.preventDefault();
54
54
  act({
55
55
  key: action.key,
56
- verb: evt.submitter?.value,
56
+ verb: (evt.submitter as any)?.value,
57
57
  inputs: Object.fromEntries(inputs.map(input => ([
58
58
  input.data.name,
59
59
  input.data.value
@@ -74,71 +74,55 @@ export const authenticatoraction = (action, act) => {
74
74
  }
75
75
  }
76
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;
77
+ export const authenticator = (
78
+ stateManager: AuthenticationApi,
79
+ initialState?: AuthenticationMachineState
80
+ ) => {
81
+ const listeners = new Set<AuthCallback>();
82
+ let lastKnownState: AuthenticationMachineState | undefined = undefined;
91
83
 
92
84
  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"));
85
+ ${node('state', html`<span>Loading ...</span>` as HTMLElement)}
86
+ </authenticator>`.extend(() => ({
87
+ renderState(state: AuthenticationMachineState | { errors: any[] }) {
88
+ if ('errors' in state && state.errors) {
89
+ alert((state as any).errors.map((e: any) => e.message).join("\n\n"));
102
90
  } else {
91
+ lastKnownState = state as AuthenticationMachineState;
103
92
  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));
93
+ <div>${lastKnownState.message || ''}</div>
94
+ <div>${Object.entries(lastKnownState.actions).map(([_key, action]) => {
95
+ return authenticatoraction({...action}, async act => {
96
+ self.renderState(await stateManager.setState(null, act));
108
97
  });
109
98
  })}</div>
110
99
  </div>`;
111
100
  }
112
101
  for (const listener of listeners) {
113
102
  try {
114
- listener(state);
103
+ listener(state as AuthenticationMachineState);
115
104
  } catch (error) {
116
105
  console.error("Error calling auth state listener.");
117
106
  }
118
107
  }
119
108
  }
120
109
  })).onadd(async (self) => {
121
- self.renderState(await stateManager.getState(true))
110
+ if (initialState) {
111
+ console.log('authenticator render state');
112
+ self.renderState(initialState)
113
+ }
122
114
  }).extend(self => ({
123
115
  data: {
124
- /**
125
- * @param {(state: AuthState) => any} callback
126
- */
127
- onchange: (callback) => {
116
+ setState: (state: AuthenticationMachineState) => self.renderState(state),
117
+ onchange: (callback: AuthCallback) => {
128
118
  listeners.add(callback);
129
119
  },
130
-
131
- /**
132
- * @param {(state: AuthState) => any} callback
133
- */
134
- removeonchange: (callback) => {
120
+ removeonchange: (callback: AuthCallback) => {
135
121
  listeners.delete(callback);
136
122
  },
137
-
138
123
  focus: () => {
139
124
  [...self.getElementsByTagName('input')].shift()?.focus();
140
125
  },
141
-
142
126
  get lastKnownState() {
143
127
  return lastKnownState;
144
128
  }
@@ -10,6 +10,7 @@ export async function generate() {
10
10
  <!doctype html>
11
11
  <html>
12
12
  <head>
13
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
14
  <title>Welcome!</title>
14
15
  </head>
15
16
  <body>
@@ -1,17 +1,17 @@
1
1
  import { html, node, list, attribute, hydrate } from 'wirejs-dom/v2';
2
2
  import { accountMenu } from '../components/account-menu.js';
3
- import { auth, todos } from 'my-api';
3
+ import { auth, todos, Todo } from 'my-api';
4
4
 
5
5
  function Todos() {
6
6
  const save = async () => {
7
7
  try {
8
- await todos.write(true, self.data.todos);
8
+ await todos.write(null, self.data.todos);
9
9
  } catch (error) {
10
10
  alert(error);
11
11
  }
12
12
  }
13
13
 
14
- const remove = todo => {
14
+ const remove = (todo: Todo) => {
15
15
  self.data.todos = self.data.todos.filter(t => t.id !== todo.id);
16
16
  save();
17
17
  }
@@ -20,34 +20,34 @@ function Todos() {
20
20
 
21
21
  const self = html`<div>
22
22
  <h4>Your Todos</h4>
23
- <ol>${list('todos', todo => html`<li>
23
+ <ol>${list('todos', (todo: Todo) => html`<li>
24
24
  ${todo.text} : <span
25
25
  style='color: darkred; font-weight: bold; cursor: pointer;'
26
26
  onclick=${() => remove(todo)}
27
27
  >X</span>
28
28
  </li>`)}</ol>
29
29
  <div>
30
- <form onsubmit=${event => {
30
+ <form onsubmit=${(event: Event) => {
31
31
  event.preventDefault();
32
32
  self.data.todos.push({ id: newid(), text: self.data.newTodo });
33
33
  self.data.newTodo = '';
34
34
  save();
35
35
  }}>
36
- <input type='text' value=${attribute('newTodo', '')} />
36
+ <input type='text' value=${attribute('newTodo', '' as string)} />
37
37
  <input type='submit' value='Add' />
38
38
  </form>
39
39
  </div>
40
40
  <div>`.onadd(async self => {
41
- self.data.todos = await todos.read(true);
41
+ self.data.todos = await todos.read(null);
42
42
  });
43
43
  return self;
44
44
  }
45
45
 
46
46
  async function App() {
47
- const accountMenuNode = accountMenu(auth);
47
+ const accountMenuNode = accountMenu({ api: auth });
48
48
 
49
49
  accountMenuNode.data.onchange(async state => {
50
- if (state.state.user) {
50
+ if (state.state === 'authenticated') {
51
51
  self.data.content = Todos();
52
52
  } else {
53
53
  self.data.content = html`<div>You need to sign in to add your todo list.</div>`;
@@ -67,6 +67,7 @@ export async function generate() {
67
67
  <!doctype html>
68
68
  <html>
69
69
  <head>
70
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
70
71
  <title>Todo App</title>
71
72
  </head>
72
73
  <body>
@@ -80,4 +81,4 @@ export async function generate() {
80
81
  return page;
81
82
  }
82
83
 
83
- hydrate('app', App);
84
+ hydrate('app', App as any);
@@ -0,0 +1,121 @@
1
+ import { marked } from 'marked';
2
+ import DOMPurify from 'dompurify';
3
+ import { html, id, text, hydrate, node, list, attribute, KindaPretty } from 'wirejs-dom/v2';
4
+ import type { AuthenticationMachineState, Context } from 'wirejs-resources';
5
+ import { accountMenu } from '../../components/account-menu.js';
6
+ import { auth, wiki } from 'my-api';
7
+
8
+ type WithoutNodes<T> = KindaPretty<{
9
+ [K in keyof T]: T[K] extends Node
10
+ ? T[K] extends { data: any } ? WithoutNodes<T[K]['data']> : undefined
11
+ : WithoutNodes<T[K]>
12
+ }>
13
+
14
+ /**
15
+ * Shallow check for a `data` hydration property. If present, returns the
16
+ * argument typed according to the given `T`.
17
+ */
18
+ function initData<T extends { data: any }>(arg0: any): WithoutNodes<T['data']> | undefined {
19
+ return arg0?.data ? arg0.data : undefined;
20
+ }
21
+
22
+ async function Wiki(init: { context?: Context, data?: any }) {
23
+ const { context } = init;
24
+
25
+ const data = initData<typeof self>(init);
26
+
27
+ console.log('Wiki init', init);
28
+ const filepath = (context || window).location.pathname;
29
+
30
+ const content = data?.content || await wiki.read(context, filepath);
31
+ const initialState: AuthenticationMachineState =
32
+ data?.initialAuthState || await auth.getState(context)
33
+ ;
34
+ const accountMenuNode = accountMenu({ api: auth, initialState });
35
+
36
+ let markdown: string = content ?? `This page doesn't exist yet`;
37
+ const signedOutAction = html`<i>(<b>Sign in</b> to edit.)</i>`;
38
+ const signedInAction = html`<button onclick=${enableEditing}>edit</button>`;
39
+ const invisibleDiv = html`<div style='display: none;'></div>`;
40
+ const editor = html`<div>
41
+ <textarea
42
+ ${id('textarea', HTMLTextAreaElement)}
43
+ style='width: 20em; height: 10em;'
44
+ ></textarea>
45
+ </div>`;
46
+
47
+ accountMenuNode.data.onchange(state => {
48
+ self.data.actions = actionsFor(state);
49
+ });
50
+
51
+ function actionsFor(state: AuthenticationMachineState) {
52
+ return state.state === 'authenticated' ? signedInAction : signedOutAction;
53
+ }
54
+
55
+ function enableEditing() {
56
+ editor.data.textarea.value = markdown;
57
+ self.data.editor = editor;
58
+ self.data.actions = html`<div>
59
+ <button onclick=${submitChanges}>save</button>
60
+ <button onclick=${cancelChanges}>cancel</button>
61
+ </div>`;
62
+ }
63
+
64
+ async function submitChanges() {
65
+ markdown = editor.data.textarea.value;
66
+ await wiki.write(context, filepath, markdown);
67
+ self.data.content = markdown;
68
+ self.data.actions = signedInAction;
69
+ self.data.editor = invisibleDiv;
70
+ }
71
+
72
+ function cancelChanges() {
73
+ self.data.actions = signedInAction;
74
+ self.data.editor = html`<div style='display: none;'></div>`;
75
+ self.data.content = markdown;
76
+ }
77
+
78
+ const self = html`<div id='wiki'>
79
+ <div style='float: right;'>${accountMenuNode}</div>
80
+ ${node('content', markdown, md =>
81
+ html`<div>${DOMPurify.sanitize(marked.parse(md!) as string)}</div>`)
82
+ }
83
+ ${node('editor', invisibleDiv)}
84
+ ${node('actions', actionsFor(initialState))}
85
+ </div>`.extend(self => ({
86
+ data: {
87
+ initialAuthState: initialState
88
+ }
89
+ }));
90
+
91
+ return self;
92
+ }
93
+
94
+ export async function generate(context: Context) {
95
+ const visiblePath = context.location.pathname
96
+ .replaceAll('/', ' > ')
97
+ .replaceAll('<', '&lt;')
98
+ .replaceAll('>', '&gt;')
99
+ .replaceAll('-', ' ')
100
+ .replace(/\s+/g, ' ')
101
+ ;
102
+
103
+ const page = html`
104
+ <!doctype html>
105
+ <html>
106
+ <head>
107
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
108
+ <title>Wiki ${visiblePath}</title>
109
+ </head>
110
+ <body>
111
+ <p><a href='/'>Home</a></p>
112
+ <h1>Wiki ${visiblePath}</h1>
113
+ ${await Wiki({ context })}
114
+ </body>
115
+ </html>
116
+ `;
117
+
118
+ return page;
119
+ }
120
+
121
+ hydrate('wiki', Wiki);
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "target": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "outDir": "./dist",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ },
13
+ "include": ["src/**/*"],
14
+ "exclude": ["node_modules"]
15
+ }
@@ -1,99 +0,0 @@
1
- import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
2
- import { defaultGreeting } from '../src/lib/sample-lib.js';
3
-
4
- const userTodos = new FileService('app', 'userTodoApp');
5
- const wikiPages = new FileService('app', 'wikiPages');
6
- const authService = new AuthenticationService('app', '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
- }));
@@ -1 +0,0 @@
1
- "1731789421822"
@@ -1,36 +0,0 @@
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;
@@ -1 +0,0 @@
1
- ${body}
@@ -1,3 +0,0 @@
1
- body {
2
- font-size: 12pt;
3
- }
@@ -1,222 +0,0 @@
1
- @import url('core.css');
2
-
3
- /*****************
4
- BASIC TAGS
5
- *****************/
6
-
7
- body {
8
- margin: 0px;
9
- padding: 0px;
10
- font-family: Arial, Helvetica, sans-serif;
11
- font-size: 11pt;
12
- line-height: 1.2;
13
- color: #333333;
14
- }
15
-
16
- @media (max-width: 50rem) {
17
- }
18
-
19
- img {
20
- border: 0px;
21
- }
22
-
23
- a, tpdc\:start {
24
- /* text-decoration: none; */
25
- }
26
-
27
- a:hover, tpdc\:start:hover {
28
- text-decoration: underline;
29
- }
30
-
31
- a.nohoverdecoration, tpcd\:start.nohoverdecoration {
32
- text-decoration: none;
33
- }
34
-
35
- hr {
36
- margin: 2rem auto;
37
- width: 65%;
38
- height: 1px;
39
- border: 0px;
40
- background-color: silver;
41
- }
42
-
43
- #header h1 {
44
- font-size: 1.6em;
45
- }
46
-
47
- #header h1 a {
48
- text-decoration: none;
49
- }
50
-
51
- #header h1 img {
52
- vertical-align: text-bottom;
53
- height: 1.25em;
54
- border: 2px solid saddlebrown;
55
- }
56
-
57
- /******************
58
- TEMPLATE PARTS
59
- ******************/
60
-
61
- #container {
62
- position: relative;
63
- width: 90%;
64
- max-width: 60rem;
65
- margin: 2rem auto;
66
- padding: 0.5rem;
67
- background-color: #f5f5f5;
68
- border: 1px solid #cccccc;
69
- border-radius: 0.25em;
70
- overflow: hidden; /* float containment */
71
- }
72
-
73
- #content {
74
- overflow: hidden;
75
- padding: 0.85rem;
76
- padding-top: 0px;
77
- line-height: 1.3rem;
78
- text-align: justify;
79
- }
80
-
81
- #footer {
82
- clear: both;
83
- text-align: center;
84
- font-family: monospace;
85
- }
86
-
87
- #sub-footer {
88
- margin-top: 2rem;
89
- font-size: smaller;
90
- color: #555555;
91
- }
92
-
93
- .footer-plug {
94
- margin-top: 2.5rem;
95
- padding-top: 1.75rem;
96
- text-align: right;
97
- color: gray;
98
- border-top: 1px dashed silver;
99
- }
100
-
101
- .footer-plug a {
102
- font-size: 1.2rem;
103
- }
104
-
105
- .padded {
106
- padding: 10px 20px;
107
- }
108
-
109
- #footer > #sub-footer tpdc\:pagebuildtime {
110
- font-weight: bold;
111
- }
112
-
113
- .modal {
114
- position: fixed;
115
- display: table-cell;
116
- text-align: center;
117
- vertical-align: middle;
118
- overflow: hidden;
119
- top: 0px;
120
- left: 0px;
121
- width: 100%;
122
- height: 100%;
123
- margin: 0;
124
- padding: 1em 0 0 0 ;
125
- background-color: #ffffff;
126
- opacity: 0.8;
127
- filter: alpha(opacity=80);
128
- }
129
-
130
- tpdc\:teaser {
131
- display: block;
132
- margin-top: 2em;
133
- border-top: 1px dashed silver;
134
- text-align: right;
135
- }
136
-
137
- tpdc\:fork {
138
- display: block;
139
- position: absolute;
140
- background-color: transparent;
141
- top: 0;
142
- right: 0;
143
- width: 7em;
144
- }
145
-
146
- tpdc\:fork > .banner {
147
- -webkit-transform: rotate(45deg);
148
- -moz-transform: rotate(45deg);
149
- -ms-transform: rotate(45deg);
150
- -o-transform: rotate(45deg);
151
- transform: rotate(45deg);
152
- width: 150%;
153
- text-align: center;
154
- position: absolute;
155
- top: 1.5em;
156
- left: -0.5em;
157
- font-weight: bold;
158
- background-color: green;
159
- }
160
-
161
- tpdc\:fork > .banner > a {
162
- color: white;
163
- }
164
-
165
- tpdc\:resultcard {
166
- display: block;
167
- border: 15px solid #fce890;
168
- padding: 15px;
169
- margin: 15px;
170
- }
171
-
172
- .result-title {
173
- text-align: center;
174
- margin: 0px;
175
- color: #aa3939;
176
- }
177
-
178
- .result-result {
179
- text-align: center;
180
- color: #3399cc;
181
- border: 2px dashed #99ccee;
182
- border-radius: 10px;
183
- padding: 1em;
184
- }
185
-
186
- .result-description {
187
- text-align: left;
188
- }
189
-
190
- .result-follow {
191
- text-align: center;
192
- margin-top: 2em;
193
- padding-top: 1em;
194
- border-top: 1px dotted silver;
195
- vertical-align: bottom;
196
- }
197
-
198
- .result-follow span {
199
- color: #bb3333;
200
- font-weight: bold;
201
- }
202
-
203
- .result-buttons {
204
- padding-top: 1em;
205
- }
206
-
207
-
208
- @media (max-width:420px) {
209
- #footer a {
210
- font-size: larger;
211
- display: inline-block;
212
- margin: 0.25em;
213
- }
214
-
215
- .link-list li {
216
- height: 2em;
217
- }
218
-
219
- .link-list li a {
220
- font-size: larger;
221
- }
222
- }
@@ -1,41 +0,0 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <title>${title + " - example.com"}</title>
5
- <link rel="shortcut icon" type="image/ico" href="/images/wirejs.svg"/>
6
- <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
7
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
8
- ${metatags}
9
- <script src='/layouts/default.js?v=${BUILD_ID}'></script>
10
- </head>
11
- <body>
12
- <div id='container'>
13
- <div id='header'>
14
- <h1>
15
- <a href='/'><img src='images/wirejs.svg' /> home</a> / ${title || "example.com"}
16
- </h1>
17
- </div>
18
- <div class='navbar'><tpdc:menu></tpdc:menu></div>
19
- <div id='content'>
20
- ${body}
21
- </div>
22
- <tpdc:teaser></tpdc:teaser>
23
- </div>
24
- <div id='footer'>
25
- <div id='sub-footer'>
26
- Do only awesome things.
27
- </div>
28
- </div>
29
-
30
- <!-- diagrams rendering for markdown files -->
31
- <script type="module">
32
- import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
33
- [...document.body.getElementsByClassName('language-mermaid')]
34
- .forEach(node => {
35
- node.classList.add('mermaid');
36
- });
37
- mermaid.init({});
38
- </script>
39
-
40
- </body>
41
- </html>
@@ -1,3 +0,0 @@
1
- // common components
2
- import Countdown from '../components/countdown.js';
3
-
@@ -1,8 +0,0 @@
1
- /**
2
- * Just a small function to demonstrate that API's can `require()`
3
- * things which get included and bundled correctly.
4
- * @returns The string "Hello"
5
- */
6
- export function defaultGreeting() {
7
- return "Hello"
8
- }
@@ -1,101 +0,0 @@
1
- import { marked } from 'marked';
2
- import DOMPurify from 'dompurify';
3
- import { html, id, text, hydrate, node, list, attribute } from 'wirejs-dom/v2';
4
- import { accountMenu } from '../../components/account-menu.js';
5
- import { auth, wiki } from 'my-api';
6
-
7
- /**
8
- * @param {{
9
- * content: string | undefined;
10
- * user: string | undefined;
11
- * }}
12
- * @returns
13
- */
14
- async function Wiki({ context }) {
15
- const filepath = (context || window).location.pathname;
16
- const content = await wiki.read(context, filepath);
17
-
18
- const accountMenuNode = accountMenu(auth);
19
- let markdown = content ?? `This page doesn't exist yet`;
20
- const signedOutAction = html`<i>(<b>Sign in</b> to edit.)</i>`;
21
- const signedInAction = html`<button onclick=${enableEditing}>edit</button>`;
22
- const invisibleDiv = html`<div style='display: none;'></div>`;
23
- const editor = html`<div>
24
- <textarea style='width: 20em; height: 10em;' ${id('textarea')}></textarea>
25
- </div>`;
26
-
27
- accountMenuNode.data.onchange(async state => {
28
- if (state.state.user) {
29
- self.data.actions = signedInAction;
30
- } else {
31
- self.data.actions = signedOutAction;
32
- }
33
- });
34
-
35
- function enableEditing() {
36
- editor.data.textarea.value = markdown;
37
- self.data.editor = editor;
38
- self.data.actions = html`<div>
39
- <button onclick=${submitChanges}>save</button>
40
- <button onclick=${cancelChanges}>cancel</button>
41
- </div>`;
42
- }
43
-
44
- async function submitChanges() {
45
- markdown = editor.data.textarea.value;
46
- await wiki.write(context, filepath, markdown);
47
- self.data.content = markdown;
48
- self.data.actions = signedInAction;
49
- self.data.editor = invisibleDiv;
50
- }
51
-
52
- function cancelChanges() {
53
- self.data.actions = signedInAction;
54
- self.data.editor = html`<div style='display: none;'></div>`;
55
- self.data.content = markdown;
56
- }
57
-
58
- const self = html`<div id='wiki'>
59
- <div style='float: right;'>${accountMenuNode}</div>
60
- ${node('content', markdown, md =>
61
- html`<div>${DOMPurify.sanitize(marked.parse(md))}</div>`)
62
- }
63
- ${node('editor', invisibleDiv)}
64
- ${node('actions', signedOutAction)}
65
- </div>`;
66
-
67
- return self;
68
- }
69
-
70
- /**
71
- *
72
- * @param {import('wirejs-services').Context} context
73
- * @returns
74
- */
75
- export async function generate(context) {
76
- const visiblePath = context.location.pathname
77
- .replaceAll('/', ' > ')
78
- .replaceAll('<', '&lt;')
79
- .replaceAll('>', '&gt;')
80
- .replaceAll('-', ' ')
81
- .replace(/\s+/g, ' ')
82
- ;
83
-
84
- const page = html`
85
- <!doctype html>
86
- <html>
87
- <head>
88
- <title>Wiki ${visiblePath}</title>
89
- </head>
90
- <body>
91
- <p><a href='/'>Home</a></p>
92
- <h1>Wiki ${visiblePath}</h1>
93
- ${await Wiki({ context })}
94
- </body>
95
- </html>
96
- `;
97
-
98
- return page;
99
- }
100
-
101
- hydrate('wiki', Wiki);