├── .changeset └── config.json ├── .github ├── FUNDING.yml └── workflows │ ├── auto-release.yml │ ├── changeset-release.yml │ ├── docs-deploy.yml │ └── test-docs-deploy.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── css_custom_data.json └── settings.json ├── LICENSE.md ├── README.md ├── apps ├── lambda-examples │ ├── .gitignore │ ├── .npmignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ │ └── liveviewjs-lambda.ts │ ├── cdk.json │ ├── lib │ │ └── liveviewjs-lambda-stack.ts │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── client │ │ │ └── index.ts │ │ ├── example │ │ │ ├── index.ts │ │ │ ├── indexHandler.ts │ │ │ └── liveTemplates.ts │ │ ├── lambda │ │ │ └── server.ts │ │ └── lambdas │ │ │ ├── http.ts │ │ │ └── websocket.ts │ ├── tsconfig-client.json │ └── tsconfig.json └── liveviewjs.com │ ├── .gitignore │ ├── .prettierrc │ ├── README.md │ ├── babel.config.js │ ├── blog │ ├── 2019-05-28-first-blog-post.md │ ├── 2019-05-29-long-blog-post.md │ ├── 2021-08-01-mdx-blog-post.mdx │ ├── 2021-08-26-welcome │ │ ├── docusaurus-plushie-banner.jpeg │ │ └── index.md │ └── authors.yml │ ├── docs │ ├── 01-overview │ │ ├── _category_.json │ │ ├── feedback.md │ │ ├── gratitude.md │ │ ├── introduction.md │ │ ├── paradigm.md │ │ └── runtimes.md │ ├── 02-quick-starts │ │ ├── _category_.json │ │ ├── deno-build-first-liveview.md │ │ ├── deno-run-examples.md │ │ ├── get-liveviewjs-repo.md │ │ ├── nodejs-build-first-liveview.md │ │ └── nodejs-run-examples.md │ ├── 03-anatomy-of-a-liveview │ │ ├── _category_.json │ │ ├── handle-event-details.md │ │ ├── handle-info-background-task.md │ │ ├── handle-info-pub-sub.md │ │ ├── handle-info-user-initiated.md │ │ ├── handle-info.md │ │ ├── handle-params.md │ │ ├── liveview-api.md │ │ ├── mount-details.md │ │ └── render-details.md │ ├── 04-liveview-socket │ │ ├── _category_.json │ │ ├── liveviewsocket-api-context.md │ │ ├── liveviewsocket-api-infos.md │ │ ├── liveviewsocket-api-misc.md │ │ ├── liveviewsocket-api-push.md │ │ ├── liveviewsocket-api-uploads.md │ │ ├── liveviewsocket-api.md │ │ └── raw-liveviewsocket-api.md │ ├── 05-lifecycle-of-a-liveview │ │ ├── _category_.json │ │ └── intro.md │ ├── 06-user-events-slash-bindings │ │ ├── _category_.json │ │ ├── additional-bindings.md │ │ ├── bindings-table.md │ │ ├── overview.md │ │ └── rate-limiting-bindings.md │ ├── 07-forms-and-changesets │ │ ├── _category_.json │ │ ├── changesets.md │ │ ├── overview.md │ │ └── use-with-forms.md │ ├── 08-file-upload │ │ ├── _category_.json │ │ ├── drag-and-drop.md │ │ ├── image-preview.md │ │ ├── overview.md │ │ └── upload-config-options.md │ ├── 09-real-time-multi-player-pub-sub │ │ ├── _category_.json │ │ ├── example-pub-sub.md │ │ └── overview.md │ ├── 10-client-javascript │ │ ├── _category_.json │ │ ├── client-hooks.md │ │ ├── example-hook.md │ │ └── overview.md │ ├── 11-js-commands │ │ ├── _category_.json │ │ ├── add-remove-class.md │ │ ├── dispatch-cmd.md │ │ ├── overview.md │ │ ├── push-cmd.md │ │ ├── set-remove-attr.md │ │ ├── show-hide-toggle-el.md │ │ └── transition-cmd.md │ ├── 12-webserver-integration │ │ ├── _category_.json │ │ ├── liveview-server-adaptor.md │ │ ├── overview.md │ │ └── support-webserver-x.md │ └── 13-misc │ │ ├── _category_.json │ │ ├── debugging-wire.md │ │ ├── flash.md │ │ ├── livecomponents.md │ │ ├── root-and-page-renderers.md │ │ ├── security-topics.md │ │ └── statics-and-dynamics.md │ ├── docusaurus.config.js │ ├── package-lock.json │ ├── package.json │ ├── sidebars.js │ ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx │ ├── static │ ├── .nojekyll │ ├── CNAME │ ├── fonts │ │ ├── LibreFranklin-Italic-VariableFont_wght.ttf │ │ └── LibreFranklin-VariableFont_wght.ttf │ └── img │ │ ├── diagrams │ │ ├── liveview-lifecycle-heartbeat.svg │ │ ├── liveview-lifecycle-http-phase.svg │ │ ├── liveview-lifecycle-shutdown.svg │ │ ├── liveview-lifecycle-user-and-server-events.svg │ │ ├── liveview-lifecycle-websocket-join.svg │ │ └── liveviewjs-lifecycle.svg │ │ ├── favicon.ico │ │ ├── features │ │ ├── multiplayer.svg │ │ ├── nirvana.svg │ │ └── simple.svg │ │ ├── logo.svg │ │ └── screenshots │ │ ├── liveviewjs_counter_liveview_rec.gif │ │ ├── liveviewjs_examples_rec.gif │ │ ├── liveviewjs_hello_liveview.png │ │ ├── liveviewjs_hello_liveview_deno.png │ │ ├── liveviewjs_hello_toggle_liveview_deno_rec.gif │ │ └── liveviewjs_hello_toggle_liveview_rec.gif │ ├── tailwind.config.js │ └── tsconfig.json ├── liveviewjs-logo.png ├── old_docs ├── README.md ├── app-directory-structure.md ├── changesets.md ├── file-uploads.md ├── js-commands.md ├── liveview_lifecycle.md ├── routing.md ├── server_options.md ├── temp-assign.md └── updating-html-title.md ├── package-lock.json ├── package.json ├── packages ├── core │ ├── .gitattributes │ ├── .gitignore │ ├── .npmignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── coverage │ │ ├── clover.xml │ │ └── coverage-final.json │ ├── dist │ │ ├── liveview.d.ts │ │ ├── liveview.js │ │ └── liveview.mjs │ ├── jest.config.js │ ├── mod.ts │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── index.ts │ │ └── server │ │ │ ├── adaptor │ │ │ ├── __snapshots__ │ │ │ │ └── http.test.ts.snap │ │ │ ├── csrfGen.ts │ │ │ ├── files.ts │ │ │ ├── flash.ts │ │ │ ├── http.test.ts │ │ │ ├── http.ts │ │ │ ├── idGen.ts │ │ │ ├── index.ts │ │ │ ├── jsonSerDe.ts │ │ │ ├── serDe.ts │ │ │ ├── sessionFlashAdaptor.ts │ │ │ ├── testFilesAdatptor.ts │ │ │ └── websocket.ts │ │ │ ├── changeset │ │ │ ├── changeset.test.ts │ │ │ ├── changeset.ts │ │ │ └── index.ts │ │ │ ├── fetch.d.ts │ │ │ ├── index.ts │ │ │ ├── live │ │ │ ├── baseLiveView.test.ts │ │ │ ├── index.ts │ │ │ ├── liveComponent.test.ts │ │ │ ├── liveComponent.ts │ │ │ ├── liveView.test.ts │ │ │ ├── liveView.ts │ │ │ ├── router.test.ts │ │ │ ├── router.ts │ │ │ └── types.ts │ │ │ ├── mime │ │ │ ├── index.ts │ │ │ └── mime.test.ts │ │ │ ├── protocol │ │ │ ├── phx.test.ts │ │ │ ├── phx.ts │ │ │ └── reply.ts │ │ │ ├── pubsub │ │ │ ├── index.ts │ │ │ ├── pubSub.ts │ │ │ └── singleProcessPubSub.ts │ │ │ ├── server.ts │ │ │ ├── session.ts │ │ │ ├── socket │ │ │ ├── index.ts │ │ │ ├── liveSocket.test.ts │ │ │ ├── liveSocket.ts │ │ │ ├── liveViewManager-Uploads.test.ts │ │ │ ├── liveViewManager.test.ts │ │ │ ├── liveViewManager.ts │ │ │ ├── structuredClone.ts │ │ │ ├── types.ts │ │ │ ├── util.test.ts │ │ │ ├── util.ts │ │ │ ├── ws │ │ │ │ ├── wsEventHandler.ts │ │ │ │ ├── wsHandler.test.ts │ │ │ │ ├── wsHandler.ts │ │ │ │ ├── wsLiveComponents.ts │ │ │ │ └── wsUploadHandler.ts │ │ │ ├── wsMessageRouter.test.ts │ │ │ └── wsMessageRouter.ts │ │ │ ├── templates │ │ │ ├── diff.test.ts │ │ │ ├── diff.ts │ │ │ ├── helpers │ │ │ │ ├── form_for.test.ts │ │ │ │ ├── form_for.ts │ │ │ │ ├── index.ts │ │ │ │ ├── inputs.test.ts │ │ │ │ ├── inputs.ts │ │ │ │ ├── live_file_input.test.ts │ │ │ │ ├── live_file_input.ts │ │ │ │ ├── live_img_preview.test.ts │ │ │ │ ├── live_img_preview.ts │ │ │ │ ├── live_patch.test.ts │ │ │ │ ├── live_patch.ts │ │ │ │ ├── live_title.ts │ │ │ │ ├── live_title_tag.test.ts │ │ │ │ ├── live_title_tag.ts │ │ │ │ ├── options_for_select.test.ts │ │ │ │ ├── options_for_select.ts │ │ │ │ ├── submit.test.ts │ │ │ │ └── submit.ts │ │ │ ├── htmlSafeString.test.ts │ │ │ ├── htmlSafeString.ts │ │ │ ├── index.ts │ │ │ ├── jsCommands.test.ts │ │ │ └── jsCommands.ts │ │ │ ├── test │ │ │ ├── liveviews.ts │ │ │ └── wsAdaptor.ts │ │ │ └── upload │ │ │ ├── binaryUploadSerDe.test.ts │ │ │ ├── binaryUploadSerDe.ts │ │ │ ├── index.ts │ │ │ ├── uploadConfig.test.ts │ │ │ ├── uploadConfig.ts │ │ │ ├── uploadEntry.test.ts │ │ │ └── uploadEntry.ts │ └── tsconfig.json ├── create-liveviewjs │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── create │ │ │ ├── cli.ts │ │ │ └── index.ts │ ├── templates │ │ └── liveviewjs-app │ │ │ ├── README.md │ │ │ ├── app │ │ │ ├── assets │ │ │ │ ├── css │ │ │ │ │ └── app.css │ │ │ │ └── js │ │ │ │ │ └── app.ts │ │ │ ├── controllers │ │ │ │ └── root │ │ │ │ │ └── rootController.ts │ │ │ ├── liveviews │ │ │ │ └── demos │ │ │ │ │ └── clickLiveView.ts │ │ │ ├── middleware │ │ │ │ └── demoTimestamp.ts │ │ │ ├── models │ │ │ │ └── counter.ts │ │ │ ├── services │ │ │ │ └── inMemory.ts │ │ │ └── views │ │ │ │ ├── liveviews │ │ │ │ └── root.ejs │ │ │ │ └── root │ │ │ │ └── index.ejs │ │ │ ├── config │ │ │ └── app.ts │ │ │ ├── env │ │ │ ├── gitignore │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── tailwind.config.js │ │ │ └── tsconfig.json │ └── tsconfig.json ├── deno │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── import_map.json │ ├── mod.ts │ ├── package.json │ ├── public │ │ ├── .keep │ │ └── favicon.ico │ ├── src │ │ ├── client │ │ │ └── index.ts │ │ ├── deno │ │ │ ├── broadcastChannelPubSub.ts │ │ │ ├── fsAdaptor.ts │ │ │ ├── index.ts │ │ │ ├── jwtSerDe.ts │ │ │ ├── server.ts │ │ │ └── wsAdaptor.ts │ │ ├── deps.ts │ │ └── example │ │ │ ├── autorun.ts │ │ │ ├── index.ts │ │ │ ├── indexHandler.ts │ │ │ ├── liveTemplates.ts │ │ │ ├── liveview │ │ │ ├── router.ts │ │ │ └── rtcounter.ts │ │ │ └── oak.ts │ └── tsconfig-client.json ├── examples │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── dist │ │ ├── liveviewjs-examples.browser.js │ │ ├── liveviewjs-examples.browser.js.map │ │ ├── liveviewjs-examples.d.ts │ │ ├── liveviewjs-examples.js │ │ └── liveviewjs-examples.mjs │ ├── mod.ts │ ├── package-lock.json │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── datastore │ │ │ └── InMemory.ts │ │ ├── fetch.d.ts │ │ ├── liveviews │ │ │ ├── autoComplete │ │ │ │ ├── data.test.ts │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── books │ │ │ │ └── index.ts │ │ │ ├── counter │ │ │ │ ├── index.ts │ │ │ │ └── realtime.ts │ │ │ ├── dashboard │ │ │ │ └── index.ts │ │ │ ├── decarbonize │ │ │ │ ├── data.ts │ │ │ │ ├── index.ts │ │ │ │ └── liveComponent.ts │ │ │ ├── fetch │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── hello │ │ │ │ ├── helloName.ts │ │ │ │ └── helloToggleEmoji.ts │ │ │ ├── jsCommands │ │ │ │ └── index.ts │ │ │ ├── liveNav │ │ │ │ └── index.ts │ │ │ ├── liveSearch │ │ │ │ ├── data.test.ts │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── pagination │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── photos │ │ │ │ └── index.ts │ │ │ ├── prints │ │ │ │ └── index.ts │ │ │ ├── servers │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── sorting │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ └── index.ts │ │ │ ├── volume │ │ │ │ └── index.ts │ │ │ └── volunteers │ │ │ │ ├── data.test.ts │ │ │ │ ├── data.ts │ │ │ │ └── index.ts │ │ ├── rollupEntry.ts │ │ └── routeDetails.ts │ └── tsconfig.json ├── express │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── dist │ │ ├── liveviewjs-express.d.ts │ │ ├── liveviewjs-express.js │ │ └── liveviewjs-express.mjs │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── .keep │ │ ├── favicon.ico │ │ └── images │ │ │ └── cats │ │ │ ├── clenil.jpg │ │ │ ├── flippers.jpg │ │ │ ├── jorts.jpg │ │ │ ├── kipper.jpg │ │ │ ├── lemmy.jpg │ │ │ ├── lissy.jpg │ │ │ ├── mikkel.jpg │ │ │ ├── minka.jpg │ │ │ ├── misty.jpg │ │ │ ├── nelly.jpg │ │ │ ├── ninj.jpg │ │ │ ├── pollito.jpg │ │ │ ├── siegfried.jpg │ │ │ ├── truman.jpg │ │ │ └── washy.jpg │ ├── rollup.config.js │ ├── src │ │ ├── client │ │ │ └── index.ts │ │ ├── example │ │ │ ├── autorun.ts │ │ │ ├── autorun_ios.ts │ │ │ ├── express.ts │ │ │ ├── index.ts │ │ │ ├── indexHandler.ts │ │ │ ├── ios.ts │ │ │ ├── iosTemplates.ts │ │ │ ├── liveTemplates.ts │ │ │ └── liveview │ │ │ │ ├── ios │ │ │ │ ├── cat.ts │ │ │ │ ├── catList.ts │ │ │ │ └── data.ts │ │ │ │ └── router.ts │ │ ├── index.ts │ │ └── node │ │ │ ├── fsAdaptor.ts │ │ │ ├── index.ts │ │ │ ├── jwtSerDe.ts │ │ │ ├── redisPubSub.ts │ │ │ ├── server.ts │ │ │ └── wsAdaptor.ts │ ├── tsconfig-client.json │ └── tsconfig.json └── gen │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── CHANGELOG.md │ ├── README.md │ ├── _templates │ ├── deno-project │ │ └── new │ │ │ ├── .gitignore.ejs.t │ │ │ ├── .prettierignore.ejs.t │ │ │ ├── .prettierrc.ejs.t │ │ │ ├── README.md.ejs.t │ │ │ ├── import_map.json.ejs.t │ │ │ ├── package.json.ejs.t │ │ │ ├── public │ │ │ └── .keep.ejs.t │ │ │ ├── src │ │ │ ├── client │ │ │ │ └── index.ts.ejs.t │ │ │ ├── deps.ts.ejs.t │ │ │ └── server │ │ │ │ ├── autorun.ts.ejs.t │ │ │ │ ├── index.ts.ejs.t │ │ │ │ ├── liveTemplates.ts.ejs.t │ │ │ │ ├── liveview │ │ │ │ ├── hello.ts.ejs.t │ │ │ │ └── router.ts.ejs.t │ │ │ │ └── oak.ts.ejs.t │ │ │ └── tsconfig-client.json.ejs.t │ └── node-project │ │ └── new │ │ ├── .gitignore.ejs.t │ │ ├── .npmrc.ejs.t │ │ ├── .prettierignore.ejs.t │ │ ├── .prettierrc.ejs.t │ │ ├── README.md.ejs.t │ │ ├── package.json.ejs.t │ │ ├── public │ │ └── .keep.ejs.t │ │ ├── src │ │ ├── client │ │ │ └── index.ts.ejs.t │ │ └── server │ │ │ ├── autorun.ts.ejs.t │ │ │ ├── express.ts.ejs.t │ │ │ ├── index.ts.ejs.t │ │ │ ├── liveTemplates.ts.ejs.t │ │ │ └── liveview │ │ │ ├── hello.ts.ejs.t │ │ │ └── router.ts.ejs.t │ │ ├── tsconfig-client.json.ejs.t │ │ └── tsconfig.json.ejs.t │ ├── dist │ ├── cli.d.mts │ ├── cli.mjs │ ├── null_logger.d.mts │ ├── null_logger.mjs │ ├── post_exec.d.mts │ ├── post_exec.mjs │ ├── project_helper.d.mts │ ├── project_helper.mjs │ ├── prompts.d.mts │ ├── prompts.mjs │ ├── yargs.d.mts │ └── yargs.mjs │ ├── package.json │ ├── src │ ├── cli.mts │ ├── null_logger.mts │ ├── post_exec.mts │ ├── project_helper.mts │ ├── prompts.mts │ └── yargs.mts │ └── tsconfig.json └── turbo.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [ 6 | ["liveviewjs", "@liveviewjs/examples", "@liveviewjs/express", "@liveviewjs/gen", "@liveviewjs/lambda-examples"] 7 | ], 8 | "linked": [], 9 | "access": "public", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": ["@liveviewjs/deno-client", "liveviewjs-com"] 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [floodfx] 2 | -------------------------------------------------------------------------------- /.github/workflows/auto-release.yml: -------------------------------------------------------------------------------- 1 | name: AutoRelease 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: AutoRelease 10 | runs-on: ubuntu-latest 11 | steps: 12 | # checkout the code 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | # setup node version 18 | - uses: actions/setup-node@v3.0.0 19 | with: 20 | node-version: 17 21 | # install dependencies and test 22 | - run: npm ci 23 | # - run: npm test 24 | # run rollup and save dist updates back to repo 25 | - run: npm run dist 26 | # commit back changes to dist 27 | - uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | commit_message: "automatically committing rollup output to dist" 30 | file_pattern: dist/* 31 | # publish to npm if version is different 32 | - uses: JS-DevTools/npm-publish@v1 33 | with: 34 | token: ${{ secrets.NPM_PUBLISH_TOKEN }} 35 | - name: Automatic GitHub Release 36 | uses: justincy/github-action-npm-release@2.0.2 37 | id: release 38 | - name: Print release output 39 | if: ${{ steps.release.outputs.released == 'true' }} 40 | run: echo Release ID ${{ steps.release.outputs.release_id }} 41 | -------------------------------------------------------------------------------- /.github/workflows/changeset-release.yml: -------------------------------------------------------------------------------- 1 | name: Changeset Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: AutoRelease 10 | runs-on: ubuntu-latest 11 | steps: 12 | # checkout the code 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | # setup node version 18 | - name: Setup Node 19 | uses: actions/setup-node@v3.0.0 20 | with: 21 | node-version: 18 22 | # install dependencies 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | # run turbo publish 27 | - name: Install dependencies 28 | run: npm run publish 29 | 30 | - name: Create Release Pull Request or Publish to npm 31 | id: changesets 32 | uses: changesets/action@v1 33 | with: 34 | publish: npm run cs:publish 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 38 | 39 | - name: Print Release 40 | if: ${{ steps.release.outputs.released == 'true' }} 41 | run: echo Release ID ${{ steps.release.outputs.release_id }} 42 | -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy to GitHub Pages 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | 18 | - name: Install dependencies in apps/liveviewjs.com 19 | run: npm install -w apps/liveviewjs.com 20 | - name: Build docusaurus website 21 | run: npm run build -w apps/liveviewjs.com 22 | 23 | # Popular action to deploy to GitHub Pages: 24 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 25 | - name: Deploy to GitHub Pages 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | # Build output to publish to the `gh-pages` branch: 30 | publish_dir: ./apps/liveviewjs.com/build 31 | user_name: floodfx 32 | user_email: donnie@floodfx.com 33 | -------------------------------------------------------------------------------- /.github/workflows/test-docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Test Docs Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | test-deploy: 13 | name: Test Docs Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | - name: Install dependencies in apps/liveviewjs.com 21 | run: npm install -w apps/liveviewjs.com 22 | - name: Build docusaurus website 23 | run: npm run build -w apps/liveviewjs.com 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | .parcel-cache/ 5 | .DS_Store 6 | coverage/lcov-report/ 7 | coverage/lcov.info 8 | diffs/ 9 | build/ 10 | *.tgz 11 | .turbo 12 | .todo.md 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | .vscode/ 3 | coverage/ 4 | node_modules/ 5 | .editorconfig 6 | .eslintrc.js 7 | .gitignore 8 | .gitattributes 9 | .npmignore 10 | jest.config.js 11 | package-lock.json 12 | tsconfig.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /.vscode/css_custom_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "atDirectives": [ 3 | { 4 | "name": "@tailwind", 5 | "description": "Use the @tailwind directive to insert Tailwind’s `base`, `components`, `utilities`, and `screens` styles into your CSS.", 6 | "references": [ 7 | { 8 | "name": "Tailwind’s “Functions & Directives” documentation", 9 | "url": "https://tailwindcss.com/docs/functions-and-directives/#tailwind" 10 | } 11 | ] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/.git/objects/**": true, 4 | "**/.git/subtree-cache/**": true, 5 | "**/node_modules/**": true, 6 | "env-*": true 7 | }, 8 | "files.exclude": { 9 | "**/.git": true, 10 | "**/.DS_Store": true 11 | }, 12 | "css.customData": [".vscode/css_custom_data.json"], 13 | "[typescript]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode", 15 | "editor.formatOnSave": true, 16 | "editor.tabSize": 2, 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": true, 19 | "source.fixAll": true 20 | } 21 | }, 22 | "deno.importMap": "packages/deno/import_map.json", 23 | "deno.enablePaths": ["packages/deno/mod.ts", "packages/deno/src/deno", "packages/deno/src/example"], 24 | "typescript.tsdk": "node_modules/typescript/lib" 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 Donald Flood 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## LiveViewJS 2 | 3 | ![LiveViewJS Logo](liveviewjs-logo.png) 4 | 5 | *An HTML-first, "Get Stuff Done"-focused library for building LiveViews in NodeJS and Deno* 6 | 7 | Learn more at [LiveViewJS.com](https://www.liveviewjs.com) 8 | -------------------------------------------------------------------------------- /apps/lambda-examples/.gitignore: -------------------------------------------------------------------------------- 1 | public/*.png 2 | public/*.gif 3 | public/*.jpg 4 | public/*.jpeg 5 | public/*.pdf 6 | public/js/ 7 | 8 | *.js 9 | !jest.config.js 10 | *.d.ts 11 | node_modules 12 | 13 | # CDK asset staging directory 14 | .cdk.staging 15 | cdk.out 16 | -------------------------------------------------------------------------------- /apps/lambda-examples/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /apps/lambda-examples/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /apps/lambda-examples/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /apps/lambda-examples/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /apps/lambda-examples/README.md: -------------------------------------------------------------------------------- 1 | # 🖼 LiveViewJS for AWS Lambda (NodeJS) 2 | 3 | The is a proof-of-concept that shows how you can host a "serverless" LiveViewJS application on AWS Lambda and API Gateway. The project loads the examples from the [LiveViewJS](https://liveviewjs.com) but you can adapt this to host your own LiveViewJS application. 4 | 5 | ## Status 6 | This is a proof-of-concept and is NOT production ready for large volumes. In particular, the following issues need to be addressed: 7 | - API Gateway Websocket requests may not be handled in order that they were received (thus race conditions can occur) 8 | - LiveViewJS currently keeps LiveView state in memory which may break in the 9 | case multiple lambda functions attempt to handle requests from the same LiveView 10 | 11 | We can address these issue by storing LiveView state in a different data store (e.g. DynamoDB) and using a queue to ensure that requests are handled in order. 12 | 13 | ## Summary AWS Architecture 14 | The set of AWS resources that are created are pretty simple: 15 | - API Gateway Websocket API passing requests to a Single Lambda function 16 | - API Gateway HTTP API passing requests to a Single Lambda function 17 | 18 | ## Pre-requisites 19 | You should have an AWS account and have the AWS CLI installed and configured with your credentials. 20 | 21 | Run `npm install` to install dependencies. 22 | 23 | ## Deploy 24 | This project uses AWS CDK to setup the infrastructure and deploy the code. 25 | 26 | Deploy to AWS Lambda using `cdk deploy [--profile YOUR_AWS_PROFILE]`. 27 | 28 | When the 29 | 30 | ## Teardown 31 | After you are done with the project you can remove the stack from your AWS account by running: 32 | 33 | `cdk destroy [--profile YOUR_AWS_PROFILE]` 34 | -------------------------------------------------------------------------------- /apps/lambda-examples/bin/liveviewjs-lambda.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from "aws-cdk-lib"; 3 | import "source-map-support/register"; 4 | import { LiveViewJSLambdaStack } from "../lib/liveviewjs-lambda-stack"; 5 | 6 | const app = new cdk.App(); 7 | new LiveViewJSLambdaStack(app, "LiveViewJSLambdaStack", { 8 | /* If you don't specify 'env', this stack will be environment-agnostic. 9 | * Account/Region-dependent features and context lookups will not work, 10 | * but a single synthesized template can be deployed anywhere. */ 11 | /* Uncomment the next line to specialize this stack for the AWS Account 12 | * and Region that are implied by the current CLI configuration. */ 13 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 14 | /* Uncomment the next line if you know exactly what Account and Region you 15 | * want to deploy the stack to. */ 16 | // env: { account: '123456789012', region: 'us-east-1' }, 17 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 18 | }); 19 | -------------------------------------------------------------------------------- /apps/lambda-examples/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/liveviewjs-lambda.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 25 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/core:checkSecretUsage": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 31 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 32 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 33 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 34 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 35 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 36 | "@aws-cdk/core:enablePartitionLiterals": true, 37 | "@aws-cdk/core:target-partitions": [ 38 | "aws", 39 | "aws-cn" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/lambda-examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/lambda-examples/public/favicon.ico -------------------------------------------------------------------------------- /apps/lambda-examples/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import { Socket } from "phoenix"; 3 | import "phoenix_html"; 4 | import { LiveSocket, ViewHook } from "phoenix_live_view"; 5 | 6 | /** 7 | * Define custom LiveView Hooks that can tap into browser events. 8 | * See: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook 9 | */ 10 | let Hooks = { 11 | /** 12 | * This hook can be used by an input element to prevent input other than numbers. 13 | * e.g. 14 | */ 15 | NumberInput: { 16 | mounted() { 17 | this.el.addEventListener("input", () => { 18 | // replace all non-numeric characters with empty string 19 | this.el.value = this.el.value.replace(/\D/g, ""); 20 | }); 21 | }, 22 | } as ViewHook, 23 | }; 24 | 25 | // WS_APIG_ID in the URL below will be replaced with the API Gateway ID at runtime 26 | // See: src/lambdas/http.ts route handler for "/js/index.js" 27 | const url = "wss://WS_APIG_ID.execute-api.us-west-2.amazonaws.com"; 28 | let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); 29 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks }); 30 | 31 | // Show progress bar on live navigation and form submits 32 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 33 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 34 | 35 | // connect if there are any LiveViews on the page 36 | liveSocket.connect(); 37 | 38 | // expose liveSocket on window for web console debug logs and latency simulation: 39 | // liveSocket.enableDebug(); 40 | // liveSocket.enableLatencySim(1000) 41 | (window as any).liveSocket = liveSocket; 42 | -------------------------------------------------------------------------------- /apps/lambda-examples/src/example/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autocompleteLiveView, 3 | booksLiveView, 4 | counterLiveView, 5 | dashboardLiveView, 6 | decarbLiveView, 7 | helloToggleEmojiLiveView, 8 | jsCmdsLiveView, 9 | paginateLiveView, 10 | photosLiveView, 11 | printLiveView, 12 | rtCounterLiveView, 13 | searchLiveView, 14 | serversLiveView, 15 | sortLiveView, 16 | volumeLiveView, 17 | volunteerLiveView, 18 | xkcdLiveView, 19 | } from "@liveviewjs/examples"; 20 | import { LiveViewRouter } from "liveviewjs"; 21 | import { LambdaLiveViewServer } from "../lambda/server"; 22 | import { htmlPageTemplate, wrapperTemplate } from "./liveTemplates"; 23 | 24 | // LiveViewRouter that maps the path to the LiveView 25 | const router: LiveViewRouter = { 26 | "/autocomplete": autocompleteLiveView, 27 | "/decarbonize": decarbLiveView, 28 | "/prints": printLiveView, 29 | "/volume": volumeLiveView, 30 | "/paginate": paginateLiveView, 31 | "/dashboard": dashboardLiveView, 32 | "/search": searchLiveView, 33 | "/servers": serversLiveView, 34 | "/sort": sortLiveView, 35 | "/volunteers": volunteerLiveView, 36 | "/counter": counterLiveView, 37 | "/jscmds": jsCmdsLiveView, 38 | "/photos": photosLiveView, 39 | "/xkcd": xkcdLiveView, 40 | "/rtcounter": rtCounterLiveView, 41 | "/books": booksLiveView, 42 | "/helloToggle": helloToggleEmojiLiveView, 43 | }; 44 | 45 | // Configure the LambdaLiveViewServer which generates handlers for HTTP 46 | // and WebSocket requests from API Gateway and AWS Lambda 47 | export const liveView = new LambdaLiveViewServer( 48 | router, 49 | htmlPageTemplate, 50 | { title: "Lambda Demo", suffix: " · LiveViewJS" }, 51 | { 52 | serDeSigningSecret: "signingSecret", 53 | wrapperTemplate, 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /apps/lambda-examples/src/lambdas/websocket.ts: -------------------------------------------------------------------------------- 1 | import { liveView } from "src/example"; 2 | 3 | /** 4 | * Use the liveView middleware to handle Websocket requests 5 | */ 6 | export const handler = liveView.wsMiddleware(); 7 | -------------------------------------------------------------------------------- /apps/lambda-examples/tsconfig-client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "ES2020", 15 | "moduleResolution": "node", 16 | "lib": ["DOM"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/client/*"], 22 | "exclude": ["build", "node_modules", "./**/*.test.ts", "cdk.out", "./src/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/lambda-examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "es2019", 15 | "moduleResolution": "node", 16 | "lib": ["es2019", "esnext.asynciterable"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/**/*", "lib/**/*"], 22 | "exclude": ["build", "node_modules", "./**/*.test.ts", "./src/client/**/*", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true, 6 | "proseWrap": "always" 7 | } 8 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ``` 8 | $ npm install 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ npm run start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without 18 | having to restart the server. 19 | 20 | ### Build 21 | 22 | ``` 23 | $ npm run build 24 | ``` 25 | 26 | This command generates static content into the `docs` directory and can be served using any static contents hosting 27 | service. 28 | 29 | ### Deployment 30 | 31 | Using SSH: 32 | 33 | ``` 34 | $ USE_SSH=true npm run deploy 35 | ``` 36 | 37 | Not using SSH: 38 | 39 | ``` 40 | $ GIT_USER= npm run deploy 41 | ``` 42 | 43 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the 44 | `gh-pages` branch. 45 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve("@docusaurus/core/lib/babel/preset")], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/blog/2019-05-28-first-blog-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-blog-post 3 | title: First Blog Post 4 | authors: [floodfx] 5 | tags: [hola, docusaurus] 6 | --- 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum 9 | tempor eros aliquam consequat. Lorem ipsum dolor sit amet 10 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/blog/2021-08-01-mdx-blog-post.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | slug: mdx-blog-post 3 | title: MDX Blog Post 4 | authors: [floodfx] 5 | tags: [docusaurus] 6 | --- 7 | 8 | Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/). 9 | 10 | :::tip 11 | 12 | Use the power of React to create interactive blog posts. 13 | 14 | ```js 15 | 16 | ``` 17 | 18 | 19 | 20 | ::: 21 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg -------------------------------------------------------------------------------- /apps/liveviewjs.com/blog/2021-08-26-welcome/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: welcome 3 | title: Welcome 4 | authors: [floodfx] 5 | tags: [facebook, hello, docusaurus] 6 | --- 7 | 8 | [Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the 9 | [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog). 10 | 11 | Simply add Markdown files (or folders) to the `blog` directory. 12 | 13 | Regular blog authors can be added to `authors.yml`. 14 | 15 | The blog post date can be extracted from filenames, such as: 16 | 17 | - `2019-05-30-welcome.md` 18 | - `2019-05-30-welcome/index.md` 19 | 20 | A blog post folder can be convenient to co-locate blog post images: 21 | 22 | ![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg) 23 | 24 | The blog supports tags as well! 25 | 26 | **And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config. 27 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/blog/authors.yml: -------------------------------------------------------------------------------- 1 | floodfx: 2 | name: Donnie Flood 3 | title: LiveViewJS Author 4 | url: https://github.com/floodfx 5 | image_url: https://github.com/floodfx.png 6 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/01-overview/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Overview", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "LiveViewJS is an open-source framework for \"LiveView\"-based, full-stack applications in NodeJS and Deno." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/01-overview/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Getting Involved 6 | 7 | ## Feedback is a 🎁 8 | 9 | Like all software, this is a work in progress. If you have any feedback, please let us know by opening an issue on the 10 | [GitHub repository](https://github.com/floodfx/liveviewjs/issues). 11 | 12 | ## Contributing is ❤️ 13 | 14 | We welcome questions, comments, documentation, and code contributions. If you'd like to contribute, please feel free to 15 | open an issue or a pull request. We'll do our best to respond quickly and get it merged! 16 | 17 | ## Sponsorship is ⛽️ 18 | 19 | If you'd like to support the project, please consider sponsoring us on [GitHub](https://github.com/sponsors/floodfx). 20 | We'll use the funds to pay for coffee, hosting, domain names, therapy, and other expenses related to the project. 21 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/01-overview/gratitude.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Gratitude 6 | 7 | 🙌 Thanks to the Phoenix LiveView team for the genius conceptualization and implementation of LiveViews and all the code 8 | we can reuse on the client side. 9 | 10 | 🙌 Thanks to [@ogrodnek](https://github.com/ogrodnek) for the early support, feedback, and the idea to reuse the Phoenix 11 | client code instead of reinventing! 12 | 13 | 🙌 Thanks to [@blimmer](https://github.com/blimmer/) for the awesome feedback, documentation suggestions, and support! 14 | 15 | Thanks to you for checking out **LiveViewJS**! We hope you enjoy it as much as we do! 16 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/02-quick-starts/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Quick Starts", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Run the examples and build your first LiveView" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/02-quick-starts/deno-run-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Deno - Run the Examples 6 | 7 | **LiveViewJS** ships with over a dozen example LiveViews that show everything from simple button-based events to 8 | real-time, multi-player views. It takes approximately 1 minute to get these examples up and running and is a good way to 9 | get a feel for the user experience of a LiveView. Let's get started! 10 | 11 | ## Prerequisite 12 | 13 | [Deno](https://deno.land/) version 1.24.x or above. (Older versions may work but haven't been tested.) 14 | 15 | If you haven't already, [download the **LiveViewJS** repo](get-liveviewjs-repo). 16 | 17 | ## Run the Examples 18 | 19 | Navigate to the `packages/deno` directory: 20 | 21 | ```bash 22 | # cd to the deno directory 23 | cd packages/deno 24 | ``` 25 | 26 | Install dependencies: 27 | 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | Then, start the Deno server with the examples: 33 | 34 | ```bash 35 | deno run --allow-run --allow-read --allow-write --allow-net --allow-env src/example/autorun.ts 36 | ``` 37 | 38 | Point your browser to [http://localhost:9001](http://localhost:9001) 39 | 40 | ## Explore the Examples 41 | 42 | You should see something like the screenshot below including a list of examples with a brief description, a link to the 43 | running LiveView, and a link to the source code for each example. 44 | 45 | ![LiveViewJS Examples Screenshot](/img/screenshots/liveviewjs_examples_rec.gif) 46 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/02-quick-starts/nodejs-run-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # NodeJS - Run the Examples 6 | 7 | **LiveViewJS** ships with over a dozen example LiveViews that show everything from simple button-based events to 8 | real-time, multi-player views. It takes approximately ⏱ 1 minute to get these examples up and running and is a good way 9 | to get a feel for the user experience of a LiveView. Let's get started! 10 | 11 | ## Prerequisite 12 | 13 | [Node.js](https://nodejs.org/en/download/) version 18.x or above. 14 | 15 | :::note 16 | 17 | We rely on the NodeJS Fetch API, which is only available in 18+. 18 | 19 | ::: 20 | 21 | If you haven't already, [download the **LiveViewJS** repo](get-liveviewjs-repo). 22 | 23 | ## Run the Examples 24 | 25 | First, load the NPM dependencies: 26 | 27 | ```bash 28 | # install the NPM dependencies 29 | npm install 30 | ``` 31 | 32 | Then, start the express server with the examples: 33 | 34 | ```bash 35 | # run the examples 36 | npm run start -w packages/express 37 | ``` 38 | 39 | Point your browser to [http://localhost:4001](http://localhost:4001) 40 | 41 | ## Explore the Examples 42 | 43 | You should see something like the screenshot below including a list of examples with a brief description, a link to the 44 | running LiveView, and a link to the source code for each example. 45 | 46 | ![LiveViewJS Examples Screenshot](/img/screenshots/liveviewjs_examples_rec.gif) 47 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/03-anatomy-of-a-liveview/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Anatomy of a LiveView", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Deep dive into the LiveView API and lifecycle functions" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/03-anatomy-of-a-liveview/handle-info.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # LiveView API - `handleInfo` 6 | 7 | `handleInfo` is how server-side events (a.k.a `Info`) are handled. These server-side events are initiated by processes 8 | that are happening on the server for example: database updates, background jobs, pub/sub messages, or some other 9 | asynchronous process. Just like `handleEvent` and `handleParams`, `handleInfo` is automatically passed the `info` event 10 | (i.e., server event) along with the `socket` and can use it to manipulate the `context` of the LiveView or otherwise 11 | respond to the `info` messages it receives. 12 | 13 | ## `handleInfo` Signature 14 | 15 | ```ts 16 | handleInfo(info: TInfos, socket: LiveViewSocket): void | Promise; 17 | ``` 18 | 19 | ## `handleInfo` Use Cases 20 | 21 | There are three main use cases for `handleInfo`: 22 | 23 | - Handling an asynchronous process initiated from a user event without blocking the UI 24 | - Handling an asynchronous process initiated from a background process 25 | - Handling a pub/sub message 26 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/04-liveview-socket/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "LiveViewSocket", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "The swiss army knife of LiveViewJS that connects the dots across a LiveView lifecycle." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/05-lifecycle-of-a-liveview/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Lifecycle of a LiveView", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "More details on the LiveView lifecycle including diagrams 📐" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/06-user-events-slash-bindings/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "User Events", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "How LiveViews listen for user events and the HTML attributes that trigger them " 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/06-user-events-slash-bindings/additional-bindings.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Additional Bindings 6 | 7 | There are additional bindings outside of the four main bindings for User Events that you will find extremely useful and 8 | that you will use often. 9 | 10 | ## Value Bindings 11 | 12 | When you need to send along additional data with an event binding you can use a "value binding" which looks something 13 | like `phx-value-[NAME]` where `[NAME]` is replaced by the key of the value you want to pass. This binding can be used in 14 | conjunction with other click, key, and focus bindings. 15 | 16 | ### Value Binding Example 17 | 18 | For example let's say you want to send the `mark_complete` event to the server along with and `id` value 19 | (e.g., `{id: "myId"}`) when the user clicks on the "Complete" button. To do this you do the following: 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | Note the `[NAME]` part of `phx-value-[NAME]` is `id` used as the object key while the attribute value (i.e., `"myId"`) is 26 | used as the object value. 27 | 28 | This example would send the following event to the server: 29 | 30 | ```ts 31 | { 32 | type: "mark_complete", 33 | id: "myId" 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/07-forms-and-changesets/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Forms & Changesets", 3 | "position": 7, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "How LiveViewJS makes form validation and submission easy with Changesets" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/07-forms-and-changesets/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Overview 6 | 7 | Forms are obviously extremely important for any web application that needs user input. Building, validating, and 8 | handling form submission is built into **LiveViewJS** forms . 9 | 10 | ## Form Bindigs 11 | 12 | We've already reviewed the form event bindings that are available in LiveViewJS. Here is a quick summary: 13 | 14 | - `phx-change` - This event is sent to the server along with all the form values when any form input is changed. 15 | - `phx-submit` - This event is sent to the server when the form is submitted alog with all the form values. 16 | 17 | Feel free to review form events in more detail in the 18 | [User Events and Bindings](/docs/user-events-slash-bindings/overview) section. 19 | 20 | ## Changesets 21 | 22 | We have not yet discussed the concept of a "changeset" in LiveViewJS. At a high level a changeset is a way to parse and 23 | validate that incoming JSON data maps to the expected constraints. You will see it is a very powerful concept that 24 | allows you to build and validate complex forms with ease. 25 | 26 | :::note 27 | 28 | Changesets are a concept that is taken from an Elixir library called 29 | [Ecto](https://hexdocs.pm/ecto/Ecto.Changeset.html). Ecto changesets are used to validate and persist data to a 30 | database. While **LiveViewJS** changeset are not ORM or DB releated, we've taken the concept of a changeset and adapted 31 | it to the Typescript world for parsing and validation. 32 | 33 | ::: 34 | 35 | We will take a deep dive into Changesets in a more detail in the next section. 36 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/08-file-upload/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Uploading Files", 3 | "position": 8, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Magically powerful file uploads with previews, progress, drag & drop, and more ✨" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/08-file-upload/drag-and-drop.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Built-in Drag and Drop 6 | 7 | LiveViewJS ships with built-in support for drag and drop file uploads. It is incredibly easy to use. All you need to do 8 | is add a `
` that has the `phx-drop-target` attribute set to the upload config ref you want to target. For 9 | example, if you want to allow users to drag and drop files into a `photos` upload config, you would do the following: 10 | 11 | ```ts 12 | ... 13 | render: (context, meta) => { 14 | ... 15 |
16 | Drop files here 17 |
18 | ... 19 | } 20 | ``` 21 | 22 | That's it! **LiveViewJS** will automatically handle the rest. The user will be able to drag and drop files into the div 23 | and they will be added to the entries of that upload config. 🤯 24 | 25 | ## Credit where credit is due 26 | 27 | Thanks to the Phoenix LiveView folks that built this! 🙌 This is a great example of why we built on top of the existing 28 | LiveView client-side JS. 29 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/08-file-upload/image-preview.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Built-in Image Preview 6 | 7 | LiveViewJS ships with build-in support for image previews when uploading files. 8 | 9 | ## Getting Entries from `meta.uploads` 10 | 11 | The list of `UploadEntry` objects for a given upload config can be found in the `meta.uploads` object based on the name 12 | you provided when configuring it using the `allowUpload` method. For example, if you configured an upload config named 13 | `photos`, you can access the list of `UploadEntry` objects using `meta.uploads.photos`. Here is an example of accessing 14 | the list of `UploadEntry` objects for a given upload config: 15 | 16 | ```ts 17 | ... 18 | render: (context, meta) => { 19 | ... 20 |
21 | ${meta.uploads.photos.map((entry) => { 22 | return html` 23 | 24 | `; 25 | })} 26 |
27 | ... 28 | } 29 | ``` 30 | 31 | ## `live_img_preview` Tag 32 | 33 | In order to use the built-in image preview, you must use the `live_img_preview` tag. This tag takes a `UploadEntry` and 34 | renders an image preview of it. 35 | 36 | ```ts 37 | ... 38 | render: (context, meta) => { 39 | ... 40 |
${live_img_preview(entry)}
41 | ... 42 | } 43 | ``` 44 | 45 | ## All Together now 46 | 47 | ```ts 48 | ... 49 | render: (context, meta) => { 50 | ... 51 |
52 | ${meta.uploads.photos.map((entry) => { 53 | return html` 54 |
${live_img_preview(entry)}
55 | `; 56 | })} 57 |
58 | ... 59 | } 60 | ``` 61 | 62 | That's it! 🤯 63 | 64 | ## Credit where credit is due 65 | 66 | Thanks to the Phoenix LiveView folks that built this! 🙌 This is a great example of why we built on top of the existing 67 | LiveView client-side JS. 68 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/08-file-upload/upload-config-options.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Upload Config Options 6 | 7 | These are the options you can pass into the `allowUpload` method to configure the upload. 8 | 9 | - `accept`: an array of strings that represent the file extensions and/or mime types that are allowed to be uploaded. 10 | For example, `[".png", ".jpg", ".jpeg", ".gif"]` will only allow images to be uploaded. See 11 | [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers) 12 | for more information. Defaults to `[]` (no restrictions) 13 | - `maxEntries`: the maximum number of files that can be uploaded. If the user tries to upload more than this number of 14 | files, an error will be present in the upload config. Defaults to `1`. 15 | - `maxFileSize`: the maximum file size (in bytes) that can be uploaded. If the user tries to upload a file that is 16 | larger than this number, an error will be present in the upload config. Defaults to `10 * 1024 * 1024` (10MB). 17 | - `autoUpload`: if `true`, the file will be uploaded as soon as it is selected by the user. If `false`, the file will 18 | not be uploaded until the user initiates the form's save event. The default is `false`. 19 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/09-real-time-multi-player-pub-sub/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Real-time / Multi-player", 3 | "position": 9, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Easy real-time, multi-player support with LiveViewJS Pub/Sub" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/10-client-javascript/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Client-side Javascript", 3 | "position": 10, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "How LiveView client-side Javascript works and how to use it" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/10-client-javascript/example-hook.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Example Hook 6 | 7 | Let's create a hook that will format a text input into a phone number as a user types. 8 | 9 | ## Hook Definition 10 | 11 | ```ts 12 | // Define the hook 13 | const PhoneNumber: ViewHook = { 14 | mounted() { 15 | this.el.addEventListener("input", (e) => { 16 | let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/); 17 | if (match) { 18 | this.el.value = `${match[1]}-${match[2]}-${match[3]}`; 19 | } 20 | }); 21 | }, 22 | }; 23 | // Add the hook to the LiveSocket 24 | let liveSocket = new LiveSocket("/live", Socket, { 25 | hooks: { PhoneNumber }, 26 | }); 27 | ``` 28 | 29 | ## Hook Usage 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | ## Credit 🙌 36 | 37 | Credit for this example goes to the 38 | [Phoenix LiveView docs](https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook). I didn't want 39 | to reinvent the wheel, so I just copied the example from the Phoenix LiveView docs, added some types, and simplified it 40 | a bit. 41 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/11-js-commands/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "JS Commands", 3 | "position": 11, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Additional utilities for more dynamic, client-side experiences" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/11-js-commands/dispatch-cmd.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Dispatch Command 6 | 7 | The `dispatch` command dispatches a DOM event from the target element 8 | 9 | ```typescript 10 | new JS().dispatch(event: string, options?: DispatchOptions) 11 | ``` 12 | 13 | - `event` - The name of the event to dispatch 14 | - `options` - Options for the command (optional) 15 | - `to` - An optional css selector to identify the element from which to dispatch. Defaults to the element that the JS 16 | Command is attached to. 17 | - `detail` - A optional map of key/value pairs to include in the event's `detail` property 18 | - `bubbles` - A optional boolean indicating whether the event should bubble up the DOM. Defaults to `true` 19 | 20 | **Note**: All events dispatched are of a type CustomEvent, with the exception of "click". For a "click", a MouseEvent is 21 | dispatched to properly simulate a UI click. 22 | 23 | For emitted CustomEvent's, the event detail will contain a dispatcher, which references the DOM node that dispatched the 24 | JS event to the target element. 25 | 26 | Examples 27 | 28 | ```html 29 | //... in your render function of a LiveView 30 | 31 | // dispatch a click 32 | 33 | 34 | // dispatch a custom event 35 | 38 |
Dispatch Target
39 | ``` 40 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/11-js-commands/push-cmd.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Push Command 6 | 7 | The `push` command sends an event to the server 8 | 9 | ```typescript 10 | new JS().push(event: string, options?: PushOptions) 11 | ``` 12 | 13 | - `event` - The name of the event to send to the server 14 | - `options` - Options for the command (optional) 15 | - `target` - An optional selector or component ID to push to 16 | - `loading` - An optional selector to apply the phx loading classes to 17 | - `page_loading` - An optional boolean indicating whether to trigger the "phx:page-loading-start" and 18 | "phx:page-loading-stop" events. Defaults to `false` 19 | - `value` An optional map of key/value pairs to include in the event's `value` property 20 | 21 | Examples 22 | 23 | ```html 24 | //... in your render function of a LiveView 25 | 26 | // push increment event to server 27 | 28 | 29 | // push decrement event to server 30 | 31 | 32 | // push increment event to server with a payload then hide the button 33 | 36 | 37 | // hide the button then push increment event 38 | 39 | 40 | // push incremenet and show page loading indicator 41 | 44 | ``` 45 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/11-js-commands/transition-cmd.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Transition Command 6 | 7 | The `transition` command dispatches a DOM event from the target element 8 | 9 | ```typescript 10 | new JS().transition(transition: Transition, options?: TransitionOptions) 11 | ``` 12 | 13 | - `transition` - The string of classes to apply to the element, or a 3-tuple containing the transition class, the class 14 | to apply to start the transition, and the class to apply to end the transition. e.g., ["ease-out duration-300", 15 | "opacity-100", "opacity-0"] 16 | - `options` - Options for the command (optional) 17 | - `to` - An optional css selector to identify the element from which to transition. Defaults to the element that the 18 | JS Command is attached to. 19 | - `time` - The time (in milliseconds) over which to apply the transition options. Defaults to 200 20 | 21 | Examples 22 | 23 | ```html 24 | //... in your render function of a LiveView 25 | 26 | // transition the target element 27 | 35 | 43 |
Transition Target
44 | 45 | // transition button with a shake 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/12-webserver-integration/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Webserver Integrations", 3 | "position": 12, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "How LiveViewJS integrates with webservers and how to add support for other webservers" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/12-webserver-integration/support-webserver-x.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Support Webserver "X" 6 | 7 | If you want to use LiveViewJS with a webserver that is not supported out of the box, you can implement the 8 | `LiveViewServerAdaptor` interface and plug it into your webserver. 9 | 10 | Essentially, you'll need to be able to intercept HTTP and websocket requests and pass them to the LiveViewJS library. 11 | The LiveViewJS library will then handle the requests and return the appropriate responses. 12 | 13 | ## Look at the existing integrations 14 | 15 | Checkout the LiveViewJS source code and look at the 16 | [`NodeExpressLiveViewServer`](https://github.com/floodfx/liveviewjs/blob/main/packages/express/src/node/server.ts) and 17 | [`DenoOakLiveViewServer`](https://github.com/floodfx/liveviewjs/blob/main/packages/deno/src/deno/server.ts) classes. 18 | These are the two webserver integrations that are supported out of the box. 19 | 20 | ## Open an issue 21 | 22 | We are happy to help you get LiveViewJS working with your webserver. If you 23 | [open an issue](https://github.com/floodfx/liveviewjs/issues) on the LiveViewJS GitHub repo, we'll be happy to support 24 | you. 25 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/13-misc/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Miscellaneous", 3 | "position": 13, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "A potpourri of topics that don't fit elsewhere or are not large enough to warrant their own category" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/13-misc/debugging-wire.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Debugging LiveViews 6 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/13-misc/flash.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Flash Messages 6 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/docs/13-misc/root-and-page-renderers.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Root and Page Renderers 6 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liveviewjs-com", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc", 16 | "format": "prettier --write '**/*.{ts,js,json,html,css,md}'" 17 | }, 18 | "dependencies": { 19 | "@docusaurus/core": "2.0.1", 20 | "@docusaurus/plugin-google-gtag": "^2.1.0", 21 | "@docusaurus/preset-classic": "2.0.1", 22 | "@mdx-js/react": "^1.6.22", 23 | "clsx": "^1.2.1", 24 | "prism-react-renderer": "^1.3.5", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "2.0.1", 30 | "@tsconfig/docusaurus": "^1.0.5", 31 | "autoprefixer": "^10.4.8", 32 | "postcss": "^8.4.16", 33 | "prettier": "^2.5.1", 34 | "tailwindcss": "^3.1.8", 35 | "typescript": "^4.7.4" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.5%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "engines": { 50 | "node": ">=16.14" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{ type: "autogenerated", dirName: "." }], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

