@sealcode/sealgen 0.8.47 → 0.8.49

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.
Files changed (160) hide show
  1. package/.log/ti-107027.log +576 -0
  2. package/.log/ti-129414.log +1355 -0
  3. package/.log/ti-135707.log +1049 -0
  4. package/.log/ti-136226.log +1049 -0
  5. package/.log/ti-147652.log +533 -0
  6. package/.log/ti-157088.log +2938 -0
  7. package/.log/ti-250667.log +548 -0
  8. package/.log/ti-29438.log +165 -0
  9. package/.log/ti-29894.log +149 -0
  10. package/.log/ti-33785.log +149 -0
  11. package/.log/ti-34262.log +149 -0
  12. package/.log/ti-35719.log +149 -0
  13. package/.log/ti-41629.log +518 -0
  14. package/.log/ti-441248.log +517 -0
  15. package/.log/ti-5352.log +517 -0
  16. package/.log/ti-76335.log +639 -0
  17. package/.log/ti-811639.log +517 -0
  18. package/.log/ti-81805.log +564 -0
  19. package/.log/ti-86459.log +517 -0
  20. package/.log/tsserver.log +6881 -0
  21. package/.log/tsserver.sync-conflict-20221010-222251-JUX7UR2.log +9412 -0
  22. package/@types/index.d.ts +1 -0
  23. package/@types/page/stateful-page.d.ts +34 -0
  24. package/@types/templates/stateful-page.d.ts +1 -0
  25. package/@types/utils/base64.d.ts +2 -0
  26. package/@types/utils/utils.d.ts +3 -0
  27. package/lib/add-route.js +3 -0
  28. package/lib/cli.js +0 -0
  29. package/lib/generate-routes.js +2 -1
  30. package/lib/index.js +1 -0
  31. package/lib/page/stateful-page.js +117 -0
  32. package/lib/templates/stateful-page.js +66 -0
  33. package/lib/utils/base64.js +12 -0
  34. package/lib/utils/utils.js +6 -1
  35. package/package.json +2 -1
  36. package/src/add-route.ts +3 -0
  37. package/src/generate-routes.ts +2 -1
  38. package/src/index.ts +1 -0
  39. package/src/page/stateful-page.ts +202 -0
  40. package/src/templates/stateful-page.ts +65 -0
  41. package/src/utils/base64.ts +7 -0
  42. package/src/utils/utils.ts +9 -0
  43. package/.arcconfig +0 -12
  44. package/.arclint +0 -18
  45. package/.eslintrc.js +0 -38
  46. package/.mocharc.js +0 -10
  47. package/.nycrc +0 -6
  48. package/.prettierrc +0 -21
  49. package/jenkins.sanity.sh +0 -3
  50. package/lib/add-collection.js.map +0 -1
  51. package/lib/add-route.js.map +0 -1
  52. package/lib/add-route.test.js.map +0 -1
  53. package/lib/build.js.map +0 -1
  54. package/lib/cli.js.map +0 -1
  55. package/lib/components/components.js.map +0 -1
  56. package/lib/components/tabs.js.map +0 -1
  57. package/lib/components/toggle-modal.js.map +0 -1
  58. package/lib/example.test.js.map +0 -1
  59. package/lib/find-scss-includes.js.map +0 -1
  60. package/lib/forms/controls/array.js.map +0 -1
  61. package/lib/forms/controls/autocomplete.js.map +0 -1
  62. package/lib/forms/controls/big-radio-group.js.map +0 -1
  63. package/lib/forms/controls/checkbox-group.js.map +0 -1
  64. package/lib/forms/controls/checkbox.js.map +0 -1
  65. package/lib/forms/controls/checkboxed-list-input.js.map +0 -1
  66. package/lib/forms/controls/collapsible-checkbox-group.js.map +0 -1
  67. package/lib/forms/controls/collapsible-field-group.js.map +0 -1
  68. package/lib/forms/controls/controls.js.map +0 -1
  69. package/lib/forms/controls/derived-frame.js.map +0 -1
  70. package/lib/forms/controls/derived.js.map +0 -1
  71. package/lib/forms/controls/dropdown.js.map +0 -1
  72. package/lib/forms/controls/editable-collection-subset.js.map +0 -1
  73. package/lib/forms/controls/field-group.js.map +0 -1
  74. package/lib/forms/controls/file.js.map +0 -1
  75. package/lib/forms/controls/form-control.js.map +0 -1
  76. package/lib/forms/controls/form-field-control.js.map +0 -1
  77. package/lib/forms/controls/form-header.js.map +0 -1
  78. package/lib/forms/controls/form-paragraph.js.map +0 -1
  79. package/lib/forms/controls/frame.js.map +0 -1
  80. package/lib/forms/controls/hidden-input.js.map +0 -1
  81. package/lib/forms/controls/html.js.map +0 -1
  82. package/lib/forms/controls/multiple-files.js.map +0 -1
  83. package/lib/forms/controls/multiple-photos.js.map +0 -1
  84. package/lib/forms/controls/number.js.map +0 -1
  85. package/lib/forms/controls/photo.js.map +0 -1
  86. package/lib/forms/controls/radio.js.map +0 -1
  87. package/lib/forms/controls/reverse-single-reference.js.map +0 -1
  88. package/lib/forms/controls/simple-input.js.map +0 -1
  89. package/lib/forms/controls/textarea.js.map +0 -1
  90. package/lib/forms/controls/tickable.js.map +0 -1
  91. package/lib/forms/fields/boolean.js.map +0 -1
  92. package/lib/forms/fields/boolean.test.js.map +0 -1
  93. package/lib/forms/fields/checkboxed-list.js.map +0 -1
  94. package/lib/forms/fields/collection-field.js.map +0 -1
  95. package/lib/forms/fields/email.js.map +0 -1
  96. package/lib/forms/fields/email.test.js.map +0 -1
  97. package/lib/forms/fields/enum-multiple.js.map +0 -1
  98. package/lib/forms/fields/enum-multiple.test.js.map +0 -1
  99. package/lib/forms/fields/enum.js.map +0 -1
  100. package/lib/forms/fields/enum.test.js.map +0 -1
  101. package/lib/forms/fields/existing-sealious-value.js.map +0 -1
  102. package/lib/forms/fields/field.js.map +0 -1
  103. package/lib/forms/fields/file.js.map +0 -1
  104. package/lib/forms/fields/multiple-files.js.map +0 -1
  105. package/lib/forms/fields/number.js.map +0 -1
  106. package/lib/forms/fields/number.test.js.map +0 -1
  107. package/lib/forms/fields/pick-from-list.js.map +0 -1
  108. package/lib/forms/fields/pick-from-list.test.js.map +0 -1
  109. package/lib/forms/fields/proxy-field.js.map +0 -1
  110. package/lib/forms/fields/regexp.js.map +0 -1
  111. package/lib/forms/fields/regexp.test.js.map +0 -1
  112. package/lib/forms/fields/reverse-single-reference.js.map +0 -1
  113. package/lib/forms/fields/simple-form-field.js.map +0 -1
  114. package/lib/forms/fields/structured-array.js.map +0 -1
  115. package/lib/forms/form.js.map +0 -1
  116. package/lib/forms/form.test.js.map +0 -1
  117. package/lib/forms/multiform.js.map +0 -1
  118. package/lib/forms/multiform.test.js.map +0 -1
  119. package/lib/generate-collections.js.map +0 -1
  120. package/lib/generate-routes.js.map +0 -1
  121. package/lib/index.js.map +0 -1
  122. package/lib/make-env.js.map +0 -1
  123. package/lib/make-hidden-inputs.js.map +0 -1
  124. package/lib/mount.js.map +0 -1
  125. package/lib/page/access-control.js.map +0 -1
  126. package/lib/page/list.js.map +0 -1
  127. package/lib/page/mountable-with-fields.js.map +0 -1
  128. package/lib/page/mountable.js.map +0 -1
  129. package/lib/page/page.js.map +0 -1
  130. package/lib/page/props-parser.js.map +0 -1
  131. package/lib/run.js.map +0 -1
  132. package/lib/sanitize.js.map +0 -1
  133. package/lib/templates/collection.js.map +0 -1
  134. package/lib/templates/form.js.map +0 -1
  135. package/lib/templates/list.js.map +0 -1
  136. package/lib/templates/multiform.js.map +0 -1
  137. package/lib/templates/page.js.map +0 -1
  138. package/lib/templates/post.js.map +0 -1
  139. package/lib/templates/redirect.js.map +0 -1
  140. package/lib/templates/route-test.js.map +0 -1
  141. package/lib/test_utils/locator_is_visible.js.map +0 -1
  142. package/lib/utils/assert-throws.js.map +0 -1
  143. package/lib/utils/browser-creator.js.map +0 -1
  144. package/lib/utils/escape-url-params.js.map +0 -1
  145. package/lib/utils/exec.js.map +0 -1
  146. package/lib/utils/format-external-url.js.map +0 -1
  147. package/lib/utils/import-path.js.map +0 -1
  148. package/lib/utils/input-wrapper.js.map +0 -1
  149. package/lib/utils/list-collections.js.map +0 -1
  150. package/lib/utils/make-slug.js.map +0 -1
  151. package/lib/utils/natural-numbers.js.map +0 -1
  152. package/lib/utils/prettier.js.map +0 -1
  153. package/lib/utils/question.js.map +0 -1
  154. package/lib/utils/render-attributes.js.map +0 -1
  155. package/lib/utils/sleep.js.map +0 -1
  156. package/lib/utils/translate.js.map +0 -1
  157. package/lib/utils/url-with-new-params.js.map +0 -1
  158. package/lib/utils/utils.js.map +0 -1
  159. package/lib/utils/walk.js.map +0 -1
  160. package/mocha_root_hooks.js +0 -6
