single-spa / single-spa-angular

Helpers for building single-spa applications which use Angular
Apache License 2.0
201 stars 78 forks source link

Asset URL in styles broken in Angular 11 #317

Closed daniloesk closed 3 years ago

daniloesk commented 3 years ago

The rewrite solution for asset URL in style files documented in https://single-spa.js.org/docs/ecosystem-angular/#styles using rebaseRootRelativeCssUrls no longer works in Angular 11 since that configuration was removed. deployUrl is also deprecated for ng serve and may be removed in the future.

It looks like such solution was actually based on rebaseRootRelativeCssUrls and deployUrl, not on public path. I built an Angular 10 demo with assert URL rewriting without setting the public path (https://github.com/daniloesk/single-spa-errors/tree/css-asset-ng10). This one works.

Also built a similiar demo for Angular 11, with only the style with asset URL, since rebaseRootRelativeCssUrls is no longer around: https://github.com/daniloesk/single-spa-errors/tree/single-spa-angular%23317. This is the one that doesn't work.

Demonstration

  1. Get the demo
  2. npm install
  3. npm run serve:prod
  4. Load it on http://single-spa-playground.org/playground/verify-app-guide using:
  5. Access http://single-spa-playground.org/angular11-basic

Expected Behavior

loader.gif is loaded from http://localhost:11200 and displayed.

Actual Behavior

loader.gif is loaded from http://single-spa-playground.org and not found.

daniloesk commented 3 years ago

Maybe a workaround using SCSS and Angular Style preprocessor options?

daniloesk commented 3 years ago

After taking a deeper look into Angular's webpack configuration I realized I misunderstood how Angular handles styles. Actually the root style.css is not processed by the webpack rules, unlike component styles. Moving the URL into a component style and using deployUrl had the static publicPath set on the build and serve contents.

Here is an updated demo on the change: https://github.com/daniloesk/single-spa-errors/tree/single-spa-angular%23317-closed (the printed webpack config is what gave me the insight)

Having a runtime publicPath evaluated from the actual hosting URL would be better, but this already makes it work.

Still not using interop of any other special publicPath configuration unlike stated in the documentation. It is very possible I am missing something here, in which case I would be very glad to have some directions. Otherwise updating the doc would be a good idea.

daniloesk commented 3 years ago

Forgot to make it clear:

joeldenning commented 3 years ago

Hi @daniloesk, thanks for the details here. Ideally we'd get Angular CLI to allow us to set the webpack public path dynamically in the browser - this is how things work in the single-spa ecosystem for react, vue, etc. The reason that --deploy-url and rebaseRootRelativeCssUrls have been encouraged with single-spa-angular is only because Angular CLI's webpack config is rigidly opinionated about having a build-time public path and I wasn't able to get it to respect a runtime public path.

The best solution here in my opinion would be to investigate if we can get Angular CLI to compile asset urls to something like __webpack_public_path__ + '/assets/img.png'.

daniloesk commented 3 years ago

Updated the https://github.com/daniloesk/single-spa-errors/tree/single-spa-angular%23317-closed tag. No idea why it was pointing to master...

daniloesk commented 3 years ago

Tried to inject some options in the rules using extra-webpack.config.js with no luck. Below are the rules after singleSpaAngularWebpack (used inspect). I am guessing they already include Angular rules.

webpack rules ```typescript rules: [ { test: /\.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani|avif)$/, loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/file-loader/dist/cjs.js', options: { name: '[name].[ext]', emitFile: true } }, { test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/, parser: { system: true } }, { test: /[\/\\]rxjs[\/\\]add[\/\\].+\.js$/, sideEffects: true }, { test: /\.m?js$/, exclude: [ /[\/\\](?:core-js|\@babel|tslib)[\/\\]/, /(ngfactory|ngstyle)\.js$/ ], use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js', options: { sourceMap: false } } ] }, { exclude: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.css$/, use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/raw-loader/dist/cjs.js' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } } ] }, { exclude: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.scss$|\.sass$/, use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/raw-loader/dist/cjs.js' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/resolve-url-loader/index.js', options: { sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/sass-loader/dist/cjs.js', options: { implementation: { render: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': '_render', '$signature': [Function (anonymous)], 'call$2': [Function: _render] { '$callName': 'call$2', '$requiredArgCount': 2, '$defaultValues': null, '$stubName': '_render', '$tearOff': [Function (anonymous)] }, 'call*': [Function: _render] { '$callName': 'call$2', '$requiredArgCount': 2, '$defaultValues': null, '$stubName': '_render', '$tearOff': [Function (anonymous)] }, '$requiredArgCount': 2, '$defaultValues': null, '$dart_jsFunction': [Circular *1] } }, renderSync: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': '_renderSync', '$signature': [Function (anonymous)], 'call$1': [Function: _renderSync] { '$callName': 'call$1', '$requiredArgCount': 1, '$defaultValues': null, '$stubName': '_renderSync', '$tearOff': [Function (anonymous)] }, 'call*': [Function: _renderSync] { '$callName': 'call$1', '$requiredArgCount': 1, '$defaultValues': null, '$stubName': '_renderSync', '$tearOff': [Function (anonymous)] }, '$requiredArgCount': 1, '$defaultValues': null, '$dart_jsFunction': [Circular *2] } }, info: 'dart-sass\t1.27.0\t(Sass Compiler)\t[Dart]\n' + 'dart2js\t2.10.1\t(Dart Compiler)\t[Dart]', types: { Boolean: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure40 { '$dart_jsFunction': [Circular *3] }, TRUE: SassBoolean { value: true }, FALSE: SassBoolean { value: false } }, Color: [Function: SassColor] { '___dart__$dart_dartClosure_ZxYxX_0_': closure245 { '_$dart_jsFunctionCaptureThis': [Circular *4] } }, List: [Function: SassList] { '___dart__$dart_dartClosure_ZxYxX_0_': closure238 { '_$dart_jsFunctionCaptureThis': [Circular *5] } }, Map: [Function: SassMap] { '___dart__$dart_dartClosure_ZxYxX_0_': closure231 { '_$dart_jsFunctionCaptureThis': [Circular *6] } }, Null: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure35 { '$dart_jsFunction': [Circular *7] }, NULL: SassNull { toString: [Function] } }, Number: [Function: SassNumber] { '___dart__$dart_dartClosure_ZxYxX_0_': closure224 { '_$dart_jsFunctionCaptureThis': [Circular *8] } }, String: [Function: SassString] { '___dart__$dart_dartClosure_ZxYxX_0_': closure220 { '_$dart_jsFunctionCaptureThis': [Circular *9] } }, Error: [Function: Error] { stackTraceLimit: 16, prepareStackTrace: undefined } }, NULL: SassNull { toString: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure36 { '$dart_jsFunction': [Circular *10] } } }, TRUE: SassBoolean { value: true }, FALSE: SassBoolean { value: false }, cli_pkg_main_0_: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _wrapMain_closure0 { main: StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': 'main', '$signature': [Function (anonymous)], 'call$1': [Function], 'call*': [Function], '$requiredArgCount': 1, '$defaultValues': null }, '$dart_jsFunction': [Circular *11] } } }, sourceMap: true, sassOptions: { precision: 8, includePaths: [], outputStyle: 'expanded' } } } ] }, { exclude: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.less$/, use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/raw-loader/dist/cjs.js' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/less-loader/dist/cjs.js', options: { sourceMap: false, lessOptions: { javascriptEnabled: true, paths: [] } } } ] }, { exclude: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.styl$/, use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/raw-loader/dist/cjs.js' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/resolve-url-loader/index.js', options: { sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/stylus-loader/dist/cjs.js', options: { sourceMap: false, webpackImporter: false, stylusOptions: { compress: false, sourceMap: { comment: false }, paths: [] } } } ] }, { include: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/css-loader/dist/cjs.js', options: { url: false, sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } } ] }, { include: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.scss$|\.sass$/, use: [ { loader: 'style-loader' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/css-loader/dist/cjs.js', options: { url: false, sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/resolve-url-loader/index.js', options: { sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/sass-loader/dist/cjs.js', options: { implementation: { render: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': '_render', '$signature': [Function (anonymous)], 'call$2': [Function: _render] { '$callName': 'call$2', '$requiredArgCount': 2, '$defaultValues': null, '$stubName': '_render', '$tearOff': [Function (anonymous)] }, 'call*': [Function: _render] { '$callName': 'call$2', '$requiredArgCount': 2, '$defaultValues': null, '$stubName': '_render', '$tearOff': [Function (anonymous)] }, '$requiredArgCount': 2, '$defaultValues': null, '$dart_jsFunction': [Circular *1] } }, renderSync: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': '_renderSync', '$signature': [Function (anonymous)], 'call$1': [Function: _renderSync] { '$callName': 'call$1', '$requiredArgCount': 1, '$defaultValues': null, '$stubName': '_renderSync', '$tearOff': [Function (anonymous)] }, 'call*': [Function: _renderSync] { '$callName': 'call$1', '$requiredArgCount': 1, '$defaultValues': null, '$stubName': '_renderSync', '$tearOff': [Function (anonymous)] }, '$requiredArgCount': 1, '$defaultValues': null, '$dart_jsFunction': [Circular *2] } }, info: 'dart-sass\t1.27.0\t(Sass Compiler)\t[Dart]\n' + 'dart2js\t2.10.1\t(Dart Compiler)\t[Dart]', types: { Boolean: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure40 { '$dart_jsFunction': [Circular *3] }, TRUE: SassBoolean { value: true }, FALSE: SassBoolean { value: false } }, Color: [Function: SassColor] { '___dart__$dart_dartClosure_ZxYxX_0_': closure245 { '_$dart_jsFunctionCaptureThis': [Circular *4] } }, List: [Function: SassList] { '___dart__$dart_dartClosure_ZxYxX_0_': closure238 { '_$dart_jsFunctionCaptureThis': [Circular *5] } }, Map: [Function: SassMap] { '___dart__$dart_dartClosure_ZxYxX_0_': closure231 { '_$dart_jsFunctionCaptureThis': [Circular *6] } }, Null: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure35 { '$dart_jsFunction': [Circular *7] }, NULL: SassNull { toString: [Function] } }, Number: [Function: SassNumber] { '___dart__$dart_dartClosure_ZxYxX_0_': closure224 { '_$dart_jsFunctionCaptureThis': [Circular *8] } }, String: [Function: SassString] { '___dart__$dart_dartClosure_ZxYxX_0_': closure220 { '_$dart_jsFunctionCaptureThis': [Circular *9] } }, Error: [Function: Error] { stackTraceLimit: 16, prepareStackTrace: undefined } }, NULL: SassNull { toString: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _closure36 { '$dart_jsFunction': [Circular *10] } } }, TRUE: SassBoolean { value: true }, FALSE: SassBoolean { value: false }, cli_pkg_main_0_: [Function (anonymous)] { '___dart__$dart_dartClosure_ZxYxX_0_': _wrapMain_closure0 { main: StaticClosure { '$initialize': [Function: StaticClosure], constructor: [Function: static_tear_off], '$static_name': 'main', '$signature': [Function (anonymous)], 'call$1': [Function], 'call*': [Function], '$requiredArgCount': 1, '$defaultValues': null }, '$dart_jsFunction': [Circular *11] } } }, sourceMap: true, sassOptions: { precision: 8, includePaths: [], outputStyle: 'expanded' } } } ] }, { include: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.less$/, use: [ { loader: 'style-loader' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/css-loader/dist/cjs.js', options: { url: false, sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/less-loader/dist/cjs.js', options: { sourceMap: false, lessOptions: { javascriptEnabled: true, paths: [] } } } ] }, { include: [ '/home/USER/single-spa-errors/angular11-basic/src/styles.css' ], test: /\.styl$/, use: [ { loader: 'style-loader' }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/css-loader/dist/cjs.js', options: { url: false, sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/postcss-loader/dist/cjs.js', options: { postcssOptions: [Function (anonymous)] } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/resolve-url-loader/index.js', options: { sourceMap: false } }, { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/stylus-loader/dist/cjs.js', options: { sourceMap: false, webpackImporter: false, stylusOptions: { compress: false, sourceMap: { comment: false }, paths: [] } } } ] }, { test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/, use: [ { loader: '/home/USER/single-spa-errors/angular11-basic/node_modules/@angular-devkit/build-optimizer/src/build-optimizer/webpack-loader.js', options: { sourceMap: false } }, '/home/USER/single-spa-errors/angular11-basic/node_modules/@ngtools/webpack/src/index.js' ] }, { parser: { system: false } } ] ```
daniloesk commented 3 years ago

I guess that for now we can work with that. =) Here is an example for working asset URLs: https://github.com/daniloesk/single-spa-examples/releases/tag/v20201211-app11