diff --git a/CHANGELOG.md b/CHANGELOG.md index 08889a3..633aef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 3.2.0 + +* Allow the controller name to be overridden by the package or user. See #70. + +* Moved TypeScript types into the package. See #55. + ## 3.1.0 * Automatically enabled Stimulus's "debug" mode when doing a dev build. You will diff --git a/README.md b/README.md index e884950..4f40cd5 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ automatically when you install [Symfony UX Packages](https://github.com/symfony/ Before you start, familiarize yourself with the basics of [Stimulus](https://stimulus.hotwired.dev/). -Symfony UX Stimulus bridge is currently considered **experimental**. - ## Installation If you don't already have Webpack Encore installed, install it with: @@ -266,8 +264,8 @@ const app = startStimulusApp(require.context( )); // the normal way to manually register controllers -application.register('clipboard', Clipboard) -application.register('autocomplete', Autocomplete) +app.register('clipboard', Clipboard); +app.register('autocomplete', Autocomplete); export { app }; ``` diff --git a/dist/index.js b/dist/index.js index 5ee09e8..0ccf82e 100644 --- a/dist/index.js +++ b/dist/index.js @@ -41,9 +41,7 @@ function startStimulusApp(context) { if (!symfonyControllers.hasOwnProperty(controllerName)) { continue; } - symfonyControllers[controllerName].then((module) => { - application.register(controllerName, module.default); - }); + application.register(controllerName, symfonyControllers[controllerName]); } return application; } diff --git a/dist/webpack/lazy-controller-loader.js b/dist/webpack/lazy-controller-loader.js index 8765a21..75b4176 100644 --- a/dist/webpack/lazy-controller-loader.js +++ b/dist/webpack/lazy-controller-loader.js @@ -30,24 +30,22 @@ var schemaUtils__namespace = /*#__PURE__*/_interopNamespace(schemaUtils); function generateLazyController (controllerPath, indentationSpaces, exportName = 'default') { const spaces = ' '.repeat(indentationSpaces); - return `${spaces}(function() { -${spaces} return class LazyController extends Controller { -${spaces} constructor(context) { -${spaces} super(context); -${spaces} this.__stimulusLazyController = true; -${spaces} } -${spaces} initialize() { -${spaces} if (this.application.controllers.find((controller) => { -${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; -${spaces} })) { -${spaces} return; -${spaces} } -${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { -${spaces} this.application.register(this.identifier, controller.${exportName}); -${spaces} }); + return `class extends Controller { +${spaces} constructor(context) { +${spaces} super(context); +${spaces} this.__stimulusLazyController = true; +${spaces} } +${spaces} initialize() { +${spaces} if (this.application.controllers.find((controller) => { +${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; +${spaces} })) { +${spaces} return; ${spaces} } +${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { +${spaces} this.application.register(this.identifier, controller.${exportName}); +${spaces} }); ${spaces} } -${spaces}})()`; +${spaces}}`; } // Reserved word lists for various dialects of the language diff --git a/dist/webpack/loader.js b/dist/webpack/loader.js index ef8ef66..c32088e 100644 --- a/dist/webpack/loader.js +++ b/dist/webpack/loader.js @@ -8,37 +8,35 @@ var LoaderDependency__default = /*#__PURE__*/_interopDefaultLegacy(LoaderDepende function generateLazyController (controllerPath, indentationSpaces, exportName = 'default') { const spaces = ' '.repeat(indentationSpaces); - return `${spaces}(function() { -${spaces} return class LazyController extends Controller { -${spaces} constructor(context) { -${spaces} super(context); -${spaces} this.__stimulusLazyController = true; -${spaces} } -${spaces} initialize() { -${spaces} if (this.application.controllers.find((controller) => { -${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; -${spaces} })) { -${spaces} return; -${spaces} } -${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { -${spaces} this.application.register(this.identifier, controller.${exportName}); -${spaces} }); + return `class extends Controller { +${spaces} constructor(context) { +${spaces} super(context); +${spaces} this.__stimulusLazyController = true; +${spaces} } +${spaces} initialize() { +${spaces} if (this.application.controllers.find((controller) => { +${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; +${spaces} })) { +${spaces} return; ${spaces} } +${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { +${spaces} this.application.register(this.identifier, controller.${exportName}); +${spaces} }); ${spaces} } -${spaces}})()`; +${spaces}}`; } function createControllersModule(config) { let controllerContents = 'export default {'; - let autoImportContents = ''; + let importStatementContents = ''; let hasLazyControllers = false; - const deprecations = []; if ('undefined' !== typeof config['placeholder']) { throw new Error('Your controllers.json file was not found. Be sure to add a Webpack alias from "@symfony/stimulus-bridge/controllers.json" to *your* controllers.json file.'); } if ('undefined' === typeof config['controllers']) { throw new Error('Your Stimulus configuration file (assets/controllers.json) lacks a "controllers" key.'); } + let controllerIndex = 0; for (const packageName in config.controllers) { let packageConfig; try { @@ -58,24 +56,19 @@ function createControllersModule(config) { continue; } const controllerMain = packageName + '/' + controllerPackageConfig.main; - let fetchMode = 'eager'; - if ('undefined' !== typeof controllerUserConfig.webpackMode) { - deprecations.push('The "webpackMode" config key is deprecated in controllers.json. Use "fetch" instead, set to either "eager" or "lazy".'); + let fetchMode = controllerUserConfig.fetch || 'eager'; + let moduleValueContents = ``; + if (fetchMode === 'eager') { + const controllerNameForVariable = `controller_${controllerIndex++}`; + importStatementContents += `import ${controllerNameForVariable} from '${controllerMain}';\n`; + moduleValueContents = controllerNameForVariable; } - if ('undefined' !== typeof controllerUserConfig.fetch) { - if (!['eager', 'lazy'].includes(controllerUserConfig.fetch)) { - throw new Error(`Invalid "fetch" value "${controllerUserConfig.fetch}" in controllers.json. Expected "eager" or "lazy".`); - } - fetchMode = controllerUserConfig.fetch; - } - let moduleValueContents = `import(/* webpackMode: "eager" */ '${controllerMain}')`; - if (fetchMode === 'lazy') { + else if (fetchMode === 'lazy') { hasLazyControllers = true; - moduleValueContents = ` -new Promise((resolve, reject) => resolve({ default: -${generateLazyController(controllerMain, 6)} - })) - `.trim(); + moduleValueContents = generateLazyController(controllerMain, 2); + } + else { + throw new Error(`Invalid fetch mode "${fetchMode}" in controllers.json. Expected "eager" or "lazy".`); } let controllerNormalizedName = controllerReference.substr(1).replace(/_/g, '-').replace(/\//g, '--'); if ('undefined' !== typeof controllerPackageConfig.name) { @@ -87,7 +80,7 @@ ${generateLazyController(controllerMain, 6)} controllerContents += `\n '${controllerNormalizedName}': ${moduleValueContents},`; for (const autoimport in controllerUserConfig.autoimport || []) { if (controllerUserConfig.autoimport[autoimport]) { - autoImportContents += "import '" + autoimport + "';\n"; + importStatementContents += "import '" + autoimport + "';\n"; } } } @@ -96,8 +89,8 @@ ${generateLazyController(controllerMain, 6)} controllerContents = `import { Controller } from '@hotwired/stimulus';\n${controllerContents}`; } return { - finalSource: `${autoImportContents}${controllerContents}\n};`, - deprecations, + finalSource: `${importStatementContents}${controllerContents}\n};`, + deprecations: [], }; } diff --git a/package.json b/package.json index 87b5327..78ec593 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@symfony/stimulus-bridge", "description": "Stimulus integration bridge for Symfony projects", - "version": "3.2.1", + "version": "3.2.2", "main": "dist/index.js", "types": "dist/index.d.ts", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 6a258da..1c98bc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,9 +34,7 @@ export function startStimulusApp(context: __WebpackModuleApi.RequireContext) { continue; } - symfonyControllers[controllerName].then((module: any) => { - application.register(controllerName, module.default); - }); + application.register(controllerName, symfonyControllers[controllerName]); } return application; diff --git a/src/webpack/create-controllers-module.ts b/src/webpack/create-controllers-module.ts index 0e8667c..ae59b60 100644 --- a/src/webpack/create-controllers-module.ts +++ b/src/webpack/create-controllers-module.ts @@ -13,9 +13,8 @@ import generateLazyController from './generate-lazy-controller'; export default function createControllersModule(config: any) { let controllerContents = 'export default {'; - let autoImportContents = ''; + let importStatementContents = ''; let hasLazyControllers = false; - const deprecations = []; if ('undefined' !== typeof config['placeholder']) { throw new Error( @@ -27,6 +26,7 @@ export default function createControllersModule(config: any) { throw new Error('Your Stimulus configuration file (assets/controllers.json) lacks a "controllers" key.'); } + let controllerIndex = 0; for (const packageName in config.controllers) { let packageConfig; try { @@ -55,32 +55,20 @@ export default function createControllersModule(config: any) { } const controllerMain = packageName + '/' + controllerPackageConfig.main; - let fetchMode = 'eager'; + let fetchMode = controllerUserConfig.fetch || 'eager'; - if ('undefined' !== typeof controllerUserConfig.webpackMode) { - deprecations.push( - 'The "webpackMode" config key is deprecated in controllers.json. Use "fetch" instead, set to either "eager" or "lazy".' - ); - } - - if ('undefined' !== typeof controllerUserConfig.fetch) { - if (!['eager', 'lazy'].includes(controllerUserConfig.fetch)) { - throw new Error( - `Invalid "fetch" value "${controllerUserConfig.fetch}" in controllers.json. Expected "eager" or "lazy".` - ); - } - - fetchMode = controllerUserConfig.fetch; - } + let moduleValueContents = ``; + if (fetchMode === 'eager') { + const controllerNameForVariable = `controller_${controllerIndex++}`; + importStatementContents += `import ${controllerNameForVariable} from '${controllerMain}';\n`; - let moduleValueContents = `import(/* webpackMode: "eager" */ '${controllerMain}')`; - if (fetchMode === 'lazy') { + // simply use the imported controller + moduleValueContents = controllerNameForVariable; + } else if (fetchMode === 'lazy') { hasLazyControllers = true; - moduleValueContents = ` -new Promise((resolve, reject) => resolve({ default: -${generateLazyController(controllerMain, 6)} - })) - `.trim(); + moduleValueContents = generateLazyController(controllerMain, 2); + } else { + throw new Error(`Invalid fetch mode "${fetchMode}" in controllers.json. Expected "eager" or "lazy".`); } // Normalize the controller name: remove the initial @ and use Stimulus format @@ -97,7 +85,7 @@ ${generateLazyController(controllerMain, 6)} for (const autoimport in controllerUserConfig.autoimport || []) { if (controllerUserConfig.autoimport[autoimport]) { - autoImportContents += "import '" + autoimport + "';\n"; + importStatementContents += "import '" + autoimport + "';\n"; } } } @@ -108,7 +96,7 @@ ${generateLazyController(controllerMain, 6)} } return { - finalSource: `${autoImportContents}${controllerContents}\n};`, - deprecations, + finalSource: `${importStatementContents}${controllerContents}\n};`, + deprecations: [], }; } diff --git a/src/webpack/generate-lazy-controller.ts b/src/webpack/generate-lazy-controller.ts index 1312749..1021bea 100644 --- a/src/webpack/generate-lazy-controller.ts +++ b/src/webpack/generate-lazy-controller.ts @@ -18,22 +18,20 @@ export default function(controllerPath: string, indentationSpaces: number, exportName = 'default') { const spaces = ' '.repeat(indentationSpaces); - return `${spaces}(function() { -${spaces} return class LazyController extends Controller { -${spaces} constructor(context) { -${spaces} super(context); -${spaces} this.__stimulusLazyController = true; -${spaces} } -${spaces} initialize() { -${spaces} if (this.application.controllers.find((controller) => { -${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; -${spaces} })) { -${spaces} return; -${spaces} } -${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { -${spaces} this.application.register(this.identifier, controller.${exportName}); -${spaces} }); + return `class extends Controller { +${spaces} constructor(context) { +${spaces} super(context); +${spaces} this.__stimulusLazyController = true; +${spaces} } +${spaces} initialize() { +${spaces} if (this.application.controllers.find((controller) => { +${spaces} return controller.identifier === this.identifier && controller.__stimulusLazyController; +${spaces} })) { +${spaces} return; ${spaces} } +${spaces} import('${controllerPath.replace(/\\/g, '\\\\')}').then((controller) => { +${spaces} this.application.register(this.identifier, controller.${exportName}); +${spaces} }); ${spaces} } -${spaces}})()`; +${spaces}}`; } diff --git a/test/fixtures/deprecated-webpack-mode.json b/test/fixtures/deprecated-webpack-mode.json deleted file mode 100644 index a05d3e1..0000000 --- a/test/fixtures/deprecated-webpack-mode.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "controllers": { - "@symfony/mock-module": { - "mock": { - "webpackMode": "eager", - "enabled": true - } - } - }, - "entrypoints": [] -} diff --git a/test/webpack/create-controllers-module.test.ts b/test/webpack/create-controllers-module.test.ts index c350f15..99abc13 100644 --- a/test/webpack/create-controllers-module.test.ts +++ b/test/webpack/create-controllers-module.test.ts @@ -33,7 +33,7 @@ describe('createControllersModule', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require('../fixtures/disabled-autoimport.json'); expect(createControllersModule(config).finalSource).toEqual( - "export default {\n 'symfony--mock-module--mock': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/controller.js'),\n};" + "import controller_0 from '@symfony/mock-module/dist/controller.js';\nexport default {\n 'symfony--mock-module--mock': controller_0,\n};" ); }); }); @@ -43,29 +43,17 @@ describe('createControllersModule', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require('../fixtures/eager-no-autoimport.json'); expect(createControllersModule(config).finalSource).toEqual( - "export default {\n 'symfony--mock-module--mock': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/controller.js'),\n};" + "import controller_0 from '@symfony/mock-module/dist/controller.js';\nexport default {\n 'symfony--mock-module--mock': controller_0,\n};" ); }); }); - describe('deprecated-webpack-mode.json', () => { - it('must return eager mode with deprecation', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const config = require('../fixtures/deprecated-webpack-mode.json'); - const result = createControllersModule(config); - expect(result.finalSource).toEqual( - "export default {\n 'symfony--mock-module--mock': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/controller.js'),\n};" - ); - expect(result.deprecations).toHaveLength(1); - }); - }); - describe('eager-autoimport.json', () => { it('must return a file with the enabled controller and auto-import', () => { // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require('../fixtures/eager-autoimport.json'); expect(createControllersModule(config).finalSource).toEqual( - "import '@symfony/mock-module/dist/style.css';\nexport default {\n 'symfony--mock-module--mock': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/controller.js'),\n};" + "import controller_0 from '@symfony/mock-module/dist/controller.js';\nimport '@symfony/mock-module/dist/style.css';\nexport default {\n 'symfony--mock-module--mock': controller_0,\n};" ); }); }); @@ -78,26 +66,22 @@ describe('createControllersModule', () => { ` import { Controller } from '@hotwired/stimulus'; export default { - 'symfony--mock-module--mock': new Promise((resolve, reject) => resolve({ default: - (function() { - return class LazyController extends Controller { - constructor(context) { - super(context); - this.__stimulusLazyController = true; - } - initialize() { - if (this.application.controllers.find((controller) => { - return controller.identifier === this.identifier && controller.__stimulusLazyController; - })) { - return; - } - import('@symfony/mock-module/dist/controller.js').then((controller) => { - this.application.register(this.identifier, controller.default); - }); - } + 'symfony--mock-module--mock': class extends Controller { + constructor(context) { + super(context); + this.__stimulusLazyController = true; + } + initialize() { + if (this.application.controllers.find((controller) => { + return controller.identifier === this.identifier && controller.__stimulusLazyController; + })) { + return; } - })() - })), + import('@symfony/mock-module/dist/controller.js').then((controller) => { + this.application.register(this.identifier, controller.default); + }); + } + }, }; `.trim() ); @@ -109,7 +93,7 @@ export default { // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require('../fixtures/load-named-controller.json'); expect(createControllersModule(config).finalSource).toEqual( - "export default {\n 'foo--custom_name': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/named_controller.js'),\n};" + "import controller_0 from '@symfony/mock-module/dist/named_controller.js';\nexport default {\n 'foo--custom_name': controller_0,\n};" ); }); }); @@ -119,7 +103,7 @@ export default { // eslint-disable-next-line @typescript-eslint/no-var-requires const config = require('../fixtures/override-name.json'); expect(createControllersModule(config).finalSource).toEqual( - "export default {\n 'foo--overridden_name': import(/* webpackMode: \"eager\" */ '@symfony/mock-module/dist/controller.js'),\n};" + "import controller_0 from '@symfony/mock-module/dist/controller.js';\nexport default {\n 'foo--overridden_name': controller_0,\n};" ); }); }); diff --git a/test/webpack/generate-lazy-controller.test.ts b/test/webpack/generate-lazy-controller.test.ts index 6e761a0..6d0898f 100644 --- a/test/webpack/generate-lazy-controller.test.ts +++ b/test/webpack/generate-lazy-controller.test.ts @@ -13,12 +13,19 @@ import generateLazyController from '../../src/webpack/generate-lazy-controller'; import { parse } from '@babel/parser'; import { isNode } from '@babel/types'; +const wrapLazyController = (controllerString: string): string => { + return ` + const { Controller } = require('@hotwired/stimulus'); + (function() { + return ${controllerString} + })() + `; +}; + describe('generateLazyControllerModule', () => { describe('generateLazyController()', () => { it('must return a functional ES6 class', () => { - const controllerCode = - "const { Controller } = require('@hotwired/stimulus');\n" + - generateLazyController('@symfony/some-module/dist/controller.js', 0); + const controllerCode = wrapLazyController(generateLazyController('@symfony/some-module/dist/controller.js', 0)); const result = parse(controllerCode, { sourceType: 'module', }); @@ -30,10 +37,8 @@ describe('generateLazyControllerModule', () => { expect(controllerCode).toContain('this.application.register(this.identifier, controller.default)'); }); - it('must return a functional ES5 class on Windows', () => { - const controllerCode = - "const { Controller } = require('@hotwired/stimulus');\n" + - generateLazyController('C:\\\\path\\to\\file.js', 0); + it('must return a functional ES6 class on Windows', () => { + const controllerCode = wrapLazyController(generateLazyController('C:\\\\path\\to\\file.js', 0)); const result = parse(controllerCode, { sourceType: 'module', }); @@ -49,9 +54,7 @@ describe('generateLazyControllerModule', () => { }); it('must use the correct, named export', () => { - const controllerCode = - "const { Controller } = require('@hotwired/stimulus');\n" + - generateLazyController('@symfony/some-module/dist/controller.js', 0, 'CustomController'); + const controllerCode = wrapLazyController(generateLazyController('@symfony/some-module/dist/controller.js', 0, 'CustomController')); const result = parse(controllerCode, { sourceType: 'module', }); diff --git a/test/webpack/lazy-controller-loader.test.ts b/test/webpack/lazy-controller-loader.test.ts index 3fd46ab..196e986 100644 --- a/test/webpack/lazy-controller-loader.test.ts +++ b/test/webpack/lazy-controller-loader.test.ts @@ -47,7 +47,7 @@ describe('lazyControllerLoader', () => { it('exports a lazy controller', () => { const src = "/* stimulusFetch: 'lazy' */ export default class extends Controller {}"; // look for a little bit of the lazy controller code - expect(callLoader(src).content).toContain('class LazyController'); + expect(callLoader(src).content).toContain('this.__stimulusLazyController'); // unfortunately, we cannot pass along sourceMap info since we changed the source expect(callLoader(src, 'source_map_contents').sourceMap).toBeUndefined(); expect(callLoader(src).errors).toHaveLength(0); @@ -66,14 +66,14 @@ describe('lazyControllerLoader', () => { it('reads ?lazy option', () => { const src = 'export default class extends Controller {}'; const results = callLoader(src, '', '?lazy=true'); - expect(results.content).toContain('class LazyController'); + expect(results.content).toContain('this.__stimulusLazyController'); expect(results.errors).toHaveLength(0); }); it('reads ?lazy and it wins over comments', () => { const src = "/* stimulusFetch: 'eager' */ export default class extends Controller {}"; const results = callLoader(src, '', '?lazy=true'); - expect(results.content).toContain('class LazyController'); + expect(results.content).toContain('this.__stimulusLazyController'); expect(results.errors).toHaveLength(0); }); @@ -81,7 +81,7 @@ describe('lazyControllerLoader', () => { const src = 'const MyController = class extends Controller {}; export { MyController };'; const results = callLoader(src, '', '?lazy=true&export=MyController'); // check that the results are lazy - expect(results.content).toContain('class LazyController'); + expect(results.content).toContain('this.__stimulusLazyController'); // check named export expect(results.content).toContain('export { controller as MyController };'); expect(results.errors).toHaveLength(0);