diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..94f480de --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..537f81f5 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +ignore-scripts=true \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9f8cbe14..54c2b6af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,10 +6,9 @@ "packages": { "": { "name": "@sasjs/cli", - "hasInstallScript": true, "license": "ISC", "dependencies": { - "@sasjs/adapter": "^4.16.0", + "@sasjs/adapter": "^4.16.1", "@sasjs/core": "4.59.9", "@sasjs/lint": "2.4.3", "@sasjs/utils": "3.5.2", @@ -108,7 +107,6 @@ "integrity": "sha512-D58mjF+Y+89UfbMJpV57UTCg+JRQIFgvROPfH7mmIfBcoFVMkwiiiJyzPyW3onN9kg9noDg7MVyI+Yt64bnfQQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.21.4", @@ -2301,13 +2299,12 @@ } }, "node_modules/@sasjs/adapter": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.16.0.tgz", - "integrity": "sha512-DzF/+s++FtSfuBmONicBbgeKI8feiwDOm1iKWlcDlmHCPmHIoj1IbI0v2fGktzurnE37/vkyp6dvHO+FhwI87Q==", - "hasInstallScript": true, + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.16.1.tgz", + "integrity": "sha512-NsW9f5n9USYJbRe7f7AghfopVegI4lDKBn8dixopGnqbmGsTmMaZOdSOiKmmRmm0qrxet/MAor7h6ic/vqDCLA==", "license": "ISC", "dependencies": { - "@sasjs/utils": "3.5.2", + "@sasjs/utils": "3.5.6", "axios": "1.12.2", "axios-cookiejar-support": "5.0.5", "form-data": "4.0.4", @@ -2315,6 +2312,41 @@ "tough-cookie": "4.1.3" } }, + "node_modules/@sasjs/adapter/node_modules/@sasjs/utils": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.6.tgz", + "integrity": "sha512-jx8zWSOysDD66vTjA0BWiZ8bcFqmqh8F+56fUCgLmJhm89eDbKrGF3mDKMQx3UE7d2+gxp9xYhJCdaBWz0Dlxw==", + "license": "ISC", + "dependencies": { + "@fast-csv/format": "4.3.5", + "@types/fs-extra": "11.0.4", + "@types/prompts": "2.0.13", + "chalk": "4.1.1", + "cli-table": "0.3.6", + "consola": "2.15.0", + "find": "0.3.0", + "fs-extra": "11.3.0", + "jwt-decode": "3.1.2", + "prompts": "2.4.1", + "valid-url": "1.0.9" + } + }, + "node_modules/@sasjs/adapter/node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@sasjs/core": { "version": "4.59.9", "resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.59.9.tgz", @@ -2639,8 +2671,7 @@ "version": "18.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz", "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/prompts": { "version": "2.0.13", @@ -2879,7 +2910,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -3195,7 +3225,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -4991,7 +5020,6 @@ "integrity": "sha512-XvK65feuEFGZT8OO0fB/QAQS+LGHvQpaadkH5p47/j3Ocqq3xf2pK9R+G0GzgfuhXVxEv76qCOOcMb5efLk6PA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.4.3", "@jest/types": "^29.4.3", @@ -7349,7 +7377,6 @@ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", @@ -7456,7 +7483,6 @@ "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -7529,7 +7555,6 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index c93f1a4b..52cf9927 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "test:mocked": "jest --silent --runInBand --config=jest.config.js --coverage", "lint:fix": "npx prettier --write \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", "lint": "npx prettier --check \"{src,test}/**/*.{ts,tsx,js,jsx,html,css,sass,less,yml,md,graphql}\"", - "preinstall": "npm run nodeVersionMessage", "prepare": "git rev-parse --git-dir && git config core.hooksPath ./.git-hooks || true", "doc": "npx compodoc -p tsconfig.doc.json --coverageTest 16 --coverageTestThresholdFail" }, @@ -51,7 +50,7 @@ "access": "public" }, "dependencies": { - "@sasjs/adapter": "^4.16.0", + "@sasjs/adapter": "^4.16.1", "@sasjs/core": "4.59.9", "@sasjs/lint": "2.4.3", "@sasjs/utils": "3.5.2", diff --git a/src/commands/build/build.ts b/src/commands/build/build.ts index e1b80e15..e0052d24 100644 --- a/src/commands/build/build.ts +++ b/src/commands/build/build.ts @@ -31,6 +31,19 @@ import { getBuildInit, getBuildTerm } from './internal/config' import { getLaunchPageCode } from './internal/getLaunchPageCode' export async function build(target: Target) { + const { macroCorePath } = process.sasjsConstants + + if (macroCorePath === '') { + throw new Error( + `The @sasjs/core folder location is unknown.\n` + + `Check that @sasjs/core is a dependency in the package.json, and \n` + + `that the package has been installed.\n` + + `Alternatively populate environment variable 'macroCorePath' with\n` + + `the path to a local @sasjs/core package's root directory.` + ) + } + process.logger?.info(`@sasjs/core found at ${macroCorePath}`) + if ( process.sasjsConstants.buildDestinationFolder && !(await folderExists(process.sasjsConstants.buildDestinationFolder)) @@ -203,11 +216,11 @@ ${ * The serverName represents the SAS 9 logical server context * There is no programmatic (SAS code) way to obtain this * So it is taken from the sasjsconfig file OR supplied at runtime, eg: - * + * * %let apploc=/my/apploc; * %let serverName=SASAppDC; * %inc thisfile; - * + * */ %global serverName; diff --git a/src/utils/setConstants.ts b/src/utils/setConstants.ts index f8574098..cb501ae3 100644 --- a/src/utils/setConstants.ts +++ b/src/utils/setConstants.ts @@ -43,7 +43,19 @@ export const setConstants = async ( const buildDestinationJobsFolder = path.join(buildDestinationFolder, 'jobs') const buildDestinationDbFolder = path.join(buildDestinationFolder, 'db') const buildDestinationDocsFolder = path.join(buildDestinationFolder, 'docs') - const macroCorePath = await getNodeModulePath('@sasjs/core') + // Edge case: @sasjs/cli has a dependency on @sasjs/core. + // When @sasjs/cli is used to submit a test of the @sasjs/core + // repo, it is desirable to use that @sasjs/core repo as the dependency rather + // than the older version in @sasjs/cli node_modules. + // To achieve this, set environment variable `macroCorePath` to the root dir + // of the local @sasjs/core package. If found, this takes precedence over + // any node_modules installations of @sasjs/core. + let macroCorePath = (process.env.macroCorePath as string) ?? '' + if (macroCorePath === '') { + // If no environment variable is set/populated then check for an installed + // @sasjs/core in locations known to node. + macroCorePath = await getNodeModulePath('@sasjs/core') + } const buildDestinationResultsFolder = getAbsolutePath( buildResultsFolder, diff --git a/src/utils/spec/setConstants.spec.ts b/src/utils/spec/setConstants.spec.ts index ddfd7302..542cdf42 100644 --- a/src/utils/spec/setConstants.spec.ts +++ b/src/utils/spec/setConstants.spec.ts @@ -3,9 +3,11 @@ import path from 'path' import * as configUtils from '../config' import { setConstants } from '../setConstants' import * as fileModule from '@sasjs/utils/file' +import * as utils from '../utils' describe('setConstants', () => { let config: Configuration + const origProcessEnv = process.env beforeAll(async () => { ;({ config } = JSON.parse( @@ -13,9 +15,12 @@ describe('setConstants', () => { )) }) + afterEach(() => { + process.env = { ...origProcessEnv } + }) + test('should set constants inside appFolder when @sasjs/core dependency is present', async () => { - const appFolder = ['some', 'app', 'folder'].join(path.sep) - process.projectDir = appFolder + process.projectDir = process.cwd() jest .spyOn(configUtils, 'getLocalOrGlobalConfig') @@ -26,14 +31,8 @@ describe('setConstants', () => { }) ) - jest.spyOn(process, 'cwd').mockImplementation(() => appFolder) - const hasSasjsCore = true - jest - .spyOn(fileModule, 'folderExists') - .mockImplementation((path: string) => Promise.resolve(hasSasjsCore)) - await setConstants() verifySasjsConstants(process.projectDir, hasSasjsCore) @@ -42,6 +41,7 @@ describe('setConstants', () => { test('should set constants inside appFolder when @sasjs/core dependency is not present', async () => { const appFolder = ['some', 'app', 'folder'].join(path.sep) process.projectDir = appFolder + process.env.macroCorePath = undefined jest .spyOn(configUtils, 'getLocalOrGlobalConfig') @@ -54,13 +54,7 @@ describe('setConstants', () => { jest.spyOn(process, 'cwd').mockImplementation(() => appFolder) - jest - .spyOn(fileModule, 'folderExists') - .mockImplementation((folderPath: string) => - Promise.resolve( - folderPath !== path.join(appFolder, 'node_modules', '@sasjs', 'core') - ) - ) + jest.spyOn(utils, 'getNodeModulePath').mockImplementation(async () => '') await setConstants() @@ -68,6 +62,8 @@ describe('setConstants', () => { }) test('should set constants outside appFolder', async () => { + process.env.macroCorePath = undefined + jest .spyOn(configUtils, 'getLocalOrGlobalConfig') .mockImplementation(async () => @@ -77,10 +73,48 @@ describe('setConstants', () => { }) ) + jest.spyOn(utils, 'getNodeModulePath').mockImplementation(async () => '') + await setConstants(false) verifySasjsConstants(undefined, false, false) }) + + test('should call getNodeModulePath once when environment variable macroCorePath is undefined', async () => { + process.env.macroCorePath = undefined + + const getNodeModulePathSpy = jest + .spyOn(utils, 'getNodeModulePath') + .mockImplementation(async (packageName: string) => Promise.resolve('')) + + await setConstants() + + expect(getNodeModulePathSpy).toHaveBeenCalledOnceWith('@sasjs/core') + }) + + test('should call getNodeModulePath once when environment variable macroCorePath is blank', async () => { + process.env.macroCorePath = '' + + const getNodeModulePathSpy = jest + .spyOn(utils, 'getNodeModulePath') + .mockImplementation(async (packageName: string) => Promise.resolve('')) + + await setConstants() + + expect(getNodeModulePathSpy).toHaveBeenCalledOnceWith('@sasjs/core') + }) + + test('should not call getNodeModulePath when environment variable macroCorePath is populated', async () => { + process.env.macroCorePath = '../core' + + const getNodeModulePathSpy = jest + .spyOn(utils, 'getNodeModulePath') + .mockImplementation(async (packageName: string) => Promise.resolve('')) + + await setConstants() + + expect(getNodeModulePathSpy).toBeCalledTimes(0) + }) }) const verifySasjsConstants = ( @@ -126,15 +160,17 @@ const verifySasjsConstants = ( path.join(prefixAppFolder, isLocal ? '' : '.sasjs', 'sasjsbuild', 'tests') ) - const corePath = hasSasjsCore - ? 'core' - : path.join('cli', 'node_modules', '@sasjs', 'core') + const corePath = hasSasjsCore ? 'core' : '' - if (appFolder) { - expect(sasjsConstants.macroCorePath).toEqual( - path.join(prefixAppFolder, 'node_modules', '@sasjs', corePath) - ) + if (hasSasjsCore) { + if (appFolder) { + expect(sasjsConstants.macroCorePath).toEqual( + path.join(prefixAppFolder, 'node_modules', '@sasjs', corePath) + ) + } else { + expect(sasjsConstants.macroCorePath).toEqual(expect.toEndWith(corePath)) + } } else { - expect(sasjsConstants.macroCorePath).toEqual(expect.toEndWith(corePath)) + expect(sasjsConstants.macroCorePath).toEqual(corePath) } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7e29c241..ffd17284 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -552,30 +552,15 @@ export const isSASjsProject = async () => { } export const getNodeModulePath = async (module: string): Promise => { - // Check if module is present in project's dependencies - const projectPath = path.join(process.cwd(), 'node_modules', module) - - if (await folderExists(projectPath)) return projectPath - - // Check if module is present in @sasjs/cli located in project's dependencies - const cliDepsPath = path.join('@sasjs', 'cli', 'node_modules') - const cliLocalPath = path.join( - process.cwd(), - 'node_modules', - cliDepsPath, - module - ) - - if (await folderExists(cliLocalPath)) return cliLocalPath - - // Check if module is present in global @sasjs/cli - const cliGlobalPath = path.join( - shelljs.exec(`npm root -g`, { silent: true }).stdout.replace(/\n/, ''), - cliDepsPath, - module - ) - - if (await folderExists(cliGlobalPath)) return cliGlobalPath + // Look for ${module}/package.json, then return only the path + try { + const nodePackagePath = path.dirname( + require.resolve(path.join(module, 'package.json')) + ) + if (nodePackagePath) return nodePackagePath + } catch (e: any) { + if (e.code !== 'MODULE_NOT_FOUND') throw e + } // Return default value return ''