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?

Upvotes: 1

Views: 97

Answers (1)

James Summerton
James Summerton

Reputation: 438

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

Upvotes: 0

Related Questions