mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat: support workspace configs
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"projects": {
|
||||
"testProject": {
|
||||
"architect": {
|
||||
"default": {
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/something.ts",
|
||||
"with": "src/something.replaced.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/something.ts",
|
||||
"with": "src/something.replaced.ios.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/something.ts",
|
||||
"with": "src/something.replaced.android.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"projects": {
|
||||
"testProject": {
|
||||
"architect": {
|
||||
"default": {
|
||||
"configurations": {
|
||||
"dev": {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"projects": {
|
||||
"testProject": {
|
||||
"architect": {
|
||||
"default": {
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/something.ts",
|
||||
"with": "src/something.replaced.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import Config from 'webpack-chain';
|
||||
import angular from '../../src/configuration/angular';
|
||||
import {
|
||||
default as angular,
|
||||
getFileReplacementsFromWorkspaceConfig,
|
||||
applyFileReplacements
|
||||
} from '../../src/configuration/angular';
|
||||
import { init } from '../../src';
|
||||
import { additionalCopyRules } from '../../src/helpers/copyRules'
|
||||
|
||||
import { resolve } from 'path'
|
||||
|
||||
jest.mock(
|
||||
'@ngtools/webpack',
|
||||
() => {
|
||||
class AngularCompilerPlugin {}
|
||||
class AngularCompilerPlugin {
|
||||
}
|
||||
|
||||
return {
|
||||
AngularCompilerPlugin,
|
||||
@@ -25,4 +33,124 @@ describe('angular configuration', () => {
|
||||
expect(angular(new Config()).toString()).toMatchSnapshot();
|
||||
});
|
||||
}
|
||||
|
||||
describe('workspace configuration', () => {
|
||||
it('no config', () => {
|
||||
init({
|
||||
ios: true,
|
||||
configuration: 'dev',
|
||||
projectName: 'testProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
''
|
||||
)
|
||||
|
||||
expect(res).toBe(null)
|
||||
})
|
||||
|
||||
it('no project', () => {
|
||||
init({
|
||||
ios: true,
|
||||
configuration: 'dev',
|
||||
projectName: 'nonProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
resolve(__dirname, './__fixtures__/workspace-without-replacements.json')
|
||||
)
|
||||
|
||||
expect(res).toBe(null);
|
||||
})
|
||||
|
||||
it('no replacements', () => {
|
||||
init({
|
||||
ios: true,
|
||||
configuration: 'dev',
|
||||
projectName: 'testProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
resolve(__dirname, './__fixtures__/workspace-without-replacements.json')
|
||||
)
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toEqual({});
|
||||
})
|
||||
|
||||
it('default file replacements', () => {
|
||||
init({
|
||||
// irrelevant to this test case - ensures getPlatformName() returns a valid platform
|
||||
ios: true,
|
||||
|
||||
configuration: 'dev',
|
||||
projectName: 'testProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
resolve(__dirname, './__fixtures__/workspace.json')
|
||||
)
|
||||
const entries = Object.entries(res)
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(entries.length).toBe(1)
|
||||
expect(entries[0]).toEqual([
|
||||
resolve(__dirname, './__fixtures__/src/something.ts'),
|
||||
resolve(__dirname, './__fixtures__/src/something.replaced.ts'),
|
||||
])
|
||||
})
|
||||
|
||||
it('ios file replacements', () => {
|
||||
init({
|
||||
ios: true,
|
||||
configuration: 'dev',
|
||||
projectName: 'testProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
resolve(__dirname, './__fixtures__/workspace-with-platform-replacements.json')
|
||||
)
|
||||
const entries = Object.entries(res)
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(entries.length).toBe(1)
|
||||
expect(entries[0]).toEqual([
|
||||
resolve(__dirname, './__fixtures__/src/something.ts'),
|
||||
resolve(__dirname, './__fixtures__/src/something.replaced.ios.ts'),
|
||||
])
|
||||
})
|
||||
|
||||
it('android file replacements', () => {
|
||||
init({
|
||||
android: true,
|
||||
configuration: 'dev',
|
||||
projectName: 'testProject'
|
||||
})
|
||||
const res = getFileReplacementsFromWorkspaceConfig(
|
||||
resolve(__dirname, './__fixtures__/workspace-with-platform-replacements.json')
|
||||
)
|
||||
const entries = Object.entries(res)
|
||||
|
||||
expect(res).toBeDefined();
|
||||
expect(entries.length).toBe(1)
|
||||
expect(entries[0]).toEqual([
|
||||
resolve(__dirname, './__fixtures__/src/something.ts'),
|
||||
resolve(__dirname, './__fixtures__/src/something.replaced.android.ts'),
|
||||
])
|
||||
})
|
||||
|
||||
it('applies file replacements', () => {
|
||||
const config = new Config();
|
||||
applyFileReplacements(config, {
|
||||
// should apply as an alias
|
||||
'foo.ts': 'foo.replaced.ts',
|
||||
|
||||
// should apply as a file replacement using the copy plugin
|
||||
'foo.json': 'foo.replaced.json'
|
||||
})
|
||||
|
||||
expect(config.resolve.alias.get('foo.ts')).toBe('foo.replaced.ts')
|
||||
expect(additionalCopyRules.length).toBe(1)
|
||||
expect(additionalCopyRules[0]).toEqual({
|
||||
from: 'foo.replaced.json',
|
||||
to: 'foo.json',
|
||||
force: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Config from 'webpack-chain';
|
||||
import { mockFile } from '../../scripts/jest.mockFiles';
|
||||
import fs from 'fs';
|
||||
import base from '../../src/configuration/base';
|
||||
import { init } from '../../src';
|
||||
|
||||
@@ -16,31 +16,64 @@ describe('base configuration', () => {
|
||||
}
|
||||
|
||||
it('supports dotenv', () => {
|
||||
mockFile('./.env', '');
|
||||
const fsSpy = jest.spyOn(fs, "existsSync")
|
||||
fsSpy.mockReturnValue(true)
|
||||
|
||||
init({
|
||||
ios: true,
|
||||
});
|
||||
const config = base(new Config());
|
||||
|
||||
expect(config.plugin('DotEnvPlugin')).toBeDefined();
|
||||
config.plugin('DotEnvPlugin').tap((args) => {
|
||||
expect(args[0].path).toEqual('__jest__/.env');
|
||||
return args;
|
||||
});
|
||||
expect(config.plugin('DotEnvPlugin')).toBeDefined();
|
||||
|
||||
fsSpy.mockRestore()
|
||||
});
|
||||
|
||||
it('supports env specific dotenv', () => {
|
||||
mockFile('./.env.prod', '');
|
||||
const fsSpy = jest.spyOn(fs, "existsSync")
|
||||
fsSpy.mockReturnValue(true)
|
||||
|
||||
init({
|
||||
ios: true,
|
||||
env: 'prod',
|
||||
});
|
||||
const config = base(new Config());
|
||||
|
||||
expect(fsSpy).toHaveBeenCalledWith('__jest__/.env.prod')
|
||||
expect(fsSpy).toHaveBeenCalledTimes(1)
|
||||
expect(config.plugin('DotEnvPlugin')).toBeDefined();
|
||||
config.plugin('DotEnvPlugin').tap((args) => {
|
||||
expect(args[0].path).toEqual('__jest__/.env.prod');
|
||||
return args;
|
||||
});
|
||||
fsSpy.mockRestore()
|
||||
});
|
||||
|
||||
it('falls back to default .env', () => {
|
||||
const fsSpy = jest.spyOn(fs, "existsSync")
|
||||
fsSpy
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(true)
|
||||
|
||||
|
||||
init({
|
||||
ios: true,
|
||||
env: 'prod',
|
||||
});
|
||||
const config = base(new Config());
|
||||
|
||||
expect(fsSpy).toHaveBeenCalledWith('__jest__/.env.prod')
|
||||
expect(fsSpy).toHaveBeenCalledWith('__jest__/.env')
|
||||
expect(fsSpy).toHaveBeenCalledTimes(2)
|
||||
expect(config.plugin('DotEnvPlugin')).toBeDefined();
|
||||
config.plugin('DotEnvPlugin').tap((args) => {
|
||||
expect(args[0].path).toEqual('__jest__/.env');
|
||||
return args;
|
||||
});
|
||||
fsSpy.mockRestore()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.13.10",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/sax": "^1.2.1",
|
||||
"babel-loader": "^8.2.1",
|
||||
"chalk": "^4.1.0",
|
||||
@@ -29,6 +30,7 @@
|
||||
"dotenv-webpack": "^7.0.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.1.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"micromatch": "^4.0.2",
|
||||
"postcss": "^8.2.7",
|
||||
"postcss-import": "^14.0.0",
|
||||
@@ -61,11 +63,9 @@
|
||||
"@types/webpack-virtual-modules": "^0.1.0",
|
||||
"jest": "^26.6.3",
|
||||
"jest-matcher-utils": "^26.6.2",
|
||||
"memfs": "^3.2.0",
|
||||
"nativescript-vue-template-compiler": "^2.8.2",
|
||||
"ts-jest": "^26.5.3",
|
||||
"typescript": "^4.2.3",
|
||||
"unionfs": "^4.4.0"
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nativescript-vue-template-compiler": "^2.8.1"
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import dedent from 'ts-dedent';
|
||||
|
||||
const mockedFiles: { [path: string]: string } = {};
|
||||
|
||||
export function mockFile(path, content) {
|
||||
const unionFS = require('unionfs').default;
|
||||
const Volume = require('memfs').Volume;
|
||||
|
||||
// reset to fs
|
||||
unionFS.reset();
|
||||
|
||||
// add mocked file
|
||||
mockedFiles[path] = dedent(content);
|
||||
|
||||
// create new volume
|
||||
const vol = Volume.fromJSON(mockedFiles, '__jest__');
|
||||
|
||||
// use the new volume
|
||||
unionFS.use(vol as any);
|
||||
}
|
||||
|
||||
// a virtual mock for package.json
|
||||
jest.mock(
|
||||
'__jest__/package.json',
|
||||
() => ({
|
||||
main: 'src/app.js',
|
||||
devDependencies: {
|
||||
typescript: '*',
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const fs = jest.requireActual('fs');
|
||||
const unionFS = require('unionfs').default;
|
||||
unionFS.reset = () => {
|
||||
unionFS.fss = [fs];
|
||||
};
|
||||
|
||||
return unionFS.use(fs);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
import './jest.mockFiles';
|
||||
// we are mocking the cwd for the tests, since webpack needs absolute paths
|
||||
// and we don't want them in tests
|
||||
process.cwd = () => '__jest__';
|
||||
@@ -65,3 +64,15 @@ jest.mock('path', () => {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// a virtual mock for package.json
|
||||
jest.mock(
|
||||
'__jest__/package.json',
|
||||
() => ({
|
||||
main: 'src/app.js',
|
||||
devDependencies: {
|
||||
typescript: '*',
|
||||
},
|
||||
}),
|
||||
{ virtual: true }
|
||||
);
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import Config from 'webpack-chain';
|
||||
import path from 'path';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import get from 'lodash.get'
|
||||
|
||||
import { getProjectRootPath } from '../helpers/project';
|
||||
import { getProjectFilePath, getProjectRootPath } from '../helpers/project';
|
||||
import { getEntryPath, getPlatformName } from '../helpers/platform';
|
||||
import { env as _env, IWebpackEnv } from '../index';
|
||||
import { getEntryPath } from '../helpers/platform';
|
||||
import { addCopyRule } from "../helpers/copyRules";
|
||||
import base from './base';
|
||||
|
||||
export default function (config: Config, env: IWebpackEnv = _env): Config {
|
||||
base(config, env);
|
||||
|
||||
const tsConfigPath = path.join(getProjectRootPath(), 'tsconfig.json');
|
||||
const tsConfigPath = getProjectFilePath('tsconfig.json')
|
||||
|
||||
applyFileReplacements(config)
|
||||
|
||||
// remove default ts rule
|
||||
config.module.rules.delete('ts');
|
||||
@@ -54,3 +59,122 @@ function getAngularCompilerPlugin() {
|
||||
const { AngularCompilerPlugin } = require('@ngtools/webpack');
|
||||
return AngularCompilerPlugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal exported for tests
|
||||
*/
|
||||
export function applyFileReplacements(
|
||||
config,
|
||||
fileReplacements = getFileReplacementsFromWorkspaceConfig()
|
||||
) {
|
||||
if (!fileReplacements) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.entries(fileReplacements).forEach(([_replace, _with]) => {
|
||||
// in case we are replacing source files - we'll use aliases
|
||||
if (_replace.match(/\.ts$/)) {
|
||||
return config.resolve.alias.set(_replace, _with)
|
||||
}
|
||||
|
||||
// otherwise we will override the replaced file with the replacement
|
||||
addCopyRule({
|
||||
from: _with, // copy the replacement file
|
||||
to: _replace, // to the original "to-be-replaced" file
|
||||
force: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// todo: move into project helper if used elsewhere
|
||||
// todo: write tests
|
||||
function findFile(fileName, currentDir): string | null {
|
||||
// console.log(`findFile(${fileName}, ${currentDir})`)
|
||||
const path = resolve(currentDir, fileName);
|
||||
|
||||
if (existsSync(path)) {
|
||||
return path
|
||||
}
|
||||
|
||||
// bail if we reached the root dir
|
||||
if (currentDir === resolve('/')) {
|
||||
return null
|
||||
}
|
||||
|
||||
// traverse to the parent folder
|
||||
return findFile(fileName, resolve(currentDir, '..'))
|
||||
}
|
||||
|
||||
function findWorkspaceConfig(): string {
|
||||
const possibleConfigNames = [
|
||||
'angular.json',
|
||||
'workspace.json'
|
||||
]
|
||||
|
||||
for (const name of possibleConfigNames) {
|
||||
const path = findFile(name, getProjectRootPath());
|
||||
|
||||
if (path) {
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// not found
|
||||
return null;
|
||||
}
|
||||
|
||||
interface IWorkspaceConfigFileReplacement {
|
||||
replace: string,
|
||||
with: string
|
||||
}
|
||||
|
||||
interface IReplacementMap {
|
||||
[from: string]: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal exported for tests
|
||||
*/
|
||||
export function getFileReplacementsFromWorkspaceConfig(
|
||||
configPath: string = findWorkspaceConfig()
|
||||
): IReplacementMap | null {
|
||||
const platform = getPlatformName();
|
||||
|
||||
if (!_env.configuration || !_env.projectName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configurations = _env.configuration.split(',').map(c => c.trim())
|
||||
|
||||
if (!configPath || configPath === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const config = require(configPath);
|
||||
const project = get(config, `projects.${_env.projectName}`)
|
||||
const targetProp = project?.architect ? 'architect' : 'targets';
|
||||
|
||||
if (!project) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const replacements = {};
|
||||
|
||||
const setReplacement = (entry: IWorkspaceConfigFileReplacement) => {
|
||||
const relativeReplace = resolve(dirname(configPath), entry.replace)
|
||||
const relativeWith = resolve(dirname(configPath), entry.with)
|
||||
replacements[relativeReplace] = relativeWith
|
||||
}
|
||||
|
||||
configurations.forEach(configuration => {
|
||||
const defaultReplacements = get(project, `${targetProp}.default.configurations.${configuration}.fileReplacements`)
|
||||
const platformReplacements = get(project, `${targetProp}.${platform}.configurations.${configuration}.fileReplacements`)
|
||||
|
||||
// add default replacements
|
||||
defaultReplacements?.map(setReplacement)
|
||||
// add platform replacements (always override defaults!)
|
||||
platformReplacements?.map(setReplacement)
|
||||
})
|
||||
|
||||
return replacements;
|
||||
}
|
||||
|
||||
@@ -12,17 +12,29 @@ import { env } from '..';
|
||||
export let copyRules = new Set([]);
|
||||
|
||||
/**
|
||||
* Utility to add new copy rules. Accepts a glob. For example
|
||||
* @internal
|
||||
*/
|
||||
export let additionalCopyRules = []
|
||||
|
||||
/**
|
||||
* Utility to add new copy rules. Accepts a glob or an object. For example
|
||||
* - **\/*.html - copy all .html files found in any sub dir.
|
||||
* - myFolder/* - copy all files from myFolder
|
||||
*
|
||||
* When passing an object - no additional processing is done, and it's
|
||||
* applied as-is. Make sure to set every required property.
|
||||
*
|
||||
* The path is relative to the folder of the entry file
|
||||
* (specified in the main field of the package.json)
|
||||
*
|
||||
* @param {string} glob
|
||||
* @param {string|object} globOrObject
|
||||
*/
|
||||
export function addCopyRule(glob: string) {
|
||||
copyRules.add(glob);
|
||||
export function addCopyRule(globOrObject: string | object) {
|
||||
if(typeof globOrObject === 'string') {
|
||||
return copyRules.add(globOrObject);
|
||||
}
|
||||
|
||||
additionalCopyRules.push(globOrObject)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +77,7 @@ export function applyCopyRules(config: Config) {
|
||||
context: entryDir,
|
||||
noErrorOnMissing: true,
|
||||
globOptions,
|
||||
})),
|
||||
})).concat(additionalCopyRules),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function applyDotEnvPlugin(config: Config) {
|
||||
config.when(path !== null, (config) => {
|
||||
config.plugin('DotEnvPlugin').use(DotEnvPlugin, [
|
||||
{
|
||||
path: getDotEnvPath(),
|
||||
path,
|
||||
silent: true, // hide any errors
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -25,6 +25,10 @@ export interface IWebpackEnv {
|
||||
// for custom platforms
|
||||
platform?: string;
|
||||
|
||||
// angular specific
|
||||
configuration?: string;
|
||||
projectName?: string;
|
||||
|
||||
production?: boolean;
|
||||
report?: boolean;
|
||||
hmr?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user