LiveViewJS

16 |

{siteConfig.tagline}

17 |
18 | 21 | Quick Start → 22 | 23 |
24 |
25 | 26 |
27 | ); 28 | } 29 | 30 | export default function Home(): JSX.Element { 31 | const {siteConfig} = useDocusaurusContext(); 32 | return ( 33 | 36 | 37 |
38 | 39 |
40 |
41 | 44 | Learn More → 45 | 46 |
47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/.nojekyll -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/CNAME: -------------------------------------------------------------------------------- 1 | www.liveviewjs.com 2 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/fonts/LibreFranklin-Italic-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/fonts/LibreFranklin-Italic-VariableFont_wght.ttf -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/fonts/LibreFranklin-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/fonts/LibreFranklin-VariableFont_wght.ttf -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/favicon.ico -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/features/simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_counter_liveview_rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_counter_liveview_rec.gif -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_examples_rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_examples_rec.gif -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_liveview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_liveview.png -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_liveview_deno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_liveview_deno.png -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_toggle_liveview_deno_rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_toggle_liveview_deno_rec.gif -------------------------------------------------------------------------------- /apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_toggle_liveview_rec.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/apps/liveviewjs.com/static/img/screenshots/liveviewjs_hello_toggle_liveview_rec.gif -------------------------------------------------------------------------------- /apps/liveviewjs.com/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | brand: { 8 | light: "rgba(12, 175, 186, 0.2);", 9 | DEFAULT: "rgba(12, 175, 186, 1)", 10 | }, 11 | }, 12 | fontFamily: { 13 | brand: ["LibreFranklin", "sans-serif"], 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /apps/liveviewjs.com/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /liveviewjs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/liveviewjs-logo.png -------------------------------------------------------------------------------- /old_docs/routing.md: -------------------------------------------------------------------------------- 1 | ## Routing 2 | 3 | LiveViewJS supports routing to both `LiveViewComponent` and traditional web route handlers. 4 | 5 | As noted in the examples, the `LiveViewRouter` is a simple object that maps paths to `LiveViewComponent`s. 6 | ```ts 7 | // import package 8 | import {LiveViewServer} from "liveviewjs"; 9 | 10 | // create new LiveViewServer 11 | const lvServer = new LiveViewServer(); 12 | 13 | // define your routes by mapping paths to LiveViewComponents 14 | const lvRouter: LiveViewRouter = { 15 | "/light": new LightLiveViewComponent(); 16 | } 17 | // AND then passing the router to the server 18 | lvServer.registerLiveViewRoutes(lvRouter); 19 | 20 | // OR register your route with the server directly 21 | // lvServer.registerLiveViewRoute("/light", new LightLiveViewComponent()); 22 | ``` 23 | 24 | HTTP GET requests to `/light` route will be intercepted by the `LiveViewServer` and routed to the `LightLiveViewComponent`. 25 | 26 | If you need to handle non-LiveView requests (for example, login routes, you can get the `expressApp` from the `LiveViewServer` and add route handlers to it in the same way you would with a traditional express app. 27 | 28 | E.g. to register the `/foo` route: 29 | ```ts 30 | lvServer.expressApp.get("/foo", (req, res) => { 31 | // my business logic 32 | res.send("Foo!") 33 | }) 34 | ``` 35 | 36 | Then when your `LiveViewServer` is started, it will handle both LiveView and non-LiveView requests. 37 | 38 | ```ts 39 | // start server 40 | lvServer.start(); 41 | ``` 42 | 43 | See `src/examples/index.ts` for a working example. 44 | -------------------------------------------------------------------------------- /old_docs/temp-assign.md: -------------------------------------------------------------------------------- 1 | ## Temporary Assign 2 | 3 | A feature of `LiveView` development is that data stored on the socket (via `socket.assign`) is stored in memory and made available on subsequent user events. In other words, `LiveView` state is kept in the socket. This makes sense for small pieces of data that change frequently (like a counter, or form input) but for other types of data, generally large, infrequently updated data, storing that data in memory is just a waste of resources. 4 | 5 | To solve this problem, LiveViewJS provides a `tempAssign` function that allows you to mark parts of the `Context` as temporary and define what value to reset the property to after each `render`. temporarily store data in the socket. 6 | 7 | Let's say you have a list of employees for a company that you query from the DB in your `mount` function. Likely, most of this data will not change. You can use `assign` to put this data into the socket and then use `tempAssign` to define what value to reset the property to after each `render`. 8 | 9 | ```ts 10 | mount(params: LiveViewMountParams, session: Partial, socket: LiveViewSocket) { 11 | socket.assign({ 12 | // assign the list of employees to the socket 13 | employees: listEmployees() 14 | }); 15 | 16 | // reset employees to empty array after each render 17 | socket.tempAssign({ employees: [] }); 18 | } 19 | ``` 20 | 21 | For your reference: [Phoenix's Temporary Assigns docs](https://hexdocs.pm/phoenix_live_view/dom-patching.html#temporary-assigns). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liveviewjs-monorepo", 3 | "version": "0.4.3", 4 | "description": "LiveViewJS brings the power of LiveView to Typescript and Javascript developers and applications.", 5 | "workspaces": [ 6 | "docs", 7 | "packages/core", 8 | "packages/deno", 9 | "packages/examples", 10 | "packages/express", 11 | "packages/gen", 12 | "apps/liveviewjs.com", 13 | "apps/lambda-examples" 14 | ], 15 | "keywords": [ 16 | "liveviewjs", 17 | "liveview", 18 | "phoenix", 19 | "typescript", 20 | "javascript" 21 | ], 22 | "author": "Donnie Flood ", 23 | "license": "MIT", 24 | "scripts": { 25 | "format": "turbo run format", 26 | "test": "turbo run test", 27 | "dist": "turbo run dist", 28 | "publish": "turbo run publish", 29 | "cs": "changeset", 30 | "cs:version": "changeset version", 31 | "cs:publish": "changeset publish" 32 | }, 33 | "devDependencies": { 34 | "@changesets/cli": "^2.23.1", 35 | "@types/node": "^17.0.23", 36 | "prettier": "^2.5.1", 37 | "ts-node": "^10.7.0", 38 | "turbo": "^1.4.3", 39 | "typescript": "^4.9.5" 40 | }, 41 | "engines": { 42 | "npm": ">=8.5.0", 43 | "node": ">=17.8.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | .parcel-cache/ 5 | .DS_Store 6 | coverage/lcov-report/ 7 | coverage/lcov.info 8 | diffs/ 9 | build/ 10 | *.tgz -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | .parcel-cache/ 2 | .vscode/ 3 | coverage/ 4 | node_modules/ 5 | .editorconfig 6 | .eslintrc.js 7 | .gitignore 8 | .gitattributes 9 | .npmignore 10 | jest.config.js 11 | package-lock.json 12 | tsconfig.json -------------------------------------------------------------------------------- /packages/core/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /packages/core/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/core/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: "ts-jest", 4 | testEnvironment: "node", 5 | collectCoverage: true, 6 | coverageDirectory: "coverage", 7 | testPathIgnorePatterns: ["/node_modules/", "/dist/"], 8 | coveragePathIgnorePatterns: ["/node_modules/", "/dist/"], 9 | roots: ["/src"], 10 | verbose: false, 11 | coverageThreshold: { 12 | global: { 13 | branches: 75, 14 | functions: 75, 15 | lines: 80, 16 | statements: 80, 17 | }, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/liveview.mjs"; 2 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | // import typescript from "rollup-plugin-typescript2"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import dts from "rollup-plugin-dts"; 4 | import resolve from "@rollup/plugin-node-resolve"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | 7 | // packages that will be loaded externally 8 | const external = ["zod", "nanoid", "path-to-regexp"]; 9 | 10 | export default [ 11 | { 12 | external, 13 | input: "./src/index.ts", 14 | output: { 15 | file: "./build/liveview.js", 16 | format: "cjs", 17 | }, 18 | plugins: [ 19 | resolve({ 20 | preferBuiltins: true, 21 | }), 22 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./rollup", declaration: true }), 23 | commonjs(), 24 | ], 25 | }, 26 | { 27 | external, 28 | input: "./src/index.ts", 29 | output: { 30 | file: "./build/liveview.mjs", 31 | format: "esm", 32 | }, 33 | plugins: [ 34 | { 35 | banner() { 36 | // add typescript types to the javascript bundle 37 | return '/// '; 38 | }, 39 | }, 40 | resolve({ 41 | preferBuiltins: true, 42 | }), 43 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./rollup", declaration: true }), 44 | commonjs(), 45 | ], 46 | }, 47 | { 48 | external, 49 | input: "./build/rollup/server/index.d.ts", 50 | output: { 51 | file: "./build/liveview.d.ts", 52 | format: "esm", 53 | }, 54 | plugins: [dts()], 55 | }, 56 | ]; 57 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./server"; 2 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/csrfGen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type that defines a function that returns a string value used for protecting requests against 3 | * Cross-site Request Forgery (CSRF) attacks. Good concrete implementations are: crypto.randomBytes, uuidv4. 4 | */ 5 | export type CsrfGenerator = () => string; 6 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/files.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstracts some simple file system operations. Necessary to support 3 | * both nodejs and deno since those APIs differ. 4 | */ 5 | export interface FileSystemAdaptor { 6 | /** 7 | * Get a temporary file path from the OS with the lastPathPart as the file name. 8 | */ 9 | tempPath: (lastPathPart: string) => string; 10 | /** 11 | * Writes the data to the given destination path. 12 | */ 13 | writeTempFile: (dest: string, data: Buffer) => void; 14 | /** 15 | * Creates and/or appends data from src to dest 16 | */ 17 | createOrAppendFile: (dest: string, src: string) => void; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/flash.ts: -------------------------------------------------------------------------------- 1 | import { SessionData } from "../session"; 2 | 3 | /** 4 | * Adatpor that implements adding flash to the session data and removing flash from the session data. 5 | */ 6 | export interface FlashAdaptor { 7 | peekFlash(session: SessionData, key: string): Promise; 8 | popFlash(session: SessionData, key: string): Promise; 9 | putFlash(session: SessionData, key: string, value: string): Promise; 10 | clearFlash(session: SessionData, key: string): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/idGen.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type that defines a function that returns a string ID used to identify a unique http request 3 | * and/or websocket connection. Should generate unique IDs for each request and connection. Good 4 | * concrete implementations are: nanoid, shortid, uuidv4 (though these are long). 5 | */ 6 | export type IdGenerator = () => string; 7 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./csrfGen"; 2 | export * from "./files"; 3 | export * from "./flash"; 4 | export * from "./http"; 5 | export * from "./idGen"; 6 | export * from "./serDe"; 7 | export * from "./sessionFlashAdaptor"; 8 | export * from "./websocket"; 9 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/jsonSerDe.ts: -------------------------------------------------------------------------------- 1 | import { SerDe } from "./serDe"; 2 | 3 | /** 4 | * A SerDe (serializer/deserializer) that uses JSON.stringify and JSON.parse. 5 | * WARNING: this is not secure so should only be used for testing. 6 | */ 7 | export class JsonSerDe implements SerDe { 8 | serialize(obj: T) { 9 | return Promise.resolve(JSON.stringify(obj)); 10 | } 11 | 12 | deserialize(data: string) { 13 | return Promise.resolve(JSON.parse(data) as T); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/serDe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A class that knows how to serialize (Ser) and deserialize (De) session data. This is used to pass 3 | * session data from the initial http request to the websocket connection. You should use a strategy that 4 | * cannot be tampered with such as signed JWT tokens or other cryptographically safe serialization/deserializations. 5 | */ 6 | export interface SerDe { 7 | serialize(data: T): Promise; 8 | deserialize(data: F): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/sessionFlashAdaptor.ts: -------------------------------------------------------------------------------- 1 | import { SessionData } from "../session"; 2 | import { FlashAdaptor } from "./flash"; 3 | 4 | /** 5 | * Naive implementation of flash adaptor that uses "__flash" property on session data 6 | * to implement flash. 7 | */ 8 | export class SessionFlashAdaptor implements FlashAdaptor { 9 | peekFlash(session: SessionData, key: string): Promise { 10 | if (!session.__flash) { 11 | // istanbul ignore next 12 | session.__flash = {}; 13 | } 14 | return Promise.resolve(session.__flash[key]); 15 | } 16 | 17 | popFlash(session: SessionData, key: string): Promise { 18 | // istanbul ignore next 19 | if (session.__flash === undefined) { 20 | // istanbul ignore next 21 | session.__flash = {}; 22 | } 23 | const value = session.__flash[key]; 24 | delete session.__flash[key]; 25 | return Promise.resolve(value); 26 | } 27 | 28 | putFlash(session: SessionData, key: string, value: string): Promise { 29 | if (!session.__flash) { 30 | // istanbul ignore next 31 | session.__flash = {}; 32 | } 33 | session.__flash[key] = value; 34 | return Promise.resolve(); 35 | } 36 | 37 | clearFlash(session: SessionData, key: string): Promise { 38 | // istanbul ignore next 39 | if (session.__flash === undefined) { 40 | // istanbul ignore next 41 | session.__flash = {}; 42 | } 43 | delete session.__flash[key]; 44 | return Promise.resolve(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/testFilesAdatptor.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import os from "os"; 3 | import path from "path"; 4 | import { FileSystemAdaptor } from "./files"; 5 | 6 | /** 7 | * A nodejs file system adaptor for testing. 8 | */ 9 | // istanbul ignore next 10 | export class TestNodeFileSystemAdatptor implements FileSystemAdaptor { 11 | tempPath(lastPathPart: string): string { 12 | return path.join(os.tmpdir(), lastPathPart); 13 | } 14 | writeTempFile(dest: string, data: Buffer) { 15 | fs.writeFileSync(dest, data); 16 | } 17 | createOrAppendFile(dest: string, src: string) { 18 | fs.appendFileSync(dest, fs.readFileSync(src)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/server/adaptor/websocket.ts: -------------------------------------------------------------------------------- 1 | export type WsMsgListener = (data: Buffer, isBinary: boolean) => void; 2 | export type WsCloseListener = () => void; 3 | /** 4 | * Adaptor that enables sending websocket messages over a concrete websocket implementation. 5 | */ 6 | export interface WsAdaptor { 7 | send(message: string, errorHandler?: (err: any) => void): void; 8 | subscribeToMessages(msgListener: WsMsgListener): Promise | void; 9 | subscribeToClose(closeListener: WsCloseListener): Promise | void; 10 | isClosed(): boolean; 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/server/changeset/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./changeset"; 2 | -------------------------------------------------------------------------------- /packages/core/src/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./adaptor"; 2 | export * from "./changeset"; 3 | export * from "./live"; 4 | export * from "./mime"; 5 | export * from "./protocol/phx"; 6 | export * from "./pubsub"; 7 | export * from "./server"; 8 | export * from "./session"; 9 | export * from "./socket"; 10 | export * from "./templates"; 11 | export * from "./upload"; 12 | -------------------------------------------------------------------------------- /packages/core/src/server/live/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./liveComponent"; 2 | export * from "./liveView"; 3 | export * from "./router"; 4 | export * from "./types"; 5 | -------------------------------------------------------------------------------- /packages/core/src/server/live/router.test.ts: -------------------------------------------------------------------------------- 1 | import { html } from "../templates"; 2 | import { createLiveView } from "./liveView"; 3 | import { LiveViewRouter, matchRoute } from "./router"; 4 | 5 | describe("test matchRoute", () => { 6 | const router: LiveViewRouter = { 7 | "/foo": createLiveView({ render: () => html`` }), 8 | "/bar/:name": createLiveView({ render: () => html`` }), 9 | "/blah/:id": createLiveView({ render: () => html`` }), 10 | "/zee/(\\d+)": createLiveView({ render: () => html`` }), 11 | }; 12 | it("test plain route", () => { 13 | let m = matchRoute(router, "/foo"); 14 | expect(m).toBeDefined(); 15 | let [lv, mr] = m!; 16 | expect(lv).toBeDefined(); 17 | expect(mr).toBeDefined(); 18 | expect(Object.keys(mr!.params).length).toEqual(0); 19 | }); 20 | 21 | it("test param route", () => { 22 | let m = matchRoute(router, "/bar/bob"); 23 | expect(m).toBeDefined(); 24 | let [lv, mr] = m!; 25 | expect(lv).toBeDefined(); 26 | expect(mr).toBeDefined(); 27 | expect(mr!.params.name).toBe("bob"); 28 | }); 29 | 30 | it("test decode route", () => { 31 | let m = matchRoute(router, "/blah/caf%C3%A9"); 32 | expect(m).toBeDefined(); 33 | let [lv, mr] = m!; 34 | expect(lv).toBeDefined(); 35 | expect(mr).toBeDefined(); 36 | expect(mr!.params.id).toBe("café"); 37 | }); 38 | 39 | it("test indexed route", () => { 40 | let m = matchRoute(router, "/zee/123"); 41 | expect(m).toBeDefined(); 42 | let [lv, mr] = m!; 43 | expect(lv).toBeDefined(); 44 | expect(mr).toBeDefined(); 45 | expect(mr!.params[0]).toBe("123"); 46 | }); 47 | 48 | it("test no match", () => { 49 | let m = matchRoute(router, "/baz"); 50 | expect(m).toBeUndefined(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/core/src/server/live/types.ts: -------------------------------------------------------------------------------- 1 | import { HtmlSafeString } from "../templates"; 2 | 3 | export interface LiveViewTemplate extends HtmlSafeString {} 4 | -------------------------------------------------------------------------------- /packages/core/src/server/mime/mime.test.ts: -------------------------------------------------------------------------------- 1 | import { mime, nodeHttpFetch } from "."; 2 | 3 | describe("test mime", () => { 4 | beforeAll(async () => { 5 | await mime.load(); 6 | }); 7 | it("lookupMime by ext", async () => { 8 | expect(mime.loaded).toBeTruthy(); 9 | expect(mime.lookupMimeType("pdf")).toContain("application/pdf"); 10 | }); 11 | 12 | it("lookupExt by mime", async () => { 13 | expect(mime.loaded).toBeTruthy(); 14 | expect(mime.lookupExtensions("application/pdf")).toContain("pdf"); 15 | }); 16 | 17 | it("http requests success", async () => { 18 | const res = await nodeHttpFetch("https://cdn.jsdelivr.net/gh/jshttp/mime-db@master/db.json"); 19 | expect(res).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/src/server/protocol/phx.test.ts: -------------------------------------------------------------------------------- 1 | import { Phx } from "./phx"; 2 | 3 | describe("phx", () => { 4 | it("encodes and decodes a message", () => { 5 | const msg: Phx.Msg = ["join", "msg", "topic", "event", {}]; 6 | const encoded = Phx.serialize(msg); 7 | const decoded = Phx.parse(encoded); 8 | expect(decoded).toEqual(msg); 9 | }); 10 | 11 | it("", () => {}); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/core/src/server/pubsub/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pubSub"; 2 | export * from "./singleProcessPubSub"; 3 | -------------------------------------------------------------------------------- /packages/core/src/server/pubsub/pubSub.ts: -------------------------------------------------------------------------------- 1 | export type SubscriberFunction = (data: T) => void; 2 | 3 | export type SubscriberId = string; 4 | 5 | /** 6 | * A Subscriber allows you to subscribe and unsubscribe to a PubSub topic providing a callback function. 7 | */ 8 | export interface Subscriber { 9 | subscribe(topic: string, subscriber: SubscriberFunction): Promise; 10 | unsubscribe(topic: string, subscriberId: SubscriberId): Promise; 11 | } 12 | 13 | /** 14 | * A Publisher allows you to publish data to a PubSub topic. 15 | */ 16 | export interface Publisher { 17 | broadcast(topic: string, data: T): Promise; 18 | } 19 | 20 | /** 21 | * A PubSub implements both a Publisher and a Subscriber. 22 | */ 23 | export interface PubSub extends Subscriber, Publisher {} 24 | -------------------------------------------------------------------------------- /packages/core/src/server/pubsub/singleProcessPubSub.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import EventEmitter from "events"; 3 | import { Publisher, Subscriber, SubscriberFunction } from "./pubSub"; 4 | 5 | /** 6 | * A PubSub implementation that uses the Node.js EventEmitter as a backend. 7 | * 8 | * Should only be used in single process environments like local development 9 | * or a single instance. In a multi-process environment, use RedisPubSub. 10 | */ 11 | const eventEmitter = new EventEmitter(); // use this singleton for all pubSub events 12 | 13 | export class SingleProcessPubSub implements Subscriber, Publisher { 14 | private subscribers: Record> = {}; 15 | 16 | public async subscribe(topic: string, subscriber: SubscriberFunction): Promise { 17 | await eventEmitter.addListener(topic, subscriber); 18 | // store connection id for unsubscribe and return for caller 19 | const subId = crypto.randomBytes(10).toString("hex"); 20 | this.subscribers[subId] = subscriber; 21 | return subId; 22 | } 23 | 24 | public async broadcast(topic: string, data: T) { 25 | await eventEmitter.emit(topic, data); 26 | } 27 | 28 | public async unsubscribe(topic: string, subscriberId: string) { 29 | try { 30 | // get subscriber function from id 31 | const subscriber = this.subscribers[subscriberId]; 32 | 33 | if (subscriber) { 34 | await eventEmitter.removeListener(topic, subscriber); 35 | } 36 | // remove subscriber from subscribers 37 | delete this.subscribers[subscriberId]; 38 | } catch (err) { 39 | console.warn("error unsubscribing from topic", topic, err); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/server/server.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for LiveViewServerAdaptors to implement for a given runtime and web server. 3 | * e.g. NodeExpressServerAdaptor or DenoOakServerAdaptor 4 | */ 5 | export interface LiveViewServerAdaptor { 6 | httpMiddleware: THttpMiddleware; 7 | wsMiddleware: TWsMiddleware; 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/server/session.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generically represent session with `string` keys to `any` value and a named member called 3 | * `_csrf_token` that is used to protect against cross-site request forgery attacks. 4 | */ 5 | export type SessionData = { 6 | /** 7 | * The CSRF token used to protect against cross-site request forgery attacks. 8 | */ 9 | _csrf_token?: string; 10 | [key: string]: any; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/server/socket/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./liveSocket"; 2 | export * from "./liveViewManager"; 3 | export * from "./ws/wsHandler"; 4 | export * from "./wsMessageRouter"; 5 | -------------------------------------------------------------------------------- /packages/core/src/server/socket/structuredClone.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if globalThis has a `structuredClone` function and if not, adds one 3 | * that uses `JSON.parse(JSON.stringify())` as a fallback. This is needed 4 | * for Node version <17. 5 | */ 6 | export function maybeAddStructuredClone() { 7 | /** 8 | * Really bad implementation of structured clone algorithm to backfill for 9 | * Node 16 (and below). 10 | */ 11 | if (globalThis && !globalThis.structuredClone) { 12 | globalThis.structuredClone = ( 13 | value: T, 14 | transfer?: 15 | | { 16 | transfer: readonly any[]; 17 | } 18 | | undefined 19 | ): T => JSON.parse(JSON.stringify(value)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/server/socket/util.test.ts: -------------------------------------------------------------------------------- 1 | import { PhxClickPayload, PhxIncomingMessage, PhxProtocol } from "./types"; 2 | import { newHeartbeatReply, newPhxReply } from "./util"; 3 | 4 | describe("test utils", () => { 5 | it("valid heartbeat", () => { 6 | const hbReply = newHeartbeatReply([null, "1", "phoenix", "heartbeat", {}]); 7 | expect(hbReply).toEqual([ 8 | null, 9 | "1", 10 | "phoenix", 11 | "phx_reply", 12 | { 13 | response: {}, 14 | status: "ok", 15 | }, 16 | ]); 17 | }); 18 | 19 | it("valid phxReply", () => { 20 | const incoming: PhxIncomingMessage = [ 21 | "1", 22 | "2", 23 | "topic", 24 | "event", 25 | { type: "click", event: "down", value: { value: "string" } }, 26 | ]; 27 | 28 | const replyPayload = { 29 | response: {}, 30 | status: "ok", 31 | }; 32 | const reply = newPhxReply(incoming, replyPayload); 33 | 34 | expect(reply).toEqual([ 35 | incoming[PhxProtocol.joinRef], 36 | incoming[PhxProtocol.messageRef], 37 | incoming[PhxProtocol.topic], 38 | "phx_reply", 39 | replyPayload, 40 | ]); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/core/src/server/socket/util.ts: -------------------------------------------------------------------------------- 1 | import { PhxIncomingMessage, PhxProtocol, PhxReply } from "./types"; 2 | 3 | export const newPhxReply = (from: PhxIncomingMessage, payload: any): PhxReply => { 4 | const joinRef = from[PhxProtocol.joinRef]; 5 | const messageRef = from[PhxProtocol.messageRef]; 6 | const topic = from[PhxProtocol.topic]; 7 | return [joinRef, messageRef, topic, "phx_reply", payload]; 8 | }; 9 | 10 | export const newHeartbeatReply = (incoming: PhxIncomingMessage<{}>): PhxReply => { 11 | return [ 12 | null, 13 | incoming[PhxProtocol.messageRef], 14 | "phoenix", 15 | "phx_reply", 16 | { 17 | response: {}, 18 | status: "ok", 19 | }, 20 | ]; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/form_for.ts: -------------------------------------------------------------------------------- 1 | import { html, safe } from "../htmlSafeString"; 2 | 3 | interface FormForOptions { 4 | phx_submit?: string; 5 | phx_change?: string; 6 | method?: "get" | "post"; 7 | id?: string; 8 | } 9 | 10 | export const form_for = (action: string, csrfToken: string, options?: FormForOptions) => { 11 | const method = options?.method ?? "post"; 12 | const phx_submit = options?.phx_submit ? safe(` phx-submit="${options.phx_submit}"`) : ""; 13 | const phx_change = options?.phx_change ? safe(` phx-change="${options.phx_change}"`) : ""; 14 | const id = options?.id ? safe(` id="${options.id}"`) : ""; 15 | // prettier-ignore 16 | return html` 17 | 18 | 19 | `; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./form_for"; 2 | export * from "./inputs"; 3 | export * from "./live_file_input"; 4 | export * from "./live_img_preview"; 5 | export * from "./live_patch"; 6 | export * from "./live_title"; 7 | export * from "./live_title_tag"; 8 | export * from "./options_for_select"; 9 | export * from "./submit"; 10 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_file_input.ts: -------------------------------------------------------------------------------- 1 | import { UploadConfig } from "../../upload/uploadConfig"; 2 | import { html, HtmlSafeString } from "../htmlSafeString"; 3 | 4 | /** 5 | * Creates the html for a file input that can be used to upload files to the server. 6 | * @param uploadConfig the upload config to use for the file input 7 | * @returns the html for the file input 8 | */ 9 | export function live_file_input(uploadConfig: UploadConfig): HtmlSafeString { 10 | const { name, accept, max_entries: maxEntries, ref, entries } = uploadConfig; 11 | const multiple = maxEntries > 1 ? "multiple" : ""; 12 | const activeRefs = entries.map((entry) => entry.ref).join(","); 13 | const doneRefs = entries 14 | .filter((entry) => entry?.done ?? false) 15 | .map((entry) => entry.ref) 16 | .join(","); 17 | const preflightedRefs = entries 18 | .filter((entry) => entry?.preflighted ?? false) 19 | .map((entry) => entry.ref) 20 | .join(","); 21 | return html` 22 | 34 | `; 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_img_preview.test.ts: -------------------------------------------------------------------------------- 1 | import { UploadConfig, UploadEntry } from "../../upload"; 2 | import { live_img_preview } from "./live_img_preview"; 3 | 4 | describe("live_img_preview test", () => { 5 | it("live_img_preview", () => { 6 | const uc = new UploadConfig("foo", { 7 | accept: [".png"], 8 | max_entries: 2, 9 | }); 10 | // override the ref so always the same for testing 11 | uc.ref = "phx-testid"; 12 | const entry = new UploadEntry( 13 | { name: "entry0", last_modified: 11111111, path: "somepath", ref: "0", size: 1000, type: "application/pdf" }, 14 | uc 15 | ); 16 | 17 | const l = live_img_preview(entry); 18 | expect(l.toString()).toMatchInlineSnapshot(` 19 | " 20 | 26 | " 27 | `); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_img_preview.ts: -------------------------------------------------------------------------------- 1 | import { UploadEntry } from "../../upload"; 2 | import { html, HtmlSafeString } from "../htmlSafeString"; 3 | 4 | export function live_img_preview(entry: UploadEntry): HtmlSafeString { 5 | const { ref, upload_ref } = entry; 6 | return html` 7 | 13 | `; 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_patch.test.ts: -------------------------------------------------------------------------------- 1 | import { live_patch } from "./live_patch"; 2 | 3 | describe("livepatch helper", () => { 4 | it("returns livepatch anchor", () => { 5 | const result = live_patch("Go to bar", { 6 | to: { 7 | path: "/bar", 8 | params: { a: "b" }, 9 | }, 10 | }).toString(); 11 | expect(result).toBe(`Go to bar`); 12 | }); 13 | 14 | it("returns livepatch anchor with no params", () => { 15 | const result = live_patch("Go to bar", { 16 | to: { 17 | path: "/bar", 18 | params: {}, 19 | }, 20 | }).toString(); 21 | expect(result).toBe(`Go to bar`); 22 | }); 23 | 24 | it("returns livepatch anchor with custom class", () => { 25 | const result = live_patch("Go to bar", { 26 | to: { 27 | path: "/bar", 28 | params: { a: "b" }, 29 | }, 30 | className: "custom-class", 31 | }).toString(); 32 | expect(result).toBe( 33 | `Go to bar` 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_patch.ts: -------------------------------------------------------------------------------- 1 | import { html, HtmlSafeString, safe } from "../htmlSafeString"; 2 | 3 | interface LiveViewPatchHelperOptions { 4 | to: { 5 | path: string; 6 | params?: Record; 7 | }; 8 | className?: string; 9 | } 10 | 11 | function buildHref(options: LiveViewPatchHelperOptions) { 12 | const { path, params } = options.to; 13 | const urlParams = new URLSearchParams(params); 14 | if (urlParams.toString().length > 0) { 15 | return `${path}?${urlParams.toString()}`; 16 | } else { 17 | return path; 18 | } 19 | } 20 | 21 | export const live_patch = ( 22 | anchorBody: HtmlSafeString | string, 23 | options: LiveViewPatchHelperOptions 24 | ): HtmlSafeString => { 25 | // prettier-ignore 26 | return html`${anchorBody}`; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_title.ts: -------------------------------------------------------------------------------- 1 | export interface LiveTitleOptions { 2 | prefix?: string; 3 | suffix?: string; 4 | title: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_title_tag.test.ts: -------------------------------------------------------------------------------- 1 | import { live_title_tag } from "./live_title_tag"; 2 | 3 | describe("live title tag helper", () => { 4 | it("returns live title tag", () => { 5 | const result = live_title_tag({ title: "title" }).toString(); 6 | expect(result).toBe(`title`); 7 | }); 8 | 9 | it("returns live title tag with prefix", () => { 10 | const result = live_title_tag({ title: "title", prefix: "prefix " }).toString(); 11 | expect(result).toBe(`prefix title`); 12 | }); 13 | 14 | it("returns live title tag with prefix and suffix", () => { 15 | const result = live_title_tag({ title: "title", prefix: "prefix ", suffix: " suffix" }).toString(); 16 | expect(result).toBe(`prefix title suffix`); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/live_title_tag.ts: -------------------------------------------------------------------------------- 1 | import { html, HtmlSafeString, safe } from "../htmlSafeString"; 2 | import { LiveTitleOptions } from "./live_title"; 3 | 4 | export const live_title_tag = (options: LiveTitleOptions): HtmlSafeString => { 5 | const { title, prefix, suffix } = options; 6 | const prefix_data = prefix ? safe(` data-prefix="${prefix}"`) : ""; 7 | const suffix_data = suffix ? safe(` data-suffix="${suffix}"`) : ""; 8 | return html`${prefix ?? ""}${title}${suffix ?? ""}`; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/options_for_select.test.ts: -------------------------------------------------------------------------------- 1 | import { options_for_select } from "."; 2 | 3 | describe("options for select", () => { 4 | it("returns array options with no selected", () => { 5 | const options = ["a", "b", "c"]; 6 | const result = options_for_select(options).toString(); 7 | expect(result).toBe(``); 8 | }); 9 | it("returns array options with selected", () => { 10 | const options = ["a", "b", "c"]; 11 | const result = options_for_select(options, "a").toString(); 12 | expect(result).toBe( 13 | `` 14 | ); 15 | }); 16 | 17 | it("returns record options with no selected", () => { 18 | const options = { a: "A", b: "B", c: "C" }; 19 | const result = options_for_select(options).toString(); 20 | expect(result).toBe(``); 21 | }); 22 | 23 | it("returns record options with selected", () => { 24 | const options = { a: "A", b: "B", c: "C" }; 25 | const result = options_for_select(options, "A").toString(); 26 | expect(result).toBe( 27 | `` 28 | ); 29 | }); 30 | 31 | it("returns record options with selected array", () => { 32 | const options = { a: "A", b: "B", c: "C" }; 33 | const result = options_for_select(options, ["A", "B"]).toString(); 34 | expect(result).toBe( 35 | `` 36 | ); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/submit.test.ts: -------------------------------------------------------------------------------- 1 | import { submit } from "./submit"; 2 | 3 | describe("submit helper", () => { 4 | it("returns expected submit button html", () => { 5 | const result = submit("submit"); 6 | expect(result.toString()).toMatchInlineSnapshot(`""`); 7 | }); 8 | 9 | it("returns expected submit button with phx-disable-with", () => { 10 | const result = submit("submit", { phx_disable_with: "Saving..." }); 11 | expect(result.toString()).toMatchInlineSnapshot( 12 | `""` 13 | ); 14 | }); 15 | 16 | it("returns with disabled", () => { 17 | const result = submit("submit", { phx_disable_with: "Saving...", disabled: true }); 18 | expect(result.toString()).toMatchInlineSnapshot( 19 | `""` 20 | ); 21 | }); 22 | 23 | it("returns with non-named attr", () => { 24 | const result = submit("submit", { phx_disable_with: "Saving...", "aria-label": "a button" }); 25 | expect(result.toString()).toMatchInlineSnapshot( 26 | `""` 27 | ); 28 | }); 29 | 30 | it("returns escaped content", () => { 31 | const result = submit("Learn More >", { phx_disable_with: "Saving...", "aria-label": "a button" }); 32 | expect(result.toString()).toMatchInlineSnapshot( 33 | `""` 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/helpers/submit.ts: -------------------------------------------------------------------------------- 1 | import { escapehtml, html, safe } from "../htmlSafeString"; 2 | 3 | interface SubmitOptions { 4 | phx_disable_with?: string; 5 | disabled?: boolean; 6 | [key: string]: string | number | boolean | undefined; 7 | } 8 | 9 | export const submit = (label: string, options?: SubmitOptions) => { 10 | const attrs = Object.entries(options || {}).reduce((acc, [key, value]) => { 11 | if (key === "disabled") { 12 | acc += value ? safe(` disabled`) : ""; 13 | } else if (key === "phx_disable_with") { 14 | acc += safe(` phx-disable-with="${escapehtml(value)}"`); 15 | } else { 16 | acc += safe(` ${key}="${escapehtml(value)}"`); 17 | } 18 | return acc; 19 | }, ""); 20 | // prettier-ignore 21 | return html``; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/server/templates/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./diff"; 2 | export * from "./helpers"; 3 | export * from "./htmlSafeString"; 4 | export * from "./jsCommands"; 5 | -------------------------------------------------------------------------------- /packages/core/src/server/test/liveviews.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView } from "../live"; 2 | import { html } from "../templates"; 3 | 4 | export namespace Test { 5 | type CounterCtx = { 6 | count: number; 7 | }; 8 | export const counterLiveView = createLiveView({ 9 | mount: (socket) => { 10 | socket.assign({ count: 0 }); 11 | }, 12 | render: (ctx) => { 13 | return html`
${ctx.count}
`; 14 | }, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/core/src/server/test/wsAdaptor.ts: -------------------------------------------------------------------------------- 1 | import { WsAdaptor, WsCloseListener, WsMsgListener } from "../adaptor"; 2 | 3 | export class TestWsAdaptor implements WsAdaptor { 4 | private sendFn: (msg: string) => void; 5 | #closeListener: WsCloseListener; 6 | #msgListener: WsMsgListener; 7 | closed: boolean = false; 8 | constructor(sendFn: (msg: string) => void) { 9 | this.sendFn = sendFn; 10 | } 11 | subscribeToClose(closeListener: WsCloseListener): void { 12 | this.#closeListener = closeListener; 13 | } 14 | send(message: string, errorHandler?: ((err: any) => void) | undefined): void { 15 | this.sendFn(message); 16 | } 17 | subscribeToMessages(msgListener: WsMsgListener): void { 18 | this.#msgListener = msgListener; 19 | } 20 | isClosed(): boolean { 21 | return this.closed; 22 | } 23 | 24 | async sendIncomingMsg(msg: string) { 25 | await this.#msgListener(Buffer.from(msg), false); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/src/server/upload/binaryUploadSerDe.test.ts: -------------------------------------------------------------------------------- 1 | import { Phx } from "../protocol/phx"; 2 | import { BinaryUploadSerDe } from "./binaryUploadSerDe"; 3 | 4 | describe("binary upload serde test", () => { 5 | it("live_img_preview", async () => { 6 | const serDe = new BinaryUploadSerDe(); 7 | 8 | const byteSize = 1000; 9 | const parts = { 10 | joinRef: "joinRef", 11 | msgRef: "msgRef", 12 | topic: "lvu:$0", 13 | event: "binary_upload", 14 | payload: Buffer.alloc(byteSize), 15 | } as Phx.UploadMsg; 16 | const data = await serDe.serialize(parts); 17 | expect(data.length).toBe( 18 | 1000 + // data length 19 | 5 + // size header 20 | parts.joinRef.length + 21 | parts.msgRef.length + 22 | parts.topic.length + 23 | parts.event.length 24 | ); 25 | 26 | const parts2 = await serDe.deserialize(data); 27 | expect(parts2.joinRef).toBe(parts.joinRef); 28 | expect(parts2.msgRef).toBe(parts.msgRef); 29 | expect(parts2.topic).toBe(parts.topic); 30 | expect(parts2.event).toBe(parts.event); 31 | expect(parts2.payload.length).toBe(byteSize); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/core/src/server/upload/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./uploadConfig"; 2 | export * from "./uploadEntry"; 3 | -------------------------------------------------------------------------------- /packages/core/src/server/upload/uploadConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { UploadConfig, UploadEntry } from "."; 2 | 3 | describe("additional uploadConfig test", () => { 4 | it("test too many files", async () => { 5 | const uc = new UploadConfig("foo", { 6 | accept: [".pdf"], 7 | max_entries: 1, 8 | }); 9 | // override the ref so always the same for testing 10 | uc.ref = "phx-testid"; 11 | const activeEntry = new UploadEntry( 12 | { name: "entry0", last_modified: 11111111, path: "somepath", ref: "0", size: 1000, type: "application/pdf" }, 13 | uc 14 | ); 15 | const doneEntry = new UploadEntry( 16 | { name: "entry1", last_modified: 11111111, path: "somepath", ref: "1", size: 1000, type: "application/pdf" }, 17 | uc 18 | ); 19 | doneEntry.updateProgress(100); 20 | uc.setEntries([activeEntry, doneEntry]); 21 | expect(uc.errors).toContain("Too many files"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/core/src/server/upload/uploadEntry.test.ts: -------------------------------------------------------------------------------- 1 | import { UploadConfig, UploadEntry } from "."; 2 | import { mime } from "../mime"; 3 | 4 | describe("uploadEntry tests", () => { 5 | beforeAll(async () => { 6 | // force mime db to load 7 | await mime.load(); 8 | }); 9 | it("test too large", async () => { 10 | const uc = new UploadConfig("foo", { 11 | accept: [".pdf"], 12 | max_entries: 1, 13 | max_file_size: 100, 14 | }); 15 | // override the ref so always the same for testing 16 | uc.ref = "phx-testid"; 17 | const activeEntry = new UploadEntry( 18 | { name: "entry0", last_modified: 11111111, path: "somepath", ref: "0", size: 101, type: "application/pdf" }, 19 | uc 20 | ); 21 | uc.setEntries([activeEntry]); 22 | expect(uc.errors).toContain("Too large"); 23 | }); 24 | 25 | it("test found mime types", async () => { 26 | const uc = new UploadConfig("foo", { 27 | accept: ["image/png"], 28 | max_entries: 1, 29 | max_file_size: 1000, 30 | }); 31 | // override the ref so always the same for testing 32 | uc.ref = "phx-testid"; 33 | const activeEntry = new UploadEntry( 34 | { name: "entry0", last_modified: 11111111, path: "somepath", ref: "0", size: 100, type: "image/png" }, 35 | uc 36 | ); 37 | uc.setEntries([activeEntry]); 38 | expect(uc.errors).toHaveLength(0); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "esnext", 15 | "lib": ["esnext"], 16 | "moduleResolution": "node", 17 | "types": ["node", "jest"], 18 | "outDir": "./build/tsc", 19 | "baseUrl": "." 20 | }, 21 | "include": ["src/index.ts", "src/server/**/*", "src/server/adaptor/.ts"], 22 | "exclude": ["build", "dist", "node_modules", "packages"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/.gitignore: -------------------------------------------------------------------------------- 1 | # make sure we don't check in local test dir 2 | my-liveviewjs-app/ 3 | dist/ -------------------------------------------------------------------------------- /packages/create-liveviewjs/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/create-liveviewjs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/create-liveviewjs/README.md: -------------------------------------------------------------------------------- 1 | ## Create LiveViewJS App 2 | 3 | Ship faster and create dynamic front-end user experiences without writing front-end code with [LiveViewJS](https://liveviewjs.com). 4 | 5 | This tool helps you to create a new LiveViewJS application. To get started, open a new shell and run the following command: 6 | 7 | ```sh 8 | $ npx create-liveviewjs-app@latest 9 | ``` 10 | 11 | Then follow the prompts... 12 | 13 | [Go here](https://liveviewjs.com) more information about LiveViewJS. -------------------------------------------------------------------------------- /packages/create-liveviewjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-liveview-app", 3 | "version": "0.0.4", 4 | "description": "Command line interface to easily create a new LiveViewJS app", 5 | "homepage": "https://liveviewjs.com", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/floodfx/liveviewjs", 10 | "directory": "packages/create-liveviewjs" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/floodfx/liveviewjs/issues" 14 | }, 15 | "main": "./dist/cli.js", 16 | "bin": { 17 | "create-liveview-app": "./dist/cli.js" 18 | }, 19 | "types": "./dist/cli.d.ts", 20 | "files": [ 21 | "dist/*.js", 22 | "dist/templates/**/*" 23 | ], 24 | "scripts": { 25 | "build": "tsc", 26 | "watch": "tsc --watch", 27 | "local": "npm run prepublish; node dist/cli.js", 28 | "copy-templates": "cp -r templates/ dist/templates", 29 | "clean": "rm -rf dist", 30 | "prepublish": "npm run clean; npm run build; npm run copy-templates" 31 | }, 32 | "keywords": [ 33 | "liveviewjs", 34 | "liveview", 35 | "phoenix", 36 | "typescript", 37 | "javascript", 38 | "framework" 39 | ], 40 | "dependencies": { 41 | "fs-extra": "^10.0.0", 42 | "inquirer": "^8.0.0", 43 | "meow": "^7.1.1", 44 | "sort-package-json": "^1.54.0" 45 | }, 46 | "devDependencies": { 47 | "@types/fs-extra": "^9.0.13", 48 | "@types/inquirer": "^7.3.1", 49 | "@types/node": "^17.0.10", 50 | "ts-jest": "^27.1.3", 51 | "ts-node": "^10.4.0", 52 | "typescript": "^4.5.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/README.md: -------------------------------------------------------------------------------- 1 | # LiveViewJS 2 | This is a [LiveViewJS](https://github.com/floodfx/liveviewjs) application! 3 | 4 | 5 | ## To Run your LiveViewJS Application 6 | - `npm i` 7 | - `npm run start` 8 | 9 | [http://localhost:8080/employees](http://localhost:8080/employees) 10 | 11 | ## Environment Configuration 12 | [dotenv](https://github.com/motdotla/dotenv) used to configure environment variables. This requires a `.env` file in the root directory of the project. 13 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | We use Tailwindcss by default. 3 | */ 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/assets/js/app.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import { Socket } from "phoenix"; 3 | import "phoenix_html"; 4 | import { LiveSocket } from "phoenix_live_view"; 5 | 6 | const url = "/live"; 7 | 8 | let Hooks = { 9 | /** 10 | * When applied (via phx-hook="NumberInput"), this hook only allows 11 | * numbers to be entered into a text input field. 12 | */ 13 | NumberInput: { 14 | mounted() { 15 | this.el.addEventListener("input", (e) => { 16 | // replace all non-numeric characters with empty string 17 | this.el.value = this.el.value.replace(/\D/g, ""); 18 | }); 19 | }, 20 | }, 21 | }; 22 | 23 | // @ts-ignore - document will be present in browser 24 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content"); 25 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks }); 26 | 27 | // Show progress bar on live navigation and form submits 28 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 29 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 30 | 31 | // connect if there are any LiveViews on the page 32 | liveSocket.connect(); 33 | 34 | // expose liveSocket on window for web console debug logs and latency simulation: 35 | liveSocket.enableDebug(); 36 | // >> liveSocket.enableLatencySim(1000) 37 | // @ts-ignore - window will be present in the browser 38 | window.liveSocket = liveSocket; 39 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/controllers/root/rootController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | 3 | /** 4 | * Placeholder / example "traditional" http controller for the root route as 5 | * opposed to a "LiveView" controller. 6 | */ 7 | class RootController { 8 | index(req: Request, res: Response) { 9 | res.render(`root/index`); 10 | } 11 | } 12 | 13 | const rootController = new RootController(); 14 | 15 | export { rootController }; 16 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/liveviews/demos/clickLiveView.ts: -------------------------------------------------------------------------------- 1 | import { SessionData } from "express-session"; 2 | import { BaseLiveView, html, LiveViewContext, LiveViewExternalEventListener, LiveViewMeta, LiveViewMountParams, LiveViewSocket, LiveViewTemplate } from "liveviewjs"; 3 | 4 | interface ClickDemoContext extends LiveViewContext{ 5 | count: number; 6 | } 7 | 8 | export class ClickDemo extends BaseLiveView implements LiveViewExternalEventListener{ 9 | 10 | mount(params: LiveViewMountParams, session: Partial, socket: LiveViewSocket): void { 11 | // set initial count to 0 12 | socket.assign({count: 0}); 13 | 14 | // override default page title 15 | socket.pageTitle("Click Demo"); 16 | } 17 | 18 | handleEvent(event: "click", params: never, socket: LiveViewSocket): void | Promise { 19 | // increment count 20 | socket.assign({count: socket.context.count + 1}); 21 | } 22 | 23 | render(context: ClickDemoContext, meta: LiveViewMeta): LiveViewTemplate | Promise { 24 | return html` 25 |
26 |
27 |

Click Demo

28 | 29 |
Count: ${context.count}
30 |
31 |
32 | ` 33 | } 34 | } -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/middleware/demoTimestamp.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | 3 | /** 4 | * Sammple middleware that logs the current timestamp to the console wiht 5 | * the request method and url. 6 | */ 7 | export const logTimestamp = (req: Request, res: Response, next: NextFunction) => { 8 | console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`); 9 | next(); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/models/counter.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/create-liveviewjs/templates/liveviewjs-app/app/models/counter.ts -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/services/inMemory.ts: -------------------------------------------------------------------------------- 1 | import { LiveViewChangesetFactory, newChangesetFactory } from "liveviewjs"; 2 | import { SomeZodObject } from "zod"; 3 | 4 | 5 | export class InMemoryService { 6 | 7 | private db: Record = {} 8 | readonly changeset: LiveViewChangesetFactory; 9 | 10 | constructor(schema: SomeZodObject) { 11 | this.changeset = newChangesetFactory(schema) 12 | } 13 | 14 | all() { 15 | return Object.values(this.db) 16 | } 17 | 18 | get(id: string) { 19 | return this.db[id] 20 | } 21 | 22 | create(newObject: Partial) { 23 | return this.change({}, newObject, 'create'); 24 | } 25 | 26 | update(currentObject: T, updatedAttrs: Partial) { 27 | return this.change(currentObject, updatedAttrs, 'update'); 28 | } 29 | 30 | private change(current: Partial, updated: Partial, action: string) { 31 | const result = this.changeset(current, updated, action); 32 | if (result.valid) { 33 | const newObject = result.data as T; 34 | this.db[newObject.id] = newObject; 35 | } 36 | return result; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/views/liveviews/root.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%- live_title_tag(page_title, {prefix: page_title_prefix, suffix: page_title_suffix}) %> 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | <%- inner_content %> 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/app/views/root/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | LiveViewJS Examples 10 | 11 | 12 | 13 | 14 | 15 |
16 |

17 | Welcome to LiveViewJS! 18 |

19 |

This is the default (traditional) page. Click here for your first LiveViewJS Demo.

20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/config/app.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { LiveViewRouter, LiveViewServer } from "liveviewjs"; 3 | import path from "path"; 4 | import { rootController } from "../app/controllers/root/rootController"; 5 | import { ClickDemo } from "../app/liveviews/demos/clickLiveView"; 6 | import { logTimestamp } from "../app/middleware/demoTimestamp"; 7 | 8 | // require APP_SIGNING_SECRET to be set 9 | const signingSecret = process.env.APP_SIGNING_SECRET; 10 | if (!signingSecret) { 11 | throw new Error("APP_SIGNING_SECRET environment variable is not set"); 12 | } 13 | 14 | declare module "express-session" { 15 | interface SessionData { 16 | csrfToken: string; 17 | } 18 | } 19 | 20 | const port = process.env.PORT || 4001; 21 | 22 | // configure the new LiveViewServer 23 | const server = new LiveViewServer({ 24 | signingSecret, 25 | viewsPath: path.join(__dirname, "..", "views"), 26 | rootView: "liveviews/root.ejs", 27 | publicPath: path.join(__dirname, "..", "public"), 28 | port: Number(port), 29 | pageTitleDefaults: { 30 | suffix: " - LiveViewJS", 31 | title: "Starter", 32 | }, 33 | middleware: [logTimestamp], 34 | }); 35 | 36 | // register web and liveview routes 37 | const router: LiveViewRouter = { 38 | "/clickdemo": new ClickDemo(), 39 | }; 40 | 41 | // register all LiveView routes 42 | server.registerLiveViewRoutes(router); 43 | 44 | // add your own "traditional" routes to the express app 45 | server.expressApp.get("/", rootController.index); 46 | 47 | // then start the server 48 | server.start(); 49 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/env: -------------------------------------------------------------------------------- 1 | APP_SIGNING_SECRET=replace_me -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .parcel-cache 4 | .env -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tsc", 5 | "watch": "tsc --watch", 6 | "dist": "npm run clean; npm run build; npm run copy-views; npm run client-build", 7 | "start": "npm run dist; nodemon -e js -w dist dist/config/app.js", 8 | "client-build": "rm -rf dist/client; npm run appjs; npm run appcss; npm run copy-images", 9 | "appjs": "npm run check;mkdir -p dist/public/js; parcel build", 10 | "appcss": "tailwindcss -i ./app/assets/css/app.css -o ./dist/public/css/app.css", 11 | "copy-views": "mkdir -p dist/views; cp -r app/views/* dist/views", 12 | "copy-images": "mkdir -p dist/public/images; cp -r app/assets/images/* dist/public/images", 13 | "check": "tsc --noEmit", 14 | "clean": "rm -rf dist" 15 | }, 16 | "targets": { 17 | "client": { 18 | "source": [ 19 | "app/assets/js/app.ts" 20 | ], 21 | "context": "browser", 22 | "distDir": "dist/public/js" 23 | } 24 | }, 25 | "dependencies": { 26 | "dotenv": "^16.0.0", 27 | "express": "^4.17.3", 28 | "express-session": "^1.17.2", 29 | "liveviewjs": "^0.2.0", 30 | "nprogress": "^0.2.0", 31 | "phoenix": "^1.6.6", 32 | "phoenix_html": "^3.2.0", 33 | "phoenix_live_view": "^0.17.7", 34 | "zod": "^3.11.6" 35 | }, 36 | "devDependencies": { 37 | "@types/express-session": "^1.17.4", 38 | "@types/node": "^17.0.17", 39 | "@types/nprogress": "^0.2.0", 40 | "@types/phoenix": "^1.5.4", 41 | "@types/phoenix_live_view": "^0.15.1", 42 | "nodemon": "^2.0.15", 43 | "parcel": "^2.3.2", 44 | "tailwindcss": "^3.0.23", 45 | "ts-node": "^10.5.0", 46 | "typescript": "^4.5.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./app/**/*.{html,ts,ejs}"], 3 | } 4 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/templates/liveviewjs-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "target": "es2019", 16 | "lib": ["es2019", "esnext.asynciterable", "DOM"], 17 | "types": ["node"], 18 | "outDir": "./dist", 19 | "baseUrl": "." 20 | }, 21 | "include": ["app/**/*", "config/**/*"], 22 | "exclude": ["dist", "node_modules", "**/*.test.ts", "app/assets/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/create-liveviewjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es5"], 6 | "declaration": true, 7 | "types": ["node"], 8 | "strict": true, 9 | 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "noEmitOnError": true, 14 | 15 | "skipLibCheck": true, 16 | 17 | "outDir": "./dist", 18 | "baseUrl": "." 19 | }, 20 | "include": ["src/create/**/*", "src/foo/**/*"], 21 | "exclude": ["dist", "node_modules", "**/*.test.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/deno/.gitignore: -------------------------------------------------------------------------------- 1 | key.json 2 | local_import_map.json 3 | public/*.png 4 | public/*.gif 5 | public/*.jpg 6 | public/*.jpeg 7 | public/*.pdf 8 | public/js/ 9 | -------------------------------------------------------------------------------- /packages/deno/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/deno/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/deno/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true, 5 | "deno.importMap": "./import_map.json", 6 | "deno.enablePaths": ["./mod.ts", "src/deno", "src/example"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/deno/import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "zod": "https://deno.land/x/zod@v3.14.2/mod.ts", 4 | "crypto": "https://deno.land/std@0.128.0/node/crypto.ts", 5 | "events": "https://deno.land/std@0.128.0/node/events.ts", 6 | "nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts", 7 | "path-to-regexp": "https://deno.land/x/path_to_regexp@v6.2.1/index.ts", 8 | "liveviewjs": "../core/mod.ts", 9 | "@liveviewjs/examples": "../examples/mod.ts" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/deno/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./src/deno/index.ts"; 2 | -------------------------------------------------------------------------------- /packages/deno/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@liveviewjs/deno-client", 4 | "description": "Only here for src/client builds", 5 | "author": "Donnie Flood ", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "@types/node": "^18.7.8", 9 | "@types/nprogress": "^0.2.0", 10 | "@types/phoenix": "^1.5.4", 11 | "@types/phoenix_live_view": "^0.15.1", 12 | "@types/ws": "^8.5.3", 13 | "nprogress": "^0.2.0", 14 | "phoenix": "^1.6.12", 15 | "phoenix_html": "^3.2.0", 16 | "phoenix_live_view": "^0.18.0", 17 | "ts-node": "^10.9.1", 18 | "typescript": "^4.5.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/deno/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/deno/public/.keep -------------------------------------------------------------------------------- /packages/deno/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/deno/public/favicon.ico -------------------------------------------------------------------------------- /packages/deno/src/client/index.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file 2 | import NProgress from "nprogress"; 3 | import { Socket } from "phoenix"; 4 | import "phoenix_html"; 5 | import { LiveSocket, ViewHook } from "phoenix_live_view"; 6 | 7 | /** 8 | * Define custom LiveView Hooks that can tap into browser events. 9 | * See: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook 10 | */ 11 | let Hooks = { 12 | /** 13 | * This hook can be used by an input element to prevent input other than numbers. 14 | * e.g. 15 | */ 16 | NumberInput: { 17 | mounted() { 18 | this.el.addEventListener("input", () => { 19 | // replace all non-numeric characters with empty string 20 | this.el.value = this.el.value.replace(/\D/g, ""); 21 | }); 22 | }, 23 | } as ViewHook, 24 | }; 25 | 26 | const url = "/live"; 27 | let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); 28 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks }); 29 | 30 | // Show progress bar on live navigation and form submits 31 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 32 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 33 | 34 | // connect if there are any LiveViews on the page 35 | liveSocket.connect(); 36 | 37 | // expose liveSocket on window for web console debug logs and latency simulation: 38 | // liveSocket.enableDebug(); 39 | // liveSocket.enableLatencySim(1000)(window as any).liveSocket = liveSocket; 40 | (window as any).liveSocket = liveSocket; 41 | -------------------------------------------------------------------------------- /packages/deno/src/deno/broadcastChannelPubSub.ts: -------------------------------------------------------------------------------- 1 | import { PubSub, SubscriberFunction } from "../deps.ts"; 2 | 3 | /** 4 | * BroadcastChannel pubsub implementation. See: https://deno.com/deploy/docs/runtime-broadcast-channel 5 | */ 6 | export class BroadcastChannelPubSub implements PubSub { 7 | private channels: Record = {}; 8 | 9 | public subscribe(topic: string, subscriber: SubscriberFunction): Promise { 10 | const channel = new BroadcastChannel(topic); 11 | channel.onmessage = (ev) => subscriber(ev.data); 12 | // store connection id for unsubscribe and return for caller 13 | const subId = crypto.randomUUID(); 14 | this.channels[subId] = channel; 15 | return Promise.resolve(subId); 16 | } 17 | 18 | public async broadcast(topic: string, data: T) { 19 | return await new BroadcastChannel(topic).postMessage(data); 20 | } 21 | 22 | public async unsubscribe(_topic: string, subscriberId: string) { 23 | // get channel by sub id 24 | const channel = this.channels[subscriberId]; 25 | if (channel) { 26 | // unsubscribe and delete 27 | channel.onmessage = null; 28 | delete this.channels[subscriberId]; 29 | } 30 | return await Promise.resolve(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/deno/src/deno/fsAdaptor.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemAdaptor } from "../deps.ts"; 2 | 3 | export class DenoFileSystemAdaptor implements FileSystemAdaptor { 4 | // make temp dir once 5 | tempDir = Deno.makeTempDirSync({ suffix: "com.liveviewjs.files" }); 6 | 7 | tempPath(lastPathPart: string): string { 8 | return `${this.tempDir}/${lastPathPart}`; 9 | } 10 | writeTempFile(dest: string, data: Buffer) { 11 | Deno.writeFileSync(dest, data, { create: true }); 12 | } 13 | createOrAppendFile(dest: string, src: string) { 14 | Deno.writeFileSync(dest, Deno.readFileSync(src), { 15 | append: true, 16 | create: true, 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/deno/src/deno/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./broadcastChannelPubSub.ts"; 2 | export * from "./fsAdaptor.ts"; 3 | export * from "./jwtSerDe.ts"; 4 | export * from "./server.ts"; 5 | export * from "./wsAdaptor.ts"; 6 | -------------------------------------------------------------------------------- /packages/deno/src/deno/wsAdaptor.ts: -------------------------------------------------------------------------------- 1 | import type { WsAdaptor, WsCloseListener, WsMsgListener } from "../deps.ts"; 2 | 3 | /** 4 | * Deno specific adaptor to enabled the WsMessageRouter to send messages back 5 | * to the client via WebSockets. This is very simple because all the logic resides 6 | * in the LiveViewJS WsMessageRouter. 7 | */ 8 | export class DenoWsAdaptor implements WsAdaptor { 9 | #ws: WebSocket; 10 | constructor(ws: WebSocket) { 11 | this.#ws = ws; 12 | } 13 | send(message: string, errorHandler?: (err: any) => void): void { 14 | try { 15 | this.#ws.send(message); 16 | } catch (e) { 17 | if (errorHandler) { 18 | errorHandler(e); 19 | } else { 20 | console.error(e); 21 | } 22 | } 23 | } 24 | subscribeToMessages(msgListener: WsMsgListener): void | Promise { 25 | this.#ws.onmessage = async (message) => { 26 | const isBinary = message.data instanceof ArrayBuffer; 27 | // prob a better way to take ArrayBuffer and turn it into a Buffer 28 | // but this works for now 29 | const data = isBinary ? Buffer.from(message.data) : message.data; 30 | await msgListener(data, isBinary); 31 | }; 32 | } 33 | subscribeToClose(closeListener: WsCloseListener): void | Promise { 34 | this.#ws.onclose = closeListener; 35 | } 36 | isClosed(): boolean { 37 | return this.#ws.readyState === WebSocket.CLOSED; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/deno/src/example/autorun.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "https://deno.land/x/esbuild@v0.15.9/mod.js"; 2 | 3 | // Build / watch the client code 4 | esbuild 5 | .build({ 6 | entryPoints: ["src/client/index.ts"], 7 | outdir: "public/js", 8 | bundle: true, 9 | format: "esm", 10 | platform: "browser", 11 | sourcemap: true, 12 | watch: { 13 | onRebuild(error) { 14 | if (error) { 15 | console.error("client rebuild failed"); 16 | console.error(error); 17 | } else { 18 | console.log("client build succeeded"); 19 | } 20 | }, 21 | }, 22 | }) 23 | .then((result) => { 24 | if (result.errors.length > 0) { 25 | console.error(result.errors); 26 | } else { 27 | console.log("client build succeeded"); 28 | } 29 | }); 30 | 31 | // Spawn the server 32 | Deno.run({ 33 | cmd: "deno run --unstable --allow-net --allow-read --allow-write --allow-env --import-map=import_map.json --watch src/example/index.ts".split( 34 | " " 35 | ), 36 | }); 37 | -------------------------------------------------------------------------------- /packages/deno/src/example/index.ts: -------------------------------------------------------------------------------- 1 | import { BroadcastChannelPubSub } from "../deno/broadcastChannelPubSub.ts"; 2 | import { DenoOakLiveViewServer } from "../deno/server.ts"; 3 | import { Application, Router } from "../deps.ts"; 4 | import { indexHandler } from "./indexHandler.ts"; 5 | import { liveHtmlTemplate, wrapperTemplate } from "./liveTemplates.ts"; 6 | import { liveRouter } from "./liveview/router.ts"; 7 | import { logRequests, serveStatic } from "./oak.ts"; 8 | 9 | // initialize the LiveViewServer 10 | const liveServer = new DenoOakLiveViewServer( 11 | liveRouter, 12 | liveHtmlTemplate, 13 | { title: "Deno Demo", suffix: " · LiveViewJS" }, 14 | { 15 | wrapperTemplate, 16 | pubSub: new BroadcastChannelPubSub(), 17 | // onError: console.error, // uncomment to see errors 18 | // debug: console.log, // uncomment to see messages 19 | } 20 | ); 21 | 22 | // configure oak router 23 | const router = new Router(); 24 | // send websocket requests to the LiveViewJS server 25 | router.get("/live/websocket", liveServer.wsMiddleware); 26 | // setup the index route 27 | router.get("/", indexHandler); 28 | // serve static files (images, js, and css) from public directory 29 | router.get("/(.*).(png|jpg|jpeg|gif|js|js.map|css)", serveStatic); 30 | 31 | // configure oak application 32 | const app = new Application(); 33 | // send http requests to the LiveViewJS server 34 | app.use(liveServer.httpMiddleware); 35 | app.use(logRequests(liveRouter)); 36 | // add oak router to app 37 | app.use(router.routes()); 38 | app.use(router.allowedMethods()); 39 | 40 | // listen for requests 41 | const port = Number(Deno.env.get("PORT") ?? 9001); 42 | console.log(`LiveViewJS (Deno) is listening at: http://localhost:${port}`); 43 | await app.listen({ port }); 44 | -------------------------------------------------------------------------------- /packages/deno/src/example/liveview/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autocompleteLiveView, 3 | booksLiveView, 4 | counterLiveView, 5 | dashboardLiveView, 6 | decarbLiveView, 7 | helloNameLiveView, 8 | helloToggleEmojiLiveView, 9 | jsCmdsLiveView, 10 | LiveViewRouter, 11 | paginateLiveView, 12 | photosLiveView, 13 | printLiveView, 14 | searchLiveView, 15 | serversLiveView, 16 | sortLiveView, 17 | volumeLiveView, 18 | volunteerLiveView, 19 | xkcdLiveView, 20 | } from "../../deps.ts"; 21 | import { rtCounterLiveView } from "../liveview/rtcounter.ts"; 22 | 23 | // configure the LiveViewRouter with the LiveViews 24 | export const liveRouter: LiveViewRouter = { 25 | "/autocomplete": autocompleteLiveView, 26 | "/decarbonize": decarbLiveView, 27 | "/prints": printLiveView, 28 | "/volume": volumeLiveView, 29 | "/paginate": paginateLiveView, 30 | "/dashboard": dashboardLiveView, 31 | "/search": searchLiveView, 32 | "/servers": serversLiveView, 33 | "/sort": sortLiveView, 34 | "/volunteers": volunteerLiveView, 35 | "/counter": counterLiveView, 36 | "/jscmds": jsCmdsLiveView, 37 | "/photos": photosLiveView, 38 | "/xkcd": xkcdLiveView, 39 | "/rtcounter": rtCounterLiveView, 40 | "/books": booksLiveView, 41 | "/helloToggle": helloToggleEmojiLiveView, 42 | "/hi/:name": helloNameLiveView, 43 | }; 44 | -------------------------------------------------------------------------------- /packages/deno/src/example/oak.ts: -------------------------------------------------------------------------------- 1 | import { Context, LiveViewRouter, send } from "../deps.ts"; 2 | 3 | export function logRequests( 4 | liveRouter: LiveViewRouter 5 | ): (ctx: Context>, next: () => Promise) => void { 6 | return async (ctx: Context>, next: () => Promise) => { 7 | const { request } = ctx; 8 | const { url, method } = request; 9 | const path = url.pathname; 10 | const isLiveView = liveRouter[path] !== undefined; 11 | console.log(`${method} ${isLiveView ? "LiveView" : ""} ${url} - ${new Date().toISOString()}`); 12 | await next(); 13 | }; 14 | } 15 | 16 | export const serveStatic = async (ctx: Context>) => { 17 | const path = ctx.request.url.pathname; 18 | await send(ctx, path, { 19 | root: "./public/", 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/deno/tsconfig-client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "ES2020", 15 | "moduleResolution": "node", 16 | "lib": ["DOM"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/client/*"], 22 | "exclude": ["./src/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/examples/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /packages/examples/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/examples/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/examples/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist/liveviewjs-examples.mjs"; 2 | -------------------------------------------------------------------------------- /packages/examples/rollup.config.js: -------------------------------------------------------------------------------- 1 | // import typescript from "rollup-plugin-typescript2"; 2 | import typescript from "@rollup/plugin-typescript"; 3 | import dts from "rollup-plugin-dts"; 4 | import resolve from "@rollup/plugin-node-resolve"; 5 | import commonjs from "@rollup/plugin-commonjs"; 6 | 7 | const external = ["liveviewjs", "zod", "nanoid", "node-fetch"]; 8 | 9 | export default [ 10 | // build the common js rollup 11 | { 12 | external, 13 | input: "./src/rollupEntry.ts", 14 | output: { 15 | file: "./build/liveviewjs-examples.js", 16 | format: "cjs", 17 | }, 18 | plugins: [ 19 | resolve(), 20 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./rollup", declaration: true }), 21 | commonjs({ 22 | exclude: "node_modules/**", 23 | }), 24 | ], 25 | }, 26 | // build the esm rollup 27 | { 28 | external, 29 | input: "./src/rollupEntry.ts", 30 | output: { 31 | file: "./build/liveviewjs-examples.mjs", 32 | format: "esm", 33 | }, 34 | plugins: [ 35 | { 36 | banner() { 37 | // add typescript types to the javascript bundle 38 | return '/// '; 39 | }, 40 | }, 41 | resolve(), 42 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./rollup", declaration: true }), 43 | commonjs(), 44 | ], 45 | }, 46 | // bundle all the *.d.ts typescript definitions into a single d.ts file 47 | { 48 | external, 49 | input: "./build/rollup/rollupEntry.d.ts", 50 | output: { 51 | file: "./build/liveviewjs-examples.d.ts", 52 | format: "esm", 53 | }, 54 | plugins: [dts()], 55 | }, 56 | ]; 57 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/autoComplete/data.test.ts: -------------------------------------------------------------------------------- 1 | import { listCities, suggest } from "./data"; 2 | 3 | describe("test cities", () => { 4 | it("cities list", () => { 5 | const cities = listCities; 6 | expect(cities.length).toBe(1001); 7 | }); 8 | 9 | it("suggest cities by prefix", () => { 10 | const cities = suggest("d"); 11 | expect(cities.length).toBe(39); 12 | }); 13 | 14 | it("suggest cities by empty prefix returns empty", () => { 15 | const cities = suggest(""); 16 | expect(cities.length).toBe(0); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/counter/index.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView, html } from "liveviewjs"; 2 | 3 | /** 4 | * A basic counter that increments and decrements a number. 5 | */ 6 | export const counterLiveView = createLiveView< 7 | { count: number }, // Define LiveView Context / State 8 | { type: "increment" } | { type: "decrement" } // Define LiveView Events 9 | >({ 10 | mount: (socket) => { 11 | // init state, set count to 0 12 | socket.assign({ count: 0 }); 13 | }, 14 | handleEvent: (event, socket) => { 15 | // handle increment and decrement events 16 | const { count } = socket.context; 17 | switch (event.type) { 18 | case "increment": 19 | socket.assign({ count: count + 1 }); 20 | break; 21 | case "decrement": 22 | socket.assign({ count: count - 1 }); 23 | break; 24 | } 25 | }, 26 | render: async (context) => { 27 | // render the view based on the state 28 | const { count } = context; 29 | return html` 30 |
31 |

Count is: ${count}

32 | 33 | 34 |
35 | `; 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/decarbonize/index.ts: -------------------------------------------------------------------------------- 1 | import { AnyLiveEvent, createLiveView, html } from "liveviewjs"; 2 | import { calcLiveComponent, footprintLiveComponent } from "./liveComponent"; 3 | 4 | // define the info this LiveView will receive from the child LiveComponent 5 | export type FootprintData = { 6 | vehicleCO2Tons: number; 7 | spaceHeatingCO2Tons: number; 8 | gridElectricityCO2Tons: number; 9 | }; 10 | 11 | export type FootprintUpdateInfo = { 12 | type: "update"; 13 | footprintData: FootprintData; 14 | }; 15 | 16 | export const decarbLiveView = createLiveView< 17 | { 18 | footprintData?: FootprintData; 19 | }, 20 | AnyLiveEvent, 21 | FootprintUpdateInfo 22 | >({ 23 | mount: (socket) => { 24 | socket.pageTitle("Decarbonize Calculator"); 25 | }, 26 | 27 | // receive the info from the stateful child LiveComponent 28 | handleInfo: (info, socket) => { 29 | const { footprintData } = info; 30 | socket.assign({ footprintData }); 31 | }, 32 | render: async (context, meta) => { 33 | // use the live_component helper to render a `LiveComponent` 34 | const { footprintData } = context; 35 | const { live_component } = meta; 36 | return html` 37 |

Decarbonize Calculator

38 |
39 | ${await live_component(calcLiveComponent, { 40 | vehicle: "gas", 41 | spaceHeating: "gas", 42 | gridElectricity: "grid", 43 | id: 1, 44 | })} 45 |
46 |
47 | ${await live_component(footprintLiveComponent, { 48 | data: footprintData, 49 | })} 50 |
51 | `; 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/fetch/data.ts: -------------------------------------------------------------------------------- 1 | // see: https://xkcd.com/json.html 2 | export interface XkcdData { 3 | month: string; 4 | day: string; 5 | year: string; 6 | num: number; 7 | link: string; 8 | news: string; 9 | safe_title: string; 10 | transcript: string; 11 | alt: string; 12 | img: string; 13 | title: string; 14 | } 15 | 16 | export function randomXkcdNum(max: number): number { 17 | return Math.floor(Math.random() * max) + 1; 18 | } 19 | 20 | export function isValidXkcd(num: number, max: number) { 21 | return num >= 1 && num <= max; 22 | } 23 | 24 | export async function fetchXkcd(num?: number, max?: number): Promise { 25 | let url = "https://xkcd.com/info.0.json"; 26 | if (num && max && isValidXkcd(num, max)) { 27 | url = `https://xkcd.com/${num}/info.0.json`; 28 | } 29 | const response = await fetch(url); 30 | const data = await response.json(); 31 | return data as XkcdData; 32 | } 33 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/hello/helloName.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView, html } from "liveviewjs"; 2 | 3 | export const helloNameLiveView = createLiveView({ 4 | mount: (socket, _, params) => { 5 | socket.assign({ name: params.name ?? "World" }); 6 | }, 7 | render: (context) => { 8 | const { name } = context; 9 | return html`👋 ${name}! `; 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/hello/helloToggleEmoji.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView, html } from "liveviewjs"; 2 | 3 | export const helloToggleEmojiLiveView = createLiveView({ 4 | mount: (socket) => { 5 | socket.assign({ useEmoji: false }); 6 | }, 7 | handleEvent(event, socket) { 8 | socket.assign({ useEmoji: !socket.context.useEmoji }); 9 | }, 10 | render: (context) => { 11 | const msg = context.useEmoji ? "👋 🌎" : "Hello World"; 12 | return html` 13 | ${msg} 14 |
15 | 16 | `; 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/liveNav/index.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView, html } from "liveviewjs"; 2 | 3 | /** 4 | * Example showing how to use server-side live navigation. 5 | */ 6 | export const liveNavLV = createLiveView< 7 | { id?: string }, // Define LiveView Context / State 8 | { type: "patch"; id: string } | { type: "redirect"; path: string } // Define LiveView Events 9 | >({ 10 | handleParams: (url, socket) => { 11 | console.log("handleParams", url); 12 | if (url.searchParams.has("id")) { 13 | socket.assign({ id: url.searchParams.get("id")! }); 14 | } 15 | }, 16 | handleEvent: (event, socket) => { 17 | switch (event.type) { 18 | case "patch": 19 | socket.assign({ id: event.id }); 20 | socket.pushPatch(socket.url.pathname, new URLSearchParams({ id: event.id })); 21 | break; 22 | case "redirect": 23 | // extract first part of path (e.g. /liveNav) 24 | const path = socket.url.pathname.split("/")[1]; 25 | socket.pushRedirect("/" + path + event.path); 26 | break; 27 | } 28 | }, 29 | render: async (context) => { 30 | // render the view based on the state 31 | const { id } = context; 32 | return html` 33 |
34 |

Query String ID: ${id}

35 | 36 | 37 |
38 | `; 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/liveSearch/data.test.ts: -------------------------------------------------------------------------------- 1 | import { listStores, searchByCity, searchByZip } from "./data"; 2 | 3 | describe("test stores", () => { 4 | it("stores list", () => { 5 | const stores = listStores(); 6 | expect(stores.length).toBe(7); 7 | }); 8 | 9 | it("stores searchByZip", () => { 10 | const stores = searchByZip("80204"); 11 | expect(stores.length).toBe(4); 12 | }); 13 | 14 | it("stores searchByCity", () => { 15 | const stores = searchByCity("Denver, CO"); 16 | expect(stores.length).toBe(4); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/servers/data.ts: -------------------------------------------------------------------------------- 1 | export interface Server { 2 | id: string; 3 | name: string; 4 | status: string; 5 | deploy_count: number; 6 | size: number; 7 | framework: string; 8 | git_repo: string; 9 | last_commit_id: string; 10 | last_commit_message: string; 11 | } 12 | 13 | export function listServers(): Server[] { 14 | return servers; 15 | } 16 | 17 | const servers: Server[] = [ 18 | { 19 | id: "1", 20 | name: "dancing-lizard", 21 | status: "up", 22 | deploy_count: 14, 23 | size: 19.5, 24 | framework: "Elixir/Phoenix", 25 | git_repo: "https://git.example.com/dancing-lizard.git", 26 | last_commit_id: "f3d41f7", 27 | last_commit_message: "If this works, I'm going disco 🕺", 28 | }, 29 | { 30 | id: "2", 31 | name: "lively-frog", 32 | status: "up", 33 | deploy_count: 12, 34 | size: 24.0, 35 | framework: "Elixir/Phoenix", 36 | git_repo: "https://git.example.com/lively-frog.git", 37 | last_commit_id: "d2eba26", 38 | last_commit_message: "Does it scale? 🤔", 39 | }, 40 | { 41 | id: "3", 42 | name: "curious-raven", 43 | status: "up", 44 | deploy_count: 21, 45 | size: 17.25, 46 | framework: "Ruby/Rails", 47 | git_repo: "https://git.example.com/curious-raven.git", 48 | last_commit_id: "a3708f1", 49 | last_commit_message: "Fixed a bug! 🐞", 50 | }, 51 | { 52 | id: "4", 53 | name: "cryptic-owl", 54 | status: "down", 55 | deploy_count: 2, 56 | size: 5.0, 57 | framework: "Elixir/Phoenix", 58 | git_repo: "https://git.example.com/cryptic-owl.git", 59 | last_commit_id: "c497e91", 60 | last_commit_message: "First big launch! 🤞", 61 | }, 62 | ]; 63 | -------------------------------------------------------------------------------- /packages/examples/src/liveviews/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function numberToCurrency(amount: number) { 2 | var formatter = new Intl.NumberFormat("en-US", { 3 | style: "currency", 4 | currency: "USD", 5 | }); 6 | return formatter.format(amount); 7 | } 8 | -------------------------------------------------------------------------------- /packages/examples/src/rollupEntry.ts: -------------------------------------------------------------------------------- 1 | export * from "./liveviews/autoComplete"; 2 | export * from "./liveviews/books"; 3 | export * from "./liveviews/counter"; 4 | export * from "./liveviews/counter/realtime"; 5 | export * from "./liveviews/dashboard"; 6 | export * from "./liveviews/decarbonize"; 7 | export * from "./liveviews/fetch"; 8 | export * from "./liveviews/hello/helloName"; 9 | export * from "./liveviews/hello/helloToggleEmoji"; 10 | export * from "./liveviews/jsCommands"; 11 | export * from "./liveviews/liveNav"; 12 | export * from "./liveviews/liveSearch"; 13 | export * from "./liveviews/pagination"; 14 | export * from "./liveviews/photos"; 15 | export * from "./liveviews/prints"; 16 | export * from "./liveviews/servers"; 17 | export * from "./liveviews/sorting"; 18 | export * from "./liveviews/volume"; 19 | export * from "./liveviews/volunteers"; 20 | export * from "./routeDetails"; 21 | -------------------------------------------------------------------------------- /packages/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "es2019", 15 | "moduleResolution": "node", 16 | "lib": ["es2019", "esnext.asynciterable"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/**/*.ts"], 22 | "exclude": ["build", "node_modules", "./**/*.test.ts", "denoRollupEntry.ts", "src/browser/index.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/express/.gitignore: -------------------------------------------------------------------------------- 1 | public/*.png 2 | public/*.gif 3 | public/*.jpg 4 | public/*.jpeg 5 | public/*.pdf 6 | public/js/ 7 | 8 | -------------------------------------------------------------------------------- /packages/express/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /packages/express/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/express/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/express/public/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/.keep -------------------------------------------------------------------------------- /packages/express/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/favicon.ico -------------------------------------------------------------------------------- /packages/express/public/images/cats/clenil.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/clenil.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/flippers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/flippers.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/jorts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/jorts.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/kipper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/kipper.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/lemmy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/lemmy.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/lissy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/lissy.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/mikkel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/mikkel.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/minka.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/minka.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/misty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/misty.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/nelly.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/nelly.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/ninj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/ninj.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/pollito.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/pollito.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/siegfried.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/siegfried.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/truman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/truman.jpg -------------------------------------------------------------------------------- /packages/express/public/images/cats/washy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/express/public/images/cats/washy.jpg -------------------------------------------------------------------------------- /packages/express/src/client/index.ts: -------------------------------------------------------------------------------- 1 | import NProgress from "nprogress"; 2 | import { Socket } from "phoenix"; 3 | import "phoenix_html"; 4 | import { LiveSocket, ViewHook } from "phoenix_live_view"; 5 | 6 | /** 7 | * Define custom LiveView Hooks that can tap into browser events. 8 | * See: https://hexdocs.pm/phoenix_live_view/js-interop.html#client-hooks-via-phx-hook 9 | */ 10 | let Hooks = { 11 | /** 12 | * This hook can be used by an input element to prevent input other than numbers. 13 | * e.g. 14 | */ 15 | NumberInput: { 16 | mounted() { 17 | this.el.addEventListener("input", () => { 18 | // replace all non-numeric characters with empty string 19 | this.el.value = this.el.value.replace(/\D/g, ""); 20 | }); 21 | }, 22 | } as ViewHook, 23 | }; 24 | 25 | const url = "/live"; 26 | let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); 27 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: Hooks }); 28 | window.addEventListener("phx:refresh", (e) => { 29 | console.log("phx:refresh", e); 30 | }); 31 | 32 | // Show progress bar on live navigation and form submits 33 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 34 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 35 | 36 | // connect if there are any LiveViews on the page 37 | liveSocket.connect(); 38 | 39 | // expose liveSocket on window for web console debug logs and latency simulation: 40 | // liveSocket.enableDebug(); 41 | // liveSocket.enableLatencySim(1000) 42 | (window as any).liveSocket = liveSocket; 43 | -------------------------------------------------------------------------------- /packages/express/src/example/autorun_ios.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { ChildProcess, spawn } from "child_process"; 3 | import esbuild from "esbuild"; 4 | 5 | const outdir = "build"; 6 | let runner: ChildProcess; 7 | 8 | function maybe_stop_child() { 9 | if (runner) { 10 | runner.kill(); 11 | } 12 | } 13 | 14 | function run_child() { 15 | maybe_stop_child(); 16 | runner = spawn("node", [`${outdir}/ios.js`]); 17 | runner.stdout!.on("data", (data) => process.stdout.write(chalk.blue(data.toString()))); 18 | runner.stderr!.on("data", (data) => process.stderr.write(chalk.red(data.toString()))); 19 | } 20 | 21 | function build_success() { 22 | console.log(chalk.green("build succeeded")); 23 | run_child(); 24 | } 25 | 26 | function build_failure(error: unknown) { 27 | console.error(chalk.red("build failed")); 28 | console.error(error); 29 | maybe_stop_child(); 30 | } 31 | 32 | esbuild 33 | .build({ 34 | entryPoints: ["src/example/ios.ts"], 35 | outdir, 36 | bundle: true, 37 | format: "cjs", 38 | platform: "node", 39 | watch: { 40 | onRebuild(error) { 41 | if (error) { 42 | build_failure(error); 43 | } else { 44 | build_success(); 45 | } 46 | }, 47 | }, 48 | }) 49 | .then((result) => { 50 | if (result.errors.length > 0) { 51 | build_failure(result); 52 | } else { 53 | build_success(); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /packages/express/src/example/express.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from "express"; 2 | import session, { MemoryStore } from "express-session"; 3 | import { LiveViewRouter } from "liveviewjs"; 4 | 5 | // declare flash object is added to session data in express-session middleware 6 | declare module "express-session" { 7 | interface SessionData { 8 | flash: any; 9 | } 10 | } 11 | 12 | export function configureExpress(sessionSecret: string) { 13 | const app = express(); 14 | 15 | // add static file serving 16 | app.use(express.static("public")); 17 | 18 | // configure express-session middleware 19 | app.use( 20 | session({ 21 | secret: sessionSecret, 22 | resave: false, 23 | rolling: true, 24 | saveUninitialized: true, 25 | cookie: { 26 | secure: process.env.NODE_ENV === "production", 27 | sameSite: "strict", 28 | maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days 29 | }, 30 | store: new MemoryStore(), 31 | }) 32 | ); 33 | 34 | return app; 35 | } 36 | 37 | export function logRequests(router: LiveViewRouter): (req: Request, res: Response, next: NextFunction) => void { 38 | return async (req: Request, res: Response, next: NextFunction) => { 39 | const isLiveView = router.hasOwnProperty(req.path); 40 | console.log(`${req.method} ${isLiveView ? "LiveView" : ""} ${req.url} - ${new Date().toISOString()}`); 41 | next(); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/express/src/example/index.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "http"; 2 | import { WebSocketServer } from "ws"; 3 | import { NodeExpressLiveViewServer } from "../node/server"; 4 | import { configureExpress, logRequests } from "./express"; 5 | import { indexHandler } from "./indexHandler"; 6 | import { htmlPageTemplate, wrapperTemplate } from "./liveTemplates"; 7 | import { liveRouter } from "./liveview/router"; 8 | 9 | // basic server options 10 | const signingSecret = process.env.SESSION_SECRET ?? "MY_VERY_SECRET_KEY"; 11 | const port = process.env.PORT ?? 4001; 12 | 13 | // configure LiveViewJS server 14 | const liveServer = new NodeExpressLiveViewServer( 15 | liveRouter, 16 | htmlPageTemplate, 17 | signingSecret, 18 | { title: "Express Demo", suffix: " · LiveViewJS" }, 19 | { 20 | wrapperTemplate, 21 | // onError: console.error, // print errors to console 22 | // debug: console.log, // print messages to console 23 | } 24 | ); 25 | 26 | // configure express server 27 | const express = configureExpress(signingSecret); 28 | express.use(liveServer.httpMiddleware); // allow LiveViewJS to handle LiveView http requests 29 | express.use(logRequests(liveRouter)); // middleware to log requests 30 | express.get("/", indexHandler); // index route handler 31 | 32 | // configure http server to send requests to express 33 | const server = new Server(); 34 | server.on("request", express); 35 | 36 | // configure websocket server to send requests to LiveViewJS 37 | const ws = new WebSocketServer({ server }); 38 | ws.on("connection", liveServer.wsMiddleware); 39 | 40 | // listen for requests 41 | server.listen(port, () => { 42 | console.log(`LiveViewJS is listening at: http://localhost:${port}`); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/express/src/example/liveview/ios/cat.ts: -------------------------------------------------------------------------------- 1 | import { createLiveView, html } from "liveviewjs"; 2 | import { favorites, scores } from "./data"; 3 | 4 | type CatCtx = { 5 | cat: string; 6 | fav: boolean; 7 | score: number; 8 | }; 9 | 10 | type CatEvent = 11 | | { 12 | type: "change-score"; 13 | value: number; 14 | } 15 | | { 16 | type: "toggle-favorite"; 17 | }; 18 | 19 | // LiveView Native Tutorial - 20 | // https://liveviewnative.github.io/liveview-client-swiftui/tutorials/phoenixliveviewnative/01-initial-list/ 21 | export const catLive = createLiveView({ 22 | mount: (socket, _, params) => { 23 | console.log("params", params); 24 | const cat = params.cat as string; 25 | const fav = favorites[cat] ?? false; 26 | const score = scores[cat] ?? 0; 27 | socket.assign({ cat, fav, score }); 28 | }, 29 | handleEvent: (event, socket) => { 30 | console.log("event", event); 31 | switch (event.type) { 32 | case "change-score": 33 | scores.cat = event.value; 34 | socket.assign({ score: event.value }); 35 | break; 36 | case "toggle-favorite": 37 | favorites.cat = !favorites.cat; 38 | socket.assign({ fav: favorites.cat }); 39 | break; 40 | } 41 | }, 42 | render: (ctx) => { 43 | const { cat, fav, score } = ctx; 44 | return html` 45 | 46 | 47 | 48 | 49 | `; 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /packages/express/src/example/liveview/ios/data.ts: -------------------------------------------------------------------------------- 1 | export const cats = [ 2 | "Clenil", 3 | "Flippers", 4 | "Jorts", 5 | "Kipper", 6 | "Lemmy", 7 | "Lissy", 8 | "Mikkel", 9 | "Minka", 10 | "Misty", 11 | "Nelly", 12 | "Ninj", 13 | "Pollito", 14 | "Siegfried", 15 | "Truman", 16 | "Washy", 17 | ]; 18 | 19 | export const favorites: Record = {}; 20 | 21 | export const scores: Record = {}; 22 | -------------------------------------------------------------------------------- /packages/express/src/example/liveview/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | autocompleteLiveView, 3 | booksLiveView, 4 | counterLiveView, 5 | dashboardLiveView, 6 | decarbLiveView, 7 | helloNameLiveView, 8 | helloToggleEmojiLiveView, 9 | jsCmdsLiveView, 10 | liveNavLV, 11 | paginateLiveView, 12 | photosLiveView, 13 | printLiveView, 14 | rtCounterLiveView, 15 | searchLiveView, 16 | serversLiveView, 17 | sortLiveView, 18 | volumeLiveView, 19 | volunteerLiveView, 20 | xkcdLiveView, 21 | } from "@liveviewjs/examples"; 22 | import { LiveViewRouter } from "liveviewjs"; 23 | 24 | // configure LiveView routes 25 | export const liveRouter: LiveViewRouter = { 26 | "/autocomplete": autocompleteLiveView, 27 | "/decarbonize": decarbLiveView, 28 | "/prints": printLiveView, 29 | "/volume": volumeLiveView, 30 | "/paginate": paginateLiveView, 31 | "/dashboard": dashboardLiveView, 32 | "/search": searchLiveView, 33 | "/servers": serversLiveView, 34 | "/sort": sortLiveView, 35 | "/volunteers": volunteerLiveView, 36 | "/counter": counterLiveView, 37 | "/jscmds": jsCmdsLiveView, 38 | "/photos": photosLiveView, 39 | "/xkcd": xkcdLiveView, 40 | "/rtcounter": rtCounterLiveView, 41 | "/books": booksLiveView, 42 | "/helloToggle": helloToggleEmojiLiveView, 43 | "/hi/:name": helloNameLiveView, 44 | "/liveNav/:sub": liveNavLV, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/express/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./node"; 2 | -------------------------------------------------------------------------------- /packages/express/src/node/fsAdaptor.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { FileSystemAdaptor } from "liveviewjs"; 3 | import os from "os"; 4 | import path from "path"; 5 | 6 | export class NodeFileSystemAdatptor implements FileSystemAdaptor { 7 | tempPath(lastPathPart: string): string { 8 | // ensure the temp directory exists 9 | const tempDir = path.join(os.tmpdir(), "com.liveviewjs.files"); 10 | if (!fs.existsSync(tempDir)) { 11 | fs.mkdirSync(tempDir); 12 | } 13 | return path.join(tempDir, lastPathPart); 14 | } 15 | writeTempFile(dest: string, data: Buffer) { 16 | fs.writeFileSync(dest, data); 17 | } 18 | createOrAppendFile(dest: string, src: string) { 19 | fs.appendFileSync(dest, fs.readFileSync(src)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/express/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fsAdaptor"; 2 | export * from "./jwtSerDe"; 3 | export * from "./redisPubSub"; 4 | export * from "./server"; 5 | export * from "./wsAdaptor"; 6 | -------------------------------------------------------------------------------- /packages/express/src/node/jwtSerDe.ts: -------------------------------------------------------------------------------- 1 | import { SerDe, SessionData } from "liveviewjs"; 2 | import jwt from "jsonwebtoken"; 3 | 4 | /** 5 | * Session data serializer/deserializer for Node using JWT tokens. 6 | */ 7 | export class NodeJwtSerDe implements SerDe { 8 | private secretOrPrivateKey: string; 9 | constructor(secretOrPrivateKey: string) { 10 | this.secretOrPrivateKey = secretOrPrivateKey; 11 | } 12 | deserialize(data: string): Promise { 13 | return Promise.resolve(jwt.verify(data, this.secretOrPrivateKey) as T); 14 | } 15 | 16 | serialize(data: T): Promise { 17 | return Promise.resolve(jwt.sign(data as unknown as object, this.secretOrPrivateKey)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/express/src/node/wsAdaptor.ts: -------------------------------------------------------------------------------- 1 | import { WsAdaptor, WsCloseListener, WsMsgListener } from "liveviewjs"; 2 | import { WebSocket } from "ws"; 3 | 4 | /** 5 | * Node specific adaptor to enabled the WsMessageRouter to send messages back 6 | * to the client via WebSockets. 7 | */ 8 | export class NodeWsAdaptor implements WsAdaptor { 9 | #ws: WebSocket; 10 | constructor(ws: WebSocket) { 11 | this.#ws = ws; 12 | } 13 | subscribeToMessages(msgListener: WsMsgListener): void | Promise { 14 | this.#ws.on("message", msgListener); 15 | } 16 | subscribeToClose(closeListener: WsCloseListener): void | Promise { 17 | this.#ws.on("close", closeListener); 18 | } 19 | send(message: string, errorHandler?: (err: any) => void): void { 20 | this.#ws.send(message, errorHandler); 21 | } 22 | isClosed(): boolean { 23 | return this.#ws.readyState === WebSocket.CLOSED; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/express/tsconfig-client.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "ES2020", 15 | "moduleResolution": "node", 16 | "lib": ["DOM"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/client/*"], 22 | "exclude": ["build", "node_modules", "./**/*.test.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/express/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "es2019", 15 | "moduleResolution": "node", 16 | "lib": ["es2019", "esnext.asynciterable"], 17 | "types": ["node"], 18 | "outDir": "./build", 19 | "baseUrl": "." 20 | }, 21 | "include": ["./src/**/*"], 22 | "exclude": ["build", "node_modules", "./**/*.test.ts", "./src/client/**/*"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/gen/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floodfx/liveviewjs/52633f3d03d0c23d8fff7fb290c54f28e7fc14da/packages/gen/.gitignore -------------------------------------------------------------------------------- /packages/gen/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /packages/gen/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache/ 3 | .env 4 | dist/ 5 | .parcel-cache/ 6 | .DS_Store 7 | coverage/ 8 | package-lock.json -------------------------------------------------------------------------------- /packages/gen/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /packages/gen/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @liveviewjs/gen 2 | 3 | ## 0.10.4 4 | 5 | ### Patch Changes 6 | 7 | - 87b658d: Detect closed sockets and handle more gracefully 8 | 9 | ## 0.10.3 10 | 11 | ### Patch Changes 12 | 13 | - 4673b8d: Run npm install for deno client-side javascript 14 | 15 | ## 0.10.2 16 | 17 | ### Patch Changes 18 | 19 | - 10fefe0: Fix package bin, main, types, and files for gen package 20 | 21 | ## 0.10.1 22 | 23 | ### Patch Changes 24 | 25 | - 2fd427a: Revamp gen package and add deno project generator 26 | 27 | ## 0.10.0 28 | 29 | ### Minor Changes 30 | 31 | - 921e914: Initial LiveViewJS code gen infrastructure and nodejs project generator 32 | -------------------------------------------------------------------------------- /packages/gen/README.md: -------------------------------------------------------------------------------- 1 | ## @liveviewjs/gen 2 | 3 | A collection of Hygen generators for LiveViewJS. 4 | 5 | [Go here](https://liveviewjs.com) more information about LiveViewJS. 6 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/.gitignore.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.gitignore 3 | --- 4 | key.json 5 | local_import_map.json 6 | public/*.png 7 | public/*.gif 8 | public/*.jpg 9 | public/*.jpeg 10 | public/*.pdf 11 | public/js/ 12 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/.prettierignore.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.prettierignore 3 | --- 4 | node_modules 5 | .cache/ 6 | .env 7 | dist/ 8 | .parcel-cache/ 9 | .DS_Store 10 | coverage/ 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/.prettierrc.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.prettierrc 3 | --- 4 | { 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 120, 8 | "bracketSameLine": true 9 | } 10 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/README.md.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/README.md 3 | --- 4 | # <%= h.inflection.camelize(name, false) %> 5 | 6 | ## About 7 | This is my awesome LiveViewJS project. There are many like it but this one is mine. 8 | 9 | ## Features 10 | * Live, dynamic user experiences without complex front-end code 11 | * Easy, automatic state management between server and client 12 | * Simple, powerful form validation using changesets 13 | * Batteries included file upload support with DnD, progress, and image previews 14 | * Real-time, multiplayer capabilities with PubSub 15 | 16 | ## Routes 17 | * / - Index redirects to /hello 18 | * /hello - simple hello world LiveView 19 | * /hello/:name - simple hello world LiveView with dynamic name 20 | 21 | ## Running Locally 22 | 23 | ### Load the Client-side JS dependencies 24 | The project uses ESBuild to bundle the client-side JS dependencies. This is done automatically when you start the server *but* it needs the dependencies to be installed into /node_modules. 25 | `npm install` 26 | 27 | ### Start the Deno Server 28 | This will start, compile (both client and server), and run the server. It will also watch for changes and recompile/restart the server. 29 | `deno run --allow-run --allow-read --allow-write --allow-net --allow-env src/server/autorun.ts` 30 | 31 | ## More documentation 32 | Visit [LiveViewJS](https://liveviewjs.com) for more documentation, examples, and guides. 33 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/import_map.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/import_map.json 3 | --- 4 | { 5 | "imports": { 6 | "zod": "https://deno.land/x/zod@v3.14.2/mod.ts", 7 | "crypto": "https://deno.land/std@0.128.0/node/crypto.ts", 8 | "events": "https://deno.land/std@0.128.0/node/events.ts", 9 | "nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts", 10 | "path-to-regexp": "https://deno.land/x/path_to_regexp@v6.2.1/index.ts", 11 | "liveviewjs": "https://raw.githubusercontent.com/floodfx/liveviewjs/main/packages/core/mod.ts", 12 | "@liveviewjs/deno": "https://raw.githubusercontent.com/floodfx/liveviewjs/main/packages/deno/mod.ts", 13 | "@liveviewjs/examples": "https://raw.githubusercontent.com/floodfx/liveviewjs/main/packages/examples/mod.ts" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/package.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/package.json 3 | --- 4 | { 5 | "private": true, 6 | "name": "deno-client-js", 7 | "description": "Only here for LiveViewJS src/client builds for Deno", 8 | "devDependencies": { 9 | "@types/node": "^18.7.8", 10 | "@types/nprogress": "^0.2.0", 11 | "@types/phoenix": "^1.5.4", 12 | "@types/phoenix_live_view": "^0.15.1", 13 | "@types/ws": "^8.5.3", 14 | "nprogress": "^0.2.0", 15 | "phoenix": "^1.6.12", 16 | "phoenix_html": "^3.2.0", 17 | "phoenix_live_view": "^0.18.0", 18 | "ts-node": "^10.9.1", 19 | "typescript": "^4.5.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/public/.keep.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/public/.keep 3 | --- 4 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/client/index.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/client/index.ts 3 | --- 4 | import NProgress from "nprogress"; 5 | import { Socket } from "phoenix"; 6 | import "phoenix_html"; 7 | import { LiveSocket } from "phoenix_live_view"; 8 | 9 | const url = "/live"; 10 | let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); 11 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: {} }); 12 | window.addEventListener("phx:refresh", (e) => { 13 | console.log("phx:refresh", e); 14 | }); 15 | 16 | // Show progress bar on live navigation and form submits 17 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 18 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 19 | 20 | // connect if there are any LiveViews on the page 21 | liveSocket.connect(); 22 | 23 | // expose liveSocket on window for web console debug logs and latency simulation: 24 | // liveSocket.enableDebug(); 25 | // liveSocket.enableLatencySim(1000) 26 | (window as any).liveSocket = liveSocket; 27 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/server/autorun.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/autorun.ts 3 | --- 4 | import * as esbuild from "https://deno.land/x/esbuild@v0.15.9/mod.js"; 5 | 6 | // Build / watch the client code 7 | esbuild 8 | .build({ 9 | entryPoints: ["src/client/index.ts"], 10 | outdir: "public/js", 11 | bundle: true, 12 | format: "esm", 13 | platform: "browser", 14 | sourcemap: true, 15 | watch: { 16 | onRebuild(error) { 17 | if (error) { 18 | console.error("client rebuild failed"); 19 | console.error(error); 20 | } else { 21 | console.log("client build succeeded"); 22 | } 23 | }, 24 | }, 25 | }) 26 | .then((result) => { 27 | if (result.errors.length > 0) { 28 | console.error(result.errors); 29 | } else { 30 | console.log("client build succeeded"); 31 | } 32 | }); 33 | 34 | // Spawn the server 35 | Deno.run({ 36 | cmd: "deno run --unstable --allow-net --allow-read --allow-write --allow-env --import-map=import_map.json --watch src/server/index.ts".split( 37 | " " 38 | ), 39 | }); 40 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/server/index.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/index.ts 3 | --- 4 | import { BroadcastChannelPubSub, DenoOakLiveViewServer } from "@liveviewjs/deno"; 5 | import { Application, Router } from "../deps.ts"; 6 | import { htmlPageTemplate } from "./liveTemplates.ts"; 7 | import { liveRouter } from "./liveview/router.ts"; 8 | import { logRequests, serveStatic } from "./oak.ts"; 9 | 10 | // initialize the LiveViewServer 11 | const liveServer = new DenoOakLiveViewServer( 12 | liveRouter, 13 | htmlPageTemplate, 14 | { title: "Deno Demo", suffix: " · LiveViewJS" }, 15 | { 16 | pubSub: new BroadcastChannelPubSub(), 17 | // onError: console.error, // uncomment to see errors 18 | // debug: console.log, // uncomment to see messages 19 | } 20 | ); 21 | 22 | // configure oak router 23 | const router = new Router(); 24 | // send websocket requests to the LiveViewJS server 25 | router.get("/live/websocket", liveServer.wsMiddleware); 26 | // redirect index to /hello 27 | router.get("/", (ctx) => ctx.response.redirect("/hello")); 28 | // serve static files (images, js, and css) from public directory 29 | router.get("/(.*).(png|jpg|jpeg|gif|js|js.map|css)", serveStatic); 30 | 31 | // configure oak application 32 | const app = new Application(); 33 | // send http requests to the LiveViewJS server 34 | app.use(liveServer.httpMiddleware); 35 | app.use(logRequests(liveRouter)); 36 | // add oak router to app 37 | app.use(router.routes()); 38 | app.use(router.allowedMethods()); 39 | 40 | // listen for requests 41 | const port = Number(Deno.env.get("PORT") ?? 9001); 42 | console.log(`LiveViewJS (Deno) is listening at: http://localhost:${port}`); 43 | await app.listen({ port }); 44 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/server/liveview/hello.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/liveview/hello.ts 3 | --- 4 | import { createLiveView, html } from "liveviewjs"; 5 | 6 | /** 7 | * A simple LiveView that toggles between "Hello" and "👋" when the button is clicked. 8 | */ 9 | export const helloLive = createLiveView({ 10 | mount: (socket, _, params) => { 11 | socket.assign({ name: params.name || "<%= h.inflection.camelize(name, false) %>", useEmoji: true }); 12 | }, 13 | handleEvent(event, socket) { 14 | if (event.type === "toggle") { 15 | socket.assign({ useEmoji: !socket.context.useEmoji }); 16 | } 17 | }, 18 | render: (context) => { 19 | const { useEmoji, name } = context; 20 | const hello = useEmoji ? "👋" : "Hello"; 21 | return html` 22 |
23 |
24 |

${hello} ${name}

25 | 28 |
29 |
30 | More documentation and examples at 31 | LiveViewJS.com 34 |
35 |
36 | `; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/server/liveview/router.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/liveview/router.ts 3 | --- 4 | import { LiveViewRouter } from "liveviewjs"; 5 | import { helloLive } from "./hello.ts"; 6 | 7 | // configure LiveView routes for <%= h.inflection.camelize(name, false) %> 8 | export const liveRouter: LiveViewRouter = { 9 | "/hello": helloLive, 10 | "/hello/:name": helloLive, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/src/server/oak.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/oak.ts 3 | --- 4 | import { Context, LiveViewRouter, send } from "../deps.ts"; 5 | 6 | export function logRequests( 7 | liveRouter: LiveViewRouter 8 | ): (ctx: Context>, next: () => Promise) => void { 9 | return async (ctx: Context>, next: () => Promise) => { 10 | const { request } = ctx; 11 | const { url, method } = request; 12 | const path = url.pathname; 13 | const isLiveView = liveRouter[path] !== undefined; 14 | console.log(`${method} ${isLiveView ? "LiveView" : ""} ${url} - ${new Date().toISOString()}`); 15 | await next(); 16 | }; 17 | } 18 | 19 | export const serveStatic = async (ctx: Context>) => { 20 | const path = ctx.request.url.pathname; 21 | await send(ctx, path, { 22 | root: "./public/", 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/gen/_templates/deno-project/new/tsconfig-client.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/tsconfig-client.json 3 | --- 4 | { 5 | "compilerOptions": { 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "strictPropertyInitialization": false, 13 | "declaration": true, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "target": "ES2020", 18 | "moduleResolution": "node", 19 | "lib": ["DOM"], 20 | "types": ["node"], 21 | "outDir": "./build", 22 | "baseUrl": "." 23 | }, 24 | "include": ["./src/client/*"], 25 | "exclude": ["./src/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/.gitignore.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.gitignore 3 | --- 4 | public/*.png 5 | public/*.gif 6 | public/*.jpg 7 | public/*.jpeg 8 | public/*.pdf 9 | public/js/ 10 | build/ 11 | node_modules/ 12 | public/css/ 13 | 14 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/.npmrc.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.npmrc 3 | --- 4 | engine-strict=true 5 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/.prettierignore.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.prettierignore 3 | --- 4 | node_modules 5 | .cache/ 6 | .env 7 | dist/ 8 | .parcel-cache/ 9 | .DS_Store 10 | coverage/ 11 | package-lock.json 12 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/.prettierrc.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/.prettierrc 3 | --- 4 | { 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 120, 8 | "bracketSameLine": true 9 | } 10 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/README.md.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/README.md 3 | --- 4 | # <%= h.inflection.camelize(name, false) %> 5 | 6 | ## About 7 | This is my awesome LiveViewJS project. There are many like it but this one is mine. 8 | 9 | ## Features 10 | * Live, dynamic user experiences without complex front-end code 11 | * Easy, automatic state management between server and client 12 | * Simple, powerful form validation using changesets 13 | * Batteries included file upload support with DnD, progress, and image previews 14 | * Real-time, multiplayer capabilities with PubSub 15 | 16 | ## Routes 17 | * / - Index redirects to /hello 18 | * /hello - simple hello world LiveView 19 | * /hello/:name - simple hello world LiveView with dynamic name 20 | 21 | ## Running Locally 22 | `npm install` 23 | `npm run dev` 24 | 25 | ## More documentation 26 | Visit [LiveViewJS](https://liveviewjs.com) for more documentation, examples, and guides. 27 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/package.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/package.json 3 | --- 4 | { 5 | "name": "<%= name %>", 6 | "version": "0.0.1", 7 | "description": "A starter project for LiveViewJS with NodeJS", 8 | "scripts": { 9 | "dev": "ts-node ./src/server/autorun.ts", 10 | "clean": "rm -rf build; rm -rf dist", 11 | "format": "prettier --write '**/*.{ts,js,json,html,css}'" 12 | }, 13 | "keywords": [ 14 | "liveviewjs", 15 | "liveview", 16 | "phoenix", 17 | "typescript", 18 | "javascript", 19 | "express" 20 | ], 21 | "dependencies": { 22 | "@liveviewjs/express": "*", 23 | "express": "^4.17.2", 24 | "express-session": "^1.17.2", 25 | "jsonwebtoken": "^8.5.1", 26 | "liveviewjs": "*", 27 | "nanoid": "^3.2.0", 28 | "ws": "^8.8.1" 29 | }, 30 | "devDependencies": { 31 | "@types/express": "^4.17.13", 32 | "@types/express-session": "^1.17.4", 33 | "@types/jsonwebtoken": "^8.5.8", 34 | "@types/node": "^18.7.8", 35 | "@types/nprogress": "^0.2.0", 36 | "@types/phoenix": "^1.5.4", 37 | "@types/phoenix_live_view": "^0.15.1", 38 | "@types/ws": "^8.5.3", 39 | "chalk": "^4.1.2", 40 | "esbuild": "^0.14.53", 41 | "nprogress": "^0.2.0", 42 | "phoenix": "^1.6.12", 43 | "phoenix_html": "^3.2.0", 44 | "phoenix_live_view": "^0.18.0", 45 | "tailwindcss": "^3.2.4", 46 | "ts-node": "^10.9.1", 47 | "typescript": "^4.5.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/public/.keep.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/public/.keep 3 | --- 4 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/src/client/index.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/client/index.ts 3 | --- 4 | import NProgress from "nprogress"; 5 | import { Socket } from "phoenix"; 6 | import "phoenix_html"; 7 | import { LiveSocket } from "phoenix_live_view"; 8 | 9 | const url = "/live"; 10 | let csrfToken = document.querySelector("meta[name='csrf-token']")?.getAttribute("content"); 11 | let liveSocket = new LiveSocket(url, Socket, { params: { _csrf_token: csrfToken }, hooks: {} }); 12 | window.addEventListener("phx:refresh", (e) => { 13 | console.log("phx:refresh", e); 14 | }); 15 | 16 | // Show progress bar on live navigation and form submits 17 | window.addEventListener("phx:page-loading-start", (info) => NProgress.start()); 18 | window.addEventListener("phx:page-loading-stop", (info) => NProgress.done()); 19 | 20 | // connect if there are any LiveViews on the page 21 | liveSocket.connect(); 22 | 23 | // expose liveSocket on window for web console debug logs and latency simulation: 24 | // liveSocket.enableDebug(); 25 | // liveSocket.enableLatencySim(1000) 26 | (window as any).liveSocket = liveSocket; 27 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/src/server/index.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/index.ts 3 | --- 4 | import { NodeExpressLiveViewServer } from "@liveviewjs/express"; 5 | import { Server } from "http"; 6 | import { WebSocketServer } from "ws"; 7 | import { configureExpress, indexHandler, logRequests } from "./express"; 8 | import { htmlPageTemplate } from "./liveTemplates"; 9 | import { liveRouter } from "./liveview/router"; 10 | 11 | // basic server options 12 | const signingSecret = process.env.SESSION_SECRET ?? "MY_VERY_SECRET_KEY"; 13 | const port = process.env.PORT ?? 4001; 14 | 15 | // configure LiveViewJS server 16 | const liveServer = new NodeExpressLiveViewServer(liveRouter, htmlPageTemplate, signingSecret, { 17 | title: "<%= h.inflection.camelize(name, false) %>", 18 | suffix: " · LiveViewJS", 19 | }); 20 | 21 | // configure express server 22 | const express = configureExpress(signingSecret); 23 | express.use(liveServer.httpMiddleware); // allow LiveViewJS to handle LiveView http requests 24 | express.use(logRequests(liveRouter)); // middleware to log requests 25 | express.get("/", indexHandler); // index route handler 26 | 27 | // configure http server to send requests to express 28 | const server = new Server(); 29 | server.on("request", express); 30 | 31 | // configure websocket server to send requests to LiveViewJS 32 | const ws = new WebSocketServer({ server }); 33 | ws.on("connection", liveServer.wsMiddleware); 34 | 35 | // listen for requests 36 | server.listen(port, () => { 37 | console.log(`<%= h.inflection.camelize(name, false) %> is listening at: http://localhost:${port}`); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/src/server/liveview/hello.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/liveview/hello.ts 3 | --- 4 | import { createLiveView, html } from "liveviewjs"; 5 | 6 | /** 7 | * A simple LiveView that toggles between "Hello" and "👋" when the button is clicked. 8 | */ 9 | export const helloLive = createLiveView({ 10 | mount: (socket, _, params) => { 11 | socket.assign({ name: params.name || "<%= h.inflection.camelize(name, false) %>", useEmoji: true }); 12 | }, 13 | handleEvent(event, socket) { 14 | if (event.type === "toggle") { 15 | socket.assign({ useEmoji: !socket.context.useEmoji }); 16 | } 17 | }, 18 | render: (context) => { 19 | const { useEmoji, name } = context; 20 | const hello = useEmoji ? "👋" : "Hello"; 21 | return html` 22 |
23 |
24 |

${hello} ${name}

25 | 28 |
29 |
30 | More documentation and examples at 31 | LiveViewJS.com 34 |
35 |
36 | `; 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/src/server/liveview/router.ts.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/src/server/liveview/router.ts 3 | --- 4 | import { LiveViewRouter } from "liveviewjs"; 5 | import { helloLive } from "./hello"; 6 | 7 | // configure LiveView routes for <%= h.inflection.camelize(name, false) %> 8 | export const liveRouter: LiveViewRouter = { 9 | "/hello": helloLive, 10 | "/hello/:name": helloLive, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/tsconfig-client.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/tsconfig-client.json 3 | --- 4 | { 5 | "compilerOptions": { 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "strictPropertyInitialization": false, 13 | "declaration": true, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "target": "ES2020", 18 | "moduleResolution": "node", 19 | "lib": ["DOM"], 20 | "types": ["node"], 21 | "outDir": "./build", 22 | "baseUrl": "." 23 | }, 24 | "include": ["./src/client/*"], 25 | "exclude": ["build", "node_modules", "./**/*.test.ts"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/gen/_templates/node-project/new/tsconfig.json.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: <%= h.changeCase.lower(name) %>/tsconfig.json 3 | --- 4 | { 5 | "compilerOptions": { 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "strictPropertyInitialization": false, 13 | "declaration": true, 14 | "resolveJsonModule": true, 15 | "strict": true, 16 | "skipLibCheck": true, 17 | "target": "es2019", 18 | "moduleResolution": "node", 19 | "lib": ["es2019", "esnext.asynciterable"], 20 | "types": ["node"], 21 | "outDir": "./build", 22 | "baseUrl": "." 23 | }, 24 | "include": ["./src/**/*"], 25 | "exclude": ["build", "node_modules", "./**/*.test.ts", "./src/client/**/*"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/gen/dist/cli.d.mts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export {}; 3 | -------------------------------------------------------------------------------- /packages/gen/dist/null_logger.d.mts: -------------------------------------------------------------------------------- 1 | import { Logger } from "hygen"; 2 | export declare class NullLogger extends Logger { 3 | constructor(); 4 | } 5 | -------------------------------------------------------------------------------- /packages/gen/dist/null_logger.mjs: -------------------------------------------------------------------------------- 1 | import { Logger } from "hygen"; 2 | // logger that ignores all messages 3 | export class NullLogger extends Logger { 4 | constructor() { 5 | super(() => { }); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/gen/dist/post_exec.d.mts: -------------------------------------------------------------------------------- 1 | import { GeneratorType } from "./prompts.mjs"; 2 | export declare function changeDirMsg(type: GeneratorType, name: string): string | undefined; 3 | export declare function installMsg(type: GeneratorType, install: boolean): "Run `npm install` to install node dependencies." | "Run `npm install` to install node dependencies for client-side javascript." | undefined; 4 | export declare function runMsg(type: GeneratorType): "Run `npm run dev` to start your LiveViewJS project.\"" | "Run `deno run --allow-run --allow-read --allow-write --allow-net --allow-env src/server/autorun.ts` to start your LiveViewJS project.\"" | undefined; 5 | -------------------------------------------------------------------------------- /packages/gen/dist/post_exec.mjs: -------------------------------------------------------------------------------- 1 | export function changeDirMsg(type, name) { 2 | switch (type) { 3 | case "node-project": 4 | case "deno-project": 5 | return `cd ${name.toLowerCase()}`; 6 | default: 7 | return undefined; 8 | } 9 | } 10 | export function installMsg(type, install) { 11 | switch (type) { 12 | case "node-project": 13 | return install ? undefined : "Run `npm install` to install node dependencies."; 14 | case "deno-project": 15 | return install ? undefined : "Run `npm install` to install node dependencies for client-side javascript."; 16 | default: 17 | return undefined; 18 | } 19 | } 20 | export function runMsg(type) { 21 | switch (type) { 22 | case "node-project": 23 | return 'Run `npm run dev` to start your LiveViewJS project."'; 24 | case "deno-project": 25 | return 'Run `deno run --allow-run --allow-read --allow-write --allow-net --allow-env src/server/autorun.ts` to start your LiveViewJS project."'; 26 | default: 27 | return undefined; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/gen/dist/project_helper.d.mts: -------------------------------------------------------------------------------- 1 | export type CreateAppArgs = { 2 | projectType: "node-project" | "deno-project"; 3 | projectDir?: string; 4 | install?: boolean; 5 | quiet?: boolean; 6 | }; 7 | declare function createApp({ projectType, projectDir, install, quiet }: CreateAppArgs): Promise; 8 | export { createApp }; 9 | -------------------------------------------------------------------------------- /packages/gen/dist/prompts.d.mts: -------------------------------------------------------------------------------- 1 | import { Prompt } from "enquirer"; 2 | type PromptOptions = NonNullable[0]>; 3 | export declare const GeneratorTypes: readonly ["node-project", "deno-project"]; 4 | export type GeneratorType = typeof GeneratorTypes[number]; 5 | export declare const GeneratorTypePromptOptions: PromptOptions; 6 | export declare const NamePromptOptions: PromptOptions; 7 | export declare const NpmInstallPromptOptions: PromptOptions; 8 | export {}; 9 | -------------------------------------------------------------------------------- /packages/gen/dist/prompts.mjs: -------------------------------------------------------------------------------- 1 | export const GeneratorTypes = ["node-project", "deno-project"]; 2 | export const GeneratorTypePromptOptions = { 3 | type: "select", 4 | name: "generator", 5 | message: "What LiveViewJS generator would you like to run?", 6 | choices: GeneratorTypes, 7 | }; 8 | export const NamePromptOptions = { 9 | type: "input", 10 | name: "name", 11 | message: "What should we call this project?", 12 | }; 13 | export const NpmInstallPromptOptions = { 14 | type: "confirm", 15 | name: "install", 16 | message: "Should we run npm install for you?", 17 | }; 18 | -------------------------------------------------------------------------------- /packages/gen/dist/yargs.d.mts: -------------------------------------------------------------------------------- 1 | export interface GenYargs { 2 | generator?: string; 3 | name?: string; 4 | quiet?: boolean; 5 | force?: boolean; 6 | } 7 | export declare const genYargs: (argv: string[]) => GenYargs; 8 | export interface ProjectYargs { 9 | install?: boolean; 10 | } 11 | export declare const projYargs: (argv: string[]) => ProjectYargs; 12 | -------------------------------------------------------------------------------- /packages/gen/dist/yargs.mjs: -------------------------------------------------------------------------------- 1 | import yargs from "yargs/yargs"; 2 | export const genYargs = (argv) => { 3 | return yargs(argv) 4 | .options({ 5 | generator: { 6 | type: "string", 7 | alias: "g", 8 | description: "Generator to run", 9 | }, 10 | name: { 11 | type: "string", 12 | alias: "n", 13 | description: "Name of the project", 14 | }, 15 | quiet: { 16 | type: "boolean", 17 | alias: "q", 18 | description: "Suppress all output", 19 | }, 20 | force: { 21 | type: "boolean", 22 | alias: "f", 23 | description: "Overwrite existing files", 24 | }, 25 | }) 26 | .parseSync(); 27 | }; 28 | export const projYargs = (argv) => { 29 | return yargs(argv) 30 | .usage("Usage: $0 [generator] [args]") 31 | .options({ 32 | install: { 33 | type: "boolean", 34 | alias: "i", 35 | description: "Run npm install", 36 | }, 37 | }) 38 | .parseSync(); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/gen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@liveviewjs/gen", 3 | "version": "0.10.4", 4 | "description": "Code generators for LiveViewJS", 5 | "homepage": "https://liveviewjs.com", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/floodfx/liveviewjs", 10 | "directory": "packages/gen" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/floodfx/liveviewjs/issues" 14 | }, 15 | "type": "module", 16 | "main": "./dist/cli.mjs", 17 | "bin": "./dist/cli.mjs", 18 | "types": "./dist/cli.d.mts", 19 | "files": [ 20 | "dist/**/*", 21 | "_templates/**/*" 22 | ], 23 | "scripts": { 24 | "build": "tsc", 25 | "watch": "tsc --watch", 26 | "clean": "rm -rf dist", 27 | "dist": "npm run clean; npm run build", 28 | "prepublish": "npm run dist", 29 | "format": "prettier --write '**/*.{ts,js,json,html,css}'" 30 | }, 31 | "keywords": [ 32 | "liveviewjs", 33 | "liveview", 34 | "phoenix", 35 | "typescript", 36 | "javascript", 37 | "framework" 38 | ], 39 | "dependencies": { 40 | "chalk": "^5.2.0", 41 | "cli-spinners": "^2.7.0", 42 | "enquirer": "^2.3.6", 43 | "execa": "^6.1.0", 44 | "fs-extra": "^11.1.0", 45 | "hygen": "^6.2.11", 46 | "log-update": "^5.0.1", 47 | "yargs": "^17.6.2" 48 | }, 49 | "devDependencies": { 50 | "@rollup/plugin-commonjs": "^24.0.1", 51 | "@rollup/plugin-json": "^6.0.0", 52 | "@rollup/plugin-node-resolve": "^13.1.3", 53 | "@rollup/plugin-typescript": "^8.3.1", 54 | "@types/execa": "^2.0.0", 55 | "@types/figlet": "^1.5.5", 56 | "@types/fs-extra": "^11.0.1", 57 | "@types/node": "^18.11.18", 58 | "rollup": "^2.70.1", 59 | "ts-node": "^10.9.1", 60 | "typescript": "^4.9.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/gen/src/null_logger.mts: -------------------------------------------------------------------------------- 1 | import { Logger } from "hygen"; 2 | 3 | // logger that ignores all messages 4 | export class NullLogger extends Logger { 5 | constructor() { 6 | super(() => {}); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/gen/src/post_exec.mts: -------------------------------------------------------------------------------- 1 | import { GeneratorType } from "./prompts.mjs"; 2 | 3 | export function changeDirMsg(type: GeneratorType, name: string) { 4 | switch (type) { 5 | case "node-project": 6 | case "deno-project": 7 | return `cd ${name.toLowerCase()}`; 8 | default: 9 | return undefined; 10 | } 11 | } 12 | 13 | export function installMsg(type: GeneratorType, install: boolean) { 14 | switch (type) { 15 | case "node-project": 16 | return install ? undefined : "Run `npm install` to install node dependencies."; 17 | case "deno-project": 18 | return install ? undefined : "Run `npm install` to install node dependencies for client-side javascript."; 19 | default: 20 | return undefined; 21 | } 22 | } 23 | 24 | export function runMsg(type: GeneratorType) { 25 | switch (type) { 26 | case "node-project": 27 | return 'Run `npm run dev` to start your LiveViewJS project."'; 28 | case "deno-project": 29 | return 'Run `deno run --allow-run --allow-read --allow-write --allow-net --allow-env src/server/autorun.ts` to start your LiveViewJS project."'; 30 | default: 31 | return undefined; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/gen/src/prompts.mts: -------------------------------------------------------------------------------- 1 | import { Prompt } from "enquirer"; 2 | 3 | // extract options type from Prompt constructor 4 | type PromptOptions = NonNullable[0]>; 5 | 6 | export const GeneratorTypes = ["node-project", "deno-project"] as const; 7 | export type GeneratorType = typeof GeneratorTypes[number]; 8 | 9 | export const GeneratorTypePromptOptions: PromptOptions = { 10 | type: "select", 11 | name: "generator", 12 | message: "What LiveViewJS generator would you like to run?", 13 | choices: GeneratorTypes as unknown as string[], 14 | }; 15 | 16 | export const NamePromptOptions: PromptOptions = { 17 | type: "input", 18 | name: "name", 19 | message: "What should we call this project?", 20 | }; 21 | 22 | export const NpmInstallPromptOptions: PromptOptions = { 23 | type: "confirm", 24 | name: "install", 25 | message: "Should we run npm install for you?", 26 | }; 27 | -------------------------------------------------------------------------------- /packages/gen/src/yargs.mts: -------------------------------------------------------------------------------- 1 | import yargs from "yargs/yargs"; 2 | 3 | export interface GenYargs { 4 | generator?: string; 5 | name?: string; 6 | quiet?: boolean; 7 | force?: boolean; 8 | } 9 | 10 | export const genYargs = (argv: string[]): GenYargs => { 11 | return yargs(argv) 12 | .options({ 13 | generator: { 14 | type: "string", 15 | alias: "g", 16 | description: "Generator to run", 17 | }, 18 | name: { 19 | type: "string", 20 | alias: "n", 21 | description: "Name of the project", 22 | }, 23 | quiet: { 24 | type: "boolean", 25 | alias: "q", 26 | description: "Suppress all output", 27 | }, 28 | force: { 29 | type: "boolean", 30 | alias: "f", 31 | description: "Overwrite existing files", 32 | }, 33 | }) 34 | .parseSync(); 35 | }; 36 | 37 | export interface ProjectYargs { 38 | install?: boolean; 39 | } 40 | 41 | export const projYargs = (argv: string[]): ProjectYargs => { 42 | return yargs(argv) 43 | .usage("Usage: $0 [generator] [args]") 44 | .options({ 45 | install: { 46 | type: "boolean", 47 | alias: "i", 48 | description: "Run npm install", 49 | }, 50 | }) 51 | .parseSync(); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/gen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "esModuleInterop": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "strictPropertyInitialization": false, 10 | "declaration": true, 11 | "resolveJsonModule": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "target": "esnext", 15 | "lib": ["esnext"], 16 | "moduleResolution": "node", 17 | "types": ["node"], 18 | "outDir": "./dist", 19 | "baseUrl": "." 20 | }, 21 | "include": ["src/**/*"], 22 | "exclude": ["dist", "node_modules/**/*", "**/*.test.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "format": { 5 | }, 6 | "dist": { 7 | "dependsOn": ["^dist", "test"], 8 | "outputs": ["dist/**", "build/**"], 9 | "inputs": ["src/**/*.ts"] 10 | }, 11 | "test": { 12 | "outputs": ["coverage/**"], 13 | "inputs": ["src/**/*.ts"] 14 | }, 15 | "publish": { 16 | "dependsOn": ["test", "dist"], 17 | "cache": false 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------