From 5eb1c6de04239a3d376620c8fd9b559e506860a3 Mon Sep 17 00:00:00 2001
From: Hugues Tavernier
Date: Mon, 20 Nov 2023 17:24:56 +0100
Subject: [PATCH 1/4] add stimulus hmr
---
playground/stimulus/assets/app.js | 9 +
.../assets/controllers/welcome_controller.js | 22 +-
.../controllers/welcome_controller.scss | 27 ++
.../stimulus/assets/react/controllers/App.jsx | 1 -
playground/stimulus/package.json | 2 +-
.../stimulus/public/o/.vite/entrypoints.json | 29 ++
.../templates/default/welcome.html.twig | 14 +-
src/vite-plugin-symfony/HELP.md | 11 +
src/vite-plugin-symfony/package-lock.json | 5 +-
src/vite-plugin-symfony/package.json | 5 +-
.../{ => entrypoints}/entryPointsHelper.ts | 2 +-
.../src/entrypoints/index.ts | 305 ++++++++++++++++
.../src/{ => entrypoints}/utils.ts | 13 +-
src/vite-plugin-symfony/src/index.ts | 328 ++----------------
src/vite-plugin-symfony/src/pluginOptions.ts | 25 +-
.../src/stimulus/helpers/util.ts | 8 +-
src/vite-plugin-symfony/src/stimulus/index.ts | 92 +++++
src/vite-plugin-symfony/src/types.d.ts | 22 +-
.../entryPointsHelper.test.ts | 4 +-
.../tests/{ => entrypoints}/index.test.ts | 45 ++-
.../tests/{ => entrypoints}/utils.test.ts | 10 +-
.../{ => entrypoints}/utils.win32.test.ts | 2 +-
src/vite-plugin-symfony/tests/mocks.ts | 4 +-
.../tests/stimulus/helpers/util.test.ts | 63 +++-
24 files changed, 673 insertions(+), 375 deletions(-)
create mode 100644 playground/stimulus/assets/controllers/welcome_controller.scss
create mode 100644 playground/stimulus/public/o/.vite/entrypoints.json
create mode 100644 src/vite-plugin-symfony/HELP.md
rename src/vite-plugin-symfony/src/{ => entrypoints}/entryPointsHelper.ts (99%)
create mode 100644 src/vite-plugin-symfony/src/entrypoints/index.ts
rename src/vite-plugin-symfony/src/{ => entrypoints}/utils.ts (97%)
create mode 100644 src/vite-plugin-symfony/src/stimulus/index.ts
rename src/vite-plugin-symfony/tests/{ => entrypoints}/entryPointsHelper.test.ts (87%)
rename src/vite-plugin-symfony/tests/{ => entrypoints}/index.test.ts (87%)
rename src/vite-plugin-symfony/tests/{ => entrypoints}/utils.test.ts (97%)
rename src/vite-plugin-symfony/tests/{ => entrypoints}/utils.win32.test.ts (94%)
diff --git a/playground/stimulus/assets/app.js b/playground/stimulus/assets/app.js
index d5658327..4caf5b5c 100644
--- a/playground/stimulus/assets/app.js
+++ b/playground/stimulus/assets/app.js
@@ -11,5 +11,14 @@ let $nav = document.querySelector("#nav");
if ($nav) {
window.addEventListener("scroll", refreshStickStatus);
refreshStickStatus();
+
+
+
}
+// if (import.meta.hot) {
+// import.meta.hot.accept((mod, ctx) => {
+// import.meta.hot.send('restart')
+// console.log(mod, ctx)
+// })
+// }
\ No newline at end of file
diff --git a/playground/stimulus/assets/controllers/welcome_controller.js b/playground/stimulus/assets/controllers/welcome_controller.js
index dc089756..b8246b0f 100644
--- a/playground/stimulus/assets/controllers/welcome_controller.js
+++ b/playground/stimulus/assets/controllers/welcome_controller.js
@@ -1,10 +1,28 @@
import { Controller } from "@hotwired/stimulus"
+import "./welcome_controller.scss";
export default class controller extends Controller {
+ static targets = [
+ "title",
+ "button"
+ ]
static values = {
- name: String
+ name: String,
+ count: {
+ type: Number,
+ default: 0
+ }
}
connect() {
- this.element.textContent = `hello ${this.nameValue}`;
+ this.titleTarget.textContent = `hello ${this.nameValue}`;
+ }
+ increment() {
+ this.countValue++
+ }
+ countValueChanged() {
+ this.updateButtonText()
+ }
+ updateButtonText() {
+ this.buttonTarget.textContent = `count : ${this.countValue}`
}
}
\ No newline at end of file
diff --git a/playground/stimulus/assets/controllers/welcome_controller.scss b/playground/stimulus/assets/controllers/welcome_controller.scss
new file mode 100644
index 00000000..5483586a
--- /dev/null
+++ b/playground/stimulus/assets/controllers/welcome_controller.scss
@@ -0,0 +1,27 @@
+[data-controller="welcome"] {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+
+ button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #f9f9f9;
+ cursor: pointer;
+ transition: border-color 0.25s;
+
+ &:focus,
+ &:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+ }
+
+ &:hover {
+ border-color: #646cff;
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/stimulus/assets/react/controllers/App.jsx b/playground/stimulus/assets/react/controllers/App.jsx
index aeda2d56..79cfbc44 100644
--- a/playground/stimulus/assets/react/controllers/App.jsx
+++ b/playground/stimulus/assets/react/controllers/App.jsx
@@ -28,7 +28,6 @@ function App(props) {
Edit component and save to test HMR
- (we have hmr with js but not with css)
diff --git a/playground/stimulus/package.json b/playground/stimulus/package.json
index 1f95c812..b5b764ef 100644
--- a/playground/stimulus/package.json
+++ b/playground/stimulus/package.json
@@ -52,4 +52,4 @@
"bootstrap": "^5.3.2",
"stimulus-color-picker": "^1.1.0"
}
-}
+}
\ No newline at end of file
diff --git a/playground/stimulus/public/o/.vite/entrypoints.json b/playground/stimulus/public/o/.vite/entrypoints.json
new file mode 100644
index 00000000..5e5ed446
--- /dev/null
+++ b/playground/stimulus/public/o/.vite/entrypoints.json
@@ -0,0 +1,29 @@
+{
+ "base": "/o/",
+ "entryPoints": {
+ "pageTranslator": {
+ "js": [
+ "http://127.0.0.1:5173/o/assets/page/translator/index.js"
+ ]
+ },
+ "app": {
+ "js": [
+ "http://127.0.0.1:5173/o/assets/app.js"
+ ]
+ },
+ "theme": {
+ "css": [
+ "http://127.0.0.1:5173/o/assets/theme.scss"
+ ]
+ }
+ },
+ "legacy": false,
+ "metadatas": {},
+ "version": [
+ "6.2.0",
+ 6,
+ 2,
+ 0
+ ],
+ "viteServer": "http://127.0.0.1:5173"
+}
\ No newline at end of file
diff --git a/playground/stimulus/templates/default/welcome.html.twig b/playground/stimulus/templates/default/welcome.html.twig
index b4c362ae..e156b5ed 100644
--- a/playground/stimulus/templates/default/welcome.html.twig
+++ b/playground/stimulus/templates/default/welcome.html.twig
@@ -5,6 +5,18 @@
{% block html_class %}page-welcome{% endblock %}
{% block content %}
-
+
+
+
+
+
+
+ assets/controllers/welcome_controller.js
+
+
+ Edit component and save to test HMR
+
+
+
{% endblock %}
diff --git a/src/vite-plugin-symfony/HELP.md b/src/vite-plugin-symfony/HELP.md
new file mode 100644
index 00000000..5ffc5c3f
--- /dev/null
+++ b/src/vite-plugin-symfony/HELP.md
@@ -0,0 +1,11 @@
+```js
+import util from "node:util";
+
+if (pluginOptions.debug) {
+ setTimeout(() => {
+ logger.info(`\n${colors.green("➜")} Vite Config \n`);
+ logger.info(util.inspect(viteConfig, { showHidden: false, depth: null, colors: true }));
+ logger.info(`\n${colors.green("➜")} End of config \n`);
+ }, 100);
+}
+```
\ No newline at end of file
diff --git a/src/vite-plugin-symfony/package-lock.json b/src/vite-plugin-symfony/package-lock.json
index 69a690d0..a7c9751d 100644
--- a/src/vite-plugin-symfony/package-lock.json
+++ b/src/vite-plugin-symfony/package-lock.json
@@ -9,6 +9,7 @@
"version": "6.2.0",
"license": "MIT",
"dependencies": {
+ "debug": "^4.3.4",
"fast-glob": "^3.3.1",
"picocolors": "^1.0.0",
"sirv": "^2.0.2"
@@ -1583,7 +1584,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -2842,8 +2842,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mz": {
"version": "2.7.0",
diff --git a/src/vite-plugin-symfony/package.json b/src/vite-plugin-symfony/package.json
index 32239e9c..652a40c2 100644
--- a/src/vite-plugin-symfony/package.json
+++ b/src/vite-plugin-symfony/package.json
@@ -1,7 +1,7 @@
{
"name": "vite-plugin-symfony",
"version": "6.2.0",
- "description": "",
+ "description": "A Vite plugin to integrate easily Vite in your Symfony application",
"main": "dist/index.js",
"exports": {
".": {
@@ -84,6 +84,7 @@
"node": "20.9.0"
},
"dependencies": {
+ "debug": "^4.3.4",
"fast-glob": "^3.3.1",
"picocolors": "^1.0.0",
"sirv": "^2.0.2"
@@ -106,4 +107,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/vite-plugin-symfony/src/entryPointsHelper.ts b/src/vite-plugin-symfony/src/entrypoints/entryPointsHelper.ts
similarity index 99%
rename from src/vite-plugin-symfony/src/entryPointsHelper.ts
rename to src/vite-plugin-symfony/src/entrypoints/entryPointsHelper.ts
index 726e10f4..0eed6108 100644
--- a/src/vite-plugin-symfony/src/entryPointsHelper.ts
+++ b/src/vite-plugin-symfony/src/entrypoints/entryPointsHelper.ts
@@ -1,7 +1,7 @@
import process from "node:process";
import type { ResolvedConfig } from "vite";
import { getLegacyName, prepareRollupInputs } from "./utils";
-import { EntryPoints, EntryPoint, StringMapping, GeneratedFiles, FileInfos, FilesMetadatas } from "./types";
+import { EntryPoints, EntryPoint, StringMapping, GeneratedFiles, FileInfos, FilesMetadatas } from "../types";
export const getDevEntryPoints = (config: ResolvedConfig, viteDevServerUrl: string): EntryPoints => {
const entryPoints: EntryPoints = {};
diff --git a/src/vite-plugin-symfony/src/entrypoints/index.ts b/src/vite-plugin-symfony/src/entrypoints/index.ts
new file mode 100644
index 00000000..2748683c
--- /dev/null
+++ b/src/vite-plugin-symfony/src/entrypoints/index.ts
@@ -0,0 +1,305 @@
+import { resolve, join, relative, dirname } from "node:path";
+import { existsSync, mkdirSync, readFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+import glob from "fast-glob";
+import process from "node:process";
+
+import { Logger, Plugin, UserConfig } from "vite";
+import sirv from "sirv";
+
+import colors from "picocolors";
+
+import type { RenderedChunk, OutputAsset, NormalizedOutputOptions, OutputChunk } from "rollup";
+
+import { getDevEntryPoints, getBuildEntryPoints, getFilesMetadatas } from "./entryPointsHelper";
+import {
+ normalizePath,
+ writeJson,
+ emptyDir,
+ isImportRequest,
+ isInternalRequest,
+ resolveDevServerUrl,
+ isAddressInfo,
+ isCssEntryPoint,
+ getFileInfos,
+ getInputRelPath,
+ parseVersionString,
+ isSubdirectory,
+} from "./utils";
+import { resolveBase, resolveOutDir, refreshPaths, resolvePublicDir } from "../pluginOptions";
+
+import {
+ StringMapping,
+ GeneratedFiles,
+ ResolvedConfigWithOrderablePlugins,
+ VitePluginSymfonyEntrypointsOptions,
+} from "../types";
+
+// src and dist directory are in the same level;
+let pluginDir = dirname(dirname(fileURLToPath(import.meta.url)));
+let pluginVersion;
+
+if (process.env.VITEST) {
+ pluginDir = dirname(pluginDir);
+ pluginVersion = ["test"];
+} else {
+ const packageJson = JSON.parse(readFileSync(join(pluginDir, "package.json")).toString());
+ pluginVersion = parseVersionString(packageJson?.version);
+}
+
+export default function symfonyEntrypoints(pluginOptions: VitePluginSymfonyEntrypointsOptions, logger: Logger): Plugin {
+ let viteConfig: ResolvedConfigWithOrderablePlugins;
+ let viteDevServerUrl: string;
+
+ const entryPointsFileName = ".vite/entrypoints.json";
+
+ const inputRelPath2outputRelPath: StringMapping = {};
+ const generatedFiles: GeneratedFiles = {};
+
+ let outputCount = 0;
+
+ return {
+ name: "symfony-entrypoints",
+ enforce: "post",
+ config(userConfig) {
+ const root = userConfig.root ? resolve(userConfig.root) : process.cwd();
+
+ if (userConfig.build.rollupOptions.input instanceof Array) {
+ logger.error(colors.red("rollupOptions.input must be an Objet like {app: './assets/app.js'}"), {
+ timestamp: true,
+ });
+ process.exit(1);
+ }
+
+ const extraConfig: UserConfig = {
+ base: userConfig.base ?? resolveBase(pluginOptions),
+ publicDir: false,
+ build: {
+ manifest: true,
+ outDir: userConfig.build?.outDir ?? resolveOutDir(pluginOptions),
+ },
+ optimizeDeps: {
+ //Set to true to force dependency pre-bundling.
+ force: true,
+ },
+ server: {
+ watch: {
+ ignored: userConfig.server?.watch?.ignored
+ ? []
+ : ["**/vendor/**", glob.escapePath(root + "/var") + "/**", glob.escapePath(root + "/public") + "/**"],
+ },
+ },
+ };
+
+ return extraConfig;
+ },
+ configResolved(config) {
+ viteConfig = config as ResolvedConfigWithOrderablePlugins;
+
+ if (pluginOptions.enforcePluginOrderingPosition) {
+ const pluginPos = viteConfig.plugins.findIndex((plugin) => plugin.name === "symfony");
+ const symfonyPlugin = viteConfig.plugins.splice(pluginPos, 1);
+
+ const manifestPos = viteConfig.plugins.findIndex((plugin) => plugin.name === "vite:reporter");
+ viteConfig.plugins.splice(manifestPos, 0, symfonyPlugin[0]);
+ }
+ },
+ configureServer(devServer) {
+ // vite server is running
+
+ const { watcher, ws } = devServer;
+ // empty the buildDir and create an entrypoints.json file inside.
+ devServer.httpServer?.once("listening", () => {
+ if (viteConfig.env.DEV) {
+ if (typeof pluginOptions.buildDirectory !== "undefined") {
+ logger.error(
+ `"buildDirectory" plugin option is deprecated and will be removed in v5.x use base: "${resolveBase(
+ pluginOptions,
+ )}" from vite config instead`,
+ {
+ timestamp: true,
+ },
+ );
+ }
+ if (typeof pluginOptions.publicDirectory !== "undefined") {
+ logger.error(
+ `${colors.red(
+ "[vite-plugin-symfony]",
+ )} "publicDirectory" plugin option is deprecated and will be removed in v5.x use build.outDir: "${resolveOutDir(
+ pluginOptions,
+ )}" from vite config instead`,
+ {
+ timestamp: true,
+ },
+ );
+ }
+ if (pluginOptions.viteDevServerHostname !== null) {
+ logger.error(
+ `${colors.red(
+ "[vite-plugin-symfony]",
+ )} "viteDevServerHostname" plugin option is deprecated and will be removed in v5.x use originOverride with protocol and port instead`,
+ {
+ timestamp: true,
+ },
+ );
+ }
+
+ const buildDir = resolve(viteConfig.root, viteConfig.build.outDir);
+ const viteDir = resolve(buildDir, ".vite");
+
+ // buildDir is not a subdirectory of the vite project root -> potentially dangerous
+ if (!isSubdirectory(viteConfig.root, buildDir) && viteConfig.build.emptyOutDir !== true) {
+ logger.error(
+ `outDir ${buildDir} is not a subDirectory of your project root. To prevent recursively deleting files anywhere else set "build.outDir" to true in your vite.config.js to confirm that you did not accidentally specify a wrong directory location.`,
+ {
+ timestamp: true,
+ },
+ );
+ process.exit(1);
+ }
+
+ if (!existsSync(buildDir)) {
+ mkdirSync(buildDir, { recursive: true });
+ }
+
+ existsSync(buildDir) && emptyDir(buildDir);
+
+ mkdirSync(viteDir, { recursive: true });
+
+ const address = devServer.httpServer?.address();
+
+ if (!isAddressInfo(address)) {
+ logger.error(
+ `address is not an object open an issue with your address value to fix the problem : ${address}`,
+ {
+ timestamp: true,
+ },
+ );
+ process.exit(1);
+ }
+
+ viteDevServerUrl = resolveDevServerUrl(address, devServer.config, pluginOptions);
+ if (pluginOptions.enforceServerOriginAfterListening) {
+ viteConfig.server.origin = viteDevServerUrl;
+ }
+
+ const entryPoints = getDevEntryPoints(viteConfig, viteDevServerUrl);
+
+ const entryPointsPath = resolve(viteConfig.root, viteConfig.build.outDir, entryPointsFileName);
+
+ writeJson(entryPointsPath, {
+ base: viteConfig.base,
+ entryPoints,
+ legacy: false,
+ metadatas: {},
+ version: pluginVersion,
+ viteServer: viteDevServerUrl,
+ });
+ }
+ });
+
+ // full reload vite dev server if twig files are modified.
+ if (pluginOptions.refresh !== false) {
+ const paths = pluginOptions.refresh === true ? refreshPaths : pluginOptions.refresh;
+ for (const path of paths) {
+ watcher.add(path);
+ }
+ watcher.on("change", function (path) {
+ if (path.endsWith(".twig")) {
+ ws.send({
+ type: "full-reload",
+ });
+ }
+ });
+ }
+
+ if (pluginOptions.servePublic !== false) {
+ // inspired by https://github.com/vitejs/vite
+ // file: packages/vite/src/node/server/middlewares/static.ts
+ const serve = sirv(resolvePublicDir(pluginOptions), {
+ dev: true,
+ etag: true,
+ extensions: [],
+ setHeaders(res, pathname) {
+ // Matches js, jsx, ts, tsx.
+ // The reason this is done, is that the .ts file extension is reserved
+ // for the MIME type video/mp2t. In almost all cases, we can expect
+ // these files to be TypeScript files, and for Vite to serve them with
+ // this Content-Type.
+ if (/\.[tj]sx?$/.test(pathname)) {
+ res.setHeader("Content-Type", "application/javascript");
+ }
+
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ },
+ });
+ devServer.middlewares.use(function viteServePublicMiddleware(req, res, next) {
+ if (req.url === "/" || req.url === "/build/") {
+ res.statusCode = 404;
+ res.end(readFileSync(join(pluginDir, "static/dev-server-404.html")));
+ return;
+ }
+
+ // skip import request and internal requests `/@fs/ /@vite-client` etc...
+ if (isImportRequest(req.url!) || isInternalRequest(req.url!)) {
+ return next();
+ }
+ serve(req, res, next);
+ });
+ }
+ },
+ async renderChunk(code: string, chunk: RenderedChunk) {
+ // we need this step because css entrypoints doesn't have a facadeModuleId in `generateBundle` step.
+ if (!isCssEntryPoint(chunk)) {
+ return;
+ }
+
+ // Here we have only css entryPoints
+ const cssAssetName = chunk.facadeModuleId
+ ? normalizePath(relative(viteConfig.root, chunk.facadeModuleId))
+ : chunk.name;
+
+ // chunk.viteMetadata.importedCss contains a Set of relative file paths of css files
+ // in our case we have only one file.
+ // eg: inputRelPath2outputRelPath['assets/theme.scss'] = 'assets/theme-44b5be96.css';
+ chunk.viteMetadata.importedCss.forEach((cssBuildFilename) => {
+ inputRelPath2outputRelPath[cssAssetName] = cssBuildFilename;
+ });
+ },
+ generateBundle(options: NormalizedOutputOptions, bundle: { [fileName: string]: OutputAsset | OutputChunk }) {
+ for (const chunk of Object.values(bundle)) {
+ const inputRelPath = getInputRelPath(chunk, options, viteConfig);
+ inputRelPath2outputRelPath[inputRelPath] = chunk.fileName;
+ generatedFiles[chunk.fileName] = getFileInfos(chunk, inputRelPath, pluginOptions);
+ }
+
+ outputCount++;
+ const output = viteConfig.build.rollupOptions?.output;
+
+ // if we have multiple build passes output is an array of each pass.
+ // else we have an object of this unique pass
+ const outputLength = Array.isArray(output) ? output.length : 1;
+
+ if (outputCount >= outputLength) {
+ const entryPoints = getBuildEntryPoints(generatedFiles, viteConfig, inputRelPath2outputRelPath);
+
+ this.emitFile({
+ fileName: entryPointsFileName,
+ source: JSON.stringify(
+ {
+ base: viteConfig.base,
+ entryPoints,
+ legacy: typeof entryPoints["polyfills-legacy"] !== "undefined",
+ metadatas: getFilesMetadatas(viteConfig.base, generatedFiles),
+ version: pluginVersion,
+ viteServer: null,
+ },
+ null,
+ 2,
+ ),
+ type: "asset",
+ });
+ }
+ },
+ };
+}
diff --git a/src/vite-plugin-symfony/src/utils.ts b/src/vite-plugin-symfony/src/entrypoints/utils.ts
similarity index 97%
rename from src/vite-plugin-symfony/src/utils.ts
rename to src/vite-plugin-symfony/src/entrypoints/utils.ts
index 18e67a55..e6a8bbff 100644
--- a/src/vite-plugin-symfony/src/utils.ts
+++ b/src/vite-plugin-symfony/src/entrypoints/utils.ts
@@ -6,7 +6,14 @@ import { writeFileSync, rmSync, readdirSync } from "fs";
import { join } from "path";
import type { RenderedChunk, OutputChunk, OutputAsset, NormalizedOutputOptions } from "rollup";
import { resolve, extname, relative } from "path";
-import { StringMapping, DevServerUrl, VitePluginSymfonyOptions, FileInfos, ParsedInputs, HashAlgorithm } from "./types";
+import {
+ StringMapping,
+ DevServerUrl,
+ FileInfos,
+ ParsedInputs,
+ HashAlgorithm,
+ VitePluginSymfonyEntrypointsOptions,
+} from "../types";
import { BinaryLike, createHash } from "node:crypto";
export const isWindows = os.platform() === "win32";
@@ -93,7 +100,7 @@ const polyfillId = "\0vite/legacy-polyfills";
export function resolveDevServerUrl(
address: AddressInfo,
config: ResolvedConfig,
- pluginOptions: VitePluginSymfonyOptions,
+ pluginOptions: VitePluginSymfonyEntrypointsOptions,
): DevServerUrl {
if (pluginOptions.originOverride) {
return pluginOptions.originOverride as DevServerUrl;
@@ -143,7 +150,7 @@ export const isCssEntryPoint = (chunk: RenderedChunk) => {
export const getFileInfos = (
chunk: OutputChunk | OutputAsset,
inputRelPath,
- pluginOptions: VitePluginSymfonyOptions,
+ pluginOptions: VitePluginSymfonyEntrypointsOptions,
): FileInfos => {
const alg = pluginOptions.sriAlgorithm;
if (chunk.type === "asset") {
diff --git a/src/vite-plugin-symfony/src/index.ts b/src/vite-plugin-symfony/src/index.ts
index 8a070521..c50a18fb 100644
--- a/src/vite-plugin-symfony/src/index.ts
+++ b/src/vite-plugin-symfony/src/index.ts
@@ -1,305 +1,25 @@
-import { resolve, join, relative, dirname } from "node:path";
-import { existsSync, mkdirSync, readFileSync } from "node:fs";
-import util from "node:util";
-import { fileURLToPath } from "node:url";
-import glob from "fast-glob";
-import process from "node:process";
-
-import { Plugin, UserConfig } from "vite";
-import sirv from "sirv";
-
-import colors from "picocolors";
-
-import type { RenderedChunk, OutputAsset, NormalizedOutputOptions, OutputChunk } from "rollup";
-
-import { getDevEntryPoints, getBuildEntryPoints, getFilesMetadatas } from "./entryPointsHelper";
-import {
- normalizePath,
- writeJson,
- emptyDir,
- isImportRequest,
- isInternalRequest,
- resolveDevServerUrl,
- isAddressInfo,
- isCssEntryPoint,
- getFileInfos,
- getInputRelPath,
- parseVersionString,
- isSubdirectory,
-} from "./utils";
-import { resolvePluginOptions, resolveBase, resolveOutDir, refreshPaths, resolvePublicDir } from "./pluginOptions";
-
-import { VitePluginSymfonyOptions, StringMapping, GeneratedFiles, ResolvedConfigWithOrderablePlugins } from "./types";
-import { createControllersModule, virtualSymfonyControllersModuleId } from "./stimulus/node/bridge";
-
-// src and dist directory are in the same level;
-const pluginDir = dirname(dirname(fileURLToPath(import.meta.url)));
-const packageJson = JSON.parse(readFileSync(join(pluginDir, "package.json")).toString());
-const pluginVersion = process.env.VITEST ? "test" : parseVersionString(packageJson?.version);
-
-export default function symfony(userOptions: Partial = {}): Plugin {
- const pluginOptions = resolvePluginOptions(userOptions);
- let viteConfig: ResolvedConfigWithOrderablePlugins;
- let viteDevServerUrl: string;
-
- const entryPointsFileName = ".vite/entrypoints.json";
-
- let stimulusControllersContent = null;
-
- const inputRelPath2outputRelPath: StringMapping = {};
- const generatedFiles: GeneratedFiles = {};
-
- let outputCount = 0;
-
- return {
- name: "symfony",
- enforce: "post",
- config(userConfig) {
- const root = userConfig.root ? resolve(userConfig.root) : process.cwd();
-
- if (userConfig.build.rollupOptions.input instanceof Array) {
- console.error("rollupOptions.input must be an Objet like {app: './assets/app.js'}");
- process.exit(1);
- }
-
- const extraConfig: UserConfig = {
- base: userConfig.base ?? resolveBase(pluginOptions),
- publicDir: false,
- build: {
- manifest: true,
- outDir: userConfig.build?.outDir ?? resolveOutDir(pluginOptions),
- },
- optimizeDeps: {
- exclude: [...(userConfig?.optimizeDeps?.exclude ?? []), virtualSymfonyControllersModuleId],
- //Set to true to force dependency pre-bundling.
- force: true,
- },
- server: {
- watch: {
- ignored: userConfig.server?.watch?.ignored
- ? []
- : ["**/vendor/**", glob.escapePath(root + "/var") + "/**", glob.escapePath(root + "/public") + "/**"],
- },
- },
- };
-
- return extraConfig;
- },
- configResolved(config) {
- viteConfig = config as ResolvedConfigWithOrderablePlugins;
-
- if (pluginOptions.enforcePluginOrderingPosition) {
- const pluginPos = viteConfig.plugins.findIndex((plugin) => plugin.name === "symfony");
- const symfonyPlugin = viteConfig.plugins.splice(pluginPos, 1);
-
- const manifestPos = viteConfig.plugins.findIndex((plugin) => plugin.name === "vite:reporter");
- viteConfig.plugins.splice(manifestPos, 0, symfonyPlugin[0]);
- }
-
- if (typeof pluginOptions.stimulus === "string") {
- stimulusControllersContent = JSON.parse(readFileSync(resolve(config.root, pluginOptions.stimulus)).toString());
- }
- },
- resolveId(id: string) {
- if (pluginOptions.stimulus !== false && id === virtualSymfonyControllersModuleId) {
- return id;
- }
- },
- load(id) {
- if (id === virtualSymfonyControllersModuleId) {
- return createControllersModule(stimulusControllersContent);
- }
- },
- configureServer(devServer) {
- // vite server is running
-
- const { watcher, ws } = devServer;
-
- // empty the buildDir and create an entrypoints.json file inside.
- devServer.httpServer?.once("listening", () => {
- if (viteConfig.env.DEV) {
- if (typeof pluginOptions.buildDirectory !== "undefined") {
- devServer.config.logger.error(
- `${colors.red(
- "[vite-plugin-symfony]",
- )} "buildDirectory" plugin option is deprecated and will be removed in v5.x use base: "${resolveBase(
- pluginOptions,
- )}" from vite config instead`,
- );
- }
- if (typeof pluginOptions.publicDirectory !== "undefined") {
- devServer.config.logger.error(
- `${colors.red(
- "[vite-plugin-symfony]",
- )} "publicDirectory" plugin option is deprecated and will be removed in v5.x use build.outDir: "${resolveOutDir(
- pluginOptions,
- )}" from vite config instead`,
- );
- }
- if (pluginOptions.viteDevServerHostname !== null) {
- devServer.config.logger.error(
- `${colors.red(
- "[vite-plugin-symfony]",
- )} "viteDevServerHostname" plugin option is deprecated and will be removed in v5.x use originOverride with protocol and port instead`,
- );
- }
-
- const buildDir = resolve(viteConfig.root, viteConfig.build.outDir);
- const viteDir = resolve(buildDir, ".vite");
-
- // buildDir is not a subdirectory of the vite project root -> potentially dangerous
- if (!isSubdirectory(viteConfig.root, buildDir) && viteConfig.build.emptyOutDir !== true) {
- devServer.config.logger.error(
- `outDir ${buildDir} is not a subDirectory of your project root. To prevent recursively deleting files anywhere else set "build.outDir" to true in your vite.config.js to confirm that you did not accidentally specify a wrong directory location.`,
- );
- process.exit(1);
- }
-
- if (!existsSync(buildDir)) {
- mkdirSync(buildDir, { recursive: true });
- }
-
- existsSync(buildDir) && emptyDir(buildDir);
-
- mkdirSync(viteDir, { recursive: true });
-
- const address = devServer.httpServer?.address();
-
- if (!isAddressInfo(address)) {
- console.error("address is not an object open an issue with your address value to fix the problem", address);
- process.exit(1);
- }
-
- viteDevServerUrl = resolveDevServerUrl(address, devServer.config, pluginOptions);
- if (pluginOptions.enforceServerOriginAfterListening) {
- viteConfig.server.origin = viteDevServerUrl;
- }
-
- const entryPoints = getDevEntryPoints(viteConfig, viteDevServerUrl);
-
- const entryPointsPath = resolve(viteConfig.root, viteConfig.build.outDir, entryPointsFileName);
-
- writeJson(entryPointsPath, {
- base: viteConfig.base,
- entryPoints,
- legacy: false,
- metadatas: {},
- version: pluginVersion,
- viteServer: viteDevServerUrl,
- });
- }
-
- if (pluginOptions.debug) {
- setTimeout(() => {
- devServer.config.logger.info(`\n${colors.green("➜")} Vite Config \n`);
- devServer.config.logger.info(util.inspect(viteConfig, { showHidden: false, depth: null, colors: true }));
- devServer.config.logger.info(`\n${colors.green("➜")} End of config \n`);
- }, 100);
- }
- });
-
- // full reload vite dev server if twig files are modified.
- if (pluginOptions.refresh !== false) {
- const paths = pluginOptions.refresh === true ? refreshPaths : pluginOptions.refresh;
- for (const path of paths) {
- watcher.add(path);
- }
- watcher.on("change", function (path) {
- if (path.endsWith(".twig")) {
- ws.send({
- type: "full-reload",
- });
- }
- });
- }
-
- if (pluginOptions.servePublic !== false) {
- // inspired by https://github.com/vitejs/vite
- // file: packages/vite/src/node/server/middlewares/static.ts
- const serve = sirv(resolvePublicDir(pluginOptions), {
- dev: true,
- etag: true,
- extensions: [],
- setHeaders(res, pathname) {
- // Matches js, jsx, ts, tsx.
- // The reason this is done, is that the .ts file extension is reserved
- // for the MIME type video/mp2t. In almost all cases, we can expect
- // these files to be TypeScript files, and for Vite to serve them with
- // this Content-Type.
- if (/\.[tj]sx?$/.test(pathname)) {
- res.setHeader("Content-Type", "application/javascript");
- }
-
- res.setHeader("Access-Control-Allow-Origin", "*");
- },
- });
- devServer.middlewares.use(function viteServePublicMiddleware(req, res, next) {
- if (req.url === "/" || req.url === "/build/") {
- res.statusCode = 404;
- res.end(readFileSync(join(pluginDir, "static/dev-server-404.html")));
- return;
- }
-
- // skip import request and internal requests `/@fs/ /@vite-client` etc...
- if (isImportRequest(req.url!) || isInternalRequest(req.url!)) {
- return next();
- }
- serve(req, res, next);
- });
- }
- },
- async renderChunk(code: string, chunk: RenderedChunk) {
- // we need this step because css entrypoints doesn't have a facadeModuleId in `generateBundle` step.
- if (!isCssEntryPoint(chunk)) {
- return;
- }
-
- // Here we have only css entryPoints
- const cssAssetName = chunk.facadeModuleId
- ? normalizePath(relative(viteConfig.root, chunk.facadeModuleId))
- : chunk.name;
-
- // chunk.viteMetadata.importedCss contains a Set of relative file paths of css files
- // in our case we have only one file.
- // eg: inputRelPath2outputRelPath['assets/theme.scss'] = 'assets/theme-44b5be96.css';
- chunk.viteMetadata.importedCss.forEach((cssBuildFilename) => {
- inputRelPath2outputRelPath[cssAssetName] = cssBuildFilename;
- });
- },
- generateBundle(options: NormalizedOutputOptions, bundle: { [fileName: string]: OutputAsset | OutputChunk }) {
- for (const chunk of Object.values(bundle)) {
- const inputRelPath = getInputRelPath(chunk, options, viteConfig);
- inputRelPath2outputRelPath[inputRelPath] = chunk.fileName;
- generatedFiles[chunk.fileName] = getFileInfos(chunk, inputRelPath, pluginOptions);
- }
-
- outputCount++;
- const output = viteConfig.build.rollupOptions?.output;
-
- // if we have multiple build passes output is an array of each pass.
- // else we have an object of this unique pass
- const outputLength = Array.isArray(output) ? output.length : 1;
-
- if (outputCount >= outputLength) {
- const entryPoints = getBuildEntryPoints(generatedFiles, viteConfig, inputRelPath2outputRelPath);
-
- this.emitFile({
- fileName: entryPointsFileName,
- source: JSON.stringify(
- {
- base: viteConfig.base,
- entryPoints,
- legacy: typeof entryPoints["polyfills-legacy"] !== "undefined",
- metadatas: getFilesMetadatas(viteConfig.base, generatedFiles),
- version: pluginVersion,
- viteServer: null,
- },
- null,
- 2,
- ),
- type: "asset",
- });
- }
- },
- };
+import { Plugin, createLogger } from "vite";
+import symfonyEntrypoints from "./entrypoints";
+import symfonyStimulus from "./stimulus";
+
+import { VitePluginSymfonyOptions } from "./types";
+import { resolvePluginOptions } from "./pluginOptions";
+
+export default function symfony(userOptions: Partial = {}): Plugin[] {
+ const { stimulus: stimulusOptions, ...entrypointsOptions } = resolvePluginOptions(userOptions);
+
+ const plugins: Plugin[] = [
+ symfonyEntrypoints(
+ entrypointsOptions,
+ createLogger("info", { prefix: "[symfony:entrypoints]", allowClearScreen: true }),
+ ),
+ ];
+
+ if (typeof stimulusOptions === "object") {
+ plugins.push(
+ symfonyStimulus(stimulusOptions, createLogger("info", { prefix: "[symfony:stimulus]", allowClearScreen: true })),
+ );
+ }
+
+ return plugins;
}
diff --git a/src/vite-plugin-symfony/src/pluginOptions.ts b/src/vite-plugin-symfony/src/pluginOptions.ts
index c0c63f16..c3655724 100644
--- a/src/vite-plugin-symfony/src/pluginOptions.ts
+++ b/src/vite-plugin-symfony/src/pluginOptions.ts
@@ -1,5 +1,5 @@
import { join } from "node:path";
-import { VitePluginSymfonyOptions } from "./types";
+import { VitePluginSymfonyEntrypointsOptions, VitePluginSymfonyOptions } from "./types";
export function resolvePluginOptions(userConfig: Partial = {}): VitePluginSymfonyOptions {
if (typeof userConfig.publicDirectory === "string") {
@@ -30,8 +30,21 @@ export function resolvePluginOptions(userConfig: Partial {
+ if (!window.${applicationGlobalVarName}) {
+ console.warn('Simulus app not available. Are you creating app with startStimulusApp() ?');
+ import.meta.hot.invalidate();
+ } else {
+ window.${applicationGlobalVarName}.register('${identifier}', newModule.default);
+ }
+ })
+ }
+ `;
+
+ return `${code}\n${metaHotFooter}`;
+ },
+ configureServer(devServer) {
+ const { watcher } = devServer;
+ watcher.on("change", (path) => {
+ if (path === controllersFilePath) {
+ logger.info("✨ controllers.json updated, we restart server.", { timestamp: true });
+ devServer.restart();
+ }
+ });
+ },
+ };
+}
diff --git a/src/vite-plugin-symfony/src/types.d.ts b/src/vite-plugin-symfony/src/types.d.ts
index c729320e..e282356f 100644
--- a/src/vite-plugin-symfony/src/types.d.ts
+++ b/src/vite-plugin-symfony/src/types.d.ts
@@ -119,7 +119,15 @@ export type DevServerUrl = `${"http" | "https"}://${string}:${number}`;
export type HashAlgorithm = false | "sha256" | "sha384" | "sha512";
-export type VitePluginSymfonyOptions = {
+export type VitePluginSymfonyOptions = VitePluginSymfonyEntrypointsOptions & {
+ /**
+ * enable controllers.json loader for Symfony UX.
+ * @default false
+ */
+ stimulus: boolean | string | VitePluginSymfonyStimulusOptions;
+};
+
+export type VitePluginSymfonyEntrypointsOptions = {
/**
* Web directory root
* Relative file path from project directory root.
@@ -202,10 +210,16 @@ export type VitePluginSymfonyOptions = {
* @default true
*/
enforceServerOriginAfterListening: boolean;
+};
+export type VitePluginSymfonyStimulusOptions = {
/**
- * enable controllers.json loader for Symfony UX.
- * @default false
+ * path to controllers.json relative to vite root
+ */
+ controllersFilePath: string;
+
+ /**
+ * enable hmr for controllers
*/
- stimulus: boolean | string;
+ hmr: boolean;
};
diff --git a/src/vite-plugin-symfony/tests/entryPointsHelper.test.ts b/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
similarity index 87%
rename from src/vite-plugin-symfony/tests/entryPointsHelper.test.ts
rename to src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
index 8bc770f1..37c99b94 100644
--- a/src/vite-plugin-symfony/tests/entryPointsHelper.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
@@ -1,8 +1,8 @@
import { describe, it } from "vitest";
-import { getDevEntryPoints } from "../src/entryPointsHelper";
+import { getDevEntryPoints } from "../../src/entrypoints/entryPointsHelper";
import type { ResolvedConfig } from "vite";
-import { viteBaseConfig } from "./mocks";
+import { viteBaseConfig } from "../mocks";
describe("getDevEntryPoints", () => {
it("generate correct entrypoints", ({ expect }) => {
diff --git a/src/vite-plugin-symfony/tests/index.test.ts b/src/vite-plugin-symfony/tests/entrypoints/index.test.ts
similarity index 87%
rename from src/vite-plugin-symfony/tests/index.test.ts
rename to src/vite-plugin-symfony/tests/entrypoints/index.test.ts
index 4848ef5c..861b538b 100644
--- a/src/vite-plugin-symfony/tests/index.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/index.test.ts
@@ -1,6 +1,6 @@
import { describe, it, vi } from "vitest";
-import vitePluginSymfony from "../src/index";
+import vitePluginSymfonyEntrypoints from "../../src/entrypoints/index";
import type { OutputChunk, OutputAsset } from "rollup";
import {
@@ -18,7 +18,9 @@ import {
circular1Js,
circular2Js,
viteUserConfigNoRoot,
-} from "./mocks";
+} from "../mocks";
+import { VitePluginSymfonyOptions } from "../../src/types";
+import { resolvePluginOptions } from "../../src/pluginOptions";
function createBundleObject(files: (OutputChunk | OutputAsset)[]) {
const bundles: {
@@ -31,9 +33,15 @@ function createBundleObject(files: (OutputChunk | OutputAsset)[]) {
return bundles;
}
-describe("vitePluginSymfony", () => {
+function plugin(userOptions: Partial) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { stimulus, ...entrypointsOptions } = resolvePluginOptions(userOptions);
+ return vitePluginSymfonyEntrypoints(entrypointsOptions);
+}
+
+describe("vitePluginSymfonyEntrypoints", () => {
it("generate correct welcome build entrypoints", ({ expect }) => {
- const welcomePluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const welcomePluginInstance = plugin({ debug: true }) as any;
welcomePluginInstance.emitFile = vi.fn();
welcomePluginInstance.configResolved({
@@ -64,7 +72,7 @@ describe("vitePluginSymfony", () => {
},
legacy: false,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -75,7 +83,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct integrity hash for build entrypoints", ({ expect }) => {
- const hashPluginInstance = vitePluginSymfony({ debug: true, sriAlgorithm: "sha256" }) as any;
+ const hashPluginInstance = plugin({ debug: true, sriAlgorithm: "sha256" }) as any;
hashPluginInstance.emitFile = vi.fn();
hashPluginInstance.configResolved({
@@ -110,7 +118,7 @@ describe("vitePluginSymfony", () => {
hash: "sha256-w+Sit18/MC+LC1iX8MrNapOiCQ8wbPX8Rb6ErbfDX1Q=",
},
},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -121,7 +129,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct pageAssets build entrypoints", ({ expect }) => {
- const pageAssetsPluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const pageAssetsPluginInstance = plugin({ debug: true }) as any;
pageAssetsPluginInstance.emitFile = vi.fn();
pageAssetsPluginInstance.configResolved({
...viteBaseConfig,
@@ -151,7 +159,7 @@ describe("vitePluginSymfony", () => {
},
legacy: false,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -162,7 +170,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct pageImports build entrypoints", ({ expect }) => {
- const pageImportsPluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const pageImportsPluginInstance = plugin({ debug: true }) as any;
pageImportsPluginInstance.emitFile = vi.fn();
pageImportsPluginInstance.configResolved({
...viteBaseConfig,
@@ -192,7 +200,7 @@ describe("vitePluginSymfony", () => {
},
legacy: false,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -203,7 +211,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct theme build entrypoints", ({ expect }) => {
- const themePluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const themePluginInstance = plugin({ debug: true }) as any;
themePluginInstance.emitFile = vi.fn();
themePluginInstance.configResolved({
...viteBaseConfig,
@@ -235,7 +243,7 @@ describe("vitePluginSymfony", () => {
},
legacy: false,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -246,7 +254,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct circular build entrypoints", ({ expect }) => {
- const circularPluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const circularPluginInstance = plugin({ debug: true }) as any;
circularPluginInstance.emitFile = vi.fn();
circularPluginInstance.configResolved({
...viteBaseConfig,
@@ -277,7 +285,7 @@ describe("vitePluginSymfony", () => {
},
legacy: false,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -288,7 +296,7 @@ describe("vitePluginSymfony", () => {
});
it("generate correct legacy build entrypoints", ({ expect }) => {
- const legacyPluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const legacyPluginInstance = plugin({ debug: true }) as any;
legacyPluginInstance.emitFile = vi.fn();
legacyPluginInstance.configResolved({
...viteBaseConfig,
@@ -335,7 +343,7 @@ describe("vitePluginSymfony", () => {
},
legacy: true,
metadatas: {},
- version: "test",
+ version: ["test"],
viteServer: null,
},
null,
@@ -346,7 +354,7 @@ describe("vitePluginSymfony", () => {
});
it("loads correctly without root user config option", ({ expect }) => {
- const pluginInstance = vitePluginSymfony({ debug: true }) as any;
+ const pluginInstance = plugin({ debug: true }) as any;
const config = pluginInstance.config(viteUserConfigNoRoot);
expect(config).toEqual({
@@ -357,7 +365,6 @@ describe("vitePluginSymfony", () => {
outDir: "public/build",
},
optimizeDeps: {
- exclude: ["virtual:symfony/controllers"],
force: true,
},
server: {
diff --git a/src/vite-plugin-symfony/tests/utils.test.ts b/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
similarity index 97%
rename from src/vite-plugin-symfony/tests/utils.test.ts
rename to src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
index 41c88d66..c1061cac 100644
--- a/src/vite-plugin-symfony/tests/utils.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
@@ -8,8 +8,8 @@ import {
isSubdirectory,
parseVersionString,
resolveDevServerUrl,
-} from "../src/utils";
-import { resolvePluginOptions } from "../src/pluginOptions";
+} from "../../src/entrypoints/utils";
+import { resolvePluginOptions } from "../../src/pluginOptions";
import { OutputChunk, OutputAsset, NormalizedOutputOptions } from "rollup";
import {
asyncDepChunk,
@@ -20,9 +20,9 @@ import {
themeCss,
welcomeJs,
welcomeLegacyJs,
-} from "./mocks";
-import { defineConfig, InlineConfig, resolveConfig, type ResolvedConfig } from "vite";
-import { VitePluginSymfonyOptions } from "../src/types";
+} from "../mocks";
+import { resolveConfig, type ResolvedConfig } from "vite";
+import { VitePluginSymfonyOptions } from "../../src/types";
const viteBaseConfig = {
root: "/home/me/project-dir",
diff --git a/src/vite-plugin-symfony/tests/utils.win32.test.ts b/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
similarity index 94%
rename from src/vite-plugin-symfony/tests/utils.win32.test.ts
rename to src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
index b78d44a5..f7fdd30c 100644
--- a/src/vite-plugin-symfony/tests/utils.win32.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
@@ -1,5 +1,5 @@
import { describe, it, vi } from "vitest";
-import { isSubdirectory, normalizePath } from "../src/utils";
+import { isSubdirectory, normalizePath } from "../../src/entrypoints/utils";
vi.mock("node:path", async () => {
const win32Path = await vi.importActual("node:path/win32");
diff --git a/src/vite-plugin-symfony/tests/mocks.ts b/src/vite-plugin-symfony/tests/mocks.ts
index 32ef466e..acce4307 100644
--- a/src/vite-plugin-symfony/tests/mocks.ts
+++ b/src/vite-plugin-symfony/tests/mocks.ts
@@ -5,12 +5,12 @@ import { ChunkMetadata } from "../src/types";
export const viteBaseConfig = {
root: "/home/me/project-dir",
base: "/build/",
- plugins: [{ name: "symfony" }, { name: "vite:reporter" }],
+ plugins: [{ name: "symfony-entrypoints" }, { name: "symfony-stimulus" }, { name: "vite:reporter" }],
} as unknown as ResolvedConfig;
export const viteUserConfigNoRoot = {
base: "/build/",
- plugins: [{ name: "symfony" }, { name: "vite:reporter" }],
+ plugins: [{ name: "symfony-entrypoints" }, { name: "symfony-stimulus" }, { name: "vite:reporter" }],
build: {
rollupOptions: {},
},
diff --git a/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts b/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts
index 781e10fb..216e6fa5 100644
--- a/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts
+++ b/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts
@@ -3,27 +3,62 @@ import { getStimulusControllerFileInfos } from "../../../src/stimulus/helpers/ut
describe("stimulus", () => {
it.each([
- { input: "./controllers/welcome_controller.js", expectedId: "welcome", expectedLazy: false },
- { input: "./controllers/welcome_lazycontroller.js", expectedId: "welcome", expectedLazy: true },
+ {
+ input: "./controllers/welcome_controller.js",
+ onlyControllersDir: false,
+ expectedId: "welcome",
+ expectedLazy: false,
+ },
+ {
+ input: "./controllers/welcome_lazycontroller.js",
+ onlyControllersDir: false,
+ expectedId: "welcome",
+ expectedLazy: true,
+ },
{
input: "./some-content-before/controllers/welcome_controller.js",
+ onlyControllersDir: false,
expectedId: "welcome",
expectedLazy: false,
},
// without controllers
- { input: "../welcome_controller.js", expectedId: "welcome", expectedLazy: false },
+ { input: "../welcome_controller.js", onlyControllersDir: false, expectedId: "welcome", expectedLazy: false },
// bare module
- { input: "library/welcome_controller.js", expectedId: "library--welcome", expectedLazy: false },
+ {
+ input: "library/welcome_controller.js",
+ onlyControllersDir: false,
+ expectedId: "library--welcome",
+ expectedLazy: false,
+ },
// some content after we add --
- { input: "./controllers/foo/bar_controller.js", expectedId: "foo--bar", expectedLazy: false },
+ {
+ input: "./controllers/foo/bar_controller.js",
+ onlyControllersDir: false,
+ expectedId: "foo--bar",
+ expectedLazy: false,
+ },
// we replace _ -> -
- { input: "./controllers/foo_bar_controller.js", expectedId: "foo-bar", expectedLazy: false },
- { input: "./controllers/my_module.js", expectedId: "my-module", expectedLazy: false },
- { input: "./path/to/file.js", expectedId: "path--to--file", expectedLazy: false },
- { input: "not a controller", expectedId: undefined, expectedLazy: false },
- ])("getStimulusControllerFileInfos generate correct infos", ({ input, expectedId, expectedLazy }) => {
- const { identifier, lazy } = getStimulusControllerFileInfos(input);
- expect(identifier).toBe(expectedId);
- expect(lazy).toBe(expectedLazy);
- });
+ {
+ input: "./controllers/foo_bar_controller.js",
+ onlyControllersDir: false,
+ expectedId: "foo-bar",
+ expectedLazy: false,
+ },
+ { input: "./controllers/my_module.js", onlyControllersDir: false, expectedId: "my-module", expectedLazy: false },
+ { input: "./path/to/file.js", onlyControllersDir: false, expectedId: "path--to--file", expectedLazy: false },
+ { input: "not a controller", onlyControllersDir: false, expectedId: undefined, expectedLazy: false },
+ {
+ input: "/home/lhapaipai/projets/symfony-vite-dev/playground/stimulus/assets/app.js",
+ onlyControllersDir: true,
+ expectedId: undefined,
+ expectedLazy: false,
+ },
+ ])(
+ "getStimulusControllerFileInfos generate correct infos",
+ ({ input, onlyControllersDir, expectedId, expectedLazy }) => {
+ const { identifier, lazy } = getStimulusControllerFileInfos(input, onlyControllersDir);
+ expect(identifier).toBe(expectedId);
+ expect(lazy).toBe(expectedLazy);
+ },
+ );
});
From 11391db109b2b69d5ecb037766d38087a99e7b5a Mon Sep 17 00:00:00 2001
From: Hugues Tavernier
Date: Mon, 20 Nov 2023 18:22:45 +0100
Subject: [PATCH 2/4] stimulus hmr update doc
---
CHANGELOG.md | 6 +
docs/.vitepress/config.ts | 1 +
docs/src/fr/index.md | 20 +-
docs/src/fr/reference/vite-plugin-symfony.md | 24 ++-
docs/src/index.md | 22 +-
docs/src/public/images/stimulus.svg | 57 +++++
docs/src/public/images/twig.svg | 210 +++++++++++++++++++
docs/src/reference/vite-plugin-symfony.md | 23 +-
src/vite-plugin-symfony/src/types.d.ts | 2 +
9 files changed, 360 insertions(+), 5 deletions(-)
create mode 100644 docs/src/public/images/stimulus.svg
create mode 100644 docs/src/public/images/twig.svg
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aac38e51..f76a0f35 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## v6.3.0
+
+- stimulus HMR
+- fix bug : stimulus restart vite dev server when controllers.json is updated
+- split vite-plugin-symfony into 2 plugins `vite-plugin-symfony-entrypoints` and `vite-plugin-symfony-stimulus`.
+
## v6.2.0
- fix #77 support Vite 5.x
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 22a7763a..37936ceb 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -4,6 +4,7 @@ import { fileURLToPath, URL } from 'node:url';
// https://vitepress.dev/reference/site-config
export default defineConfig({
vite: {
+ publicDir: "./public",
resolve: {
alias: [
{
diff --git a/docs/src/fr/index.md b/docs/src/fr/index.md
index ec876f6b..977533a3 100644
--- a/docs/src/fr/index.md
+++ b/docs/src/fr/index.md
@@ -25,11 +25,29 @@ features:
- icon: ⚡️
title: Configuration facile
details: Installation rapide avec la recette Bundle Flex et le plugin Vite préconfiguré.
- - icon: 🛠️
+ - icon:
+ src: /images/twig.svg
+ wrap: true
+ width: 32
+ height: 23
title: Fonctions Twig
details: Associez vos points d'entrée dans vos modèles Twig avec des fonctions Twig.
- icon: 📦
title: Gestion des ressources
details: Intégrez vos ressources dans Symfony avec une stratégie de version d'asset personnalisée.
+ - icon:
+ src: /images/stimulus.svg
+ width: 32
+ height: 32
+ wrap: true
+ title: Stimulus / Symfony UX
+ details: Intégration de vos composants Symfony UX avec HMR.
+ - icon: 🧩
+ title: Fonctionnalités avancées
+ details: Attributs personnalisés, configuration multiple, injection de dépendances.
+ - icon: 🚀
+ title: Performances
+ details: Préchargement de vos fichiers, mise en cache de votre configuration.
+
---
diff --git a/docs/src/fr/reference/vite-plugin-symfony.md b/docs/src/fr/reference/vite-plugin-symfony.md
index c69cdf3c..efa17e53 100644
--- a/docs/src/fr/reference/vite-plugin-symfony.md
+++ b/docs/src/fr/reference/vite-plugin-symfony.md
@@ -74,13 +74,35 @@ Génère des clés de hachage lors de la génération de vos fichiers. À utilis
## stimulus
-- **Type :** `boolean | string`
+- **Type :** `boolean | string | VitePluginSymfonyStimulusOptions`
- **Valeur par défaut :** `false`
Active le bridge qui va interpréter le fichier `assets/controllers.json` pour les contrôleurs tiers de Stimulus (incluant Symfony UX).
Saisir `true` si votre fichier est situés à l'emplacement par défaut `assets/controllers.json` sinon spécifiez le chemin vers votre fichier de référence.
+vous pouvez aussi préciser un objet de configuration.
+
+```ts
+type VitePluginSymfonyStimulusOptions = {
+ /**
+ * path to controllers.json relative to vite root
+ * @default ./assets/controller.json
+ */
+ controllersFilePath: string;
+
+ /**
+ * enable hmr for controllers
+ * @default true
+ */
+ hmr: boolean;
+}
+```
+
+:::warning
+Par défaut le HMR est activé sur vos controlleurs Stimulus. Si ces derniers ne sont pas idempotents (voir [doc Stimulus](https://turbo.hotwired.dev/handbook/building#making-transformations-idempotent)), vous risquez de rencontrez des problèmes (les HMR ne fonctionnera pas comme attendu et vous devrez rafraîchir manuellement votre page). Dans ce cas il est préférable de désactiver l'option `hmr: false`. Ainsi, toute modification du fichier entrainera quand même un rafraichissement automatique de la page.
+:::
+
## viteDevServerHostname
- **Type :** `null | string`
diff --git a/docs/src/index.md b/docs/src/index.md
index 01bcb98a..126f3517 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -37,10 +37,28 @@ features:
- icon: ⚡️
title: Easy configuration
details: Fast installation with Bundle Flex recipe and preconfigured Vite plugin.
- - icon: 🛠️
+ - icon:
+ src: /images/twig.svg
+ wrap: true
+ width: 32
+ height: 23
title: Twig functions
- details: Associate your entrypoints in your Twig templates with Twig functions.
+ details: Provide Twig functions for your Twig templates.
- icon: 📦
title: Assets management
details: Integrate your assets into Symfony with custom Asset version Strategy.
+ - icon:
+ src: /images/stimulus.svg
+ width: 32
+ height: 32
+ wrap: true
+ title: Stimulus / Symfony UX
+ details: Easy integration with Symfony UX components and HMR.
+ - icon: 🧩
+ title: Advanced features
+ details: Custom attributes, Multiple configurations, Dependency injection
+ - icon: 🚀
+ title: Performances
+ details: Preload assets, Cache for config files
+
---
diff --git a/docs/src/public/images/stimulus.svg b/docs/src/public/images/stimulus.svg
new file mode 100644
index 00000000..43241778
--- /dev/null
+++ b/docs/src/public/images/stimulus.svg
@@ -0,0 +1,57 @@
+
+
diff --git a/docs/src/public/images/twig.svg b/docs/src/public/images/twig.svg
new file mode 100644
index 00000000..e2793c41
--- /dev/null
+++ b/docs/src/public/images/twig.svg
@@ -0,0 +1,210 @@
+
+
+
diff --git a/docs/src/reference/vite-plugin-symfony.md b/docs/src/reference/vite-plugin-symfony.md
index 891420ca..485c78fa 100644
--- a/docs/src/reference/vite-plugin-symfony.md
+++ b/docs/src/reference/vite-plugin-symfony.md
@@ -75,13 +75,34 @@ Generates hash keys when generating your files. Use if you want to deploy your a
## stimulus
-- **Type:** `boolean | string`
+- **Type:** `boolean | string | VitePluginSymfonyStimulusOptions`
- **Default value:** `false`
Enables the bridge that will interpret the `assets/controllers.json` file for third-party Stimulus controllers (including Symfony UX).
Enter `true` if your file is located in the default location `assets/controllers.json` otherwise specify the path to your reference file.
+You can also specify a configuration object.
+
+```ts
+type VitePluginSymfonyStimulusOptions = {
+ /**
+ * path to controllers.json relative to vite root
+ * @default ./assets/controller.json
+ */
+ controllersFilePath: string;
+
+ /**
+ * enable hmr for controllers
+ * @default true
+ */
+ hmr: boolean;
+}
+```
+
+:::warning
+By default, HMR is activated on your Stimulus controllers. If these are not idempotent (see [Stimulus doc](https://turbo.hotwired.dev/handbook/building#making-transformations-idempotent)), you may encounter problems (HMRs will not work as expected and you will have to manually refresh your page). In this case it is preferable to deactivate the `hmr: false` option. Therefore, any modification of the file will still result in an automatic refresh of the page.
+:::
## viteDevServerHostname
diff --git a/src/vite-plugin-symfony/src/types.d.ts b/src/vite-plugin-symfony/src/types.d.ts
index e282356f..8d0cf898 100644
--- a/src/vite-plugin-symfony/src/types.d.ts
+++ b/src/vite-plugin-symfony/src/types.d.ts
@@ -215,11 +215,13 @@ export type VitePluginSymfonyEntrypointsOptions = {
export type VitePluginSymfonyStimulusOptions = {
/**
* path to controllers.json relative to vite root
+ * @default ./assets/controller.json
*/
controllersFilePath: string;
/**
* enable hmr for controllers
+ * @default true
*/
hmr: boolean;
};
From 4555cb462edd56071f5a72e64d2985fb4e8c98ed Mon Sep 17 00:00:00 2001
From: Hugues Tavernier
Date: Mon, 20 Nov 2023 23:27:09 +0100
Subject: [PATCH 3/4] stimulus hmr add tests
---
playground/stimulus/assets/app.js | 12 ---
src/vite-plugin-symfony/.eslintrc | 3 +-
.../src/stimulus/helpers/base.ts | 2 +-
src/vite-plugin-symfony/src/stimulus/index.ts | 32 ++++----
.../src/stimulus/node/bridge.ts | 2 +-
.../src/stimulus/node/util.ts | 7 --
.../src/stimulus/{helpers => }/util.ts | 8 ++
.../entrypoints/entryPointsHelper.test.ts | 2 +-
.../tests/entrypoints/index.test.ts | 9 ++-
.../tests/entrypoints/utils.test.ts | 6 +-
.../tests/entrypoints/utils.win32.test.ts | 2 +-
.../tests/pluginOptions.test.ts | 4 +-
.../tests/stimulus/helpers/vue.test.ts | 5 +-
.../tests/stimulus/index.test.ts | 76 +++++++++++++++++++
.../tests/stimulus/node/bridge.test.ts | 2 +-
.../tests/stimulus/node/util.test.ts | 14 ----
.../tests/stimulus/{helpers => }/util.test.ts | 16 +++-
src/vite-plugin-symfony/tsconfig.json | 3 +-
18 files changed, 134 insertions(+), 71 deletions(-)
delete mode 100644 src/vite-plugin-symfony/src/stimulus/node/util.ts
rename src/vite-plugin-symfony/src/stimulus/{helpers => }/util.ts (71%)
create mode 100644 src/vite-plugin-symfony/tests/stimulus/index.test.ts
delete mode 100644 src/vite-plugin-symfony/tests/stimulus/node/util.test.ts
rename src/vite-plugin-symfony/tests/stimulus/{helpers => }/util.test.ts (77%)
diff --git a/playground/stimulus/assets/app.js b/playground/stimulus/assets/app.js
index 4caf5b5c..a93dbc25 100644
--- a/playground/stimulus/assets/app.js
+++ b/playground/stimulus/assets/app.js
@@ -1,6 +1,3 @@
-// import { registerVueControllerComponents } from '@symfony/ux-vue';
-// import { registerSvelteControllerComponents } from '@symfony/ux-svelte';
-// import { registerReactControllerComponents } from '@symfony/ux-react';
import './bootstrap.js';
function refreshStickStatus() {
@@ -11,14 +8,5 @@ let $nav = document.querySelector("#nav");
if ($nav) {
window.addEventListener("scroll", refreshStickStatus);
refreshStickStatus();
-
-
-
}
-// if (import.meta.hot) {
-// import.meta.hot.accept((mod, ctx) => {
-// import.meta.hot.send('restart')
-// console.log(mod, ctx)
-// })
-// }
\ No newline at end of file
diff --git a/src/vite-plugin-symfony/.eslintrc b/src/vite-plugin-symfony/.eslintrc
index 76e19121..5c593734 100644
--- a/src/vite-plugin-symfony/.eslintrc
+++ b/src/vite-plugin-symfony/.eslintrc
@@ -20,7 +20,8 @@
},
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
- "@typescript-eslint/no-explicit-any": "off"
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/ban-ts-comment": "off"
},
"ignorePatterns": [
"dist/*.js"
diff --git a/src/vite-plugin-symfony/src/stimulus/helpers/base.ts b/src/vite-plugin-symfony/src/stimulus/helpers/base.ts
index 8a006c9c..64c7e1cb 100644
--- a/src/vite-plugin-symfony/src/stimulus/helpers/base.ts
+++ b/src/vite-plugin-symfony/src/stimulus/helpers/base.ts
@@ -1,6 +1,6 @@
import { Application, Controller } from "@hotwired/stimulus";
import thirdPartyControllers from "virtual:symfony/controllers";
-import { getStimulusControllerFileInfos } from "./util";
+import { getStimulusControllerFileInfos } from "~/stimulus/util";
import { ControllerModule, ImportedModules, LazyModule } from "./types";
declare module "@hotwired/stimulus" {
diff --git a/src/vite-plugin-symfony/src/stimulus/index.ts b/src/vite-plugin-symfony/src/stimulus/index.ts
index 6882b060..f84f8bbd 100644
--- a/src/vite-plugin-symfony/src/stimulus/index.ts
+++ b/src/vite-plugin-symfony/src/stimulus/index.ts
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { Logger, Plugin, ResolvedConfig, UserConfig } from "vite";
import { VitePluginSymfonyStimulusOptions } from "~/types";
-import { getStimulusControllerFileInfos } from "./helpers/util";
+import { getStimulusControllerFileInfos } from "./util";
const applicationGlobalVarName = "$$stimulusApp$$";
@@ -41,11 +41,8 @@ export default function symfonyStimulus(pluginOptions: VitePluginSymfonyStimulus
}
},
transform(code, id, options) {
- if (viteCommand !== "serve") {
- return;
- }
- if (options?.ssr || process.env.VITEST) {
- return;
+ if (viteCommand !== "serve" || (options?.ssr && !process.env.VITEST) || id.includes("node_modules")) {
+ return null;
}
if (id.endsWith("bootstrap.js") || id.endsWith("bootstrap.ts")) {
@@ -61,21 +58,20 @@ export default function symfonyStimulus(pluginOptions: VitePluginSymfonyStimulus
// we don't need lazy behavior, the module is already loaded and we are in a dev environment
const { identifier } = getStimulusControllerFileInfos(id, true);
- if (!identifier) return;
+ if (!identifier) return null;
logger.info(`controller ${identifier}`, { timestamp: true });
const metaHotFooter = `
- if (import.meta.hot) {
- import.meta.hot.accept(newModule => {
- if (!window.${applicationGlobalVarName}) {
- console.warn('Simulus app not available. Are you creating app with startStimulusApp() ?');
- import.meta.hot.invalidate();
- } else {
- window.${applicationGlobalVarName}.register('${identifier}', newModule.default);
- }
- })
- }
- `;
+if (import.meta.hot) {
+ import.meta.hot.accept(newModule => {
+ if (!window.${applicationGlobalVarName}) {
+ console.warn('Simulus app not available. Are you creating app with startStimulusApp() ?');
+ import.meta.hot.invalidate();
+ } else {
+ window.${applicationGlobalVarName}.register('${identifier}', newModule.default);
+ }
+ })
+}`;
return `${code}\n${metaHotFooter}`;
},
diff --git a/src/vite-plugin-symfony/src/stimulus/node/bridge.ts b/src/vite-plugin-symfony/src/stimulus/node/bridge.ts
index ed73abbd..4b251715 100644
--- a/src/vite-plugin-symfony/src/stimulus/node/bridge.ts
+++ b/src/vite-plugin-symfony/src/stimulus/node/bridge.ts
@@ -1,4 +1,4 @@
-import { generateStimulusId } from "./util";
+import { generateStimulusId } from "../util";
import { createRequire } from "node:module";
export const virtualSymfonyControllersModuleId = "virtual:symfony/controllers";
diff --git a/src/vite-plugin-symfony/src/stimulus/node/util.ts b/src/vite-plugin-symfony/src/stimulus/node/util.ts
deleted file mode 100644
index a1dee28e..00000000
--- a/src/vite-plugin-symfony/src/stimulus/node/util.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Normalize the controller name: remove the initial @ and use Stimulus format
-export function generateStimulusId(packageName: string) {
- if (packageName.startsWith("@")) {
- packageName = packageName.substring(1);
- }
- return packageName.replace(/_/g, "-").replace(/\//g, "--");
-}
diff --git a/src/vite-plugin-symfony/src/stimulus/helpers/util.ts b/src/vite-plugin-symfony/src/stimulus/util.ts
similarity index 71%
rename from src/vite-plugin-symfony/src/stimulus/helpers/util.ts
rename to src/vite-plugin-symfony/src/stimulus/util.ts
index de266683..afdcfe40 100644
--- a/src/vite-plugin-symfony/src/stimulus/helpers/util.ts
+++ b/src/vite-plugin-symfony/src/stimulus/util.ts
@@ -16,3 +16,11 @@ export function getStimulusControllerFileInfos(key: string, onlyControllersDir =
lazy: lazy === "lazy",
};
}
+
+// Normalize the controller name: remove the initial @ and use Stimulus format
+export function generateStimulusId(packageName: string) {
+ if (packageName.startsWith("@")) {
+ packageName = packageName.substring(1);
+ }
+ return packageName.replace(/_/g, "-").replace(/\//g, "--");
+}
diff --git a/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts b/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
index 37c99b94..d1ac7095 100644
--- a/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/entryPointsHelper.test.ts
@@ -1,5 +1,5 @@
import { describe, it } from "vitest";
-import { getDevEntryPoints } from "../../src/entrypoints/entryPointsHelper";
+import { getDevEntryPoints } from "~/entrypoints/entryPointsHelper";
import type { ResolvedConfig } from "vite";
import { viteBaseConfig } from "../mocks";
diff --git a/src/vite-plugin-symfony/tests/entrypoints/index.test.ts b/src/vite-plugin-symfony/tests/entrypoints/index.test.ts
index 861b538b..60f2dbad 100644
--- a/src/vite-plugin-symfony/tests/entrypoints/index.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/index.test.ts
@@ -1,6 +1,6 @@
import { describe, it, vi } from "vitest";
-import vitePluginSymfonyEntrypoints from "../../src/entrypoints/index";
+import vitePluginSymfonyEntrypoints from "~/entrypoints/index";
import type { OutputChunk, OutputAsset } from "rollup";
import {
@@ -19,8 +19,9 @@ import {
circular2Js,
viteUserConfigNoRoot,
} from "../mocks";
-import { VitePluginSymfonyOptions } from "../../src/types";
-import { resolvePluginOptions } from "../../src/pluginOptions";
+import { VitePluginSymfonyOptions } from "~/types";
+import { resolvePluginOptions } from "~/pluginOptions";
+import { createLogger } from "vite";
function createBundleObject(files: (OutputChunk | OutputAsset)[]) {
const bundles: {
@@ -36,7 +37,7 @@ function createBundleObject(files: (OutputChunk | OutputAsset)[]) {
function plugin(userOptions: Partial) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { stimulus, ...entrypointsOptions } = resolvePluginOptions(userOptions);
- return vitePluginSymfonyEntrypoints(entrypointsOptions);
+ return vitePluginSymfonyEntrypoints(entrypointsOptions, createLogger());
}
describe("vitePluginSymfonyEntrypoints", () => {
diff --git a/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts b/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
index c1061cac..e34554e4 100644
--- a/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/utils.test.ts
@@ -8,8 +8,8 @@ import {
isSubdirectory,
parseVersionString,
resolveDevServerUrl,
-} from "../../src/entrypoints/utils";
-import { resolvePluginOptions } from "../../src/pluginOptions";
+} from "~/entrypoints/utils";
+import { resolvePluginOptions } from "~/pluginOptions";
import { OutputChunk, OutputAsset, NormalizedOutputOptions } from "rollup";
import {
asyncDepChunk,
@@ -22,7 +22,7 @@ import {
welcomeLegacyJs,
} from "../mocks";
import { resolveConfig, type ResolvedConfig } from "vite";
-import { VitePluginSymfonyOptions } from "../../src/types";
+import { VitePluginSymfonyOptions } from "~/types";
const viteBaseConfig = {
root: "/home/me/project-dir",
diff --git a/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts b/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
index f7fdd30c..6bdfc368 100644
--- a/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
+++ b/src/vite-plugin-symfony/tests/entrypoints/utils.win32.test.ts
@@ -1,5 +1,5 @@
import { describe, it, vi } from "vitest";
-import { isSubdirectory, normalizePath } from "../../src/entrypoints/utils";
+import { isSubdirectory, normalizePath } from "~/entrypoints/utils";
vi.mock("node:path", async () => {
const win32Path = await vi.importActual("node:path/win32");
diff --git a/src/vite-plugin-symfony/tests/pluginOptions.test.ts b/src/vite-plugin-symfony/tests/pluginOptions.test.ts
index ac6a4d74..a96c9b38 100644
--- a/src/vite-plugin-symfony/tests/pluginOptions.test.ts
+++ b/src/vite-plugin-symfony/tests/pluginOptions.test.ts
@@ -1,6 +1,6 @@
import { describe, it } from "vitest";
-import { resolvePluginOptions, resolveBase, resolveOutDir } from "../src/pluginOptions";
-import { VitePluginSymfonyOptions } from "../src/types";
+import { resolvePluginOptions, resolveBase, resolveOutDir } from "~/pluginOptions";
+import { VitePluginSymfonyOptions } from "~/types";
describe("resolvePluginOptions", () => {
it("resolves with default options when no config", ({ expect }) => {
diff --git a/src/vite-plugin-symfony/tests/stimulus/helpers/vue.test.ts b/src/vite-plugin-symfony/tests/stimulus/helpers/vue.test.ts
index 26b9dd8b..18a41fa3 100644
--- a/src/vite-plugin-symfony/tests/stimulus/helpers/vue.test.ts
+++ b/src/vite-plugin-symfony/tests/stimulus/helpers/vue.test.ts
@@ -2,14 +2,15 @@
* @vitest-environment jsdom
*/
import { describe, expect, it } from "vitest";
-import { registerVueControllerComponents } from "../../../src/stimulus/helpers/vue";
+import { registerVueControllerComponents } from "~/stimulus/helpers/vue";
+import { ImportedModules, VueModule } from "~/stimulus/helpers/types";
const fakeVueComponent = () => ({});
const createFakeImportedModules = () => {
return {
"./vue/controllers/Hello.vue": () => Promise.resolve(fakeVueComponent),
- };
+ } as any as ImportedModules;
};
describe("registerVueControllerComponents", () => {
diff --git a/src/vite-plugin-symfony/tests/stimulus/index.test.ts b/src/vite-plugin-symfony/tests/stimulus/index.test.ts
new file mode 100644
index 00000000..967c408c
--- /dev/null
+++ b/src/vite-plugin-symfony/tests/stimulus/index.test.ts
@@ -0,0 +1,76 @@
+import { ConfigEnv, Plugin, UserConfig, createLogger } from "vite";
+import { describe, it, expect } from "vitest";
+import symfonyStimulus from "~/stimulus";
+
+const generateStimulusPlugin = (command: "build" | "serve") => {
+ const plugin: Plugin = symfonyStimulus(
+ {
+ controllersFilePath: "./assets.controllers.json",
+ hmr: true,
+ },
+ createLogger(),
+ );
+ const userConfig: UserConfig = {};
+ const envConfig: ConfigEnv = { command, mode: "development" };
+ // @ts-ignore
+ plugin.config(userConfig, envConfig);
+
+ return plugin;
+};
+describe("stimulus index", () => {
+ it("inject correctly Application global var when server is started", () => {
+ const plugin = generateStimulusPlugin("serve");
+ // @ts-ignore
+ const returnValue = plugin.transform(`const myApp = startStimulusApp();`, "/path/to/bootstrap.js", {});
+ expect(returnValue).toMatchInlineSnapshot(`
+ "const myApp = startStimulusApp();
+ window.$$stimulusApp$$ = myApp"
+ `);
+ });
+ it("doesn't insert Application global var when startStimulusApp is not present", () => {
+ const plugin = generateStimulusPlugin("serve");
+ // @ts-ignore
+ const returnValue = plugin.transform(`const hello = "world;`, "/path/to/bootstrap.js", {});
+ expect(returnValue).toBeNull();
+ });
+ it("doesn't insert Application global var when server is started", () => {
+ const plugin = generateStimulusPlugin("build");
+ // @ts-ignore
+ const returnValue = plugin.transform(`const myApp = startStimulusApp();`, "/path/to/bootstrap.js", {});
+ expect(returnValue).toBeNull();
+ });
+
+ it("inject correctly Controller hot accept", () => {
+ const plugin = generateStimulusPlugin("serve");
+ // @ts-ignore
+ const returnValue = plugin.transform(
+ `export default class controller extends Controller {}`,
+ "/path/to/controllers/welcome_controller.js",
+ {},
+ );
+ expect(returnValue).toMatchInlineSnapshot(`
+ "export default class controller extends Controller {}
+
+ if (import.meta.hot) {
+ import.meta.hot.accept(newModule => {
+ if (!window.$$stimulusApp$$) {
+ console.warn('Simulus app not available. Are you creating app with startStimulusApp() ?');
+ import.meta.hot.invalidate();
+ } else {
+ window.$$stimulusApp$$.register('welcome', newModule.default);
+ }
+ })
+ }"
+ `);
+ });
+ it("doesn't insert Controller hot accept", () => {
+ const plugin = generateStimulusPlugin("serve");
+ // @ts-ignore
+ const returnValue = plugin.transform(
+ `export default class controller extends Controller {}`,
+ "/path/to/assets/other.js",
+ {},
+ );
+ expect(returnValue).toBeNull();
+ });
+});
diff --git a/src/vite-plugin-symfony/tests/stimulus/node/bridge.test.ts b/src/vite-plugin-symfony/tests/stimulus/node/bridge.test.ts
index b78c9902..64e988a7 100644
--- a/src/vite-plugin-symfony/tests/stimulus/node/bridge.test.ts
+++ b/src/vite-plugin-symfony/tests/stimulus/node/bridge.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest";
-import { createControllersModule } from "../../../src/stimulus/node/bridge";
+import { createControllersModule } from "~/stimulus/node/bridge";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
diff --git a/src/vite-plugin-symfony/tests/stimulus/node/util.test.ts b/src/vite-plugin-symfony/tests/stimulus/node/util.test.ts
deleted file mode 100644
index ada0062b..00000000
--- a/src/vite-plugin-symfony/tests/stimulus/node/util.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { describe, it } from "vitest";
-import { generateStimulusId } from "../../../src/stimulus/node/util";
-
-describe("stimulus", () => {
- it("identifierFromThirdParty generate correct identifier", ({ expect }) => {
- const list = [
- ["@symfony/ux-toggle-password/toggle-password", "symfony--ux-toggle-password--toggle-password"],
- ["my-custom-package/toggle-password", "my-custom-package--toggle-password"],
- ];
- list.forEach(([input, result]) => {
- expect(generateStimulusId(input)).toBe(result);
- });
- });
-});
diff --git a/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts b/src/vite-plugin-symfony/tests/stimulus/util.test.ts
similarity index 77%
rename from src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts
rename to src/vite-plugin-symfony/tests/stimulus/util.test.ts
index 216e6fa5..659ecc20 100644
--- a/src/vite-plugin-symfony/tests/stimulus/helpers/util.test.ts
+++ b/src/vite-plugin-symfony/tests/stimulus/util.test.ts
@@ -1,7 +1,19 @@
import { describe, it, expect } from "vitest";
-import { getStimulusControllerFileInfos } from "../../../src/stimulus/helpers/util";
+import { getStimulusControllerFileInfos, generateStimulusId } from "~/stimulus/util";
-describe("stimulus", () => {
+describe("stimulus generateStimulusId", () => {
+ it("identifierFromThirdParty generate correct identifier", ({ expect }) => {
+ const list = [
+ ["@symfony/ux-toggle-password/toggle-password", "symfony--ux-toggle-password--toggle-password"],
+ ["my-custom-package/toggle-password", "my-custom-package--toggle-password"],
+ ];
+ list.forEach(([input, result]) => {
+ expect(generateStimulusId(input)).toBe(result);
+ });
+ });
+});
+
+describe("stimulus getStimulusControllerFileInfos", () => {
it.each([
{
input: "./controllers/welcome_controller.js",
diff --git a/src/vite-plugin-symfony/tsconfig.json b/src/vite-plugin-symfony/tsconfig.json
index 6abaaae1..39a75797 100644
--- a/src/vite-plugin-symfony/tsconfig.json
+++ b/src/vite-plugin-symfony/tsconfig.json
@@ -16,6 +16,7 @@
}
},
"include": [
- "src/**/*.ts"
+ "src/**/*.ts",
+ "tests/**/*.ts"
]
}
\ No newline at end of file
From 835f48f5a201df58b8903858e63a056dbeea0184 Mon Sep 17 00:00:00 2001
From: Hugues Tavernier
Date: Tue, 21 Nov 2023 09:15:17 +0100
Subject: [PATCH 4/4] doc mermaid chart implementation
---
CHANGELOG.md | 2 +
docs/.gitignore | 2 +
docs/.vitepress/config.ts | 5 +-
docs/.vitepress/mermaid.config.json | 3 +
docs/.vitepress/mermaid.ts | 84 +++
docs/.vitepress/puppeteer-config.json | 3 +
docs/env.d.ts | 1 +
docs/package.json | 2 +
docs/pnpm-lock.yaml | 535 ++++++++++++++++++
docs/src/fr/stimulus/installation.md | 36 ++
docs/src/guide/getting-started.md | 2 +
docs/src/stimulus/installation.md | 36 ++
src/vite-plugin-symfony/.eslintrc | 3 +-
.../src/entrypoints/entryPointsHelper.ts | 19 +-
.../src/entrypoints/index.ts | 51 +-
.../src/entrypoints/pathMapping.ts | 15 +
.../src/entrypoints/utils.ts | 23 +-
src/vite-plugin-symfony/src/index.ts | 3 +-
src/vite-plugin-symfony/src/logger.ts | 166 ++++++
src/vite-plugin-symfony/src/stimulus/index.ts | 6 +-
.../tests/entrypoints/index.test.ts | 3 +-
.../tests/entrypoints/utils.test.ts | 51 ++
22 files changed, 974 insertions(+), 77 deletions(-)
create mode 100644 docs/.vitepress/mermaid.config.json
create mode 100644 docs/.vitepress/mermaid.ts
create mode 100644 docs/.vitepress/puppeteer-config.json
create mode 100644 docs/env.d.ts
create mode 100644 src/vite-plugin-symfony/src/entrypoints/pathMapping.ts
create mode 100644 src/vite-plugin-symfony/src/logger.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f76a0f35..2322567f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
- stimulus HMR
- fix bug : stimulus restart vite dev server when controllers.json is updated
- split vite-plugin-symfony into 2 plugins `vite-plugin-symfony-entrypoints` and `vite-plugin-symfony-stimulus`.
+- add new tests to vite-plugin-symfony
+- doc : add mermaid charts
## v6.2.0
diff --git a/docs/.gitignore b/docs/.gitignore
index 60327a26..d8e5f809 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -5,3 +5,5 @@ node_modules/
.vitepress/dist/
.vscode/
.local/
+
+graphs/
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 37936ceb..e0b774ab 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -1,10 +1,13 @@
import { defineConfig } from 'vitepress';
import { fileURLToPath, URL } from 'node:url';
+import { renderMermaidGraphsPlugin } from './mermaid'
// https://vitepress.dev/reference/site-config
export default defineConfig({
vite: {
- publicDir: "./public",
+ plugins: [
+ renderMermaidGraphsPlugin()
+ ],
resolve: {
alias: [
{
diff --git a/docs/.vitepress/mermaid.config.json b/docs/.vitepress/mermaid.config.json
new file mode 100644
index 00000000..5a8cafb8
--- /dev/null
+++ b/docs/.vitepress/mermaid.config.json
@@ -0,0 +1,3 @@
+{
+ "themeCSS": "* { line-height: 1.5; } span.edgeLabel { padding: 2px 5px; }"
+}
diff --git a/docs/.vitepress/mermaid.ts b/docs/.vitepress/mermaid.ts
new file mode 100644
index 00000000..dab037f4
--- /dev/null
+++ b/docs/.vitepress/mermaid.ts
@@ -0,0 +1,84 @@
+import { exec } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { readFile, writeFile } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { promisify } from 'node:util';
+import type { Plugin, PluginOption } from 'vite';
+import { mkdir, readdir } from 'node:fs/promises'
+
+export async function getFilesInDirectory(directory: URL): Promise {
+ return (await readdir(directory)).filter(file => file[0] !== '.');
+}
+
+
+const execPromise = promisify(exec);
+const graphsDirectory = new URL('graphs/', import.meta.url);
+
+const mermaidRegExp = /^```mermaid\n([\S\s]*?)\n```/gm;
+const greaterThanRegExp = />/g;
+const styleTagRegExp = /