pinstripe 0.26.0 → 0.29.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.
Files changed (136) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -3
  3. package/babel.config.cjs +18 -0
  4. package/cli.js +46 -0
  5. package/jest.config.cjs +6 -0
  6. package/lib/app.js +37 -0
  7. package/lib/apps/_file_importer.js +1 -0
  8. package/lib/apps/main.js +6 -0
  9. package/lib/class.js +63 -1
  10. package/lib/client.js +20 -0
  11. package/lib/command.js +43 -0
  12. package/lib/commands/_file_importer.js +1 -0
  13. package/lib/commands/drop_database.js +6 -0
  14. package/lib/commands/generate_app.js +41 -0
  15. package/lib/commands/generate_command.js +39 -0
  16. package/lib/commands/generate_component.js +51 -0
  17. package/lib/commands/generate_migration.js +55 -0
  18. package/lib/commands/generate_model.js +43 -0
  19. package/lib/commands/generate_project.js +142 -0
  20. package/lib/commands/generate_service.js +34 -0
  21. package/lib/commands/generate_static_site.js +94 -0
  22. package/lib/commands/generate_view.js +80 -0
  23. package/lib/commands/init_database.js +9 -0
  24. package/lib/commands/list_apps.js +15 -0
  25. package/lib/commands/list_commands.js +15 -0
  26. package/lib/commands/list_components.js +16 -0
  27. package/lib/commands/list_migrations.js +15 -0
  28. package/lib/commands/list_models.js +15 -0
  29. package/lib/commands/list_services.js +15 -0
  30. package/lib/commands/list_views.js +39 -0
  31. package/lib/commands/migrate_database.js +6 -0
  32. package/lib/commands/reset_database.js +9 -0
  33. package/lib/commands/seed_database.js +6 -0
  34. package/lib/commands/show_config.js +6 -0
  35. package/lib/commands/start_repl.js +6 -0
  36. package/lib/commands/start_server.js +31 -0
  37. package/lib/component.js +102 -3
  38. package/lib/components/helpers.js +18 -5
  39. package/lib/components/{a.js → pinstripe_anchor.js} +4 -4
  40. package/lib/components/{document.js → pinstripe_document.js} +2 -2
  41. package/lib/components/{form.js → pinstripe_form.js} +12 -3
  42. package/lib/components/pinstripe_frame.js +7 -7
  43. package/lib/components/pinstripe_modal.js +3 -1
  44. package/lib/components/{script.js → pinstripe_script.js} +0 -2
  45. package/lib/constants.js +26 -1
  46. package/lib/context.js +40 -0
  47. package/lib/database/client.js +257 -0
  48. package/lib/database/column_reference.js +13 -0
  49. package/lib/database/constants.js +87 -0
  50. package/lib/database/index.js +7 -0
  51. package/lib/database/migration.js +32 -0
  52. package/lib/database/migrator.js +28 -0
  53. package/lib/database/row.js +392 -0
  54. package/lib/database/singleton.js +12 -0
  55. package/lib/database/table.js +518 -0
  56. package/lib/database/table_reference.js +33 -0
  57. package/lib/database/union.js +130 -0
  58. package/lib/database.js +139 -0
  59. package/lib/defer.js +35 -0
  60. package/lib/defer.test.js +37 -0
  61. package/lib/escape_html.js +2 -0
  62. package/lib/html.js +72 -0
  63. package/lib/import_all.js +94 -0
  64. package/lib/index.js +12 -2
  65. package/lib/inflector.js +184 -1
  66. package/lib/lru_cache.js +52 -1
  67. package/lib/lru_cache.test.js +45 -0
  68. package/lib/markdown.js +58 -0
  69. package/lib/model.js +110 -0
  70. package/lib/project.js +72 -0
  71. package/lib/registry.js +137 -1
  72. package/lib/service_consumer.js +16 -0
  73. package/lib/service_factory.js +22 -0
  74. package/lib/services/_file_importer.js +1 -0
  75. package/lib/services/app.js +11 -0
  76. package/lib/services/args.js +9 -0
  77. package/lib/services/bot.js +70 -0
  78. package/lib/services/cli_utils.js +77 -0
  79. package/lib/services/client_builder.js +70 -0
  80. package/lib/services/config.js +66 -0
  81. package/lib/services/cookies.js +19 -0
  82. package/lib/services/create_model.js +8 -0
  83. package/lib/services/css_classes_for.js +13 -0
  84. package/lib/services/database.js +14 -0
  85. package/lib/services/defer.js +8 -0
  86. package/lib/services/fetch.js +115 -0
  87. package/lib/services/format_date.js +8 -0
  88. package/lib/services/fs_builder.js +132 -0
  89. package/lib/services/inflector.js +8 -0
  90. package/lib/services/initial_params.js +13 -0
  91. package/lib/services/params.js +13 -0
  92. package/lib/services/parse_html.js +8 -0
  93. package/lib/services/project.js +8 -0
  94. package/lib/services/render_form.js +118 -0
  95. package/lib/services/render_html.js +8 -0
  96. package/lib/services/render_markdown.js +9 -0
  97. package/lib/services/render_view.js +6 -0
  98. package/lib/services/repl.js +54 -0
  99. package/lib/services/run_command.js +8 -0
  100. package/lib/services/run_in_new_workspace.js +11 -0
  101. package/lib/services/send_mail.js +47 -0
  102. package/lib/services/server.js +105 -0
  103. package/lib/services/session.js +19 -0
  104. package/lib/services/trapify.js +8 -0
  105. package/lib/services/view.js +6 -0
  106. package/lib/singleton.js +13 -0
  107. package/lib/string_reader.js +22 -0
  108. package/lib/trapify.js +31 -1
  109. package/lib/unescape_html.js +2 -0
  110. package/lib/unescape_html.test.js +9 -0
  111. package/lib/util.js +12 -0
  112. package/lib/validation_error.js +7 -0
  113. package/lib/view.js +115 -0
  114. package/lib/view_file_importers/js.js +38 -0
  115. package/lib/view_file_importers/md.js +50 -0
  116. package/lib/views/_file_importer.js +1 -0
  117. package/lib/views/shared/_button.js +59 -0
  118. package/lib/views/shared/_content.js +85 -0
  119. package/lib/views/shared/_editable_area.js +37 -0
  120. package/lib/views/shared/_form.js +239 -0
  121. package/lib/views/shared/_navbar.js +138 -0
  122. package/lib/views/shared/_panel.js +65 -0
  123. package/lib/views/shared/_section.js +41 -0
  124. package/lib/views/shared/assets/javascripts/all.js.js +7 -0
  125. package/lib/views/shared/assets/javascripts/all.js.map.js +7 -0
  126. package/lib/views/shared/assets/stylesheets/all.css +4 -0
  127. package/lib/views/shared/assets/stylesheets/global.css +126 -0
  128. package/lib/views/shared/assets/stylesheets/reset.css +74 -0
  129. package/lib/views/shared/assets/stylesheets/vars.css +25 -0
  130. package/lib/views/shared/assets/stylesheets/view.css.js +43 -0
  131. package/lib/virtual_node.js +171 -1
  132. package/lib/virtual_node.test.js +28 -0
  133. package/lib/workspace.js +21 -0
  134. package/package.json +40 -7
  135. package/lib/internal.js +0 -7
  136. /package/lib/components/{body.js → pinstripe_body.js} +0 -0
