├── modules ├── @apostrophecms │ ├── home-page │ │ ├── public │ │ │ └── public.txt │ │ ├── index.js │ │ └── views │ │ │ └── page.html │ ├── express │ │ └── index.js │ ├── asset │ │ └── index.js │ ├── page │ │ ├── views │ │ │ └── notFound.html │ │ └── index.js │ └── settings │ │ └── index.js ├── asset │ ├── index.js │ └── ui │ │ ├── src │ │ ├── tailwind.css │ │ ├── index.js │ │ ├── scss │ │ │ ├── _widgets.scss │ │ │ ├── _settings.scss │ │ │ ├── _nav.scss │ │ │ ├── _apps.scss │ │ │ ├── _layout.scss │ │ │ ├── _type.scss │ │ │ └── _welcome.scss │ │ └── main.scss │ │ └── svg │ │ └── vite.svg ├── counter-page │ ├── views │ │ └── page.html │ └── index.js ├── counter │ ├── ui │ │ └── src │ │ │ ├── index.js │ │ │ └── player.js │ ├── views │ │ └── counterApp.html │ └── index.js ├── counter-vue-widget │ ├── index.js │ ├── views │ │ └── widget.html │ └── ui │ │ └── src │ │ ├── app │ │ ├── assets │ │ │ └── vue.svg │ │ └── App.vue │ │ └── counter-vue.js ├── counter-react-widget │ ├── index.js │ ├── views │ │ └── widget.html │ └── ui │ │ └── src │ │ ├── counter-react.js │ │ └── app │ │ ├── App.jsx │ │ └── assets │ │ └── react.svg ├── counter-svelte-widget │ ├── index.js │ ├── views │ │ └── widget.html │ └── ui │ │ └── src │ │ ├── counter-svelte.js │ │ └── app │ │ ├── assets │ │ └── svelte.svg │ │ └── App.svelte └── vite-react │ ├── views │ └── reactRefresh.html │ └── index.js ├── .eslintignore ├── public └── images │ └── logo.png ├── postcss.config.js ├── .eslintrc ├── deployment ├── README ├── settings.staging ├── migrate ├── rsync_exclude.txt ├── stop ├── settings ├── dependencies └── start ├── svelte.config.js ├── compose.yaml ├── tailwind.config.js ├── jsconfig.json ├── .npmrc ├── .gitignore ├── apos.vite.config.js ├── LICENSE ├── .devcontainer ├── docker-compose.yml ├── Dockerfile └── devcontainer.json ├── scripts ├── sync-down └── sync-up ├── package.json ├── app.js ├── views └── layout.html └── README.md /modules/@apostrophecms/home-page/public/public.txt: -------------------------------------------------------------------------------- 1 | home-page 2 | -------------------------------------------------------------------------------- /modules/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | init() { } 3 | }; 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /public/apos-frontend 2 | /data/temp 3 | /apos-build 4 | /modules/asset/ui/public 5 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apostrophecms/vite-demo/main/public/images/logo.png -------------------------------------------------------------------------------- /modules/asset/ui/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | -------------------------------------------------------------------------------- /modules/asset/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import './tailwind.css'; 2 | import './main.scss'; 3 | 4 | export default () => {}; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_widgets.scss: -------------------------------------------------------------------------------- 1 | .bp-video-widget { 2 | width: 100%; 3 | } 4 | 5 | .bp-image-widget { 6 | max-width: 100%; 7 | } -------------------------------------------------------------------------------- /modules/counter-page/views/page.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block main %} 4 | {% area data.page, 'main' %} 5 | {% endblock %} 6 | 7 | 8 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_settings.scss: -------------------------------------------------------------------------------- 1 | $color-purple: #6236ff; 2 | $color-purple-light: #8264f1; 3 | $color-pink: #fe5599; 4 | $color-green: #0c8; 5 | $color-gold: #f7b500; -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "apostrophe" 4 | ], 5 | "globals": { 6 | "apos": true 7 | }, 8 | "rules": { 9 | "no-var": "error", 10 | "no-console": 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/@apostrophecms/express/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | session: { 4 | // If this still says `undefined`, set a real secret! 5 | secret: 'supersecret' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /modules/@apostrophecms/asset/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // When not in production, refresh the page on restart 3 | options: { 4 | // hmr: 'apos', 5 | // productionSourceMaps: true, 6 | refreshOnRestart: true 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /deployment/README: -------------------------------------------------------------------------------- 1 | This is a deployment folder for use with Stagecoach. 2 | 3 | You don't have to use Stagecoach. 4 | 5 | It's just a neat solution for deploying node apps. 6 | 7 | See: 8 | 9 | http://github.com/apostrophecms/stagecoach 10 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess() 7 | }; 8 | -------------------------------------------------------------------------------- /modules/counter/ui/src/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from './player.js'; 2 | 3 | export default () => { 4 | // Demonstrating how one can pass helper functions to be used by other modules 5 | // apart from a direct import via aliasing. 6 | apos.util.parsePlayerData = parse; 7 | }; 8 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mongo: 3 | image: mongo:8 4 | # Preserve data between container restarts 5 | volumes: 6 | - apos-mongodb-data:/data/db 7 | ports: 8 | - "27017:27017" 9 | expose: 10 | - "27017" 11 | volumes: 12 | apos-mongodb-data: 13 | -------------------------------------------------------------------------------- /deployment/settings.staging: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Settings specific to the 'master' deployment target. 4 | # USER is the ssh user, SERVER is the ssh host. USER should 5 | # match the USER setting in /opt/stagecoach/settings on 6 | # the server 7 | 8 | USER=nodeapps 9 | SERVER=staging.apos.dev 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './apos-build/@apostrophecms/vite/default/src/**/*', 5 | './modules/**/views/**/*.html', 6 | './views/**/*.html' 7 | ], 8 | theme: { 9 | extend: {} 10 | }, 11 | plugins: [] 12 | }; 13 | -------------------------------------------------------------------------------- /deployment/migrate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Run any necessary database migration tasks that should happen while the 6 | # site is paused here. 7 | # 8 | # We don't have any, 3.x policy is safe migrations only. -Tom 9 | 10 | # node app @apostrophecms/migration:migrate 11 | # 12 | #echo "Site migrated" 13 | -------------------------------------------------------------------------------- /modules/counter/views/counterApp.html: -------------------------------------------------------------------------------- 1 |
6 | {# Async Server Component defined in `modules/counter/index.js` #} 7 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/views/notFound.html: -------------------------------------------------------------------------------- 1 | {# 2 | Use this template to build out your 404 error pages. Like page templates, 3 | it inherits a global layout. 4 | #} 5 | 6 | {% extends "layout.html" %} 7 | 8 | {% block title %}404 - Page not found{% endblock %} 9 | 10 | {% block main %} 11 | We're sorry. We couldn't find the page you're looking for. 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./apos-build/@apostrophecms/vite/default", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | }, 7 | "module": "ESNext", 8 | "moduleResolution": "bundler" 9 | }, 10 | "exclude": [ 11 | "apos-build/@apostrophecms/vite/default/dist", 12 | "node_modules", 13 | "public", 14 | "data" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_nav.scss: -------------------------------------------------------------------------------- 1 | .nav-items { 2 | display: flex; 3 | flex-wrap: wrap; 4 | list-style: none; 5 | } 6 | 7 | .nav-item { 8 | margin: 0 20px; 9 | } 10 | 11 | .nav-link { 12 | display: inline-block; 13 | padding: 5px; 14 | text-decoration: none; 15 | color: #52648e; 16 | text-decoration: none; 17 | &:hover { 18 | text-decoration: underline 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/asset/ui/src/main.scss: -------------------------------------------------------------------------------- 1 | @import url('normalize.css'); 2 | // NOTE: We're using a `.bp-` namespace to indicate these are the boilerplate 3 | // styles. 4 | @import './scss/_settings'; 5 | @import './scss/_type'; 6 | @import './scss/_layout'; 7 | @import './scss/_welcome'; 8 | @import './scss/_nav'; 9 | @import './scss/_widgets'; 10 | // Counter apps global styles 11 | @import './scss/_apps'; 12 | -------------------------------------------------------------------------------- /modules/@apostrophecms/page/index.js: -------------------------------------------------------------------------------- 1 | // This configures the @apostrophecms/page module to add a "home" page type to the 2 | // pages menu 3 | 4 | export default { 5 | options: { 6 | types: [ 7 | { 8 | name: 'counter-page', 9 | label: 'Counter Apps Page' 10 | }, 11 | { 12 | name: '@apostrophecms/home-page', 13 | label: 'Home' 14 | } 15 | ] 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /modules/counter-vue-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'Vue Counter App' 5 | }, 6 | build: { 7 | vite: { 8 | bundles: { 9 | 'counter-vue': {} 10 | } 11 | } 12 | }, 13 | fields: { 14 | add: { 15 | title: { 16 | label: 'Title', 17 | type: 'string', 18 | required: true 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /modules/counter-react-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'React Counter App' 5 | }, 6 | build: { 7 | vite: { 8 | bundles: { 9 | 'counter-react': {} 10 | } 11 | } 12 | }, 13 | fields: { 14 | add: { 15 | title: { 16 | label: 'Title', 17 | type: 'string', 18 | required: true 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /modules/counter-svelte-widget/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/widget-type', 3 | options: { 4 | label: 'Svelte Counter App' 5 | }, 6 | build: { 7 | vite: { 8 | bundles: { 9 | 'counter-svelte': {} 10 | } 11 | } 12 | }, 13 | fields: { 14 | add: { 15 | title: { 16 | label: 'Title', 17 | type: 'string', 18 | required: true 19 | } 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /modules/counter-react-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {# 2 | Send the data to the counterApp component and let it render the markup. 3 | `options` is the object defined in the area schema. 4 | Use a generic `counterApp` component to render the markup. 5 | See `modules/counter/views/counterApp.html` 6 | #} 7 | {% component 'counter:counterApp' with { 8 | framework: "react", 9 | widget: data.widget, 10 | page: data.page, 11 | options: data.options 12 | } %} 13 | -------------------------------------------------------------------------------- /modules/counter-vue-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {# 2 | Send the data to the counterApp component and let it render the markup. 3 | `options` is the object defined in the area schema. 4 | Use a generic `counterApp` component to render the markup. 5 | See `modules/counter/views/counterApp.html` 6 | #} 7 | {% component 'counter:counterApp' with { 8 | framework: "vue", 9 | widget: data.widget, 10 | page: data.page, 11 | options: data.options 12 | } %} 13 | -------------------------------------------------------------------------------- /modules/@apostrophecms/settings/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | subforms: { 4 | title: { 5 | fields: [ 'title' ], 6 | protection: true, 7 | reload: true 8 | }, 9 | changePassword: { 10 | fields: [ 'password' ] 11 | } 12 | }, 13 | 14 | groups: { 15 | account: { 16 | label: 'Account', 17 | subforms: [ 'title', 'changePassword' ] 18 | } 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /modules/counter-svelte-widget/views/widget.html: -------------------------------------------------------------------------------- 1 | {# 2 | Send the data to the counterApp component and let it render the markup. 3 | `options` is the object defined in the area schema. 4 | Use a generic `counterApp` component to render the markup. 5 | See `modules/counter/views/counterApp.html` 6 | #} 7 | {% component 'counter:counterApp' with { 8 | framework: "svelte", 9 | widget: data.widget, 10 | page: data.page, 11 | options: data.options 12 | } %} 13 | -------------------------------------------------------------------------------- /modules/vite-react/views/reactRefresh.html: -------------------------------------------------------------------------------- 1 | {# From the official documentation for custom integration of React: 2 | https://vite.dev/guide/backend-integration.html #} 3 | 10 | -------------------------------------------------------------------------------- /modules/counter-vue-widget/ui/src/app/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # ; All packages are hoisted to node_modules/.pnpm/node_modules 2 | # hoist-pattern[]=* 3 | 4 | # ; All types are hoisted to the root in order to make TypeScript happy 5 | # public-hoist-pattern[]=*types* 6 | 7 | # ; All ESLint-related packages are hoisted to the root as well 8 | # public-hoist-pattern[]=*eslint* 9 | # public-hoist-pattern[]=*tiptap* 10 | # public-hoist-pattern[]=*vue* 11 | 12 | # This is what works for the apos build because 13 | # of "dependencies of dependencies" that should be hoisted 14 | shamefully-hoist=true 15 | strict-peer-dependencies=true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | /locales 3 | npm-debug.log 4 | /data 5 | /public/uploads 6 | /public/apos-minified 7 | /data/temp/uploadfs 8 | node_modules 9 | # This folder is created on the fly and contains symlinks updated at startup (we'll come up with a Windows solution that actually copies things) 10 | /public/modules 11 | # Don't commit build files 12 | /apos-build 13 | /public/apos-frontend 14 | /modules/asset/ui/public/site.js 15 | # Don't commit masters generated on the fly at startup, these import all the rest 16 | /public/css/master-*.less 17 | .jshintrc 18 | .DS_Store 19 | 20 | -------------------------------------------------------------------------------- /modules/counter/ui/src/player.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {HTMLElement} el 4 | * @returns {{id: string, editMode: string, widget: object, options: object}} 5 | */ 6 | export function parse(el) { 7 | if (!el) { 8 | return {}; 9 | } 10 | 11 | const id = el.dataset.aposWidgetId; 12 | const editMode = el.dataset.aposEditMode; 13 | const widget = el.dataset.aposWidget; 14 | const options = el.dataset.aposOptions; 15 | 16 | return { 17 | id, 18 | editMode, 19 | widget: JSON.parse(widget || '{}'), 20 | options: JSON.parse(options || '{}') 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /deployment/rsync_exclude.txt: -------------------------------------------------------------------------------- 1 | # List files and folders that shouldn't be deployed (such as data folders and runtime status files) here. 2 | # In our projects .git and .gitignore are good candidates, also 'data' which contains persistent files 3 | # that are *not* part of deployment. A good place for things like data/port, data/pid, and any 4 | # sqlite databases or static web content you may need 5 | data 6 | temp 7 | public/uploads 8 | public/apos-frontend 9 | .git 10 | .gitignore 11 | # We don't deploy these anymore, instead we always 'npm install' to ensure 12 | # that any compiled C++ modules are built for the right architecture 13 | node_modules 14 | -------------------------------------------------------------------------------- /modules/counter-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | extend: '@apostrophecms/page-type', 3 | options: { 4 | label: 'Counter Apps Page' 5 | }, 6 | fields: { 7 | add: { 8 | main: { 9 | type: 'area', 10 | options: { 11 | widgets: { 12 | 'counter-vue': { 13 | example: 'options from the counter-page schema' 14 | }, 15 | 'counter-svelte': { 16 | example: 'options from the counter-page schema' 17 | }, 18 | 'counter-react': { 19 | example: 'options from the counter-page schema' 20 | } 21 | } 22 | } 23 | } 24 | }, 25 | group: { 26 | basics: { 27 | label: 'Basics', 28 | fields: [ 29 | 'title', 30 | 'main' 31 | ] 32 | } 33 | } 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_apps.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 3.2em; 3 | line-height: 1.1; 4 | } 5 | 6 | .cbutton { 7 | border-radius: 8px; 8 | border: 1px solid transparent; 9 | padding: 0.6em 1.2em; 10 | font-size: 2em; 11 | font-weight: 500; 12 | font-family: inherit; 13 | background-color: #1a1a1a; 14 | cursor: pointer; 15 | transition: border-color 0.25s; 16 | color: white; 17 | } 18 | .cbutton:hover { 19 | border-color: #646cff; 20 | } 21 | .cbutton:focus, 22 | .cbutton:focus-visible { 23 | outline: 4px auto -webkit-focus-ring-color; 24 | } 25 | .card { 26 | padding: 2em; 27 | text-align: center; 28 | } 29 | 30 | .logo { 31 | height: 6em; 32 | padding: 1.5em; 33 | will-change: filter; 34 | transition: filter 300ms; 35 | } 36 | 37 | .logo:hover { 38 | filter: drop-shadow(0 0 2em #646cffaa); 39 | } 40 | 41 | .logo.react:hover { 42 | filter: drop-shadow(0 0 2em #61dafbaa); 43 | } 44 | -------------------------------------------------------------------------------- /deployment/stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Shut the site down, for instance by tweaking a .htaccess file to display 6 | # a 'please wait' notice, or stopping a node server 7 | 8 | if [ ! -f "app.js" ]; then 9 | echo "I don't see app.js in the current directory." 10 | exit 1 11 | fi 12 | 13 | # Stop the node app via 'forever'. You'll get a harmless warning if the app 14 | # was not already running. Use `pwd` to make sure we have a full path, 15 | # forever is otherwise easily confused and will stop every server with 16 | # the same filename 17 | forever stop `pwd`/app.js && echo "Site stopped" 18 | 19 | # Stop the app without 'forever'. We recommend using 'forever' for node apps, 20 | # but this may be your best bet for non-node apps 21 | # 22 | # if [ -f "data/pid" ]; then 23 | # kill `cat data/pid` 24 | # rm data/pid 25 | # echo "Site stopped" 26 | # else 27 | # echo "Site was not running" 28 | # fi 29 | 30 | -------------------------------------------------------------------------------- /deployment/settings: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Settings shared by all targets (staging, production, etc). Usually the 4 | # shortname of the project (which is also the hostname for the frontend 5 | # proxy server used for staging sites) and the directory name. For our 6 | # web apps that use sc-proxy we make sure each is a subdirectory 7 | # of /opt/stagecoach/apps 8 | 9 | # Should match the repo name = short name = everything name! 10 | PROJECT=a3-boilerplate 11 | 12 | DIR=/opt/stagecoach/apps/$PROJECT 13 | 14 | # Adjust the PATH environment variable on the remote host. Here's an example 15 | # for deploying to MacPorts 16 | #ADJUST_PATH='export PATH=/opt/local/bin:$PATH' 17 | 18 | # ... But you probably won't need to on real servers. I just find it handy for 19 | # testing parts of stagecoach locally on a Mac. : is an acceptable "no-op" (do-nothing) statement 20 | ADJUST_PATH=':' 21 | 22 | # ssh port. Sensible people leave this set to 22 but it's common to do the 23 | # "security by obscurity" thing alas 24 | SSH_PORT=22 25 | -------------------------------------------------------------------------------- /apos.vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@apostrophecms/vite/vite'; 2 | 3 | // import react from '@vitejs/plugin-react'; 4 | import vue from '@vitejs/plugin-vue'; 5 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 6 | 7 | import path from 'node:path'; 8 | 9 | export default defineConfig({ 10 | plugins: [ 11 | // See ./modules/vite-react/index.js for ReactJS integration via module 12 | // `build` configuration. 13 | vue(), 14 | svelte({ 15 | // We need to tell Svelte where to find the config file, 16 | // because the Vite root is not the same as the Apostrophe root. 17 | configFile: path.join(process.cwd(), 'svelte.config.js') 18 | }) 19 | ], 20 | server: { 21 | watch: { 22 | // So that Tailwind CSS changes in the nunjucks templates do not trigger Vite 23 | // page reloads. This is done by `nodemon` because we need a process restart. 24 | ignored: [ 25 | '**/modules/views/**/*.html', 26 | '**/views/**/*.html' 27 | ] 28 | } 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Apostrophe Technologies 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. 8 | -------------------------------------------------------------------------------- /modules/counter-svelte-widget/ui/src/counter-svelte.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte'; 2 | import { parse } from '@/counter/src/player'; 3 | import App from './app/App.svelte'; 4 | 5 | // Environments are available here (`import.meta.env.PROD`, `import.meta.env.DEV`, etc.) 6 | // https://vite.dev/guide/env-and-mode.html 7 | 8 | // Apos widget player is executed when needed - initial page load, widget 9 | // refresh, etc. 10 | export default () => { 11 | apos.util.widgetPlayers['counter-svelte'] = { 12 | selector: '[data-apos-svelte-widget]', 13 | player 14 | }; 15 | }; 16 | 17 | function player(el) { 18 | // 1. Do nothing if no element or if already mounted 19 | if (!el || el.childNodes.length > 0) { 20 | return; 21 | } 22 | // 2. Grab the data from the `data-*` attributes. 23 | // See `modules/asset/ui/src/index.js` for the implementation 24 | const { 25 | id, 26 | widget, 27 | options 28 | } = parse(el); 29 | 30 | // 3. Mount and render the app 31 | mount(App, { 32 | target: el, 33 | props: { 34 | id, 35 | widget, 36 | options 37 | } 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /modules/counter-vue-widget/ui/src/counter-vue.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { parse } from '@/counter/src/player'; 3 | import App from './app/App.vue'; 4 | 5 | // Environments are available here (`import.meta.env.PROD`, `import.meta.env.DEV`, etc.) 6 | // https://vite.dev/guide/env-and-mode.html 7 | 8 | // Apos widget player is executed when needed - initial page load, widget 9 | // refresh, etc. 10 | export default () => { 11 | apos.util.widgetPlayers['counter-vue'] = { 12 | selector: '[data-apos-vue-widget]', 13 | player 14 | }; 15 | }; 16 | 17 | function player(el) { 18 | // 1. Do nothing if no element or if already mounted 19 | if (!el || el.childNodes.length > 0) { 20 | return; 21 | } 22 | // 2. Grab the data from the `data-*` attributes. 23 | // See `modules/asset/ui/src/player.js` for the implementation. 24 | // Showcasing module aliasing (see the import statement at the top). 25 | const { 26 | id, 27 | widget, 28 | options 29 | } = parse(el); 30 | // 3. Mount and render the app 31 | createApp(App, { 32 | id, 33 | widget, 34 | options 35 | }).mount(el); 36 | } 37 | -------------------------------------------------------------------------------- /modules/counter-react-widget/ui/src/counter-react.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { createElement } from 'react'; 3 | import { parse } from '@/counter/src/player'; 4 | import App from './app/App.jsx'; 5 | 6 | // Environments are available here (`import.meta.env.PROD`, `import.meta.env.DEV`, etc.) 7 | // https://vite.dev/guide/env-and-mode.html 8 | 9 | // Apos widget player is executed when needed - initial page load, widget 10 | // refresh, etc. 11 | export default () => { 12 | apos.util.widgetPlayers['counter-react'] = { 13 | selector: '[data-apos-react-widget]', 14 | player 15 | }; 16 | }; 17 | 18 | function player(el) { 19 | // 1. Do nothing if no element or if already mounted 20 | if (!el || el.childNodes.length > 0) { 21 | return; 22 | } 23 | // 2. Grab the data from the `data-*` attributes. 24 | // See `modules/asset/ui/src/index.js` for the implementation 25 | const { 26 | id, 27 | widget, 28 | options 29 | } = parse(el); 30 | // 3. Mount and render the app 31 | const app = createElement(App, { 32 | id, 33 | widget, 34 | options 35 | }); 36 | createRoot(el).render(app); 37 | } 38 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - ../..:/workspaces:cached 10 | 11 | # Overrides default command so things don't shut down after the process ends. 12 | command: sleep infinity 13 | 14 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 15 | network_mode: service:db 16 | 17 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 18 | # (Adding the "ports" property to this file will not forward from a Codespace.) 19 | 20 | db: 21 | image: mongo:8 22 | restart: unless-stopped 23 | volumes: 24 | - mongodb-data:/data/db 25 | 26 | # Uncomment to change startup options 27 | # environment: 28 | # MONGO_INITDB_ROOT_USERNAME: root 29 | # MONGO_INITDB_ROOT_PASSWORD: example 30 | # MONGO_INITDB_DATABASE: your-database-here 31 | 32 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 33 | # (Adding the "ports" property to this file will not forward from a Codespace.) 34 | 35 | volumes: 36 | mongodb-data: 37 | -------------------------------------------------------------------------------- /scripts/sync-down: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TARGET="$1" 4 | if [ -z "$TARGET" ]; then 5 | echo "Usage: ./scripts/sync-down production" 6 | echo "(or as appropriate)" 7 | exit 1 8 | fi 9 | 10 | source deployment/settings || exit 1 11 | source "deployment/settings.$TARGET" || exit 1 12 | 13 | #Enter the Mongo DB name (should be same locally and remotely). 14 | dbName=$PROJECT 15 | 16 | #Enter the Project name (should be what you called it for stagecoach). 17 | projectName=$PROJECT 18 | 19 | #Enter the SSH username/url for the remote server. 20 | remoteSSH="-p $SSH_PORT $USER@$SERVER" 21 | rsyncTransport="ssh -p $SSH_PORT" 22 | rsyncDestination="$USER@$SERVER" 23 | 24 | echo "Syncing MongoDB" 25 | ssh $remoteSSH mongodump -d $dbName -o /tmp/mongodump.$dbName && 26 | rsync -av -e "$rsyncTransport" $rsyncDestination:/tmp/mongodump.$dbName/ /tmp/mongodump.$dbName && 27 | ssh $remoteSSH rm -rf /tmp/mongodump.$dbName && 28 | # noIndexRestore increases compatibility between 3.x and 2.x, 29 | # and Apostrophe will recreate the indexes correctly at startup 30 | mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName && 31 | echo "Syncing Files" && 32 | rsync -av --delete -e "$rsyncTransport" $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads/ ./public/uploads && 33 | echo "Synced down from $TARGET" 34 | echo "YOU MUST RESTART THE SITE LOCALLY TO REBUILD THE MONGODB INDEXES." 35 | -------------------------------------------------------------------------------- /modules/asset/ui/svg/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_layout.scss: -------------------------------------------------------------------------------- 1 | // Use flex to keep the footer at the bottom. 2 | body, 3 | [data-apos-refreshable], 4 | .bp-wrapper { 5 | display: flex; 6 | flex-direction: column; 7 | } 8 | 9 | [data-apos-refreshable], 10 | .bp-wrapper, 11 | main { 12 | flex-grow: 1; 13 | } 14 | 15 | body { 16 | min-height: 100vh; 17 | // Nice system fonts. 18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 19 | } 20 | 21 | .bp-header, 22 | main, 23 | .bp-footer { 24 | width: 100%; 25 | max-width: 800px; 26 | margin-left: auto; 27 | margin-right: auto; 28 | } 29 | 30 | .bp-header { 31 | display: flex; 32 | align-items: center; 33 | justify-content: space-between; 34 | flex-wrap: wrap; 35 | margin-bottom: 10px; 36 | padding: 40px 0; 37 | } 38 | 39 | .bp-footer { 40 | padding: 40px 0; 41 | } 42 | 43 | .bp-footer__links { 44 | padding-left: 0px; 45 | list-style: none; 46 | text-align: center; 47 | 48 | li { 49 | display: inline-block; 50 | margin-right: 20px; 51 | } 52 | li:last-child { margin-right: 0; } 53 | } 54 | 55 | .bp-header__logo { 56 | display: block; 57 | width: 190px; 58 | max-width: 100%; 59 | object-fit: contain; 60 | } 61 | 62 | .bp-header__login { 63 | 64 | &:visited { 65 | color: white; 66 | } 67 | } 68 | 69 | .bp-content { 70 | max-width: 600px; 71 | margin-left: auto; 72 | margin-right: auto; 73 | } 74 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_type.scss: -------------------------------------------------------------------------------- 1 | h1, 2 | h2, 3 | h3 { 4 | text-align: center; 5 | } 6 | 7 | h1 { 8 | font-size: 4em; 9 | font-weight: 200; 10 | } 11 | 12 | a { 13 | color: $color-purple; 14 | text-decoration: none; 15 | 16 | &:hover, 17 | &:focus { 18 | text-decoration: underline; 19 | } 20 | 21 | &:visited { 22 | color: $color-purple-light; 23 | } 24 | } 25 | 26 | ::selection { 27 | background-color: $color-gold; 28 | } 29 | 30 | code { 31 | white-space: normal; 32 | } 33 | 34 | pre { 35 | display: flex; 36 | position: relative; 37 | font-family: Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; 38 | color: white; 39 | font-size: 14px; 40 | text-align: left; 41 | white-space: pre; 42 | word-spacing: normal; 43 | word-break: normal; 44 | word-wrap: normal; 45 | line-height: 1.8; 46 | tab-size: 4; 47 | hyphens: none; 48 | border-radius: 6px; 49 | margin-bottom: 1.5rem; 50 | padding: 1.5rem; 51 | overflow: auto; 52 | background: #2b2b2b; 53 | max-width: 600px; 54 | margin: 0 auto; 55 | } 56 | 57 | .bp-button { 58 | display: inline-block; 59 | padding: 12px 24px 10px 24px; 60 | text-decoration: none; 61 | font-size: 14px; 62 | border-radius: 30px; 63 | line-height: 1; 64 | font-weight: 500; 65 | background: $color-purple; 66 | color: white; 67 | &:visited, &:hover, &:active { 68 | color: white; 69 | } 70 | } 71 | 72 | .bp-button--cta { 73 | font-size: 16px; 74 | font-weight: 400; 75 | padding: 20px 30px; 76 | } -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm 2 | 3 | # Install MongoDB command line tools - though mongo-database-tools not available on arm64 4 | ARG MONGO_TOOLS_VERSION=6.0 5 | RUN . /etc/os-release \ 6 | && curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ 7 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian ${VERSION_CODENAME}/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ 8 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get install -y mongodb-mongosh \ 10 | && if [ "$(dpkg --print-architecture)" = "amd64" ]; then apt-get install -y mongodb-database-tools; fi \ 11 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* 12 | 13 | # [Optional] Uncomment this section to install additional OS packages. 14 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 15 | # && apt-get -y install --no-install-recommends 16 | 17 | # [Optional] Uncomment if you want to install an additional version of node using nvm 18 | # ARG EXTRA_NODE_VERSION=10 19 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 20 | 21 | # [Optional] Uncomment if you want to install more global node modules 22 | # RUN su node -c "npm install -g " 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node-mongo 3 | { 4 | "name": "Node.js & Mongo DB", 5 | "dockerComposeFile": "docker-compose.yml", 6 | "service": "app", 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | // "features": {}, 11 | 12 | // Configure tool-specific properties. 13 | "customizations": { 14 | // Configure properties specific to VS Code. 15 | "vscode": { 16 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": [ 18 | "mongodb.mongodb-vscode", 19 | "Vue.volar", 20 | "svelte.svelte-vscode" 21 | ] 22 | } 23 | }, 24 | "features": { 25 | "ghcr.io/devcontainers/features/git-lfs:1": { 26 | "autoPull": true, 27 | "version": "latest" 28 | }, 29 | "ghcr.io/devcontainers/features/github-cli:1": { 30 | "installDirectlyFromGitHubRelease": true, 31 | "version": "latest" 32 | } 33 | }, 34 | 35 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 36 | "forwardPorts": [3000, 27017], 37 | 38 | // Use 'postCreateCommand' to run commands after the container is created. 39 | "postCreateCommand": "npm install" 40 | 41 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 42 | // "remoteUser": "root" 43 | } 44 | -------------------------------------------------------------------------------- /scripts/sync-up: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | TARGET="$1" 4 | if [ -z "$TARGET" ]; then 5 | echo "Usage: ./scripts/sync-up production" 6 | echo "(or as appropriate)" 7 | echo 8 | echo "THIS WILL CLOBBER EVERYTHING ON THE" 9 | echo "TARGET SITE. MAKE SURE THAT IS WHAT" 10 | echo "YOU WANT!" 11 | exit 1 12 | fi 13 | 14 | read -p "THIS WILL CRUSH THE SITE'S CONTENT ON $TARGET. Are you sure? " -n 1 -r 15 | echo 16 | if [[ ! $REPLY =~ ^[Yy]$ ]] 17 | then 18 | exit 1 19 | fi 20 | 21 | source deployment/settings || exit 1 22 | source "deployment/settings.$TARGET" || exit 1 23 | 24 | #Enter the Mongo DB name (should be same locally and remotely). 25 | dbName=$PROJECT 26 | 27 | #Enter the Project name (should be what you called it for stagecoach). 28 | projectName=$PROJECT 29 | 30 | #Enter the SSH username/url for the remote server. 31 | remoteSSH="-p $SSH_PORT $USER@$SERVER" 32 | rsyncTransport="ssh -p $SSH_PORT" 33 | rsyncDestination="$USER@$SERVER" 34 | 35 | echo "Syncing MongoDB" 36 | mongodump -d $dbName -o /tmp/mongodump.$dbName && 37 | echo rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName && 38 | rsync -av -e "$rsyncTransport" /tmp/mongodump.$dbName/ $rsyncDestination:/tmp/mongodump.$dbName && 39 | rm -rf /tmp/mongodump.$dbName && 40 | # noIndexRestore increases compatibility between 3.x and 2.x, 41 | # and Apostrophe will recreate the indexes correctly at startup 42 | ssh $remoteSSH mongorestore --noIndexRestore --drop -d $dbName /tmp/mongodump.$dbName/$dbName && 43 | echo "Syncing Files" && 44 | rsync -av --delete -e "$rsyncTransport" ./public/uploads/ $rsyncDestination:/opt/stagecoach/apps/$projectName/uploads && 45 | echo "Synced up to $TARGET" 46 | echo "YOU MUST RESTART THE SITE ON $TARGET TO REBUILD THE MONGODB INDEXES." 47 | -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | options: { 3 | label: 'Home Page' 4 | }, 5 | fields: { 6 | add: { 7 | main: { 8 | type: 'area', 9 | options: { 10 | widgets: { 11 | '@apostrophecms/rich-text': { 12 | toolbar: [ 13 | 'styles', 14 | '|', 15 | 'bold', 16 | 'italic', 17 | 'strike', 18 | 'link', 19 | '|', 20 | 'bulletList', 21 | 'orderedList' 22 | ], 23 | styles: [ 24 | { 25 | tag: 'p', 26 | label: 'Paragraph (P)' 27 | }, 28 | { 29 | tag: 'h3', 30 | label: 'Heading 3 (H3)' 31 | }, 32 | { 33 | tag: 'h4', 34 | label: 'Heading 4 (H4)' 35 | } 36 | ], 37 | insert: [ 38 | 'table', 39 | 'image' 40 | ] 41 | }, 42 | '@apostrophecms/image': {}, 43 | '@apostrophecms/video': {}, 44 | 'counter-vue': { 45 | example: 'options from the home page schema' 46 | }, 47 | 'counter-svelte': { 48 | example: 'options from the home page schema' 49 | }, 50 | 'counter-react': { 51 | example: 'options from the home page schema' 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | group: { 58 | basics: { 59 | label: 'Basics', 60 | fields: [ 61 | 'title', 62 | 'main' 63 | ] 64 | } 65 | } 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /deployment/dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=production 4 | 5 | # Also a good place to ensure any data folders 6 | # that are *not* supposed to be replaced on every deployment exist 7 | # and create a symlink to them from the latest deployment directory. 8 | 9 | # The real 'data' folder is shared. It lives two levels up and one over 10 | # (we're in a deployment dir, which is a subdir of 'deployments', which 11 | # is a subdir of the project's main dir) 12 | 13 | HERE=`pwd` 14 | mkdir -p ../../data 15 | ln -s ../../data $HERE/data 16 | 17 | # We also have a shared uploads folder which is convenient to keep 18 | # in a separate place so we don't have to have two express.static calls 19 | 20 | mkdir -p ../../uploads 21 | ln -s ../../../uploads $HERE/public/uploads 22 | 23 | # Install any dependencies that can't just be rsynced over with 24 | # the deployment. Example: node apps have npm modules in a 25 | # node_modules folder. These may contain compiled C++ code that 26 | # won't work portably from one server to another. 27 | 28 | # This script runs after the rsync, but before the 'stop' script, 29 | # so your app is not down during the npm installation. 30 | 31 | # Make sure node_modules exists so npm doesn't go searching 32 | # up the filesystem tree 33 | mkdir -p node_modules 34 | 35 | # If there is no package.json file then we don't need npm install 36 | if [ -f './package.json' ]; then 37 | # Install npm modules 38 | # Use a suitable version of Python 39 | # export PYTHON=/usr/bin/python26 40 | npm install 41 | if [ $? -ne 0 ]; then 42 | echo "Error during npm install!" 43 | exit 1 44 | fi 45 | fi 46 | 47 | node app @apostrophecms/migration:migrate 48 | # Generate new static asset files for this 49 | # deployment of the app without shutting down 50 | # (TODO: for 3.0 this is actually disruptive because 51 | # we don't have a generation identifier yet) 52 | npm run build 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-demo", 3 | "version": "1.0.0", 4 | "description": "ApostropheCMS Vite Demo", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node app", 8 | "dev": "nodemon", 9 | "build": "NODE_ENV=production node app @apostrophecms/asset:build", 10 | "serve": "NODE_ENV=production node app", 11 | "release": "npm install && npm run build && node app @apostrophecms/migration:migrate", 12 | "db": "docker compose up -d --remove-orphans", 13 | "db:stop": "docker compose stop", 14 | "db:remove": "docker compose down" 15 | }, 16 | "nodemonConfig": { 17 | "delay": 1000, 18 | "verbose": true, 19 | "watch": [ 20 | "./app.js", 21 | "./apos.vite.config.js", 22 | "./modules/**/*", 23 | "./lib/**/*.js", 24 | "./views/**/*.html" 25 | ], 26 | "ignoreRoot": [ 27 | ".git" 28 | ], 29 | "ignore": [ 30 | "**/ui/", 31 | "locales/*.json", 32 | "public/uploads/", 33 | "public/apos-frontend/*.js", 34 | "data/" 35 | ], 36 | "ext": "json, js, html, scss, vue" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/apostrophecms/vite-demo" 41 | }, 42 | "author": "Apostrophe Technologies, Inc.", 43 | "license": "MIT", 44 | "dependencies": { 45 | "@apostrophecms/vite": "1.0.0", 46 | "apostrophe": "4.11.1", 47 | "lodash": "^4.17.21", 48 | "react": "^18.3.1", 49 | "react-dom": "^18.3.1", 50 | "svelte": "^5.1.3", 51 | "vue": "^3.5.12" 52 | }, 53 | "devDependencies": { 54 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 55 | "@vitejs/plugin-react": "^4.3.3", 56 | "@vitejs/plugin-vue": "^5.1.4", 57 | "autoprefixer": "^10.4.20", 58 | "eslint": "^8.0.0", 59 | "eslint-config-apostrophe": "^4.0.0", 60 | "nodemon": "^3.0.1", 61 | "normalize.css": "^8.0.1", 62 | "tailwindcss": "^3.4.14" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /modules/counter-svelte-widget/ui/src/app/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import apostrophe from 'apostrophe'; 2 | 3 | apostrophe({ 4 | shortName: 'a4-playground', 5 | root: import.meta, 6 | modules: { 7 | // Apostrophe module configuration 8 | // ******************************* 9 | // 10 | // NOTE: most configuration occurs in the respective modules' directories. 11 | // See modules/@apostrophecms/page/index.js for an example. 12 | // 13 | // Any modules that are not present by default in Apostrophe must at least 14 | // have a minimal configuration here to turn them on: `moduleName: {}` 15 | // *********************************************************************** 16 | // `className` options set custom CSS classes for Apostrophe core widgets. 17 | '@apostrophecms/rich-text-widget': { 18 | options: { 19 | className: 'bp-rich-text' 20 | } 21 | }, 22 | '@apostrophecms/image-widget': { 23 | options: { 24 | className: 'bp-image-widget' 25 | } 26 | }, 27 | '@apostrophecms/video-widget': { 28 | options: { 29 | className: 'bp-video-widget' 30 | } 31 | }, 32 | // The starter kit base styles. Integrates Tailwind CSS for the 33 | // purpose of this demo. Adds the global counter apps styles as seen 34 | // in the default `vite create` template. 35 | asset: {}, 36 | // use vite for asset bundling and HMR in development. 37 | '@apostrophecms/vite': {}, 38 | 39 | // `vite-react` adds React support to the project's Vite configuration. 40 | // Also adds the React Refresh runtime to the head of the page in HMR mode. 41 | // All other frameworks are configured in the `apos.vite.config.js` file. 42 | 'vite-react': {}, 43 | 44 | // module delivering counter apps backend features (route, templates, etc.) 45 | counter: {}, 46 | 47 | // The page containing the counter apps as widgets. Widgets are also 48 | // available (shared with) in the Home page (@apostrophecms/home-page). 49 | 'counter-page': {}, 50 | 51 | // The counter app widgets 52 | 'counter-vue-widget': {}, 53 | 'counter-svelte-widget': {}, 54 | 'counter-react-widget': {} 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /modules/asset/ui/src/scss/_welcome.scss: -------------------------------------------------------------------------------- 1 | .bp-welcome { 2 | max-width: 800px; 3 | margin-left: auto; 4 | margin-right: auto; 5 | 6 | } 7 | 8 | .bp-welcome__area { 9 | margin-top: 40px; 10 | } 11 | 12 | .bp-welcome__headline { 13 | position: relative; 14 | margin: 0 0 50px; 15 | } 16 | 17 | .bp-welcome__verson-wrapper { 18 | display: flex; 19 | justify-content: center; 20 | } 21 | 22 | .bp-welcome__version { 23 | position: relative; 24 | background-color: #a992ff; 25 | padding: 4px 5px; 26 | border-radius: 2px; 27 | display: inline-block; 28 | color: #300ab7; 29 | font-size: 12px; 30 | box-shadow: 0 25px 50px rgba(64,70,104,.1); 31 | font-weight: 500; 32 | letter-spacing: 2px; 33 | text-transform: uppercase; 34 | } 35 | 36 | .bp-welcome__help { 37 | font-weight: 600; 38 | font-size: 22px; 39 | margin-bottom: 24px; 40 | } 41 | 42 | .bp-welcome p { 43 | max-width: 500px; 44 | margin: 20px auto; 45 | line-height: 1.5; 46 | font-size: 1.1rem; 47 | } 48 | 49 | .bp-welcome__code__context { 50 | position: absolute; 51 | display: inline-block; 52 | bottom: auto; 53 | left: auto; 54 | right: 0; 55 | top: 0; 56 | width: auto; 57 | height: auto; 58 | text-transform: uppercase; 59 | font-size: 11px; 60 | font-family: code-saver,sans-serif; 61 | letter-spacing: .025em; 62 | padding: 5px 10px; 63 | -webkit-user-select: none; 64 | -moz-user-select: none; 65 | -ms-user-select: none; 66 | user-select: none; 67 | border-top-right-radius: 2px; 68 | background: #ffffff20; 69 | color: #ffffff96; 70 | letter-spacing: 1px; 71 | } 72 | 73 | .bp-welcome__code { 74 | margin: 40px auto; 75 | } 76 | 77 | p.bp-welcome__cta { 78 | text-align: center; 79 | margin-bottom: 40px; 80 | } 81 | 82 | .bp-mode { 83 | display: inline-block; 84 | padding: 5px 5px; 85 | font-size: 12px; 86 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 87 | line-height: 10px; 88 | color: black; 89 | vertical-align: middle; 90 | background-color: white; 91 | border-bottom-color: black; 92 | border: 1px solid black; 93 | border-radius: 6px; 94 | box-shadow: inset 0 -1px 0 black; 95 | } -------------------------------------------------------------------------------- /modules/@apostrophecms/home-page/views/page.html: -------------------------------------------------------------------------------- 1 | {# 2 | This is an example home page template. It inherits and extends a layout template 3 | that lives in the top-level views/ folder for convenience 4 | #} 5 | 6 | {% extends "layout.html" %} 7 | 8 | {% block main %} 9 |
10 |

11 | Vite Demo 12 |

13 |
14 | 17 | Hello World 18 | From Tailwind CSS 19 | 20 |
21 | {% if not data.user %} 22 | {# Message only for logged out users. #} 23 |

First time spinning up the ApostropheCMS 3 demo?

24 |

25 | Use the credentials created during setup with the CLI tool or create a new user with the CLI command: 26 |

27 |
28 |         Command Line
29 |         
30 |           node app @apostrophecms/user:add myUsername admin
31 |         
32 |       
33 |

34 | Then log in here 35 |

36 | {% endif %} 37 |

38 | For a guide on how to configure and customize this project, please check out the Apostrophe documentation. 39 |

40 |
41 | {% if data.user %} 42 | {# Message only for logged in users. #} 43 | {% if not data.query['aposEdit'] %} 44 |

45 | Enter Edit mode from the admin bar 👆 to begin. 46 |

47 | {% else %} 48 |

49 | Add and edit content below in the content area. 👇 50 |

51 | {% endif %} 52 | {% endif %} 53 | {% area data.page, 'main' %} 54 |
55 |
56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /modules/vite-react/index.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@apostrophecms/vite/vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // NOTE: The Vite configuration is needed only when building 5 | // (task or development application boot). 6 | // 7 | // In a production boot (`NODE_ENV=production node app`), the configuration is never used. 8 | // You might want to use dynamic imports here (`await import(...)`) to 9 | // prevent development dependencies from being included in production mode. 10 | // This can be achieved by conditionally assign `enableReact = defineConfig({ ... })` 11 | // based on an environment variable. Keep in mind root level async/await is supported 12 | // only in ESM projects ("type": "module" in package.json). 13 | // 14 | // For example the following only sends configuration 15 | // to Vite when not in production or when in a CI environment: 16 | // let enableReact = {}; 17 | // if (process.env.NODE_ENV !== 'production' || process.env.CI === '1') { 18 | // const { defineConfig } = await import('@apostrophecms/vite/vite'); 19 | // const react = await import('@vitejs/plugin-react'); 20 | // enableReact = defineConfig({ plugins: [ react.default() ] }); 21 | // } 22 | // 23 | // and below in the build object: 24 | // vite: { 25 | // extensions: { 26 | // enableReact 27 | // } 28 | // } 29 | 30 | export default { 31 | build: { 32 | vite: { 33 | extensions: { 34 | enableReact: defineConfig({ 35 | plugins: [ react() ] 36 | }) 37 | // This is the same as: 38 | // enableReact: { 39 | // plugins: [ react() ] 40 | // } 41 | } 42 | } 43 | }, 44 | init(self) { 45 | // Add the React Refresh runtime to the head of the page 46 | // but only in HMR mode. 47 | self.apos.template.prepend({ 48 | where: 'head', 49 | when: 'hmr:public', 50 | bundler: 'vite', 51 | component: 'vite-react:reactRefresh' 52 | }); 53 | }, 54 | components(self) { 55 | return { 56 | // Our async server component, see `./views/reactRefresh.html`. 57 | reactRefresh(req, data) { 58 | return {}; 59 | } 60 | }; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /deployment/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make the site live again, for instance by tweaking a .htaccess file 4 | # or starting a node server. In this example we also set up a 5 | # data/port file so that sc-proxy.js can figure out what port 6 | # to forward traffic to for this site. The idea is that every 7 | # folder in /var/webapps represents a separate project with a separate 8 | # node process, each listening on a specific port, and they all 9 | # need traffic forwarded from a reverse proxy server on port 80 10 | 11 | # Useful for debugging 12 | #set -x verbose 13 | 14 | # Express should not reveal information on errors, 15 | # also optimizes Express performance 16 | export NODE_ENV=production 17 | 18 | if [ ! -f "app.js" ]; then 19 | echo "I don't see app.js in the current directory." 20 | exit 1 21 | fi 22 | 23 | # Assign a port number if we don't yet have one 24 | 25 | if [ -f "data/port" ]; then 26 | PORT=`cat data/port` 27 | else 28 | # No port set yet for this site. Scan and sort the existing port numbers if any, 29 | # grab the highest existing one 30 | PORT=`cat ../../../*/data/port 2>/dev/null | sort -n | tail -1` 31 | if [ "$PORT" == "" ]; then 32 | echo "First app ever, assigning port 3000" 33 | PORT=3000 34 | else 35 | # Bash is much nicer than sh! We can do math without tears! 36 | let PORT+=1 37 | fi 38 | echo $PORT > data/port 39 | echo "First startup, chose port $PORT for this site" 40 | fi 41 | 42 | # Run the app via 'forever' so that it restarts automatically if it fails 43 | # Use `pwd` to make sure we have a full path, forever is otherwise easily confused 44 | # and will stop every server with the same filename 45 | 46 | # Use a "for" loop. A classic single-port file will do the 47 | # right thing, but so will a file with multiple port numbers 48 | # for load balancing across multiple cores 49 | for port in $PORT 50 | do 51 | export PORT=$port 52 | forever --minUptime=1000 --spinSleepTime=10000 -o data/console.log -e data/error.log start `pwd`/app.js && echo "Site started" 53 | done 54 | 55 | # Run the app without 'forever'. Record the process id so 'stop' can kill it later. 56 | # We recommend installing 'forever' instead for node apps. For non-node apps this code 57 | # may be helpful 58 | # 59 | # node app.js >> data/console.log 2>&1 & 60 | # PID=$! 61 | # echo $PID > data/pid 62 | # 63 | #echo "Site started" 64 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | {# Automatically extends the right outer layout and also handles AJAX siutations #} 2 | {% extends data.outerLayout %} 3 | 4 | {% set title = data.piece.title or data.page.title %} 5 | {% block title %} 6 | {{ title }} 7 | {% if not title %} 8 | {{ apos.log('Looks like you forgot to override the title block in a template that does not have access to an Apostrophe page or piece.') }} 9 | {% endif %} 10 | {% endblock %} 11 | 12 | {% block extraHead %} 13 | 17 | {% endblock %} 18 | 19 | {% block beforeMain %} 20 |
21 |
22 | 23 | 24 | 25 | {# children of the home page #} 26 | 36 | {% if not data.user %} 37 | 38 | {% endif %} 39 |
40 |
41 | {% endblock %} 42 | 43 | {% block main %} 44 | {# 45 | Usually, your page templates in the @apostrophecms/pages module will override 46 | this block. It is safe to assume this is where your page-specific content 47 | should go. 48 | #} 49 | {% endblock %} 50 | 51 | {% block afterMain %} 52 |
53 | 62 |
{# Close .bp-wrapper #} 63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /modules/counter-svelte-widget/ui/src/app/App.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
36 | 46 | 47 | 48 |

{widget.title}

49 | 50 | 51 | {#if message} 52 |

[Server Message] {message}

53 | {/if} 54 | 55 | 57 |
58 | 61 |
62 | 63 | 64 |

65 | 68 |

69 |
70 |
{debug}
71 |
72 |
73 | 74 | 79 | -------------------------------------------------------------------------------- /modules/counter-vue-widget/ui/src/app/App.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 79 | 80 | 85 | -------------------------------------------------------------------------------- /modules/counter-react-widget/ui/src/app/App.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { 3 | useState, useCallback, StrictMode, 4 | useMemo 5 | } from 'react'; 6 | /* eslint-enable no-unused-vars */ 7 | 8 | // Assets 9 | import viteLogo from '@/asset/svg/vite.svg'; 10 | import reactLogo from './assets/react.svg'; 11 | 12 | function App({ 13 | id, widget, options 14 | }) { 15 | 16 | // Vanilla React is a mess... 17 | const [ count, setCount ] = useState(widget.counter?.count || 0); 18 | const [ debugState, setDebugState ] = useState(false); 19 | const [ message, setMessage ] = useState(''); 20 | 21 | // Optimize the callbacks, it's a demo but why not? 22 | const onClick = useCallback(() => { 23 | setMessage(''); 24 | 25 | apos.http 26 | .post('/api/v1/counter/count', { 27 | body: { 28 | type: widget.type, 29 | id: widget._id, 30 | count: count + 1 31 | } 32 | }) 33 | .then(console.log) 34 | .catch((err) => setMessage(err.body?.data?.message || 'Server Error')); 35 | 36 | setCount((count) => count + 1); 37 | }, [ count ]); 38 | 39 | const onDebugClick = useCallback(() => { 40 | setDebugState((debugState) => !debugState); 41 | }, [ debugState ]); 42 | 43 | // Optimized data, it never changes (comming from the server as props). 44 | const { debug, title } = useMemo(() => { 45 | return { 46 | title: widget.title, 47 | debug: JSON.stringify({ 48 | id, 49 | widget, 50 | options 51 | }, null, 2) 52 | }; 53 | }, []); 54 | 55 | const debugLabel = debugState ? 'Hide Debug' : 'Show Debug'; 56 | 57 | return ( 58 | 59 |
60 |
61 | 62 | {/* Use imported svg from another module */} 63 | Vite Logo 64 | 65 | 66 | {/* ...or from the same module */} 67 | React logo 68 | 69 |
70 | 71 | {/* Title from the widget data */} 72 |

{title}

73 | 74 | {/* A server error message will appear here */} 75 | {message &&

[Server Message] {message}

} 76 | 77 | {/* The Button. No tailwind CSS because we grab it directly 78 | from the vite template installs. */} 79 |
80 | 83 |
84 | 85 | {/* A toggle for debugging - show App props (coming from the server) */} 86 |

87 | 90 |

91 |
92 |
 93 |             {debug}
 94 |           
95 |
96 |
97 |
98 | ); 99 | } 100 | 101 | export default App; 102 | -------------------------------------------------------------------------------- /modules/counter/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | async init(self) { 3 | // Create a custom mongodb collection to store counter data 4 | self.db = await self.apos.db.collection('counterData'); 5 | await self.db.createIndex({ type: 1 }); 6 | 7 | // A nunjucks filter to convert an object to a `data-*` attribute value 8 | self.apos.template.addFilter({ 9 | toAttributeValue: self.toAttributeValue 10 | }); 11 | }, 12 | 13 | // Define server side async components 14 | components(self) { 15 | return { 16 | // This component is generating the markup used for mounting every counter 17 | // app. It also serializes the data to be used in the client-side app and 18 | // assigns it to the `data-` attributes of the root element. 19 | // The client side code then reads and deserializes this data and 20 | // sends it to the respective `App.xxx` component via `props`. 21 | // See the component template `./views/counterApp.html`. 22 | // The component accespts the following arguments: 23 | // - framework: The framework used in the app 24 | // - widget: The current widget data object 25 | // - page: The current page data object 26 | // - options: The widget options as defined in the current page schema 27 | async counterApp(req, { 28 | framework, widget, page, options 29 | }) { 30 | const counter = (await self.apos.modules.counter 31 | .getWidgetCounter(widget._id)) ?? {}; 32 | return { 33 | framework, 34 | widget, 35 | page, 36 | options, 37 | counter 38 | }; 39 | } 40 | }; 41 | }, 42 | methods(self) { 43 | return { 44 | // Get counter data from the database, used in the async server component. 45 | // See `modules/asset/index.js` 46 | async getWidgetCounter(id) { 47 | return self.db.findOne({ _id: id }); 48 | }, 49 | // A helper to convert an object to an HTML element attribute 50 | toAttributeValue(obj) { 51 | if (typeof obj === 'undefined' || obj === null) { 52 | obj = ''; 53 | } 54 | const json = JSON.stringify(obj); 55 | return self.apos.template.safe( 56 | self.apos.util.escapeHtml(json, { single: true }) 57 | ); 58 | } 59 | }; 60 | }, 61 | apiRoutes(self) { 62 | return { 63 | post: { 64 | // A custom API route to update the counter data per widget. 65 | // The route path is automatically prefixed with `/api/v1/`, 66 | // the module name and the lowercase, slugified method name. 67 | // POST /api/v1/counter/count 68 | async count(req) { 69 | const { 70 | count, id, type 71 | } = req.body; 72 | 73 | if (!id) { 74 | throw self.apos.error('invalid', 'Missing widget ID', { 75 | invalid: [ 'id' ] 76 | }); 77 | } 78 | // Test and showcase frontend error handling 79 | if (count % 9 === 0) { 80 | throw self.apos.error('invalid', { 81 | message: 'I don\'t like numbers that divide by 9 so I\'m rejecting it!', 82 | invalid: [ 'id' ] 83 | }); 84 | } 85 | self.db.updateOne({ _id: id }, { 86 | $set: { 87 | count, 88 | type 89 | } 90 | }, { upsert: true }); 91 | 92 | return { 93 | ok: true, 94 | count, 95 | type 96 | }; 97 | } 98 | } 99 | }; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /modules/counter-react-widget/ui/src/app/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApostropheCMS & Vite Demo (based on the Essentials Starter Kit) 2 | 3 | This is a demo project that showcases advanced features of ApostropheCMS and Vite integration. It is based on the [Essentials Starter Kit](https://github.com/apostrophecms/starter-kit-assembly-essentials/tree/next). The Vite integration is still experimental (beta) and this demo uses a "nightly" version of ApostropheCMS and Apostrophe Vite module. The goal of this demo is to showcase the familiar for Vite based products features (like HMR, fast builds, and modern front-end frameworks support), configuration (the usual root level configuration files), but also some more powerful ways to integrate Vite specific features from within the ApostropheCMS project. 4 | 5 | - [Installation](#installation) 6 | - [GitHub Codespaces](#github-codespaces) 7 | - [Local installation](#local-installation) 8 | - [The demo](#the-demo) 9 | - [The frameworks setup](#the-frameworks-setup) 10 | - ["Smarter" Counter apps as widgets](#smarter-counter-apps-as-widgets) 11 | - [In depth setup explanation](#in-depth-setup-explanation) 12 | - [`modules/asset`](#modulesasset) 13 | - [`modules/counter`](#modulescounter) 14 | - [`modules/counter-page`](#modulescounter-page) 15 | - [`modules/@apostrophecms/home-page`](#modulesapostrophecmshome-page) 16 | - [`modules/counter-{vue|svelte|react}-widget`](#modulescounter-vuesveltereact-widget) 17 | - [Tailwind CSS configuration steps](#tailwind-css-configuration-steps) 18 | - [How it works (for nerds)](#how-it-works-for-nerds) 19 | - [Sources discovery](#sources-discovery) 20 | - [Sources aggregation](#sources-aggregation) 21 | - [Synthetic entrypoints](#synthetic-entrypoints) 22 | - [Build pipelines](#build-pipelines) 23 | - [Dev Server \& HMR](#dev-server--hmr) 24 | - [The known problems and limitations](#the-known-problems-and-limitations) 25 | 26 | 27 | ## Installation 28 | 29 | ### GitHub Codespaces 30 | 31 | You can use GitHub Codespaces to run this demo in the cloud. Click the button below to create a new Codespace: 32 | 33 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/apostrophecms/vite-demo/tree/PRO-6807-demo-features) 34 | 35 | Open a new terminal when the Codespace is ready and run the following commands: 36 | 37 | ```bash 38 | $ node app @apostrophecms/user:add admin admin 39 | ## Type `admin` as the password when prompted 40 | $ npm run dev 41 | ``` 42 | 43 | **Only if in the web VSCode editor:** 44 | The easiest way to test HMR is to open the VSCode Preview (Ports tab in the bottom panel, click the Preview icon in the Forwarded Address section for port 3000). You can also open the app in your browser (VSCode will open a new tab with the preview URL) but in order to see the HMR in action you need to make port 3000 Public accessible (right click on the port number in the Ports tab and select Port Visibility -> Public). 45 | 46 | ApostropheCMS uses MongoDB to store and manage data. The container comes with pre-installed MongoDB. Although not necessary for the demo, the container also has the VSCode extension for MongoDB installed so you can inspect the DB. You can use the `mongodb://llocalhost:27017` connection string and directly browse the dbu or use a mongodb playground to run e.g.: 47 | 48 | ```js 49 | /* global use, db */ 50 | use('a4-playground'); 51 | db.getCollection('aposDocs').findOne({ 52 | type: '@apostrophecms/home-page', 53 | aposMode: 'draft' 54 | }); 55 | ``` 56 | 57 | ### Local installation 58 | 59 | Clone the repository and install the dependencies: 60 | 61 | ```bash 62 | $ git clone https://github.com/apostrophecms/vite-demo.git 63 | $ cd vite-demo 64 | $ npm install 65 | ``` 66 | 67 | If you don't have a MongoDB server running and you have docker compose installed, you can start a MongoDB server with: 68 | 69 | ```bash 70 | $ docker compose up -d --remove-orphans 71 | ``` 72 | 73 | You can stop the MongoDB server later with: 74 | 75 | ```bash 76 | $ docker-compose down 77 | ``` 78 | 79 | If this is the first time you are running the application, you will need to create an admin user: 80 | 81 | ```bash 82 | $ node app @apostrophecms/user:add admin admin 83 | ``` 84 | Type `admin` as the password when prompted. 85 | 86 | Finally, start the ApostropheCMS application: 87 | 88 | ```bash 89 | $ npm run dev 90 | ``` 91 | 92 | You can test a production build with: 93 | 94 | ```bash 95 | $ npm run build 96 | $ npm run serve 97 | ``` 98 | 99 | ## The demo 100 | 101 | Open your browser and navigate to `http://localhost:3000`. Follow the login link and login with the username `admin` and the password `admin`. 102 | 103 | Hit Edit on the home page and add any desired number of "Vue Counter App", "Svelte Counter App", and "React Counter App" widgets by clicking on the "Add Content" button. After publishing using the button in the upper right corner (Update then Preview), you will see the counter apps in action. 104 | 105 | You can also create a new page of type "Counter Apps Page" by going to the pages menu, selecting "New Page" and then selecting the page type from the menu on the right. Choose a title, publish and navigate to the page. Edit the page and add the "Vue Counter App", "React Counter App", and "Svelte Counter App" widgets to the main area. Widgets can be shared between basically any document type. 106 | 107 | The counter apps will "remember" their state (until the application is restarted) even if you navigate away from the page or reload it. You can add multiple instances of the same widget to the page and they will work independently. 108 | 109 | The apps are not loading the counter state via HTTP requests, but are using the server-side rendered initial data. 110 | 111 | Open your favourite code editor and navigate to the `modules` folder. Inside you will find the code for each of the widgets and some other modules we will discuss. The `ui/src/app` directories of the `counter-react-widget`, `counter-vue-widget`, and `counter-svelte-widget` modules contain the files for the main app code of each. You can modify the counter apps (`App.vue`, `App.svelte` and `App.jsx`) and see the changes reflected in the browser without a full page reload (HMR). 112 | 113 | ## The frameworks setup 114 | 115 | All frameworks except ReactJS are integrated via single project level `apos.vite.config.mjs` file. Any additional configuration files are also supported (e.g. `svelte.config.js`, `postcss.config.js`, etc). 116 | 117 | For demonstration purposes (and because by default it requires additional page injection), ReactJS is configured via its own project module `vite-react`. Looking inside the `index.js` file of that module, this is accomplished using the Apostrophe Vite `build.vite` configuration that can be added to any project module. The module also injects the React refresh runtime required for React HMR, using the new conditional injection feature within the `init(self)` block. You can read more about this in the [documentation](https://docs.apostrophecms.org/guide/vite.html#development-specific-features). 118 | 119 | This demo also has Tailwind CSS integrated site-wide and can be used in both front-end and back-end (Nunjucks) code. The configuration steps used while creating the demo are described below. 120 | 121 | ## "Smarter" Counter apps as widgets 122 | 123 | The default template when creating a Vite app for React, Vue, or Svelte is a counter app. In this demo those are ported to the ApostropheCMS widgets: `counter-react-widget`, `counter-vue-widget`, and `counter-svelte-widget` respectively. The respective UI code can be found in `ui/src` directories of these modules. Every widget has its own bundle, which is loaded only when the widget is present on the page (and no user is logged in). 124 | 125 | The widgets are registering [widget players](https://docs.apostrophecms.org/guide/custom-widgets.html#client-side-javascript-for-widgets) as a standard approach in ApostropheCMS. These players are client-side code that is registered by ApostropheCMS to be handled during page edit or refresh. This ensures that our apps will be re-mounted when the page is reloaded, but also when widget configuration changes. 126 | 127 | Additionally, the default counter apps are enhanced to get initial data (props) from the server and save their state back to the server. 128 | 129 | The Counter Apps are made available in the Home page widget area. 130 | 131 | A page module `modules/counter-page` is created to demonstrate sharing these widgets between different document types. It has an area `main` where the widgets are registered. 132 | 133 | ### In depth setup explanation 134 | 135 | Let's demystify the counter apps and follow their integration step by step. There is ApostropheCMS specific context along the way, that I'll try to explain in the most simple way. 136 | 137 | #### `modules/asset` 138 | 139 | The module is inherited from the original Starter Kit Essentials repository and is simply a convenience for organizing some of our assets. It provides the original CSS used in the starter kit. For the purposes of this demo, we added the Tailwind CSS entrypoint (see `ui/src/index.js`) and a common `svg` asset files (`ui/svg`) referenced by the Counter App UI components. 140 | 141 | #### `modules/counter` 142 | 143 | This module contributes the back-end logic required to save the counter value per App Counter widget (on counter button click). The module provides: 144 | - A simple API endpoint using the Apostrophe `apiRoutes(self)` configuration method to save the counter value in the MongoDB database per widget instance. 145 | - A method, `getWidgetCounter(id)`, to get the counter value per widget instance, used in the async server component to pass that value as a prop to the front-end app. 146 | - A Nunjucks helper filter (`toAttributeValue(obj)`) and a server component. The component is defined in the `counterApp()` component method and the template is located in the `views` folder. In ApostropheCMS, components act much like they do in other frameworks, allowing you to add specific functionality to any of your templates. In this case, we are serializing server-side data and sending it to the front-end app via `data-*` attributes. 147 | 148 | #### `modules/counter-page` 149 | 150 | A simple Apostrophe page that provides a widget area containing only the Counter App widgets. In ApostropheCMS, you can configure many modules site-wide by configuring the `options` object in the `index.js` file of the module. Many widgets also allow for configuration options to be added "per area". In other words, each area can have widgets with different configurations. This module demonstrates sending `options` defined within the widget configuration object to the front-end app. The `example` property can be seen in the JSON object seen by the "Show Debug" toggle for any of the counters. 151 | 152 | #### `modules/@apostrophecms/home-page` 153 | 154 | It [improves](https://docs.apostrophecms.org/reference/module-api/module-overview.html#improve) the Apostrophe core Home page module. It's originally used by the Starter Kit to provide a styled home page. In this demo, we are adding the Counter App widgets to the widget area, alongside the existing Rich Text, Image, and Video widgets. 155 | 156 | #### `modules/counter-{vue|svelte|react}-widget` 157 | 158 | `counter-vue-widget`, `counter-svelte-widget`, and `counter-react-widget` are the Counter App widgets. They use an identical setup, with the only difference being the front-end framework used (`App.vue`, `App.svelte`, and `App.jsx` respectively). The widgets are registering a widget player within the `ui/src` folder that mounts the front-end app on the page, importing the App from the `ui/src/app` folder. No initial HTTP requests for the counter value are made, the initial data is passed from the server to the front-end app via `data-*` attributes. On every counter button click, the counter value is saved to the server. After refreshing the page, the counter server value is used as the initial value for the counter app. 159 | 160 | **Be careful**, the counter back-end doesn't like the number `9` for some reason! The reason is artificial for the purposes of this demo. However, the UI apps are smart enough to handle server errors and display a message to the user. 161 | 162 | Let's look at the `counter-vue-widget` as an example: 163 | 164 | - `index.js` - The module definition. It adds a single schema field `title` to the widget. You can [extend the schema with additional fields](https://docs.apostrophecms.org/reference/field-types/). The widget data is sent to `App.vue` as the `widget` prop. There is also `build.vite.bundles` configuration that tells ApostropheCMS to bundle the UI source of this widget separately and load it only when the widget is present on the page (if an editor is logged in, all bundles are loaded). The UI entrypoint becomes the bundle name `ui/src/counter-vue.js` instead of the default `ui/src/index.js`. 165 | - `views/widget.html` - The widget template. It invokes the Nunjucks server component `counterApp` created in `modules/counter` to generate the markup for the widget. 166 | - `ui/src/counter-vue.js` - The entrypoint for the widget UI. Every module UI entrypoint should have a default export function that acts as an "application" bootstrap. In this case, the entrypoint registers a [Widget Player](https://docs.apostrophecms.org/guide/custom-widgets.html#client-side-javascript-for-widgets) (a selector and handling function) that mounts the Vue app on the page and passing server data as props. 167 | - `ui/src/app/App.vue` - The Vue Counter app. It receives the `widget` prop with the initial data from the server. The app has a single `counter` state that is updated on the button click. The counter value is saved to the server on every button click. The app is using the `svg` assets provided by the `modules/asset` module. The "Show Debug" toggle shows the component props received from the server. 168 | 169 | ## Tailwind CSS configuration steps 170 | 171 | The following steps were performed to integrate Tailwind CSS with ApostropheCMS, following the official guide: https://tailwindcss.com/docs/guides/vite 172 | 173 | It's not necessary to follow these steps to use the demo. They are provided as a reference for those who want to integrate Tailwind CSS with ApostropheCMS. 174 | 175 | 1. Install Tailwind CSS (we skip `postcss` because it's internally managed by `vite`): 176 | ```bash 177 | npm install -D tailwindcss autoprefixer 178 | ``` 179 | 180 | 2. Init 181 | ```bash 182 | npx tailwindcss init -p 183 | ``` 184 | 185 | 3. Edit the created `tailwind.config.js` to become: 186 | ```js 187 | /** @type {import('tailwindcss').Config} */ 188 | module.exports = { 189 | content: [ 190 | './apos-build/@apostrophecms/vite/default/src/**/*.{js,jsx}', 191 | './modules/**/views/**/*.html', 192 | './views/**/*.html', 193 | ], 194 | theme: { 195 | extend: {}, 196 | }, 197 | plugins: [], 198 | } 199 | ``` 200 | Edit `apos.vite.config.js` to exclude the nunjucks templates from triggering page reloads: 201 | ```js 202 | // ... 203 | server: { 204 | watch: { 205 | // So that Tailwind CSS changes in the nunjucks templates do not trigger 206 | // page reloads. This is done by `nodemon` because we need a process restart. 207 | ignored: [ 208 | path.join(__dirname, 'modules/views/**/*.html'), 209 | path.join(__dirname, 'views/**/*.html') 210 | ] 211 | } 212 | } 213 | // ... 214 | ``` 215 | 216 | 4. Create `./modules/asset/ui/src/tailwind.css` with the following content: 217 | ```css 218 | @tailwind base; 219 | @tailwind components; 220 | @tailwind utilities; 221 | ``` 222 | 223 | 5. Edit `./modules/asset/ui/src/index.js` to import the CSS file: 224 | ```js 225 | import './tailwind.css' 226 | // The rest is the same 227 | ``` 228 | > The `tailwind.css` file could also be imported into a `./modules/asset/ui/src/index.scss` file. Here we were adapting a site with existing styling, so it was cleaner to bring it into the `index.js` file. 229 | 230 | 6. Edit `./modules/@apostrophecms/home-page/views/page.html` and add (server side rendering testing): 231 | ```html 232 |
233 | 234 | Hello World From Tailwind CSS 235 | 236 |
237 | ``` 238 | 239 | 7. `npm run dev` 240 | 241 | Tailwind now works for both server-side and client-side rendering (HMR included). The original starter kit styles are preserved. 242 | 243 | ## How it works (for nerds) 244 | 245 | The demo uses our brand new `@apostrophecms/vite` module to integrate Vite with ApostropheCMS. In order for us to achieve that, we developed a brand new system in the core to support "external build tools" and went from hardcoded page script injection to a manifest-based approach. Additionally, we added an abstract public API to the core, that simplifies source discovery and synthetic entrypoints, so that build tools can concentrate on bundle vendor specific logic. This architecture allows us to support multiple build tools and configurations in the future, if the need for that arises. 246 | 247 | The internal Apostrophe Webpack build is still fully supported, using the legacy build system. 248 | 249 | ### Sources discovery 250 | 251 | ApostropheCMS is a fully "module-based" platform. Every piece of code is contributed by an Apostrophe module - both front and server side. Modules can be `npm` packages or local directories. Local modules live in `./modules` directory of the project. Local modules can also extend or improve other modules, including npm and ApostropheCMS core modules. 252 | 253 | Every module can contribute to the front-end code. The front-end code is located in the `ui/` directory of the module. A module can also extend the Apostrophe admin UI, by providing/overriding additional Vue components, CSS, or JavaScript code. 254 | 255 | There is a clear distinction between "public" and "admin" UI code. We call them `public` and `apos` builds respectively. We are building those in separate pipelines, with separate configurations, and we are serving the code from separate directories. The project can only configure the `public` build, the `apos` build is managed entirely internally. 256 | 257 | The entrypoints for the `public` build are discovered by scanning the `ui/src` directories of all modules registered in `app.js`. The default entrypoint is `ui/src/index.js`, but modules can also define `bundles` in their configuration (which is done in this demo) that results in `ui/src/[bundle-name].js` being used as an entrypoint. Additionally, bundles are only loaded when the module that defines them is present on the page (or if a user is logged in). 258 | 259 | Every entrypoint should have a default export function that acts as "application" bootstrap - it's internally called by ApostropheCMS when the page is loaded. 260 | 261 | The `apos` build sources are scanned in a similar way, but in `ui/apos` directories. I'm not going to deep dive into the `apos` build specifics here, for those interested there is [extensive documentation in the ApostropheCMS documentation site](https://docs.apostrophecms.org/guide/custom-ui.html#components-with-a-logic-mixin-are-safer-and-easier-to-override). 262 | 263 | ### Sources aggregation 264 | 265 | It's impractical to build/watch sources scattered across multiple directory trees, including inside `node_modules/`. Furthermore, smart bundlers are optimizing sources located in `node_modules/` and doesn't allow HMR for them - something that we don't want in some cases. Keep in mind that the Apostrophe admin UI is also not pre-built, the entire UI is built in and for the project, so that any module can modify it. 266 | 267 | We are aggregating all sources into a single directory tree, that is then used by Vite to build the final bundles. This is done by computing something we call `build metadata` that contains every file considered a UI `source` and its relation to an Apostrophe module. This opens a lot of awesome possibilities, but also (there is no free lunch) introduces unique (fun) problems to solve. 268 | 269 | All sources are copied to `./apos-build` directory of the project. To be more precise - `./apos-build/@apostrophecms/vite/default/src` is the exact location, where that same path excluding the `src` folder is the build (Vite) root. The namespacing is required to avoid conflicts between different build tools (in the future) and configurations (configured project namespace, Apostrophe Assembly multisite just to name a few). When copying the sources, we are preserving the original directory structure by only "skipping" the `ui/` part of the path. This way building the sources becomes a trivial task for Vite. The "smart copy" is also handling (in an extremely efficient way) source overrides as a result of module inheritance (extend/improve). 270 | 271 | We can also easily support editor autocompletion and other goodies, by introducing a universal alias `@/` that points to the `./apos-build/@apostrophecms/vite/default/src` directory and configure it in `jsconfig.json` or `tsconfig.json` of the project (to make editors happy). 272 | 273 | ### Synthetic entrypoints 274 | 275 | The `build metadata` is used to generate synthetic entrypoints for Vite. They are internally registered as `build.rollupOptions.input` in the Vite configuration. 276 | 277 | For `public` builds, every `input` is an auto-generated `[input-name].js` file that imports previously discovered module sources. All `ui/src/index.js` apps are imported and executed in a single `input`, while every configured `bundle` is imported and executed in a separate `input`. The import paths are relative to the `./apos-build/@apostrophecms/vite/default/src` directory. 278 | 279 | The `apos` build contains a single auto-generated `input` that handles everything, from component registration, 3rd party modules integration to admin UI specific `apps`. 280 | 281 | ### Build pipelines 282 | 283 | Historically, ApostropheCMS builds `apos` (admin UI) and `public` (site UI) code in separate pipelines. There is a good reason for that - we don't want the admin UI to interfere with the project UI and vice versa. The same problem exists with the Vite integration. We made an attempt to build everything in a single pipeline, but it was a disaster. The main problem comes from configration that can't be shared between the two builds, mostly Sass and PostCSS (generally CSS) configuration. 284 | 285 | Keep in mind that the `apos` bundle is loaded on a page only when editor is logged in, so the performance impact is not that big an issue. 286 | 287 | ### Dev Server & HMR 288 | 289 | As a consequence of the above, the dev server (Vite middleware) can run in only "one mode" - `public` or `apos` (Apostrophe `asset` module configuration). While not ideal, this was a good compromise that allows us to have a fast and reliable development experience. 290 | 291 | The Vite module is reusing (in-memory) the same metadata used for copying sources in order to deliver another feature that makes HMR possible - watch mode handling. The core system is using `chokidar` to watch for changes in every known `ui/` directory of the project or in symlinked npm package. The Vite module is using additional index to ensure fast reaction on changes including "smart copy" of the changed files to the `./apos-build` directory. This is the trigger for Vite to deliver HMR to the browser. 292 | 293 | ## The known problems and limitations 294 | 295 | - The `apos` HMR is not working when the `public` build contains Vue apps. The reason for that lies in the fact that the `apos` and `public` builds can't share the same Vue instance. As a result of that, HMR is available to the "first" Vue instance that is loaded on the page. There are some ideas that we are exploring to solve this problem. 296 | - No SSR yet. UI Apps can receive initial data from the server, but the real server-side rendering (render on the server per framework, mount and hydrate on the client) is yet to be planned and implemented. 297 | - Following alias imports (`@/path/to/file`) in an editor will lead to the `apos-build` directory, not the original source. This is confusing and far from a good DX. We are exploring ways to solve this problem for all editors that support Typescript configuration files. 298 | 299 | --------------------------------------------------------------------------------