package/@types/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * as Controls from "./forms/controls/controls";
11
11
  export * as Fields from "./forms/fields/field";
12
12
  export * from "./page/mountable";
13
13
  export * from "./page/page";
14
+ export * from "./page/stateful-page";
14
15
  export * from "./forms/form";
15
16
  export * from "./page/props-parser";
16
17
  export * from "./utils/utils";
@@ -0,0 +1,34 @@
1
+ /// <reference types="koa__router" />
2
+ import Router from "@koa/router";
3
+ import { BaseContext } from "koa";
4
+ import { Templatable } from "tempstream";
5
+ import { ExtractTail } from "../utils/utils";
6
+ import { Mountable } from "./mountable";
7
+ export declare abstract class StatefulPage<State extends Record<string, unknown>, Actions extends Record<string, (state: State, inputs: Record<string, string>, ...args: unknown[]) => State>> extends Mountable {
8
+ abstract actions: Actions;
9
+ abstract getInitialState(): State;
10
+ abstract render(ctx: BaseContext, state: State, inputs: Record<string, string>): Templatable | Promise<Templatable>;
11
+ canAccess(): Promise<{
12
+ readonly canAccess: true;
13
+ readonly message: "";
14
+ }>;
15
+ constructor();
16
+ abstract wrapInLayout(ctx: BaseContext, content: Templatable): Templatable;
17
+ wrapInForm(state: State, content: Templatable): Templatable;
18
+ makeActionButton<ActionName extends keyof Actions>(state: State, action_description: {
19
+ action: ActionName;
20
+ label?: string;
21
+ } | ActionName, ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>): string;
22
+ extractState(ctx: BaseContext): {
23
+ state: State;
24
+ inputs: Record<string, string>;
25
+ action: keyof Actions;
26
+ action_args: string;
27
+ } | {
28
+ state: State;
29
+ inputs: Record<string, string>;
30
+ action: null;
31
+ action_args: null;
32
+ };
33
+ mount(router: Router, path: string): void;
34
+ }
@@ -0,0 +1 @@
1
+ export declare function statefulPageTemplate(action_name: string, newfilefullpath: string): Promise<string>;
@@ -0,0 +1,2 @@
1
+ export declare function to_base64(s: string): string;
2
+ export declare function from_base64(s: string): string;
@@ -1,5 +1,7 @@
1
1
  export declare type Awaited<T> = T extends Promise<infer U> ? U : T;
