typescriptenvironment-variablespbiviz

How do I a adds secret to a pbiviz custom visual in written in typescript with react?


I am trying to create maps with custom visuals for powerBi, and I am trying to add the feature of search Geolocation to the map with auto-complete. To do the auto-complete, I intend to use the API, specifically the Azure maps API. But I don't have a way to conceal the API key.

I tried to add environment variables to the visual, but if I use dotenv.config(), I get the error "ReferenceError: global is not defined". Otherwise the process.env.API_KEY return "undefined".

Is there is a way to use environment variables or otherwise use secret, in custom visuals in pbiviz?


Solution

  • I use the following code to bring all env variables through for webpack and my custom viz.

    This is up the top of my webpack config

    const envFile = dotenv.config({
      path: `.env.${currentDotEnv}`, 
      defaults: '.env', 
      systemvars: true, 
      silent: true, 
    }).parsed
    
    // Combine env variables from the .env file and process.env
    const combinedEnv = {
      ...process.env,
      ...envFile,
    }
    
    // Create an object with all env vars prefixed with 'process.env'
    const envKeys = Object.keys(combinedEnv).reduce((prev, next) => {
      prev[`process.env.${next}`] = JSON.stringify(combinedEnv[next])
      return prev
    }, {})
    

    From there I get access to all of them in my running code via process.env.XXXX.

    This lets me have .env files for dev and for production builds in GitHub Actions I can pass secrets in securely to the build process.

    Full webpack.config.js

    const os = require('os')
    const path = require('path')
    const fs = require('fs')
    const dotenv = require('dotenv')
    const DotenvWebpack = require('dotenv-webpack')
    
    const optimize = process.env.OPTIMIZE === 'true'
    const currentDotEnv = optimize ? 'prod' : 'dev' // this is for loading the correct .env file only
    const isProduction = process.env.NODE_ENV === 'production'
    
    const envFile = dotenv.config({
      path: `.env.${currentDotEnv}`, // Load environment-specific .env file
      defaults: '.env', // Load the base .env file as defaults
      systemvars: true, // Load system environment variables
      silent: true, // Suppress warnings if .env files are missing
    }).parsed
    
    // Combine env variables from the .env file and process.env
    const combinedEnv = {
      ...process.env,
      ...envFile,
    }
    
    // Create an object with all env vars prefixed with 'process.env'
    const envKeys = Object.keys(combinedEnv).reduce((prev, next) => {
      prev[`process.env.${next}`] = JSON.stringify(combinedEnv[next])
      console.log(`process.env.${next}`, JSON.stringify(combinedEnv[next]))
      return prev
    }, {})
    
    console.log('currentDotEnv', currentDotEnv)
    console.log('isProduction', isProduction)
    console.log('optimize', optimize)
    
    // werbpack plugin
    const webpack = require('webpack')
    const { PowerBICustomVisualsWebpackPlugin, LocalizationLoader } = require('powerbi-visuals-webpack-plugin')
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
    const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin')
    const { VueLoaderPlugin } = require('vue-loader')
    const PnpWebpackPlugin = require('pnp-webpack-plugin')
    
    // api configuration
    const powerbiApi = require('powerbi-visuals-api')
    
    // visual configuration json path
    const pbivizPath = './pbiviz.json'
    const pbivizFile = require(path.join(__dirname, pbivizPath))
    
    // the visual capabilities content
    const capabilitiesPath = './capabilities.json'
    const capabilities = require(path.join(__dirname, capabilitiesPath))
    
    const pluginLocation = './.tmp/precompile/visualPlugin.ts' // path to visual plugin file, the file generates by the plugin
    const visualSourceLocation = '../../src/visual' // This path is used inside of the generated plugin, so it depends on pluginLocation
    
    // Build out the viz config
    const pbiPluginConfig = {
      ...pbivizFile,
      compression: optimize ? 9 : 0,
      capabilities,
      visualSourceLocation,
      pluginLocation,
      apiVersion: powerbiApi.version,
      capabilitiesSchema: powerbiApi.schemas.capabilities,
      dependenciesSchema: powerbiApi.schemas.dependencies,
      devMode: false,
      generatePbiviz: true,
      generateResources: optimize,
      modules: true,
      packageOutPath: path.join(__dirname, 'dist'),
    }
    
    if (process.env.VERSION && pbiPluginConfig.visual != null) {
      pbiPluginConfig.visual.version = process.env.VERSION
    }
    
    // string resources
    const resourcesFolder = path.join('.', 'stringResources')
    const localizationFolders = fs.existsSync(resourcesFolder) && fs.readdirSync(resourcesFolder)
    
    // babel options to support IE11
    const babelOptions = {
      presets: [
        [
          require.resolve('@babel/preset-env'),
          {
            useBuiltIns: 'entry',
            corejs: 3,
            modules: false,
          },
        ],
      ],
      plugins: [
        [
          require.resolve('babel-plugin-module-resolver'),
          {
            root: ['./'],
          },
        ],
        PnpWebpackPlugin.moduleLoader(module),
      ],
      sourceType: 'unambiguous', // tell to babel that the project can contains different module types, not only es2015 modules
      cacheDirectory: path.join('.tmp', 'babelCache'), // path for chace files
    }
    
    const devServerConfig = {
      static: {
        directory: path.join(__dirname, '.tmp', 'drop'), // path with assets generated by webpack plugin
        publicPath: '/assets/',
        watch: true,
      },
      compress: true,
      port: 8080, // dev server port
      hot: false,
      server: {
        type: 'https',
        options: {
          key: optimize ? undefined : fs.readFileSync(path.resolve(`${os.homedir}/.office-addin-dev-certs/localhost.key`)),
          cert: optimize ? undefined : fs.readFileSync(path.resolve(`${os.homedir}/.office-addin-dev-certs/localhost.crt`)),
          ca: optimize ? undefined : fs.readFileSync(path.resolve(`${os.homedir}/.office-addin-dev-certs/ca.crt`)),
        },
      },
    
      devMiddleware: {
        writeToDisk: true,
      },
    
      headers: {
        'access-control-allow-origin': '*',
        'cache-control': 'public, max-age=0',
      },
    }
    
    module.exports = {
      entry: {
        visual: pluginLocation,
      },
      optimization: {
        concatenateModules: optimize,
        flagIncludedChunks: optimize,
        mangleExports: optimize,
        mergeDuplicateChunks: optimize,
        minimize: optimize,
        moduleIds: optimize ? 'size' : 'named',
        realContentHash: optimize,
        removeAvailableModules: optimize,
        removeEmptyChunks: optimize,
      },
      target: 'web',
      devtool: 'source-map',
      mode: 'development',
      module: {
        rules: [
          {
            test: /\.vue$/,
            use: [
              {
                loader: require.resolve('vue-loader'),
                options: {
                  compilerOptions: {
                    isCustomElement: tag => tag.startsWith('fluent-') || tag.startsWith('office-') || tag.startsWith('ms-'),
                  },
                },
              },
            ],
          },
          {
            parser: {
              amd: false,
            },
          },
          {
            test: /(\.ts)x|\.ts$/,
            use: [
              {
                loader: require.resolve('babel-loader'),
                options: {
                  presets: [
                    // '@babel/react',
                    '@babel/env',
                  ],
                },
              },
              {
                loader: require.resolve('ts-loader'),
                options: {
                  transpileOnly: false,
                  experimentalWatchApi: false,
                  appendTsSuffixTo: [/\.vue$/],
                },
              },
            ],
            exclude: [/node_modules/],
            include: /powerbi-visuals-|src|precompile\\visualPlugin.ts/,
          },
          {
            test: /(\.js)x|\.js$/,
            use: [
              {
                loader: require.resolve('babel-loader'),
                options: babelOptions,
              },
            ],
            exclude: [/node_modules/],
          },
          {
            test: /\.json$/,
            loader: require.resolve('json-loader'),
            type: 'javascript/auto',
          },
          {
            test: /\.(css)?$/,
            use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
          },
          {
            test: /\.(woff|ttf|ico|woff2|jpg|jpeg|png|webp|svg)$/i,
            use: [
              {
                loader: 'base64-inline-loader',
              },
            ],
          },
        ],
      },
      externals: { 'powerbi-visuals-api': 'null' },
      resolve: {
        extensions: ['.tsx', '.ts', '.jsx', '.js', '.css', '.json', '.vue'],
        alias: {
          '@': path.resolve(__dirname, 'src'),
        },
      },
      output: {
        publicPath: '/assets',
        path: path.join(__dirname, '/.tmp', 'drop'),
        library: +powerbiApi.version.replace(/\./g, '') >= 320 ? pbivizFile.visual.guid : undefined,
        libraryTarget: +powerbiApi.version.replace(/\./g, '') >= 320 ? 'var' : undefined,
      },
      devServer: optimize ? {} : devServerConfig,
      externals:
        powerbiApi.version.replace(/\./g, '') >= 320
          ? {
              'powerbi-visuals-api': 'null',
              fakeDefine: 'false',
            }
          : {
              'powerbi-visuals-api': 'null',
              fakeDefine: 'false',
              corePowerbiObject: "Function('return this.powerbi')()",
              realWindow: "Function('return this')()",
            },
      plugins: [
        new DotenvWebpack({
          path: `.env.${currentDotEnv}`, // Load environment-specific .env file
          defaults: '.env', // Load the base .env file as defaults
          systemvars: true, // Load system environment variables
          silent: true, // Suppress warnings if .env files are missing
        }),
        new webpack.DefinePlugin({
          __VUE_OPTIONS_API__: JSON.stringify(true),
          __VUE_PROD_DEVTOOLS__: JSON.stringify(false),
          __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: JSON.stringify(false),
        }),
        new VueLoaderPlugin(),
        new MiniCssExtractPlugin({
          filename: '[name].css',
        }),
    
        // visual plugin regenerates with the visual source, but it does not require relaunching dev server
        new webpack.WatchIgnorePlugin({
          paths: [path.join(__dirname, pluginLocation), './.tmp/**/*.*'],
        }),
    
        // custom visuals plugin instance with options
        new PowerBICustomVisualsWebpackPlugin(pbiPluginConfig),
        new ExtraWatchWebpackPlugin({
          files: [pbivizPath, capabilitiesPath],
        }),
        powerbiApi.version.replace(/\./g, '') >= 320
          ? new webpack.ProvidePlugin({
              define: 'fakeDefine',
            })
          : new webpack.ProvidePlugin({
              window: 'realWindow',
              define: 'fakeDefine',
              powerbi: 'corePowerbiObject',
            }),
      ],
    }
    enter code here