@@ -0,0 +1,34 @@
1
+
2
+ export default {
3
+ async run(){
4
+ const [ name = '' ] = this.args;
5
+ if(name == ''){
6
+ console.error('A service name must be given.');
7
+ process.exit();
8
+ }
9
+
10
+ const { inProjectRootDir, generateFile, line, indent } = this.fsBuilder;
11
+
12
+ await inProjectRootDir(async () => {
13
+
14
+ await generateFile(`lib/services/_file_importer.js`, { skipIfExists: true }, () => {
15
+ line();
16
+ line(`export { ServiceFactory as default } from 'pinstripe';`);
17
+ line();
18
+ });
19
+
20
+ await generateFile(`lib/services/${this.inflector.snakeify(name)}.js`, () => {
21
+ line(`export default {`);
22
+ indent(() => {
23
+ line('create(){');
24
+ indent(() => {
25
+ line(`return 'Example ${this.inflector.camelize(name)} service'`);
26
+ });
27
+ line('}');
28
+ });
29
+ line('};');
30
+ });
31
+
32
+ });
33
+ }
34
+ };
@@ -0,0 +1,94 @@
1
+
2
+ import { default as mimeTypes } from 'mime-types';
3
+
4
+ import { App, View } from 'pinstripe';
5
+
6
+ export default {
7
+ async run(){
8
+
9
+ const { extractOptions } = this.cliUtils;
10
+
11
+ const { app = 'main' } = extractOptions();
12
+
13
+ const { viewNames } = View.mapperFor(App.create(app, this.context).compose());
14
+
15
+ this.pages = {};
16
+ const urls = viewNames.filter(path => !path.match(/(^|\/)_/)).map(path => {
17
+ return new URL(path, 'http://127.0.0.1/');
18
+ });
19
+
20
+ urls.push(new URL('http://127.0.0.1/404'));
21
+
22
+ while(urls.length){
23
+ await this.crawlPage({ _url: urls.shift(), _headers: { 'x-app': app } });
24
+ }
25
+
26
+ const pages = Object.values(this.pages).filter(page => {
27
+ const { _method, _url, _headers, ...otherParams } = page.params;
28
+ return _url.pathname == '/404' || (page.status == 200 && !Object.keys(otherParams).length)
29
+ });
30
+
31
+ const { inProjectRootDir, generateDir, generateFile, echo } = this.fsBuilder;
32
+
33
+ const isGenerated = {};
34
+
35
+ await inProjectRootDir(async () => {
36
+ await generateDir('build/static', async () => {
37
+ while(pages.length){
38
+ const { params, headers } = pages.shift();
39
+ const contentType = headers['content-type'];
40
+
41
+ const path = params._url.pathname;
42
+ let filePath = path.replace(/^\//, '');
43
+ if(filePath.match(/(^|\/)$/)){
44
+ filePath = `${filePath}index`;
45
+ }
46
+ if(!filePath.match(/[^/]+\.[^/]+$/)){
47
+ filePath = `${filePath}.${mimeTypes.extension(contentType)}`
48
+ }
49
+
50
+ const data = (await this.fetch({ _url: new URL(path, 'http://127.0.0.1/'), _headers: { 'x-app': app } }))[2];
51
+
52
+ if(!isGenerated[filePath]){
53
+ isGenerated[filePath] = true;
54
+ await generateFile(filePath, () => {
55
+ echo(data.join(''));
56
+ });
57
+ }
58
+ }
59
+ });
60
+ });
61
+ },
62
+
63
+ async crawlPage(params){
64
+ const hash = JSON.stringify(params);
65
+ if(this.pages[hash]) return;
66
+ const page = { params };
67
+ this.pages[hash] = page;
68
+ const [ status, headers, data ] = await this.fetch(params);
69
+ page.status = status;
70
+ page.headers = headers;
71
+ if(status != 200 || headers['content-type'] != 'text/html') return;
72
+ const html = data.join('');
73
+ const virtualDom = this.parseHtml(html);
74
+ const urls = this.extractUrls(virtualDom);
75
+ while(urls.length){
76
+ const url = urls.shift();
77
+ await this.crawlPage({ ...url.params, _headers: params._headers, _url: url });
78
+ }
79
+ },
80
+
81
+ extractUrls(virtualDom){
82
+ const out = [];
83
+ virtualDom.traverse(({ attributes }) => {
84
+ ['src', 'href'].forEach(name => {
85
+ const value = attributes[name];
86
+ if(!value) return;
87
+ const url = new URL(value, 'http://127.0.0.1/');
88
+ if(url.host != '127.0.0.1') return;
89
+ out.push(url);
90
+ });
91
+ });
92
+ return out;
93
+ }
94
+ };
@@ -0,0 +1,80 @@
1
+
2
+ import { View } from 'pinstripe';
3
+ import { readFile } from 'fs/promises';
4
+
5
+ export default {
6
+ async run(){
7
+ const [ name = '' ] = this.args;
8
+ let normalizedName = name.replace(/^\//, '');
9
+ if(name == ''){
10
+ console.error('A view name must be given.');
11
+ process.exit();
12
+ }
13
+
14
+ const existingFilePaths = [ ...View.for(normalizedName).filePaths ];
15
+ const existingFilePath = existingFilePaths.pop();
16
+ const existingFileExtension = (existingFilePath ? existingFilePath.match(/^.*\.([^\/]+)$/) : [])[1];
17
+ if(!normalizedName.match(/\.[^\/]+$/)){
18
+ if(existingFileExtension) {
19
+ normalizedName = `${normalizedName}.${existingFileExtension}`;
20
+ } else {
21
+ normalizedName = `${normalizedName}.js`;
22
+ }
23
+ }
24
+
25
+ const normalizedNameExtension = normalizedName.match(/^.*\.([^\/]+)$/)[1];
26
+
27
+ const useExistingFile = normalizedNameExtension == existingFileExtension;
28
+
29
+ const existingFileData = useExistingFile ? (await readFile(existingFilePath)).toString('utf8') : '';
30
+
31
+ const { inProjectRootDir, generateFile, line, indent, echo } = this.fsBuilder;
32
+
33
+ await inProjectRootDir(async () => {
34
+
35
+ await generateFile(`lib/views/_file_importer.js`, { skipIfExists: true }, () => {
36
+ line();
37
+ line(`export { View as default } from 'pinstripe';`);
38
+ line();
39
+ });
40
+
41
+ await generateFile(`lib/views/${normalizedName}`, () => {
42
+ if(useExistingFile){
43
+ echo(existingFileData);
44
+ } else if(normalizedNameExtension == 'js') {
45
+ line();
46
+ line('export const styles = `');
47
+ indent(() => {
48
+ line(".root {");
49
+ indent(() => {
50
+ line("background: yellow;");
51
+ });
52
+ line("}");
53
+ });
54
+ line('`;');
55
+ line();
56
+ line('export default {');
57
+ indent(() => {
58
+ line('render(){')
59
+ indent(() => {
60
+ line('return this.renderHtml`')
61
+ indent(() => {
62
+ line('<div class="${this.cssClasses.root}">');
63
+ indent(() => {
64
+ line(`<h1>${normalizedName} view</h1>`);
65
+ });
66
+ line(`</div>`)
67
+ });
68
+ line('`;');
69
+ });
70
+ line('}');
71
+ });
72
+ line('};');
73
+ line();
74
+ } else {
75
+ line();
76
+ }
77
+ });
78
+ });
79
+ }
80
+ };
@@ -0,0 +1,9 @@
1
+
2
+ import { Command } from 'pinstripe';
3
+
4
+ export default {
5
+ async run(){
6
+ await this.runCommand('migrate-database');
7
+ await this.runCommand('seed-database');
8
+ }
9
+ }
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { App } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following apps are available:');
9
+ console.log('');
10
+ App.names.forEach(appName => {
11
+ console.log(` * ${chalk.green(appName)} (composed of ${JSON.stringify(App.create(appName, this.context).compose())} views)`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { Command } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following commands are available:');
9
+ console.log('');
10
+ Command.names.forEach(commandName => {
11
+ console.log(` * ${chalk.green(commandName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,16 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { Component } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following components are available:');
9
+ console.log('');
10
+ Component.names.forEach(componentName => {
11
+ console.log(` * ${chalk.green(componentName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
16
+
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { Migration } from 'pinstripe/database';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following migrations are available:');
9
+ console.log('');
10
+ Migration.names.forEach(migrationName => {
11
+ console.log(` * ${chalk.green(migrationName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { Row } from 'pinstripe/database';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following models are available:');
9
+ console.log('');
10
+ Row.names.forEach(modelName => {
11
+ console.log(` * ${chalk.green(modelName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,15 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { ServiceFactory } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ console.log('');
8
+ console.log('The following views are available:');
9
+ console.log('');
10
+ ServiceFactory.names.forEach(serviceFactoryName => {
11
+ console.log(` * ${chalk.green(serviceFactoryName)}`);
12
+ });
13
+ console.log('');
14
+ }
15
+ };
@@ -0,0 +1,39 @@
1
+
2
+ import chalk from 'chalk';
3
+ import { App, View } from 'pinstripe';
4
+
5
+ export default {
6
+ run(){
7
+ const { extractOptions } = this.cliUtils;
8
+
9
+ const { app } = extractOptions();
10
+
11
+ if(app){
12
+ this.listComposedViews(typeof app == 'string' ? app : 'main');
13
+ } else {
14
+ this.listAllViews();
15
+ }
16
+ },
17
+
18
+ listComposedViews(appName){
19
+ const { viewNames, resolveView } = View.mapperFor(App.create(appName, this.context).compose());
20
+ console.log('');
21
+ console.log(`The following views have been composed for app "${appName}":`);
22
+ console.log('');
23
+ viewNames.forEach(viewName => {
24
+ console.log(` * ${chalk.green(viewName)} -> ${chalk.green(resolveView(viewName))}`);
25
+ });
26
+ console.log('');
27
+ },
28
+
29
+ listAllViews(){
30
+ console.log('');
31
+ console.log(`The following views are available:`);
32
+ console.log('');
33
+ View.names.forEach(viewName => {
34
+ console.log(` * ${chalk.green(viewName)}`);
35
+ });
36
+ console.log('');
37
+ }
38
+ };
39
+
@@ -0,0 +1,6 @@
1
+
2
+ export default {
3
+ async run(){
4
+ await this.database.migrate();
5
+ }
6
+ };
@@ -0,0 +1,9 @@
1
+
2
+ import { Command } from 'pinstripe';
3
+
4
+ export default {
5
+ async run(){
6
+ await this.runCommand('drop-database');
7
+ await this.runCommand('init-database');
8
+ }
9
+ };
@@ -0,0 +1,6 @@
1
+
2
+ export default {
3
+ run(){
4
+ // do nothing
5
+ }
6
+ };
@@ -0,0 +1,6 @@
1
+
2
+ export default {
3
+ async run(){
4
+ console.log(JSON.stringify(await this.config, null, 2));
5
+ }
6
+ };
@@ -0,0 +1,6 @@
1
+
2
+ export default {
3
+ async run(){
4
+ await this.repl.start()
5
+ }
6
+ };
@@ -0,0 +1,31 @@
1
+
2
+ export default {
3
+ run(){
4
+ const { extractOptions } = this.cliUtils;
5
+
6
+ const { app, withoutBot } = extractOptions({
7
+ app: `main:${process.env.HOST || '127.0.0.1'}:${parseInt(process.env.PORT || '3000')}`,
8
+ withoutBot: false
9
+ });
10
+
11
+
12
+ const apps = [];
13
+ let currentPort = 3000;
14
+
15
+ app.trim().split(/\s+/).forEach((app) => {
16
+ const [name, ...serverConfig] = app.split(/:/);
17
+ const [ port, host = '127.0.0.1'] = serverConfig.reverse();
18
+
19
+ if(port){
20
+ apps.push({ name, port: parseInt(port), host });
21
+ } else {
22
+ while(apps.filter(app => app.host == host).map(({ port }) => port).includes(currentPort)) currentPort++;
23
+ apps.push({ name, port: currentPort, host });
24
+ }
25
+ });
26
+
27
+ this.server.start(apps);
28
+
29
+ if(!withoutBot) this.bot.start();
30
+ }
31
+ };
package/lib/component.js CHANGED
@@ -1,10 +1,13 @@
1
1
 
2
+ import { fileURLToPath } from 'url'; // pinstripe-if-client: const fileURLToPath = undefined;
3
+
2
4
  import { Class } from './class.js';
3
- import { TEXT_ONLY_TAGS } from './constants.js';
5
+ import { TEXT_ONLY_TAGS, IS_SERVER } from './constants.js';
4
6
  import { Inflector } from './inflector.js';
5
7
  import { VirtualNode } from './virtual_node.js';
6
8
  import { Registry } from './registry.js';
7
9
  import { ComponentEvent } from './component_event.js';
10
+ import { Client } from './client.js'; // pinstripe-if-client: const Client = undefined;
8
11
 
9
12
  export const Component = Class.extend().include({
10
13
  meta(){
@@ -17,9 +20,15 @@ export const Component = Class.extend().include({
17
20
  if(!node._component){
18
21
  node._component = Component.new(node, true);
19
22
  node._component = Component.create(
20
- node._component.attributes['data-component'] || node._component.type,
23
+ node._component.attributes['data-component'] || (node._component.type == '#document' ? 'pinstripe-document' : node._component.type),
21
24
  node
22
25
  );
26
+ (node._component.attributes.class || '').trim().split(/\s+/).forEach((className) => {
27
+ const decoratorMethodName = `.${className}`;
28
+ if(typeof node._component[decoratorMethodName] == 'function'){
29
+ node._component[decoratorMethodName]();
30
+ }
31
+ });
23
32
  if(node.isConnected) node._component.trigger('init', { bubbles: false });
24
33
  }
25
34
  return node._component;
@@ -29,6 +38,30 @@ export const Component = Class.extend().include({
29
38
  return Inflector.instance.dasherize(name);
30
39
  }
31
40
  });
41
+
42
+ this.FileImporter.register('js', {
43
+ meta(){
44
+ const { importFile } = this.prototype;
45
+
46
+ this.include({
47
+ async importFile(params){
48
+ const { filePath, relativeFilePathWithoutExtension } = params;
49
+ if((await import(filePath)).default){
50
+ Client.instance.addModule(`
51
+ import { Component } from ${JSON.stringify(fileURLToPath(`${import.meta.url}/../index.js`))};
52
+ import include from ${JSON.stringify(filePath)};
53
+ Component.register(${JSON.stringify(relativeFilePathWithoutExtension)}, include);
54
+ `);
55
+ } else {
56
+ Client.instance.addModule(`
57
+ import ${JSON.stringify(filePath)};
58
+ `);
59
+ }
60
+ return importFile.call(this, params);
61
+ }
62
+ });
63
+ }
64
+ });
32
65
  },
33
66
 
34
67
  initialize(node, skipInit = false){
@@ -236,6 +269,14 @@ export const Component = Class.extend().include({
236
269
  return this.parents.find(({ isOverlay }) => isOverlay);
237
270
  },
238
271
 
272
+ get modal(){
273
+ return this.parents.find(({ isModal }) => isModal);
274
+ },
275
+
276
+ get form(){
277
+ return this.parents.find(({ isForm }) => isForm);
278
+ },
279
+
239
280
  get shadow(){
240
281
  if(!this.node.shadowRoot){
241
282
  this.node.attachShadow({ mode: 'open' });
@@ -403,7 +444,7 @@ export const Component = Class.extend().include({
403
444
  },
404
445
 
405
446
  async fetch(url, options = {}){
406
- const { minimumDelay = 0, ...otherOptions } = options;
447
+ const { minimumDelay = 0, requiresProofOfWork = false, ...otherOptions } = options;
407
448
  const { progressBar } = this.document;
408
449
  const frame = this.frame || this;
409
450
  const normalizedUrl = new URL(url, frame.url);
@@ -417,6 +458,12 @@ export const Component = Class.extend().include({
417
458
  progressBar.stop();
418
459
  };
419
460
  try {
461
+ if(requiresProofOfWork){
462
+ if(!(otherOptions.body instanceof FormData)) throw new Error(`Proof of work requires form data to be present`);
463
+ const values = {};
464
+ otherOptions.body.forEach((value, key) => values[key] = value);
465
+ otherOptions.body.append('_proofOfWork', await generateProofOfWork(values, abortController.signal))
466
+ }
420
467
  const promises = [
421
468
  fetch(normalizedUrl, { signal: abortController.signal, ...otherOptions }),
422
469
  new Promise(resolve => minimumDelayTimeout = setTimeout(resolve, minimumDelay))
@@ -645,6 +692,58 @@ function normalizeVirtualNode(){
645
692
  if(this.parent && this.parent.type == 'textarea' && this.type == '#text'){
646
693
  this.attributes.value = this.attributes.value.replace(/^\n/, '');
647
694
  }
695
+
696
+ if(!this.attributes['data-component']){
697
+ if(this.type == 'a'){
698
+ this.attributes['data-component'] = 'pinstripe-anchor';
699
+ }
700
+
701
+ if(this.type == 'body'){
702
+ this.attributes['data-component'] = 'pinstripe-body';
703
+ }
704
+
705
+ if(this.type == 'form'){
706
+ this.attributes['data-component'] = 'pinstripe-form';
707
+ }
708
+
709
+ if(this.type == 'script' && this.attributes.type == 'pinstripe'){
710
+ this.attributes['data-component'] = 'pinstripe-script';
711
+ }
712
+ }
713
+ }
714
+
715
+ async function generateProofOfWork(values, abortSignal){
716
+ const stringifiedValues = JSON.stringify(values);
717
+ const timestamp = getUTCTimestamp();
718
+ const random = btoa(`${Math.random()}`);
719
+ let counter = 0;
720
+ while(!abortSignal.aborted){
721
+ const candidateSolution = `1:20:${timestamp}:?::${random}:${btoa(`${counter}`)}`;
722
+ const hash = await createSha1Hash(candidateSolution.replace(/\?/, stringifiedValues));
723
+ if(hash.match(/^00000/)) return candidateSolution;
724
+ counter++;
725
+ }
726
+ return '';
727
+ }
728
+
729
+ function getUTCTimestamp() {
730
+ const now = new Date();
731
+
732
+ const year = now.getUTCFullYear() % 100;
733
+ const month = String(now.getUTCMonth() + 1).padStart(2, '0');
734
+ const day = String(now.getUTCDate()).padStart(2, '0');
735
+ const hours = String(now.getUTCHours()).padStart(2, '0');
736
+ const minutes = String(now.getUTCMinutes()).padStart(2, '0');
737
+ const seconds = String(now.getUTCSeconds()).padStart(2, '0');
738
+
739
+ return `${year}${month}${day}${hours}${minutes}${seconds}`;
740
+ }
741
+
742
+
743
+ async function createSha1Hash(input) {
744
+ const buffer = new TextEncoder().encode(input);
745
+ const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer)));
746
+ return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
648
747
  }
649
748
 
650
749
  ComponentEvent.Component = Component;
@@ -3,7 +3,7 @@ import { LruCache } from '../lru_cache.js';
3
3
 
4
4
  export const loadCache = LruCache.new();
5
5
 
6
- export function loadFrame(confirm, target, method, url, placeholderUrl){
6
+ export function loadFrame(confirm, target, method, url, placeholderUrl, requiresProofOfWork = false){
7
7
  if(confirm && !window.confirm(confirm)){
8
8
  return;
9
9
  }
@@ -20,20 +20,20 @@ export function loadFrame(confirm, target, method, url, placeholderUrl){
20
20
  if(!frame) return;
21
21
  }
22
22
 
23
- url = new URL(url || frame.url, this.frame.url);
23
+ url = normalizeUrl(url || frame.url, this.frame.url);
24
24
  if(url.protocol != 'data:' && (url.host != frame.url.host || url.port != frame.url.port)){
25
25
  return;
26
26
  }
27
27
 
28
- if(placeholderUrl) placeholderUrl = new URL(placeholderUrl, this.frame.url);
28
+ if(placeholderUrl) placeholderUrl = normalizeUrl(placeholderUrl, this.frame.url);
29
29
 
30
30
  if(method.match(/POST|PUT|PATCH/i)){
31
31
  const formData = new FormData();
32
32
  const values = this.values;
33
33
  Object.keys(values).forEach((name) => formData.append(name, values[name]));
34
- frame.load(url, { method, body: formData, placeholderUrl });
34
+ frame.load(url, { method, body: formData, placeholderUrl, requiresProofOfWork });
35
35
  } else {
36
- frame.load(url, { method, placeholderUrl });
36
+ frame.load(url, { method, placeholderUrl, requiresProofOfWork });
37
37
  }
38
38
  }
39
39
 
@@ -46,3 +46,16 @@ export function getFrame(target){
46
46
  }
47
47
  return this.frame.descendants.find(n => n.isFrame && n.data.name == target);
48
48
  }
49
+
50
+ export function normalizeUrl(url, referenceUrl = window.location){
51
+ const matches = `${url}`.match(/^&(.*)$/);
52
+ const out = matches ? new URL(referenceUrl) : new URL(url, referenceUrl);
53
+ if(matches){
54
+ if(out.search){
55
+ out.search = `${out.search}&${matches[1]}`;
56
+ } else {
57
+ out.search = `?${matches[1]}`;
58
+ }
59
+ }
60
+ return out;
61
+ }
@@ -1,12 +1,12 @@
1
1
 
2
- import { loadFrame, getFrame } from "./helpers.js";
2
+ import { loadFrame, getFrame, normalizeUrl } from "./helpers.js";
3
3
 
4
4
  export default {
5
5
  initialize(...args){
6
6
  this.constructor.parent.prototype.initialize.call(this, ...args);
7
7
  this.on('click', (event) => {
8
8
  const { confirm, target = '_self', method = 'GET', href, placeholder } = this.params;
9
- if(new URL(href, window.location.href).host != window.location.host) return;
9
+ if(normalizeUrl(href, window.location.href).host != window.location.host) return;
10
10
  event.preventDefault();
11
11
  event.stopPropagation();
12
12
  loadFrame.call(this, confirm, target, method, href, placeholder);
@@ -15,8 +15,8 @@ export default {
15
15
  const { target = '_self', method = 'GET', href, placeholder, preload } = this.params;
16
16
  if(method == 'GET' && target != '_blank'){
17
17
  const frame = target == '_overlay' ? this.frame : getFrame.call(this, target);
18
- if(preload != undefined) this.document.preload(new URL(href, frame.url));
19
- if(placeholder != undefined) this.document.preload(new URL(placeholder, frame.url));
18
+ if(preload != undefined) this.document.preload(normalizeUrl(href, frame.url));
19
+ if(placeholder != undefined) this.document.preload(normalizeUrl(placeholder, frame.url));
20
20
  }
21
21
 
22
22
  if(this.is('input, textarea')) this.on('keyup', (event) => this.trigger('click'));