2
2
  export declare type UnwrapArray<T> = T extends Array<infer U> ? U : T;
3
+ export declare function keys<S extends string>(object: Record<S, unknown>): S[];
4
+ export declare type ExtractTail<T extends any[]> = T extends [infer _, ...infer Tail] ? Tail : never;
3
5
  export { sleep } from "./sleep";
4
6
  export { naturalNumbers } from "./natural-numbers";
5
7
  export { UrlWithNewParams } from "./url-with-new-params";
@@ -11,3 +13,4 @@ export { assertThrowsAsync } from "./assert-throws";
11
13
  export * from "./make-slug";
12
14
  export * from "./render-attributes";
13
15
  export * from "./format-external-url";
16
+ export * from "./base64";
package/lib/add-route.js CHANGED
@@ -17,6 +17,7 @@ const route_test_1 = require("./templates/route-test");
17
17
  const escape_url_params_1 = require("./utils/escape-url-params");
18
18
  const prompts = require("prompts");
19
19
  const post_1 = require("./templates/post");
20
+ const stateful_page_1 = require("./templates/stateful-page");
20
21
  async function addRoute(params, app_directory = process.cwd()) {
21
22
  const target_locreq = locreq_1.default(app_directory);
22
23
  prompts.override(params);
@@ -39,6 +40,7 @@ async function addRoute(params, app_directory = process.cwd()) {
39
40
  message: "What kind of route is it?",
40
41
  choices: [
41
42
  { title: "page", value: "page" },
43
+ { title: "stateful page", value: "sreact" },
42
44
  { title: "form", value: "form" },
43
45
  { title: "list", value: "list" },
44
46
  { title: "multiform", value: "multiform" },
@@ -57,6 +59,7 @@ async function addRoute(params, app_directory = process.cwd()) {
57
59
  multiform: multiform_1.multiformTemplate,
58
60
  redirect: redirect_1.redirectTemplate,
59
61
  post: post_1.postTemplate,
62
+ sreact: stateful_page_1.statefulPageTemplate,
60
63
  }[mode];
61
64
  if (!template) {
62
65
  throw new Error(`Could not get template ${mode}`);
package/lib/cli.js CHANGED
File without changes
@@ -66,7 +66,8 @@ async function generateRoutes() {
66
66
  f.endsWith(".multiform.ts") ||
67
67
  f.endsWith(".list.ts") ||
68
68
  f.endsWith(".redirect.ts") ||
69
- f.endsWith(".post.ts"))
69
+ f.endsWith(".post.ts") ||
70
+ f.endsWith(".sreact.ts"))
70
71
  .map(async (fullpath) => ({
71
72
  fullpath,
72
73
  actionName: await extractActionName(fullpath),
package/lib/index.js CHANGED
@@ -27,6 +27,7 @@ exports.Controls = __importStar(require("./forms/controls/controls"));
27
27
  exports.Fields = __importStar(require("./forms/fields/field"));
28
28
  __exportStar(require("./page/mountable"), exports);
29
29
  __exportStar(require("./page/page"), exports);
30
+ __exportStar(require("./page/stateful-page"), exports);
30
31
  __exportStar(require("./forms/form"), exports);
31
32
  __exportStar(require("./page/props-parser"), exports);
32
33
  __exportStar(require("./utils/utils"), exports);
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.StatefulPage = void 0;
7
+ const ts_predicates_1 = require("@sealcode/ts-predicates");
8
+ const deepmerge_1 = __importDefault(require("deepmerge"));
9
+ const tempstream_1 = require("tempstream");
10
+ const base64_1 = require("../utils/base64");
11
+ const mountable_1 = require("./mountable");
12
+ class StatefulPage extends mountable_1.Mountable {
13
+ async canAccess() {
14
+ return { canAccess: true, message: "" };
15
+ }
16
+ constructor() {
17
+ super();
18
+ const original_render = this.render.bind(this);
19
+ this.render = async (ctx, state, inputs) => {
20
+ return this.wrapInLayout(ctx, this.wrapInForm(state, await original_render(ctx, state, inputs)));
21
+ };
22
+ }
23
+ wrapInForm(state, content) {
24
+ return tempstream_1.tempstream /* HTML */ `<form action="./" method="POST">
25
+ <input
26
+ name="state"
27
+ type="hidden"
28
+ value="${base64_1.to_base64(JSON.stringify(state))}"
29
+ />
30
+ ${content}
31
+ </form>`;
32
+ }
33
+ makeActionButton(state, action_description, ...args) {
34
+ let label, action;
35
+ if (ts_predicates_1.is(action_description, ts_predicates_1.predicates.object)) {
36
+ action = action_description.action.toString();
37
+ label = action_description.label || action;
38
+ }
39
+ else {
40
+ action = action_description.toString();
41
+ label = action;
42
+ }
43
+ return /* HTML */ `
44
+ <input
45
+ type="submit"
46
+ value=${label}
47
+ formaction="./?action=${action}&args=${base64_1.to_base64(JSON.stringify(args))}"
48
+ />
49
+ `;
50
+ }
51
+ extractState(ctx) {
52
+ if (!ts_predicates_1.hasShape({
53
+ action: ts_predicates_1.predicates.maybe(ts_predicates_1.predicates.string),
54
+ state: ts_predicates_1.predicates.string,
55
+ args: ts_predicates_1.predicates.maybe(ts_predicates_1.predicates.string),
56
+ $: ts_predicates_1.predicates.maybe(ts_predicates_1.predicates.object),
57
+ }, ctx.$body)) {
58
+ console.error("Wrong data: ", ctx.$body);
59
+ throw new Error("wrong formdata shape");
60
+ }
61
+ const inputs = Object.fromEntries(Object.entries(ctx.$body).filter(([key]) => !["action", "state", "args", "$"].includes(key)));
62
+ // the "$" key is parsed as dot notation and overrides the state
63
+ const original_state = JSON.parse(base64_1.from_base64(ctx.$body.state));
64
+ const modified_state = deepmerge_1.default(original_state, ctx.$body.$ || {}, {
65
+ arrayMerge: (target, source, options) => {
66
+ // https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
67
+ const destination = target.slice();
68
+ source.forEach((item, index) => {
69
+ if (typeof destination[index] === "undefined") {
70
+ destination[index] =
71
+ options.cloneUnlessOtherwiseSpecified(item, options);
72
+ }
73
+ else if (options.isMergeableObject(item)) {
74
+ destination[index] = deepmerge_1.default(target[index], item, options);
75
+ }
76
+ else if (target.indexOf(item) === -1) {
77
+ destination.push(item);
78
+ }
79
+ });
80
+ return destination;
81
+ },
82
+ });
83
+ if (ctx.$body.action && ctx.$body.args) {
84
+ return {
85
+ state: modified_state,
86
+ action: ctx.$body.action,
87
+ inputs,
88
+ action_args: ctx.$body.args,
89
+ };
90
+ }
91
+ else {
92
+ return {
93
+ state: modified_state,
94
+ action: null,
95
+ action_args: null,
96
+ inputs,
97
+ };
98
+ }
99
+ }
100
+ mount(router, path) {
101
+ router.get(path, (ctx) => {
102
+ ctx.body = this.render(ctx, this.getInitialState(), {});
103
+ });
104
+ router.post(path, (ctx) => {
105
+ const { action, state, inputs, action_args } = this.extractState(ctx);
106
+ if (action) {
107
+ ctx.body = this.render(ctx, this.actions[action](state, inputs, ...JSON.parse(base64_1.from_base64(action_args))), inputs);
108
+ }
109
+ else {
110
+ ctx.body = this.render(ctx, state, inputs);
111
+ }
112
+ ctx.status = 433;
113
+ });
114
+ }
115
+ }
116
+ exports.StatefulPage = StatefulPage;
117
+ //# sourceMappingURL=stateful-page.js.map
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.statefulPageTemplate = void 0;
4
+ const import_path_1 = require("../utils/import-path");
5
+ async function statefulPageTemplate(action_name, newfilefullpath) {
6
+ const rel = import_path_1.curryImportPath(newfilefullpath);
7
+ return `import { BaseContext } from "koa";
8
+ import { tempstream, Templatable } from "tempstream";
9
+ import { StatefulPage } from "@sealcode/sealgen";
10
+ import html from "${rel("src/back/html")}";
11
+
12
+ export const actionName = "${action_name}";
13
+
14
+ const actions = <const>{
15
+ add: (state: State, inputs: Record<string, string>) => {
16
+ console.log({ inputs });
17
+ return {
18
+ ...state,
19
+ elements: [...state.elements, inputs.element_to_add || "new element"],
20
+ };
21
+ },
22
+ remove: (state: State, _: unknown, index_to_remove: number) => {
23
+ return {
24
+ ...state,
25
+ elements: state.elements.filter((_, index) => index != index_to_remove),
26
+ };
27
+ },
28
+ };
29
+
30
+ type State = {
31
+ elements: string[];
32
+ };
33
+
34
+ export default new (class ${action_name}Page extends StatefulPage<State, typeof actions> {
35
+ actions = actions;
36
+
37
+ getInitialState() {
38
+ return { elements: ["one", "two", "three"] };
39
+ }
40
+
41
+ wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
42
+ return html(ctx, "${action_name}", content);
43
+ }
44
+
45
+ render(ctx: BaseContext, state: State, inputs: Record<string, string>) {
46
+ return tempstream/* HTML */ \`<ul>
47
+ \${state.elements.map(
48
+ (e, index) => /* HTML */ \`<li>
49
+ \${e} \${this.makeActionButton(state, "remove", index)}
50
+ </li>\`
51
+ )}
52
+ </ul>
53
+ <div>
54
+ <input
55
+ name="element_to_add"
56
+ type="text"
57
+ value="\${inputs.element_to_add || ""}"
58
+ />
59
+ \${this.makeActionButton(state, "add")}
60
+ </div>\`;
61
+ }
62
+ })();
63
+ `;
64
+ }
65
+ exports.statefulPageTemplate = statefulPageTemplate;
66
+ //# sourceMappingURL=stateful-page.js.map
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.from_base64 = exports.to_base64 = void 0;
4
+ function to_base64(s) {
5
+ return Buffer.from(s).toString("base64");
6
+ }
7
+ exports.to_base64 = to_base64;
8
+ function from_base64(s) {
9
+ return Buffer.from(s, "base64").toString("utf-8");
10
+ }
11
+ exports.from_base64 = from_base64;
12
+ //# sourceMappingURL=base64.js.map
@@ -10,7 +10,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
10
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.assertThrowsAsync = exports.walkDir = exports.listCollections = exports.curryImportPath = exports.importPath = exports.exec = exports.UrlWithNewParams = exports.naturalNumbers = exports.sleep = void 0;
13
+ exports.assertThrowsAsync = exports.walkDir = exports.listCollections = exports.curryImportPath = exports.importPath = exports.exec = exports.UrlWithNewParams = exports.naturalNumbers = exports.sleep = exports.keys = void 0;
14
+ function keys(object) {
15
+ return Object.keys(object);
16
+ }
17
+ exports.keys = keys;
14
18
  var sleep_1 = require("./sleep");
15
19
  Object.defineProperty(exports, "sleep", { enumerable: true, get: function () { return sleep_1.sleep; } });
16
20
  var natural_numbers_1 = require("./natural-numbers");
@@ -31,4 +35,5 @@ Object.defineProperty(exports, "assertThrowsAsync", { enumerable: true, get: fun
31
35
  __exportStar(require("./make-slug"), exports);
32
36
  __exportStar(require("./render-attributes"), exports);
33
37
  __exportStar(require("./format-external-url"), exports);
38
+ __exportStar(require("./base64"), exports);
34
39
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.8.47",
3
+ "version": "0.8.49",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -47,6 +47,7 @@
47
47
  "dependencies": {
48
48
  "@koa/router": "^10.1.1",
49
49
  "@sealcode/ts-predicates": "^0.4.3",
50
+ "deepmerge": "^4.3.1",
50
51
  "esbuild": "^0.15.5",
51
52
  "esbuild-sass-plugin": "^2.3.1",
52
53
  "koa": "^2.13.0",
package/src/add-route.ts CHANGED
@@ -11,6 +11,7 @@ import { testTemplate } from "./templates/route-test";
11
11
  import { escape_url_params } from "./utils/escape-url-params";
12
12
  import prompts = require("prompts");
13
13
  import { postTemplate } from "./templates/post";
14
+ import { statefulPageTemplate } from "./templates/stateful-page";
14
15
 
15
16
  export async function addRoute(
16
17
  params: Partial<{ [key in "action" | "url" | "mode"]: string }>,
@@ -40,6 +41,7 @@ export async function addRoute(
40
41
  message: "What kind of route is it?",
41
42
  choices: [
42
43
  { title: "page", value: "page" },
44
+ { title: "stateful page", value: "sreact" },
43
45
  { title: "form", value: "form" },
44
46
  { title: "list", value: "list" },
45
47
  { title: "multiform", value: "multiform" },
@@ -64,6 +66,7 @@ export async function addRoute(
64
66
  multiform: multiformTemplate,
65
67
  redirect: redirectTemplate,
66
68
  post: postTemplate,
69
+ sreact: statefulPageTemplate,
67
70
  }[mode];
68
71
 
69
72
  if (!template) {
@@ -56,7 +56,8 @@ export async function generateRoutes(): Promise<void> {
56
56
  f.endsWith(".multiform.ts") ||
57
57
  f.endsWith(".list.ts") ||
58
58
  f.endsWith(".redirect.ts") ||
59
- f.endsWith(".post.ts")
59
+ f.endsWith(".post.ts") ||
60
+ f.endsWith(".sreact.ts")
60
61
  )
61
62
  .map(async (fullpath) => ({
62
63
  fullpath,
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * as Controls from "./forms/controls/controls";
13
13
  export * as Fields from "./forms/fields/field";
14
14
  export * from "./page/mountable";
15
15
  export * from "./page/page";
16
+ export * from "./page/stateful-page";
16
17
  export * from "./forms/form";
17
18
  export * from "./page/props-parser";
18
19
  export * from "./utils/utils";
@@ -0,0 +1,202 @@
1
+ import Router from "@koa/router";
2
+ import { predicates, hasShape, is } from "@sealcode/ts-predicates";
3
+ import deepmerge, { ArrayMergeOptions } from "deepmerge";
4
+ import { BaseContext } from "koa";
5
+ import { Templatable, tempstream } from "tempstream";
6
+ import { from_base64, to_base64 } from "../utils/base64";
7
+ import { ExtractTail } from "../utils/utils";
8
+ import { Mountable } from "./mountable";
9
+
10
+ export abstract class StatefulPage<
11
+ State extends Record<string, unknown>,
12
+ Actions extends Record<
13
+ string,
14
+ (
15
+ state: State,
16
+ inputs: Record<string, string>,
17
+ ...args: unknown[]
18
+ ) => State
19
+ >
20
+ > extends Mountable {
21
+ abstract actions: Actions;
22
+ abstract getInitialState(): State;
23
+ abstract render(
24
+ ctx: BaseContext,
25
+ state: State,
26
+ inputs: Record<string, string>
27
+ ): Templatable | Promise<Templatable>;
28
+
29
+ async canAccess() {
30
+ return <const>{ canAccess: true, message: "" };
31
+ }
32
+
33
+ constructor() {
34
+ super();
35
+ const original_render = this.render.bind(this);
36
+ this.render = async (
37
+ ctx: BaseContext,
38
+ state: State,
39
+ inputs: Record<string, string>
40
+ ) => {
41
+ return this.wrapInLayout(
42
+ ctx,
43
+ this.wrapInForm(
44
+ state,
45
+ await original_render(ctx, state, inputs)
46
+ )
47
+ );
48
+ };
49
+ }
50
+
51
+ abstract wrapInLayout(ctx: BaseContext, content: Templatable): Templatable;
52
+
53
+ wrapInForm(state: State, content: Templatable): Templatable {
54
+ return tempstream/* HTML */ `<form action="./" method="POST">
55
+ <input
56
+ name="state"
57
+ type="hidden"
58
+ value="${to_base64(JSON.stringify(state))}"
59
+ />
60
+ ${content}
61
+ </form>`;
62
+ }
63
+
64
+ makeActionButton<ActionName extends keyof Actions>(
65
+ state: State,
66
+ action_description:
67
+ | {
68
+ action: ActionName;
69
+ label?: string;
70
+ }
71
+ | ActionName,
72
+ ...args: ExtractTail<ExtractTail<Parameters<Actions[ActionName]>>>
73
+ ) {
74
+ let label, action: string;
75
+ if (is(action_description, predicates.object)) {
76
+ action = action_description.action.toString();
77
+ label = action_description.label || action;
78
+ } else {
79
+ action = action_description.toString();
80
+ label = action;
81
+ }
82
+ return /* HTML */ `
83
+ <input
84
+ type="submit"
85
+ value=${label}
86
+ formaction="./?action=${action}&args=${to_base64(
87
+ JSON.stringify(args)
88
+ )}"
89
+ />
90
+ `;
91
+ }
92
+
93
+ extractState(ctx: BaseContext):
94
+ | {
95
+ state: State;
96
+ inputs: Record<string, string>;
97
+ action: keyof Actions;
98
+ action_args: string;
99
+ }
100
+ | {
101
+ state: State;
102
+ inputs: Record<string, string>;
103
+ action: null;
104
+ action_args: null;
105
+ } {
106
+ if (
107
+ !hasShape(
108
+ {
109
+ action: predicates.maybe(predicates.string),
110
+ state: predicates.string,
111
+ args: predicates.maybe(predicates.string),
112
+ $: predicates.maybe(predicates.object),
113
+ },
114
+ ctx.$body
115
+ )
116
+ ) {
117
+ console.error("Wrong data: ", ctx.$body);
118
+ throw new Error("wrong formdata shape");
119
+ }
120
+ const inputs = Object.fromEntries(
121
+ Object.entries(ctx.$body).filter(
122
+ ([key]) => !["action", "state", "args", "$"].includes(key)
123
+ )
124
+ ) as Record<string, string>;
125
+ // the "$" key is parsed as dot notation and overrides the state
126
+
127
+ const original_state = JSON.parse(from_base64(ctx.$body.state));
128
+
129
+ const modified_state = deepmerge(original_state, ctx.$body.$ || {}, {
130
+ arrayMerge: (
131
+ target: any[],
132
+ source: any[],
133
+ options: ArrayMergeOptions
134
+ ) => {
135
+ // https://github.com/TehShrike/deepmerge#user-content-arraymerge-example-combine-arrays
136
+ const destination = target.slice();
137
+
138
+ source.forEach((item, index) => {
139
+ if (typeof destination[index] === "undefined") {
140
+ destination[index] =
141
+ options.cloneUnlessOtherwiseSpecified(
142
+ item,
143
+ options
144
+ );
145
+ } else if (options.isMergeableObject(item)) {
146
+ destination[index] = deepmerge(
147
+ target[index],
148
+ item,
149
+ options
150
+ );
151
+ } else if (target.indexOf(item) === -1) {
152
+ destination.push(item);
153
+ }
154
+ });
155
+ return destination;
156
+ },
157
+ }) as State;
158
+
159
+ if (ctx.$body.action && ctx.$body.args) {
160
+ return {
161
+ state: modified_state,
162
+ action: ctx.$body.action as string,
163
+ inputs,
164
+ action_args: ctx.$body.args,
165
+ };
166
+ } else {
167
+ return {
168
+ state: modified_state,
169
+ action: null,
170
+ action_args: null,
171
+ inputs,
172
+ };
173
+ }
174
+ }
175
+
176
+ mount(router: Router, path: string) {
177
+ router.get(path, (ctx) => {
178
+ ctx.body = this.render(ctx, this.getInitialState(), {});
179
+ });
180
+
181
+ router.post(path, (ctx) => {
182
+ const { action, state, inputs, action_args } =
183
+ this.extractState(ctx);
184
+ if (action) {
185
+ ctx.body = this.render(
186
+ ctx,
187
+ this.actions[action](
188
+ state,
189
+ inputs,
190
+ ...(JSON.parse(
191
+ from_base64(action_args as string)
192
+ ) as unknown[])
193
+ ),
194
+ inputs
195
+ );
196
+ } else {
197
+ ctx.body = this.render(ctx, state, inputs);
198
+ }
199
+ ctx.status = 433;
200
+ });
201
+ }
202
+ }
@@ -0,0 +1,65 @@
1
+ import { curryImportPath } from "../utils/import-path";
2
+
3
+ export async function statefulPageTemplate(
4
+ action_name: string,
5
+ newfilefullpath: string
6
+ ): Promise<string> {
7
+ const rel = curryImportPath(newfilefullpath);
8
+ return `import { BaseContext } from "koa";
9
+ import { tempstream, Templatable } from "tempstream";
10
+ import { StatefulPage } from "@sealcode/sealgen";
11
+ import html from "${rel("src/back/html")}";
12
+
13
+ export const actionName = "${action_name}";
14
+
15
+ const actions = <const>{
16
+ add: (state: State, inputs: Record<string, string>) => {
17
+ console.log({ inputs });
18
+ return {
19
+ ...state,
20
+ elements: [...state.elements, inputs.element_to_add || "new element"],
21
+ };
22
+ },
23
+ remove: (state: State, _: unknown, index_to_remove: number) => {
24
+ return {
25
+ ...state,
26
+ elements: state.elements.filter((_, index) => index != index_to_remove),
27
+ };
28
+ },
29
+ };
30
+
31
+ type State = {
32
+ elements: string[];
33
+ };
34
+
35
+ export default new (class ${action_name}Page extends StatefulPage<State, typeof actions> {
36
+ actions = actions;
37
+
38
+ getInitialState() {
39
+ return { elements: ["one", "two", "three"] };
40
+ }
41
+
42
+ wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
43
+ return html(ctx, "${action_name}", content);
44
+ }
45
+
46
+ render(ctx: BaseContext, state: State, inputs: Record<string, string>) {
47
+ return tempstream/* HTML */ \`<ul>
48
+ \${state.elements.map(
49
+ (e, index) => /* HTML */ \`<li>
50
+ \${e} \${this.makeActionButton(state, "remove", index)}
51
+ </li>\`
52
+ )}
53
+ </ul>
54
+ <div>
55
+ <input
56
+ name="element_to_add"
57
+ type="text"
58
+ value="\${inputs.element_to_add || ""}"
59
+ />
60
+ \${this.makeActionButton(state, "add")}
61
+ </div>\`;
62
+ }
63
+ })();
64
+ `;
65
+ }
@@ -0,0 +1,7 @@
1
+ export function to_base64(s: string) {
2
+ return Buffer.from(s).toString("base64");
3
+ }
4
+
5
+ export function from_base64(s: string) {
6
+ return Buffer.from(s, "base64").toString("utf-8");
7
+ }