├── .github ├── FUNDING.yml └── workflows │ └── build_and_test.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── CONTRIBUTING.md ├── README.md ├── babel.config.json ├── eslint.config.mjs ├── generator.js ├── index.js ├── package-lock.json ├── package.json ├── template ├── .env.standalone └── src │ ├── main-vue-2.js │ └── main-vue-3.js └── tests └── testVue.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [joeldenning] 4 | patreon: singlespa 5 | open_collective: 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: "22" 16 | - run: npm ci 17 | - run: npm run check-format 18 | - run: npm run lint 19 | - run: npm run test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | .idea 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | # Test Directories 66 | tests/fixtures 67 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm exec pretty-quick --staged && npm exec eslint . && npm exec ejslint ./template/src 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | template/src 2 | .prettierignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you'd like to report an issue, please create a Github issue. To propose a code change, please submit a Github pull request. 4 | 5 | This project is a Vue CLI plugin. Vue provides [this development guide](https://cli.vuejs.org/dev-guide/plugin-dev.html) that explains how to develop a Vue CLI plugin. 6 | 7 | ## Local development 8 | 9 | See [this guide](https://cli.vuejs.org/dev-guide/plugin-dev.html#installing-plugin-locally). 10 | 11 | _Make sure to test both Vue 2 and Vue 3, with separate test-apps!_ 12 | 13 | Manually: 14 | 15 | ``` 16 | vue create test-app 17 | cd test-app 18 | npm install -D ../vue-cli-plugin-single-spa 19 | vue invoke single-spa 20 | ``` 21 | 22 | Automatically: 23 | 24 | ``` 25 | npm run test 26 | ``` 27 | 28 | You can also run the tests separately for Vue2 and Vue3 29 | 30 | ``` 31 | npm run test:vue2:esm 32 | npm run test:vue3:esm 33 | ``` 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-cli-plugin-single-spa 2 | 3 | A [Vue CLI Plugin](https://cli.vuejs.org/guide/plugins-and-presets.html#plugins) for [single-spa](https://single-spa.js.org). 4 | 5 | [Full documentation](https://single-spa.js.org/docs/ecosystem-vue.html#vue-cli) 6 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [] 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { fileURLToPath } from "node:url"; 3 | import js from "@eslint/js"; 4 | import { FlatCompat } from "@eslint/eslintrc"; 5 | import babelParser from "@babel/eslint-parser"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }); 14 | 15 | export default [ 16 | { 17 | ignores: ["template/src", "tests/fixtures"], 18 | }, 19 | ...compat.extends("important-stuff"), 20 | { 21 | languageOptions: { 22 | parser: babelParser, 23 | globals: { 24 | process: true, 25 | }, 26 | }, 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /generator.js: -------------------------------------------------------------------------------- 1 | const semver = require("semver"); 2 | 3 | module.exports = (api) => { 4 | const isTs = api.entryFile.endsWith(".ts"); 5 | const packageJsonPath = api.resolve("package.json"); 6 | const { dependencies, name } = require(packageJsonPath); 7 | if (!dependencies) { 8 | throw Error( 9 | `Could not find any dependencies declared in ${packageJsonPath}.`, 10 | ); 11 | } 12 | const usesRouter = Boolean(dependencies && dependencies["vue-router"]); 13 | const usesStore = Boolean(dependencies && dependencies["vuex"]); 14 | const usesVuetify = Boolean(dependencies && dependencies["vuetify"]); 15 | const appName = name || "appName"; 16 | const vueVersion = dependencies.vue; 17 | if (!vueVersion) { 18 | throw Error(`Could not find vue dependency in package.json`); 19 | } 20 | const minVueVersion = semver.minVersion(vueVersion); 21 | const isVue2 = semver.satisfies(minVueVersion, "<3"); 22 | 23 | api.render( 24 | { 25 | [api.entryFile]: `./template/src/main-vue-${isVue2 ? "2" : "3"}.js`, 26 | ".env.standalone": `./template/.env.standalone`, 27 | }, 28 | { 29 | isTs, 30 | usesRouter, 31 | usesStore, 32 | usesVuetify, 33 | appName, 34 | }, 35 | ); 36 | 37 | api.extendPackage({ 38 | scripts: { 39 | "serve:standalone": "vue-cli-service serve --mode standalone", 40 | }, 41 | dependencies: { 42 | "single-spa-vue": "^3.0.1", 43 | }, 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const SystemJSPublicPathWebpackPlugin = require("systemjs-webpack-interop/SystemJSPublicPathWebpackPlugin"); 2 | const StandaloneSingleSpaPlugin = require("standalone-single-spa-webpack-plugin"); 3 | 4 | function lessThanWebpack5() { 5 | const semver = require("semver"); 6 | const webpack = require( 7 | require.resolve("webpack", { 8 | paths: [require.resolve("@vue/cli-service")], 9 | }), 10 | ); 11 | return semver.satisfies(webpack.version, "<5"); 12 | } 13 | 14 | module.exports = (api, options) => { 15 | options.css.extract = false; 16 | 17 | const outputSystemJS = 18 | options?.pluginOptions?.["single-spa"].outputSystemJS === true; 19 | 20 | const packageJsonPath = api.resolve("package.json"); 21 | const { name } = require(packageJsonPath); 22 | if (!name) { 23 | throw Error( 24 | `vue-cli-plugin-single-spa: could not determine package name -- change your package json name field`, 25 | ); 26 | } 27 | 28 | api.chainWebpack((webpackConfig) => { 29 | webpackConfig.optimization.delete("splitChunks"); 30 | 31 | if (outputSystemJS) { 32 | // Terser error occurs when libraryTarget is set to "system" rather than "umd" 33 | webpackConfig.output.libraryTarget("umd"); 34 | 35 | webpackConfig 36 | .plugin("SystemJSPublicPathWebpackPlugin") 37 | .use(SystemJSPublicPathWebpackPlugin, [ 38 | { 39 | rootDirectoryLevel: 2, 40 | systemjsModuleName: name, 41 | }, 42 | ]); 43 | } else { 44 | webpackConfig.output.libraryTarget("module"); 45 | webpackConfig.set("experiments", { outputModule: true }); 46 | // webpack doesn't yet support HMR for native modules 47 | webpackConfig.devServer.hot(false); 48 | webpackConfig.target("web"); 49 | } 50 | 51 | webpackConfig.output.devtoolNamespace(name); 52 | 53 | webpackConfig.set("devtool", "source-map"); 54 | 55 | webpackConfig 56 | .plugin("StandaloneSingleSpaPlugin") 57 | .use(StandaloneSingleSpaPlugin, [ 58 | { 59 | appOrParcelName: name, 60 | disabled: process.env.STANDALONE_SINGLE_SPA !== "true", 61 | }, 62 | ]); 63 | 64 | if (lessThanWebpack5()) { 65 | webpackConfig.output.set("jsonpFunction", `webpackJsonp__${name}`); 66 | 67 | webpackConfig.devServer 68 | .headers({ 69 | "Access-Control-Allow-Origin": "*", 70 | }) 71 | .set("disableHostCheck", true); 72 | } else { 73 | webpackConfig.devServer 74 | .headers({ 75 | "Access-Control-Allow-Origin": "*", 76 | }) 77 | .set("allowedHosts", "all"); 78 | } 79 | 80 | webpackConfig.externals(["single-spa"]); 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli-plugin-single-spa", 3 | "description": "A vue-cli plugin for single-spa applications and parcels", 4 | "version": "4.0.0", 5 | "scripts": { 6 | "format": "prettier --write .", 7 | "check-format": "prettier --check .", 8 | "lint": "eslint . && ejslint ./template/src", 9 | "prepare": "husky", 10 | "clean": "rimraf tests/fixtures", 11 | "test": "npm run clean && npm run test:vue2:systemjs && npm run test:vue2:esm && npm run test:vue3:systemjs && npm run test:vue3:esm", 12 | "test:vue2:systemjs": "tests/testVue.sh 2 true", 13 | "test:vue2:esm": "tests/testVue.sh 2 false", 14 | "test:vue3:systemjs": "tests/testVue.sh 3 true", 15 | "test:vue3:esm": "tests/testVue.sh 3 false" 16 | }, 17 | "author": "Joel Denning", 18 | "license": "MIT", 19 | "homepage": "https://single-spa.js.org/docs/ecosystem-vue.html#vue-cli", 20 | "repository": "https://github.com/single-spa/vue-cli-plugin-single-spa", 21 | "bugs": "https://github.com/single-spa/vue-cli-plugin-single-spa/issues", 22 | "dependencies": { 23 | "semver": "^7.3.5", 24 | "standalone-single-spa-webpack-plugin": "^5.0.0", 25 | "systemjs-webpack-interop": "^2.3.7" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.26.7", 29 | "@babel/eslint-parser": "^7.26.5", 30 | "@eslint/eslintrc": "^3.2.0", 31 | "@eslint/js": "^9.19.0", 32 | "@vue/cli-service": "^5.0.8", 33 | "ejs-lint": "^2.0.1", 34 | "eslint": "^9.19.0", 35 | "eslint-config-important-stuff": "^1.1.0", 36 | "husky": "^9.1.7", 37 | "prettier": "^3.4.2", 38 | "pretty-quick": "^4.0.0", 39 | "rimraf": "^6.0.1" 40 | }, 41 | "peerDependencies": { 42 | "webpack": "*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /template/.env.standalone: -------------------------------------------------------------------------------- 1 | STANDALONE_SINGLE_SPA=true -------------------------------------------------------------------------------- /template/src/main-vue-2.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import singleSpaVue from 'single-spa-vue'; 3 | 4 | import App from './App.vue';<% if (usesRouter) { %> 5 | import router from './router';<% } if (usesStore) { %> 6 | import store from './store';<% } if (usesVuetify) { %> 7 | import vuetify from './plugins/vuetify'<% } %> 8 | 9 | Vue.config.productionTip = false; 10 | 11 | const vueLifecycles = singleSpaVue({ 12 | Vue, 13 | appOptions: { 14 | render(h<% if(isTs) {%>: any<% } %>) { 15 | return h(App, { 16 | props: { 17 | // single-spa props are available on the "this" object. Forward them to your component as needed. 18 | // https://single-spa.js.org/docs/building-applications#lifecycle-props 19 | // if you uncomment these, remember to add matching prop definitions for them in your App.vue file. 20 | /* 21 | name: this.name, 22 | mountParcel: this.mountParcel, 23 | singleSpa: this.singleSpa, 24 | */ 25 | }, 26 | }); 27 | },<% if (usesRouter) { %> 28 | router,<% } if (usesStore) { %> 29 | store,<% } if (usesVuetify) { %> 30 | vuetify,<% } %> 31 | }, 32 | }); 33 | 34 | export const bootstrap = vueLifecycles.bootstrap; 35 | export const mount = vueLifecycles.mount; 36 | export const unmount = vueLifecycles.unmount; 37 | -------------------------------------------------------------------------------- /template/src/main-vue-3.js: -------------------------------------------------------------------------------- 1 | import { h, createApp } from 'vue'; 2 | import singleSpaVue from 'single-spa-vue'; 3 | 4 | import App from './App.vue';<% if (usesRouter) { %> 5 | import router from './router';<% } if (usesStore) { %> 6 | import store from './store';<% } if (usesVuetify) { %> 7 | import vuetify from './plugins/vuetify'<% } %> 8 | 9 | const vueLifecycles = singleSpaVue({ 10 | createApp, 11 | appOptions: { 12 | render() { 13 | return h(App, { 14 | // single-spa props are available on the "this" object. Forward them to your component as needed. 15 | // https://single-spa.js.org/docs/building-applications#lifecycle-props 16 | // if you uncomment these, remember to add matching prop definitions for them in your App.vue file. 17 | /* 18 | name: this.name, 19 | mountParcel: this.mountParcel, 20 | singleSpa: this.singleSpa, 21 | */ 22 | }); 23 | }, 24 | },<% if (usesRouter || usesStore || usesVuetify) { %> 25 | handleInstance(app) {<% if (usesRouter) { %> 26 | app.use(router);<% } if (usesStore) { %> 27 | app.use(store);<% } if (usesVuetify) { %> 28 | app.use(vuetify);<% } %> 29 | },<% } %> 30 | }); 31 | 32 | export const bootstrap = vueLifecycles.bootstrap; 33 | export const mount = vueLifecycles.mount; 34 | export const unmount = vueLifecycles.unmount; 35 | -------------------------------------------------------------------------------- /tests/testVue.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | i= basename "$PWD" 4 | 5 | if [ $1 == 2 ] || [ $1 == 3 ]; 6 | then 7 | ProjectName="test-app-vue$1-$2" 8 | 9 | if [ "$i" != "fixtures" ]; then 10 | mkdir -p tests/fixtures 11 | cd tests/fixtures 12 | fi 13 | 14 | echo "creating vue project" 15 | 16 | npx @vue/cli create $ProjectName --no-git --inlinePreset "{\"useConfigFiles\": true,\"plugins\": {},\"vueVersion\": \"$1\"}" --packageManager=npm || ERRCODE=$? 17 | 18 | cd $ProjectName 19 | 20 | echo "installing local vue-cli-plugin-single-spa" 21 | npm install -D ../../.. || ERRCODE=$? 22 | echo "invoking vue-cli-plugin-single-spa" 23 | yes | npx @vue/cli invoke single-spa || ERRCODE=$? 24 | echo "creating vue.config.js" 25 | echo "module.exports={pluginOptions: {'single-spa': {outputSystemJS: $2}}}" > vue.config.js 26 | echo "building" 27 | npm run build || ERRCODE=$? 28 | 29 | exit $ERRCODE 30 | else 31 | echo "$1 is no valid Vue Version" 32 | exit 1 33 | fi 34 | 35 | --------------------------------------------------------------------------------