diff --git a/.gitignore b/.gitignore index 19cbb4d7..ae1d78dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +# G # Manual additions *.auto.tfvars dev.env @@ -277,3 +278,4 @@ $RECYCLE.BIN/ cli/keyconjurer-darwin* cli/keyconjurer-linux* cli/keyconjurer-windows.exe +vendor diff --git a/Makefile b/Makefile index 89f9e5f6..cb87ac4f 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ RELEASE ?= dev VERSION ?= $(shell git rev-parse --short HEAD) +# This runs on Linux machines. Mac users should override TIMESTAMP. +TIMESTAMP ?= $(shell date --iso-8601=minutes) ## Standard targets for all Makefiles in our team @@ -33,7 +35,6 @@ frontend/build/index.html: frontend/node_modules cd frontend && \ REACT_APP_VERSION='$$(git rev-parse --short HEAD)-$(RELEASE)' \ REACT_APP_API_URL=${API_URL} \ - REACT_APP_BINARY_NAME=${BINARY_NAME} \ REACT_APP_DOCUMENTATION_URL=${REACT_APP_DOCUMENTATION_URL} \ REACT_APP_CLIENT=webUI npm run-script build @@ -66,7 +67,7 @@ cli/keyconjurer: -X main.Version=$(shell git rev-parse --short HEAD)-$(RELEASE) \ -X main.ClientID=$(CLIENT_ID) \ -X main.OIDCDomain=$(OIDC_DOMAIN) \ - -X main.BuildTimestamp='$(shell date --iso-8601=minutes)' \ + -X main.BuildTimestamp='$(TIMESTAMP)' \ -X main.ServerAddress=$(SERVER_ADDRESS)" \ -o $(BUILD_TARGET) diff --git a/cli/accounts.go b/cli/accounts.go index 39445186..1b25610d 100644 --- a/cli/accounts.go +++ b/cli/accounts.go @@ -23,7 +23,6 @@ var ( func init() { accountsCmd.Flags().Bool(FlagNoRefresh, false, "Indicate that the account list should not be refreshed when executing this command. This is useful if you're not able to reach the account server.") - // TODO: Replace the address accountsCmd.Flags().String(FlagServerAddress, ServerAddress, "The address of the account server. This does not usually need to be changed or specified.") } @@ -34,11 +33,14 @@ var accountsCmd = &cobra.Command{ config := ConfigFromCommand(cmd) stdOut := cmd.OutOrStdout() noRefresh, _ := cmd.Flags().GetBool(FlagNoRefresh) + loud := !ShouldUseMachineOutput(cmd.Flags()) if noRefresh { - config.DumpAccounts(stdOut) - if q, _ := cmd.Flags().GetBool(FlagQuiet); !q { + config.DumpAccounts(stdOut, loud) + + if loud { cmd.PrintErrf("--%s was specified - these results may be out of date, and you may not have access to accounts in this list.\n", FlagNoRefresh) } + return nil } @@ -70,7 +72,7 @@ var accountsCmd = &cobra.Command{ } config.UpdateAccounts(accounts) - config.DumpAccounts(stdOut) + config.DumpAccounts(stdOut, loud) return nil }, } diff --git a/cli/config.go b/cli/config.go index bc1c8392..57fcc5a3 100644 --- a/cli/config.go +++ b/cli/config.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "sort" "time" "strings" @@ -73,8 +74,18 @@ func generateDefaultAlias(name string) string { } func (a *accountSet) ForEach(f func(id string, account Account, alias string)) { - for id, acc := range a.accounts { - f(id, *acc, acc.Alias) + // Golang does not maintain the order of maps, so we create a slice which is sorted instead. + var accounts []*Account + for _, acc := range a.accounts { + accounts = append(accounts, acc) + } + + sort.SliceStable(accounts, func(i, j int) bool { + return accounts[i].Name < accounts[j].Name + }) + + for _, acc := range accounts { + f(acc.ID, *acc, acc.Alias) } } @@ -169,12 +180,18 @@ func (a *accountSet) ReplaceWith(other []Account) { } } -func (a accountSet) WriteTable(w io.Writer) { +func (a accountSet) WriteTable(w io.Writer, withHeaders bool) { tbl := csv.NewWriter(w) - tbl.Write([]string{"id,name,alias"}) + tbl.Comma = '\t' + + if withHeaders { + tbl.Write([]string{"id", "name", "alias"}) + } + a.ForEach(func(id string, acc Account, alias string) { tbl.Write([]string{id, acc.Name, alias}) }) + tbl.Flush() } @@ -290,8 +307,8 @@ func (c *Config) UpdateAccounts(entries []Account) { c.Accounts.ReplaceWith(entries) } -func (c *Config) DumpAccounts(w io.Writer) { - c.Accounts.WriteTable(w) +func (c *Config) DumpAccounts(w io.Writer, withHeaders bool) { + c.Accounts.WriteTable(w, withHeaders) } func EnsureConfigFileExists(fp string) (io.ReadWriteCloser, error) { diff --git a/cli/get.go b/cli/get.go index b5824b6d..c59231a4 100644 --- a/cli/get.go +++ b/cli/get.go @@ -5,7 +5,6 @@ import ( "os" "time" - "github.com/RobotsAndPencils/go-saml" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" @@ -109,11 +108,12 @@ A role must be specified when using this command through the --role flag. You ma return nil } - if roleName == "" && account.MostRecentRole != "" { + if roleName == "" { + if account.MostRecentRole == "" { + cmd.PrintErrln("You must specify the --role flag with this command") + return nil + } roleName = account.MostRecentRole - } else { - cmd.PrintErrln("You must specify the --role flag with this command") - return nil } if config.TimeRemaining != 0 && timeRemaining == DefaultTimeRemaining { @@ -143,14 +143,14 @@ A role must be specified when using this command through the --role flag. You ma return nil } - assertionBytes, err := ExchangeWebSSOTokenForSAMLAssertion(cmd.Context(), client, oidcDomain, tok) + assertion, err := ExchangeWebSSOTokenForSAMLAssertion(cmd.Context(), client, oidcDomain, tok) if err != nil { cmd.PrintErrf("failed to fetch SAML assertion: %s\n", err) return nil } - assertionStr := string(assertionBytes) - samlResponse, err := saml.ParseEncodedResponse(assertionStr) + assertionStr := string(assertion) + samlResponse, err := ParseBase64EncodedSAMLResponse(assertionStr) if err != nil { cmd.PrintErrf("could not parse assertion: %s\n", err) return nil diff --git a/cli/login.go b/cli/login.go index 26651263..92c9683a 100644 --- a/cli/login.go +++ b/cli/login.go @@ -2,17 +2,24 @@ package main import ( "context" + "fmt" "os" + "github.com/pkg/browser" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/exp/slog" "golang.org/x/oauth2" ) -var FlagURLOnly = "url-only" +var ( + FlagURLOnly = "url-only" + FlagNoBrowser = "no-browser" +) func init() { loginCmd.Flags().BoolP(FlagURLOnly, "u", false, "Print only the URL to visit rather than a user-friendly message") + loginCmd.Flags().BoolP(FlagNoBrowser, "b", false, "Do not open a browser window, printing the URL instead") } // ShouldUseMachineOutput indicates whether or not we should write to standard output as if the user is a machine. @@ -38,8 +45,17 @@ var loginCmd = &cobra.Command{ oidcDomain, _ := cmd.Flags().GetString(FlagOIDCDomain) clientID, _ := cmd.Flags().GetString(FlagClientID) urlOnly, _ := cmd.Flags().GetBool(FlagURLOnly) - isMachineOutput := ShouldUseMachineOutput(cmd.Flags()) || urlOnly - token, err := Login(cmd.Context(), oidcDomain, clientID, isMachineOutput) + + var outputMode LoginOutputMode = LoginOutputModeBrowser{} + if noBrowser, _ := cmd.Flags().GetBool(FlagNoBrowser); noBrowser { + if ShouldUseMachineOutput(cmd.Flags()) || urlOnly { + outputMode = LoginOutputModeURLOnly{} + } else { + outputMode = LoginOutputModeHumanFriendlyMessage{} + } + } + + token, err := Login(cmd.Context(), oidcDomain, clientID, outputMode) if err != nil { return err } @@ -48,7 +64,7 @@ var loginCmd = &cobra.Command{ }, } -func Login(ctx context.Context, domain, clientID string, machineOutput bool) (*oauth2.Token, error) { +func Login(ctx context.Context, domain, clientID string, outputMode LoginOutputMode) (*oauth2.Token, error) { oauthCfg, err := DiscoverOAuth2Config(ctx, domain, clientID) if err != nil { return nil, err @@ -64,5 +80,30 @@ func Login(ctx context.Context, domain, clientID string, machineOutput bool) (*o return nil, err } - return RedirectionFlow(ctx, oauthCfg, state, codeChallenge, codeVerifier, machineOutput) + return RedirectionFlow(ctx, oauthCfg, state, codeChallenge, codeVerifier, outputMode) +} + +type LoginOutputMode interface { + PrintURL(url string) error +} + +type LoginOutputModeBrowser struct{} + +func (LoginOutputModeBrowser) PrintURL(url string) error { + slog.Debug("trying to open browser window", slog.String("url", url)) + return browser.OpenURL(url) +} + +type LoginOutputModeURLOnly struct{} + +func (LoginOutputModeURLOnly) PrintURL(url string) error { + fmt.Fprintln(os.Stdout, url) + return nil +} + +type LoginOutputModeHumanFriendlyMessage struct{} + +func (LoginOutputModeHumanFriendlyMessage) PrintURL(url string) error { + fmt.Printf("Visit the following link in your terminal: %s\n", url) + return nil } diff --git a/cli/oauth2.go b/cli/oauth2.go index 7307033c..dee77baa 100644 --- a/cli/oauth2.go +++ b/cli/oauth2.go @@ -19,6 +19,10 @@ import ( "golang.org/x/oauth2" ) +// stateBufSize is the size of the buffer used to generate the state parameter. +// 43 is a magic number - It generates states that are not too short or long for Okta's validation. +const stateBufSize = 43 + func NewHTTPClient() *http.Client { // Some Darwin systems require certs to be loaded from the system certificate store or attempts to verify SSL certs on internal websites may fail. tr := http.DefaultTransport @@ -136,7 +140,7 @@ func (e OAuth2Error) Error() string { } func GenerateCodeVerifierAndChallenge() (string, string, error) { - codeVerifierBuf := make([]byte, 43) + codeVerifierBuf := make([]byte, stateBufSize) rand.Read(codeVerifierBuf) codeVerifier := base64.RawURLEncoding.EncodeToString(codeVerifierBuf) codeChallengeHash := sha256.Sum256([]byte(codeVerifier)) @@ -145,12 +149,12 @@ func GenerateCodeVerifierAndChallenge() (string, string, error) { } func GenerateState() (string, error) { - stateBuf := make([]byte, 43) + stateBuf := make([]byte, stateBufSize) rand.Read(stateBuf) return base64.URLEncoding.EncodeToString([]byte(stateBuf)), nil } -func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeChallenge, codeVerifier string, machineOutput bool) (*oauth2.Token, error) { +func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeChallenge, codeVerifier string, outputMode LoginOutputMode) (*oauth2.Token, error) { listener := NewOAuth2Listener() go listener.Listen(ctx) oauthCfg.RedirectURL = "http://localhost:57468" @@ -159,10 +163,9 @@ func RedirectionFlow(ctx context.Context, oauthCfg *oauth2.Config, state, codeCh oauth2.SetAuthURLParam("code_challenge", codeChallenge), ) - if machineOutput { - fmt.Println(url) - } else { - fmt.Printf("Visit the following link in your terminal: %s\n", url) + if err := outputMode.PrintURL(url); err != nil { + // This is unlikely to ever happen + return nil, fmt.Errorf("failed to display link: %w", err) } code, err := listener.WaitForAuthorizationCode(ctx, state) diff --git a/cli/roles.go b/cli/roles.go index 9f291ec8..59ea49e2 100644 --- a/cli/roles.go +++ b/cli/roles.go @@ -1,7 +1,6 @@ package main import ( - "github.com/RobotsAndPencils/go-saml" "github.com/spf13/cobra" ) @@ -44,8 +43,7 @@ var rolesCmd = cobra.Command{ return nil } - assertionStr := string(assertionBytes) - samlResponse, err := saml.ParseEncodedResponse(assertionStr) + samlResponse, err := ParseBase64EncodedSAMLResponse(string(assertionBytes)) if err != nil { cmd.PrintErrf("could not parse assertion: %s\n", err) return nil diff --git a/cli/saml.go b/cli/saml.go index 0b973a44..575e980e 100644 --- a/cli/saml.go +++ b/cli/saml.go @@ -96,3 +96,7 @@ func getARN(value string) RoleProviderPair { } return p } + +func ParseBase64EncodedSAMLResponse(xml string) (*saml.Response, error) { + return saml.ParseEncodedResponse(xml) +} diff --git a/cli/saml_test.go b/cli/saml_test.go index 3797a1e7..904b8f9a 100644 --- a/cli/saml_test.go +++ b/cli/saml_test.go @@ -8,7 +8,7 @@ import ( ) func TestAwsFindRoleDoesntBreakIfYouHaveMultipleRoles(t *testing.T) { - resp := saml.Response{} + var resp saml.Response resp.AddAttribute("https://aws.amazon.com/SAML/Attributes/Role", "arn:cloud:iam::1234:saml-provider/Okta,arn:cloud:iam::1234:role/Admin") resp.AddAttribute("https://aws.amazon.com/SAML/Attributes/Role", "arn:cloud:iam::1234:saml-provider/Okta,arn:cloud:iam::1234:role/Power") pair, err := FindRoleInSAML("Power", &resp) diff --git a/example.env b/example.env index 6d9a449e..80829838 100644 --- a/example.env +++ b/example.env @@ -11,7 +11,6 @@ export S3_TF_BUCKET_NAME='' export S3_TF_BUCKET_TAGS="TagSet=[{Key=Name,Value=keyconjurer}]" export S3_FRONTEND_BUCKET='' -export BINARY_NAME='keyconjurer' export LOGSTASH_ENDPOINT='' export SECRETS_RETRIEVER='kms_blob' diff --git a/frontend/config/env.js b/frontend/config/env.js new file mode 100644 index 00000000..ffa7e496 --- /dev/null +++ b/frontend/config/env.js @@ -0,0 +1,104 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); + +// Make sure that including paths.js after env.js will read .env variables. +delete require.cache[require.resolve('./paths')]; + +const NODE_ENV = process.env.NODE_ENV; +if (!NODE_ENV) { + throw new Error( + 'The NODE_ENV environment variable is required but was not specified.' + ); +} + +// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use +const dotenvFiles = [ + `${paths.dotenv}.${NODE_ENV}.local`, + // Don't include `.env.local` for `test` environment + // since normally you expect tests to produce the same + // results for everyone + NODE_ENV !== 'test' && `${paths.dotenv}.local`, + `${paths.dotenv}.${NODE_ENV}`, + paths.dotenv, +].filter(Boolean); + +// Load environment variables from .env* files. Suppress warnings using silent +// if this file is missing. dotenv will never modify any environment variables +// that have already been set. Variable expansion is supported in .env files. +// https://github.com/motdotla/dotenv +// https://github.com/motdotla/dotenv-expand +dotenvFiles.forEach(dotenvFile => { + if (fs.existsSync(dotenvFile)) { + require('dotenv-expand')( + require('dotenv').config({ + path: dotenvFile, + }) + ); + } +}); + +// We support resolving modules according to `NODE_PATH`. +// This lets you use absolute paths in imports inside large monorepos: +// https://github.com/facebook/create-react-app/issues/253. +// It works similar to `NODE_PATH` in Node itself: +// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders +// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. +// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. +// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 +// We also resolve them to make sure all tools using them work consistently. +const appDirectory = fs.realpathSync(process.cwd()); +process.env.NODE_PATH = (process.env.NODE_PATH || '') + .split(path.delimiter) + .filter(folder => folder && !path.isAbsolute(folder)) + .map(folder => path.resolve(appDirectory, folder)) + .join(path.delimiter); + +// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be +// injected into the application via DefinePlugin in webpack configuration. +const REACT_APP = /^REACT_APP_/i; + +function getClientEnvironment(publicUrl) { + const raw = Object.keys(process.env) + .filter(key => REACT_APP.test(key)) + .reduce( + (env, key) => { + env[key] = process.env[key]; + return env; + }, + { + // Useful for determining whether we’re running in production mode. + // Most importantly, it switches React into the correct mode. + NODE_ENV: process.env.NODE_ENV || 'development', + // Useful for resolving the correct path to static assets in `public`. + // For example, . + // This should only be used as an escape hatch. Normally you would put + // images into the `src` and `import` them in code to get their paths. + PUBLIC_URL: publicUrl, + // We support configuring the sockjs pathname during development. + // These settings let a developer run multiple simultaneous projects. + // They are used as the connection `hostname`, `pathname` and `port` + // in webpackHotDevClient. They are used as the `sockHost`, `sockPath` + // and `sockPort` options in webpack-dev-server. + WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST, + WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH, + WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT, + // Whether or not react-refresh is enabled. + // It is defined here so it is available in the webpackHotDevClient. + FAST_REFRESH: process.env.FAST_REFRESH !== 'false', + } + ); + // Stringify all values so we can feed into webpack DefinePlugin + const stringified = { + 'process.env': Object.keys(raw).reduce((env, key) => { + env[key] = JSON.stringify(raw[key]); + return env; + }, {}), + }; + + return { raw, stringified }; +} + +module.exports = getClientEnvironment; diff --git a/frontend/config/getHttpsConfig.js b/frontend/config/getHttpsConfig.js new file mode 100644 index 00000000..013d493c --- /dev/null +++ b/frontend/config/getHttpsConfig.js @@ -0,0 +1,66 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const chalk = require('react-dev-utils/chalk'); +const paths = require('./paths'); + +// Ensure the certificate and key provided are valid and if not +// throw an easy to debug error +function validateKeyAndCerts({ cert, key, keyFile, crtFile }) { + let encrypted; + try { + // publicEncrypt will throw an error with an invalid cert + encrypted = crypto.publicEncrypt(cert, Buffer.from('test')); + } catch (err) { + throw new Error( + `The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}` + ); + } + + try { + // privateDecrypt will throw an error with an invalid key + crypto.privateDecrypt(key, encrypted); + } catch (err) { + throw new Error( + `The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${ + err.message + }` + ); + } +} + +// Read file and throw an error if it doesn't exist +function readEnvFile(file, type) { + if (!fs.existsSync(file)) { + throw new Error( + `You specified ${chalk.cyan( + type + )} in your env, but the file "${chalk.yellow(file)}" can't be found.` + ); + } + return fs.readFileSync(file); +} + +// Get the https config +// Return cert files if provided in env, otherwise just true or false +function getHttpsConfig() { + const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env; + const isHttps = HTTPS === 'true'; + + if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) { + const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE); + const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE); + const config = { + cert: readEnvFile(crtFile, 'SSL_CRT_FILE'), + key: readEnvFile(keyFile, 'SSL_KEY_FILE'), + }; + + validateKeyAndCerts({ ...config, keyFile, crtFile }); + return config; + } + return isHttps; +} + +module.exports = getHttpsConfig; diff --git a/frontend/config/jest/babelTransform.js b/frontend/config/jest/babelTransform.js new file mode 100644 index 00000000..5b391e40 --- /dev/null +++ b/frontend/config/jest/babelTransform.js @@ -0,0 +1,29 @@ +'use strict'; + +const babelJest = require('babel-jest').default; + +const hasJsxRuntime = (() => { + if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { + return false; + } + + try { + require.resolve('react/jsx-runtime'); + return true; + } catch (e) { + return false; + } +})(); + +module.exports = babelJest.createTransformer({ + presets: [ + [ + require.resolve('babel-preset-react-app'), + { + runtime: hasJsxRuntime ? 'automatic' : 'classic', + }, + ], + ], + babelrc: false, + configFile: false, +}); diff --git a/frontend/config/jest/cssTransform.js b/frontend/config/jest/cssTransform.js new file mode 100644 index 00000000..8f651148 --- /dev/null +++ b/frontend/config/jest/cssTransform.js @@ -0,0 +1,14 @@ +'use strict'; + +// This is a custom Jest transformer turning style imports into empty objects. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process() { + return 'module.exports = {};'; + }, + getCacheKey() { + // The output is always the same. + return 'cssTransform'; + }, +}; diff --git a/frontend/config/jest/fileTransform.js b/frontend/config/jest/fileTransform.js new file mode 100644 index 00000000..aab67618 --- /dev/null +++ b/frontend/config/jest/fileTransform.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); +const camelcase = require('camelcase'); + +// This is a custom Jest transformer turning file imports into filenames. +// http://facebook.github.io/jest/docs/en/webpack.html + +module.exports = { + process(src, filename) { + const assetFilename = JSON.stringify(path.basename(filename)); + + if (filename.match(/\.svg$/)) { + // Based on how SVGR generates a component name: + // https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6 + const pascalCaseFilename = camelcase(path.parse(filename).name, { + pascalCase: true, + }); + const componentName = `Svg${pascalCaseFilename}`; + return `const React = require('react'); + module.exports = { + __esModule: true, + default: ${assetFilename}, + ReactComponent: React.forwardRef(function ${componentName}(props, ref) { + return { + $$typeof: Symbol.for('react.element'), + type: 'svg', + ref: ref, + key: null, + props: Object.assign({}, props, { + children: ${assetFilename} + }) + }; + }), + };`; + } + + return `module.exports = ${assetFilename};`; + }, +}; diff --git a/frontend/config/modules.js b/frontend/config/modules.js new file mode 100644 index 00000000..d63e41d7 --- /dev/null +++ b/frontend/config/modules.js @@ -0,0 +1,134 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const paths = require('./paths'); +const chalk = require('react-dev-utils/chalk'); +const resolve = require('resolve'); + +/** + * Get additional module paths based on the baseUrl of a compilerOptions object. + * + * @param {Object} options + */ +function getAdditionalModulePaths(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return ''; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + // We don't need to do anything if `baseUrl` is set to `node_modules`. This is + // the default behavior. + if (path.relative(paths.appNodeModules, baseUrlResolved) === '') { + return null; + } + + // Allow the user set the `baseUrl` to `appSrc`. + if (path.relative(paths.appSrc, baseUrlResolved) === '') { + return [paths.appSrc]; + } + + // If the path is equal to the root directory we ignore it here. + // We don't want to allow importing from the root directly as source files are + // not transpiled outside of `src`. We do allow importing them with the + // absolute path (e.g. `src/Components/Button.js`) but we set that up with + // an alias. + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return null; + } + + // Otherwise, throw an error. + throw new Error( + chalk.red.bold( + "Your project's `baseUrl` can only be set to `src` or `node_modules`." + + ' Create React App does not support other values at this time.' + ) + ); +} + +/** + * Get webpack aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getWebpackAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + src: paths.appSrc, + }; + } +} + +/** + * Get jest aliases based on the baseUrl of a compilerOptions object. + * + * @param {*} options + */ +function getJestAliases(options = {}) { + const baseUrl = options.baseUrl; + + if (!baseUrl) { + return {}; + } + + const baseUrlResolved = path.resolve(paths.appPath, baseUrl); + + if (path.relative(paths.appPath, baseUrlResolved) === '') { + return { + '^src/(.*)$': '/src/$1', + }; + } +} + +function getModules() { + // Check if TypeScript is setup + const hasTsConfig = fs.existsSync(paths.appTsConfig); + const hasJsConfig = fs.existsSync(paths.appJsConfig); + + if (hasTsConfig && hasJsConfig) { + throw new Error( + 'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.' + ); + } + + let config; + + // If there's a tsconfig.json we assume it's a + // TypeScript project and set up the config + // based on tsconfig.json + if (hasTsConfig) { + const ts = require(resolve.sync('typescript', { + basedir: paths.appNodeModules, + })); + config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; + // Otherwise we'll check if there is jsconfig.json + // for non TS projects. + } else if (hasJsConfig) { + config = require(paths.appJsConfig); + } + + config = config || {}; + const options = config.compilerOptions || {}; + + const additionalModulePaths = getAdditionalModulePaths(options); + + return { + additionalModulePaths: additionalModulePaths, + webpackAliases: getWebpackAliases(options), + jestAliases: getJestAliases(options), + hasTsConfig, + }; +} + +module.exports = getModules(); diff --git a/frontend/config/paths.js b/frontend/config/paths.js new file mode 100644 index 00000000..f0a6cd9c --- /dev/null +++ b/frontend/config/paths.js @@ -0,0 +1,77 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); + +// Make sure any symlinks in the project folder are resolved: +// https://github.com/facebook/create-react-app/issues/637 +const appDirectory = fs.realpathSync(process.cwd()); +const resolveApp = relativePath => path.resolve(appDirectory, relativePath); + +// We use `PUBLIC_URL` environment variable or "homepage" field to infer +// "public path" at which the app is served. +// webpack needs to know it to put the right