chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import { convertNxExecutor } from '@nx/devkit';
|
||||
import fileServer from './file-server.impl';
|
||||
|
||||
export default convertNxExecutor(fileServer);
|
||||
@@ -0,0 +1,291 @@
|
||||
import { signalToCode } from '@nx/devkit/internal';
|
||||
import { execFileSync, fork } from 'child_process';
|
||||
import * as pc from 'picocolors';
|
||||
import {
|
||||
ExecutorContext,
|
||||
getPackageManagerCommand,
|
||||
output,
|
||||
parseTargetString,
|
||||
readTargetOptions,
|
||||
} from '@nx/devkit';
|
||||
import { copyFileSync, unlinkSync } from 'fs';
|
||||
import { Schema } from './schema';
|
||||
import { join, resolve } from 'path';
|
||||
import { readModulePackageJson } from 'nx/src/utils/package-json';
|
||||
import { daemonClient } from 'nx/src/daemon/client/client';
|
||||
import { interpolate } from 'nx/src/tasks-runner/utils';
|
||||
import { stripGlobToBaseDir } from '@nx/js/internal';
|
||||
import detectPort from 'detect-port';
|
||||
|
||||
function getHttpServerArgs(options: Schema) {
|
||||
const {
|
||||
buildTarget,
|
||||
parallel,
|
||||
host,
|
||||
proxyUrl,
|
||||
ssl,
|
||||
sslCert,
|
||||
sslKey,
|
||||
proxyOptions,
|
||||
watch,
|
||||
spa,
|
||||
cacheSeconds,
|
||||
...rest
|
||||
} = options;
|
||||
const args = [`-c${options.cacheSeconds}`];
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
if (typeof value === 'boolean' && value) {
|
||||
args.push(`--${key}`);
|
||||
} else if (typeof value === 'string') {
|
||||
args.push(`--${key}=${value}`);
|
||||
}
|
||||
}
|
||||
if (host) {
|
||||
args.push(`-a=${host}`);
|
||||
}
|
||||
if (ssl) {
|
||||
args.push(`-S`);
|
||||
}
|
||||
if (sslCert) {
|
||||
args.push(`-C=${sslCert}`);
|
||||
}
|
||||
if (sslKey) {
|
||||
args.push(`-K=${sslKey}`);
|
||||
}
|
||||
if (proxyUrl) {
|
||||
args.push(`-P=${proxyUrl}`);
|
||||
}
|
||||
if (proxyOptions) {
|
||||
Object.keys(options.proxyOptions).forEach((key) => {
|
||||
args.push(`--proxy-options.${key}=${options.proxyOptions[key]}`);
|
||||
});
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function getBuildTargetCommand(options: Schema, context: ExecutorContext) {
|
||||
const target = parseTargetString(options.buildTarget, context);
|
||||
const cmd = ['nx', 'run'];
|
||||
|
||||
if (target.configuration) {
|
||||
cmd.push(`${target.project}:${target.target}:${target.configuration}`);
|
||||
} else {
|
||||
cmd.push(`${target.project}:${target.target}`);
|
||||
}
|
||||
|
||||
if (options.parallel) {
|
||||
cmd.push(`--parallel`);
|
||||
}
|
||||
if (options.maxParallel) {
|
||||
cmd.push(`--maxParallel=${options.maxParallel}`);
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
function getBuildTargetOutputPath(options: Schema, context: ExecutorContext) {
|
||||
if (options.staticFilePath) {
|
||||
return options.staticFilePath;
|
||||
}
|
||||
|
||||
let outputPath: string;
|
||||
try {
|
||||
const target = parseTargetString(options.buildTarget, context);
|
||||
const buildOptions = readTargetOptions(target, context);
|
||||
if (buildOptions?.outputPath) {
|
||||
outputPath = buildOptions.outputPath;
|
||||
} else {
|
||||
const project = context.projectGraph.nodes[context.projectName];
|
||||
const buildTarget = project.data.targets[target.target];
|
||||
outputPath = buildTarget.outputs?.[0];
|
||||
if (outputPath) {
|
||||
outputPath = interpolate(outputPath, {
|
||||
projectName: project.data.name,
|
||||
projectRoot: project.data.root,
|
||||
});
|
||||
outputPath = stripGlobToBaseDir(outputPath);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid buildTarget: ${options.buildTarget}`);
|
||||
}
|
||||
|
||||
if (!outputPath) {
|
||||
throw new Error(
|
||||
`Unable to get the outputPath from buildTarget ${options.buildTarget}. Make sure ${options.buildTarget} has an outputPath property or manually provide an staticFilePath property`
|
||||
);
|
||||
}
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
function createFileWatcher(
|
||||
project: string | undefined,
|
||||
changeHandler: () => void
|
||||
) {
|
||||
return daemonClient.registerFileWatcher(
|
||||
{
|
||||
watchProjects: project ? [project] : 'all',
|
||||
includeGlobalWorkspaceFiles: true,
|
||||
includeDependencies: true,
|
||||
},
|
||||
async (error, val) => {
|
||||
if (error === 'reconnecting') {
|
||||
// Silent - daemon restarts automatically on lockfile changes
|
||||
return;
|
||||
} else if (error === 'reconnected') {
|
||||
// Silent - reconnection succeeded
|
||||
return;
|
||||
} else if (error === 'closed') {
|
||||
throw new Error(
|
||||
'Failed to reconnect to daemon after multiple attempts'
|
||||
);
|
||||
} else if (error) {
|
||||
throw new Error(`Watch error: ${error?.message ?? 'Unknown'}`);
|
||||
} else if (val?.changedFiles.length > 0) {
|
||||
changeHandler();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default async function* fileServerExecutor(
|
||||
options: Schema,
|
||||
context: ExecutorContext
|
||||
) {
|
||||
if (!options.buildTarget && !options.staticFilePath) {
|
||||
throw new Error("You must set either 'buildTarget' or 'staticFilePath'.");
|
||||
}
|
||||
|
||||
if (options.watch && !options.buildTarget) {
|
||||
throw new Error(
|
||||
"Watch error: You can only specify 'watch' when 'buildTarget' is set."
|
||||
);
|
||||
}
|
||||
|
||||
let running = false;
|
||||
let disposeWatch: () => void;
|
||||
|
||||
if (options.buildTarget) {
|
||||
// Run the build target through the workspace package manager so workspaces
|
||||
// that pin a non-npm manager (e.g. devEngines.packageManager) don't fail.
|
||||
const pmc = getPackageManagerCommand();
|
||||
const run = () => {
|
||||
if (!running) {
|
||||
running = true;
|
||||
/**
|
||||
* Expose a variable to the build target to know if it's being run by the serve-static executor
|
||||
* This is useful because a config might need to change if it's being run by serve-static without the user's input
|
||||
* or if being ran by another executor (eg. E2E tests)
|
||||
* */
|
||||
process.env.NX_SERVE_STATIC_BUILD_RUNNING = 'true';
|
||||
try {
|
||||
const args = getBuildTargetCommand(options, context);
|
||||
execFileSync(pmc.exec, args, {
|
||||
stdio: [0, 1, 2],
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
});
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Build target failed: ${pc.bold(options.buildTarget)}`
|
||||
);
|
||||
} finally {
|
||||
process.env.NX_SERVE_STATIC_BUILD_RUNNING = undefined;
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!daemonClient.enabled() && options.watch) {
|
||||
output.warn({
|
||||
title:
|
||||
'Nx Daemon is not enabled. Static server is not watching for changes.',
|
||||
});
|
||||
}
|
||||
if (daemonClient.enabled() && options.watch) {
|
||||
disposeWatch = await createFileWatcher(context.projectName, run);
|
||||
}
|
||||
|
||||
// perform initial run
|
||||
run();
|
||||
}
|
||||
|
||||
const port = await detectPort(options.port || 8080);
|
||||
const outputPath = getBuildTargetOutputPath(options, context);
|
||||
|
||||
if (options.spa) {
|
||||
const src = join(outputPath, 'index.html');
|
||||
const dst = join(outputPath, '404.html');
|
||||
|
||||
// See: https://github.com/http-party/http-server#magic-files
|
||||
copyFileSync(src, dst);
|
||||
|
||||
// We also need to ensure the proxyUrl is set, otherwise the browser will continue to throw a 404 error
|
||||
// This can cause unexpected behaviors and failures especially in automated test suites
|
||||
options.proxyUrl ??= `http${options.ssl ? 's' : ''}://localhost:${port}?`;
|
||||
}
|
||||
|
||||
const args = getHttpServerArgs(options);
|
||||
|
||||
const { path: pathToHttpServerPkgJson, packageJson } = readModulePackageJson(
|
||||
'http-server',
|
||||
module.paths
|
||||
);
|
||||
const pathToHttpServerBin = packageJson.bin['http-server'];
|
||||
const pathToHttpServer = resolve(
|
||||
pathToHttpServerPkgJson.replace('package.json', ''),
|
||||
pathToHttpServerBin
|
||||
);
|
||||
|
||||
// detect port as close to when used to prevent port being used by another process
|
||||
// when running in parallel
|
||||
args.push(`-p=${port}`);
|
||||
|
||||
const serve = fork(pathToHttpServer, [outputPath, ...args], {
|
||||
stdio: 'pipe',
|
||||
cwd: context.root,
|
||||
env: {
|
||||
FORCE_COLOR: 'true',
|
||||
...process.env,
|
||||
},
|
||||
});
|
||||
|
||||
const processExitListener = () => {
|
||||
serve.kill();
|
||||
if (disposeWatch) {
|
||||
disposeWatch();
|
||||
}
|
||||
|
||||
if (options.spa) {
|
||||
unlinkSync(join(outputPath, '404.html'));
|
||||
}
|
||||
};
|
||||
process.on('exit', processExitListener);
|
||||
process.on('SIGTERM', processExitListener);
|
||||
|
||||
serve.stdout.on('data', (chunk) => {
|
||||
if (chunk.toString().indexOf('GET') === -1) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
});
|
||||
serve.stderr.on('data', (chunk) => {
|
||||
process.stderr.write(chunk);
|
||||
});
|
||||
|
||||
yield {
|
||||
success: true,
|
||||
baseUrl: `${options.ssl ? 'https' : 'http'}://${options.host}:${port}`,
|
||||
};
|
||||
|
||||
return new Promise<{ success: boolean }>((res) => {
|
||||
serve.on('exit', (code, signal) => {
|
||||
if (code === null) code = signalToCode(signal);
|
||||
if (code == 0) {
|
||||
res({ success: true });
|
||||
} else {
|
||||
res({ success: false });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface Schema {
|
||||
host?: string;
|
||||
port?: number;
|
||||
ssl?: boolean;
|
||||
sslKey?: string;
|
||||
sslCert?: string;
|
||||
proxyUrl?: string;
|
||||
buildTarget?: string;
|
||||
parallel: boolean;
|
||||
maxParallel?: number;
|
||||
withDeps: boolean;
|
||||
proxyOptions?: object;
|
||||
watch?: boolean;
|
||||
spa: boolean;
|
||||
staticFilePath?: string;
|
||||
cors?: boolean;
|
||||
gzip?: boolean;
|
||||
brotli?: boolean;
|
||||
cacheSeconds?: number;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"version": 2,
|
||||
"continuous": true,
|
||||
"outputCapture": "direct-nodejs",
|
||||
"title": "File Server",
|
||||
"description": "Serve a web application from a folder. This executor is a wrapper around the [http-server](https://www.npmjs.com/package/http-server) package.",
|
||||
"type": "object",
|
||||
"cli": "nx",
|
||||
"properties": {
|
||||
"buildTarget": {
|
||||
"type": "string",
|
||||
"description": "Target which builds the application."
|
||||
},
|
||||
"parallel": {
|
||||
"type": "boolean",
|
||||
"description": "Build the target in parallel.",
|
||||
"default": true
|
||||
},
|
||||
"maxParallel": {
|
||||
"type": "number",
|
||||
"description": "Max number of parallel jobs."
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "Port to listen on.",
|
||||
"default": 4200
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "Host to listen on.",
|
||||
"default": "localhost"
|
||||
},
|
||||
"ssl": {
|
||||
"type": "boolean",
|
||||
"description": "Serve using `HTTPS`.",
|
||||
"default": false
|
||||
},
|
||||
"sslKey": {
|
||||
"type": "string",
|
||||
"description": "SSL key to use for serving `HTTPS`."
|
||||
},
|
||||
"sslCert": {
|
||||
"type": "string",
|
||||
"description": "SSL certificate to use for serving `HTTPS`."
|
||||
},
|
||||
"proxyUrl": {
|
||||
"type": "string",
|
||||
"description": "URL to proxy unhandled requests to. _Note: If the 'spa' flag is set to true, manually setting this value will override the catch-all redirect functionality from http-server which may lead to unexpected behavior._"
|
||||
},
|
||||
"proxyOptions": {
|
||||
"type": "object",
|
||||
"description": "Options for the proxy used by `http-server`.",
|
||||
"default": {},
|
||||
"properties": {
|
||||
"secure": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"watch": {
|
||||
"type": "boolean",
|
||||
"description": "Watch for file changes.",
|
||||
"default": true
|
||||
},
|
||||
"spa": {
|
||||
"type": "boolean",
|
||||
"description": "Redirect 404 errors to index.html (useful for SPA's)",
|
||||
"default": false,
|
||||
"x-priority": "important"
|
||||
},
|
||||
"staticFilePath": {
|
||||
"type": "string",
|
||||
"description": "Path where the build artifacts are located. If not provided then it will be infered from the buildTarget executor options as outputPath"
|
||||
},
|
||||
"cors": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Enable CORS",
|
||||
"default": true
|
||||
},
|
||||
"gzip": {
|
||||
"type": "boolean",
|
||||
"description": "Enable gzip compression",
|
||||
"default": false
|
||||
},
|
||||
"brotli": {
|
||||
"type": "boolean",
|
||||
"description": "Enable brotli compression",
|
||||
"default": false
|
||||
},
|
||||
"cacheSeconds": {
|
||||
"type": "number",
|
||||
"description": "Set cache time (in seconds) for cache-control max-age header. To disable caching, use -1. Caching defaults to disabled.",
|
||||
"default": -1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"examplesFile": "../../../docs/file-server-examples.md"
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`app not nested should generate files if bundler is vite 1`] = `
|
||||
"import { defineConfig, devices } from '@playwright/test';
|
||||
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
// For CI, you may want to set BASE_URL to the deployed application.
|
||||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import 'dotenv/config';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*
|
||||
* Generated as a .mts file so Node forces ESM regardless of workspace
|
||||
* \`type\`. Playwright routes \`.mts\` through its ESM loader (dynamic import,
|
||||
* bypassing the pirates CJS-compile path), and Nx's native TS strip loads
|
||||
* \`.mts\` directly. Playwright's configLoader auto-discovers
|
||||
* \`playwright.config.mts\` via its extension list
|
||||
* (.ts/.js/.mts/.mjs/.cts/.cjs).
|
||||
*/
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(import.meta.dirname, { testDir: './src' }),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npx nx run my-app:preview',
|
||||
url: 'http://localhost:4300',
|
||||
reuseExistingServer: true,
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
// Uncomment for mobile browsers support
|
||||
/* {
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
}, */
|
||||
|
||||
// Uncomment for branded browsers
|
||||
/* {
|
||||
name: 'Microsoft Edge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app not nested should setup playwright e2e project correctly for webpack 1`] = `
|
||||
"import { defineConfig, devices } from '@playwright/test';
|
||||
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
// For CI, you may want to set BASE_URL to the deployed application.
|
||||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import 'dotenv/config';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*
|
||||
* Generated as a .mts file so Node forces ESM regardless of workspace
|
||||
* \`type\`. Playwright routes \`.mts\` through its ESM loader (dynamic import,
|
||||
* bypassing the pirates CJS-compile path), and Nx's native TS strip loads
|
||||
* \`.mts\` directly. Playwright's configLoader auto-discovers
|
||||
* \`playwright.config.mts\` via its extension list
|
||||
* (.ts/.js/.mts/.mjs/.cts/.cjs).
|
||||
*/
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(import.meta.dirname, { testDir: './src' }),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npx nx run cool-app:serve-static',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: true,
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
// Uncomment for mobile browsers support
|
||||
/* {
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
}, */
|
||||
|
||||
// Uncomment for branded browsers
|
||||
/* {
|
||||
name: 'Microsoft Edge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app not nested should use serve target and port if bundler=vite, e2eTestRunner=playwright, addPlugin=false 1`] = `
|
||||
"import { defineConfig, devices } from '@playwright/test';
|
||||
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
// For CI, you may want to set BASE_URL to the deployed application.
|
||||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import 'dotenv/config';
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*
|
||||
* Generated as a .mts file so Node forces ESM regardless of workspace
|
||||
* \`type\`. Playwright routes \`.mts\` through its ESM loader (dynamic import,
|
||||
* bypassing the pirates CJS-compile path), and Nx's native TS strip loads
|
||||
* \`.mts\` directly. Playwright's configLoader auto-discovers
|
||||
* \`playwright.config.mts\` via its extension list
|
||||
* (.ts/.js/.mts/.mjs/.cts/.cjs).
|
||||
*/
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(import.meta.dirname, { testDir: './src' }),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npx nx run my-app:preview',
|
||||
url: 'http://localhost:4300',
|
||||
reuseExistingServer: true,
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
// Uncomment for mobile browsers support
|
||||
/* {
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
}, */
|
||||
|
||||
// Uncomment for branded browsers
|
||||
/* {
|
||||
name: 'Microsoft Edge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app setup web app with --bundler=vite should setup vite configuration 1`] = `null`;
|
||||
|
||||
exports[`app should setup eslint (eslintrc) 1`] = `
|
||||
"{
|
||||
"extends": ["../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app should setup the web build builder 1`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '../dist/my-app'),
|
||||
clean: true,
|
||||
},
|
||||
devServer: {
|
||||
port: 4200,
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
tsConfig: './tsconfig.app.json',
|
||||
compiler: 'babel',
|
||||
main: './src/main.ts',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.css'],
|
||||
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
|
||||
optimization: process.env['NODE_ENV'] === 'production',
|
||||
}),
|
||||
],
|
||||
};
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`app should setup the web dev server 1`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '../dist/my-app'),
|
||||
clean: true,
|
||||
},
|
||||
devServer: {
|
||||
port: 4200,
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
tsConfig: './tsconfig.app.json',
|
||||
compiler: 'babel',
|
||||
main: './src/main.ts',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.css'],
|
||||
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
|
||||
optimization: process.env['NODE_ENV'] === 'production',
|
||||
}),
|
||||
],
|
||||
};
|
||||
"
|
||||
`;
|
||||
@@ -0,0 +1,237 @@
|
||||
import 'nx/src/internal-testing-utils/mock-project-graph';
|
||||
|
||||
import { getInstalledCypressMajorVersion } from '@nx/cypress/internal';
|
||||
import { getProjects, readProjectConfiguration, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { applicationGenerator } from './application';
|
||||
// need to mock cypress otherwise it'll use the nx installed version from package.json
|
||||
// which is v9 while we are testing for the new v10 version
|
||||
jest.mock('@nx/cypress/internal', () => ({
|
||||
...jest.requireActual('@nx/cypress/internal'),
|
||||
getInstalledCypressMajorVersion: jest.fn(),
|
||||
}));
|
||||
describe('web app generator (legacy)', () => {
|
||||
let tree: Tree;
|
||||
let mockedInstalledCypressVersion: jest.Mock<
|
||||
ReturnType<typeof getInstalledCypressMajorVersion>
|
||||
> = getInstalledCypressMajorVersion as never;
|
||||
|
||||
let originalEnv: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.NX_ADD_PLUGINS;
|
||||
process.env.NX_ADD_PLUGINS = 'false';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NX_ADD_PLUGINS = originalEnv;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockedInstalledCypressVersion.mockReturnValue(10);
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should setup webpack configuration', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'my-app',
|
||||
});
|
||||
const project = readProjectConfiguration(tree, 'my-app');
|
||||
expect(project).toMatchInlineSnapshot(`
|
||||
{
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "my-app",
|
||||
"projectType": "application",
|
||||
"root": "my-app",
|
||||
"sourceRoot": "my-app/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"configurations": {
|
||||
"production": {
|
||||
"extractLicenses": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "my-app/src/environments/environment.ts",
|
||||
"with": "my-app/src/environments/environment.prod.ts",
|
||||
},
|
||||
],
|
||||
"namedChunks": false,
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"vendorChunk": false,
|
||||
},
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"assets": [
|
||||
"my-app/src/favicon.ico",
|
||||
"my-app/src/assets",
|
||||
],
|
||||
"baseHref": "/",
|
||||
"compiler": "babel",
|
||||
"index": "my-app/src/index.html",
|
||||
"main": "my-app/src/main.ts",
|
||||
"outputPath": "dist/my-app",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"my-app/src/styles.css",
|
||||
],
|
||||
"target": "web",
|
||||
"tsConfig": "my-app/tsconfig.app.json",
|
||||
"webpackConfig": "my-app/webpack.config.js",
|
||||
},
|
||||
"outputs": [
|
||||
"{options.outputPath}",
|
||||
],
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
},
|
||||
"serve": {
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "my-app:build:production",
|
||||
},
|
||||
},
|
||||
"executor": "@nx/webpack:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "my-app:build",
|
||||
},
|
||||
},
|
||||
"serve-static": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": {
|
||||
"jestConfig": "my-app/jest.config.cts",
|
||||
},
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const webpackConfig = tree.read('my-app/webpack.config.js', 'utf-8');
|
||||
expect(webpackConfig).toMatchInlineSnapshot(`
|
||||
"const { composePlugins, withNx, withWeb } = require('@nx/webpack');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(withNx(), withWeb(), (config) => {
|
||||
// Update the webpack config as needed here.
|
||||
// e.g. \`config.plugins.push(new MyPlugin())\`
|
||||
config.output.clean = true;
|
||||
return config;
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add targets for vite', async () => {
|
||||
await applicationGenerator(tree, {
|
||||
directory: 'my-vite-app',
|
||||
bundler: 'vite',
|
||||
});
|
||||
const projects = getProjects(tree);
|
||||
expect(projects.get('my-vite-app')).toMatchInlineSnapshot(`
|
||||
{
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"name": "my-vite-app",
|
||||
"projectType": "application",
|
||||
"root": "my-vite-app",
|
||||
"sourceRoot": "my-vite-app/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"configurations": {
|
||||
"development": {
|
||||
"mode": "development",
|
||||
},
|
||||
"production": {
|
||||
"mode": "production",
|
||||
},
|
||||
},
|
||||
"defaultConfiguration": "production",
|
||||
"executor": "@nx/vite:build",
|
||||
"options": {
|
||||
"outputPath": "dist/my-vite-app",
|
||||
},
|
||||
"outputs": [
|
||||
"{options.outputPath}",
|
||||
],
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
},
|
||||
"preview": {
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "my-vite-app:build:development",
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "my-vite-app:build:production",
|
||||
},
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/vite:preview-server",
|
||||
"options": {
|
||||
"buildTarget": "my-vite-app:build",
|
||||
},
|
||||
},
|
||||
"serve": {
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "my-vite-app:build:development",
|
||||
"hmr": true,
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "my-vite-app:build:production",
|
||||
"hmr": false,
|
||||
},
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"executor": "@nx/vite:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "my-vite-app:build",
|
||||
},
|
||||
},
|
||||
"serve-static": {
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-vite-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"options": {
|
||||
"jestConfig": "my-vite-app/jest.config.cts",
|
||||
},
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,718 @@
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
ensureRootProjectName,
|
||||
addBuildTargetDefaults,
|
||||
logShowProjectCommand,
|
||||
E2EWebServerDetails,
|
||||
} from '@nx/devkit/internal';
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
addProjectConfiguration,
|
||||
ensurePackage,
|
||||
formatFiles,
|
||||
generateFiles,
|
||||
GeneratorCallback,
|
||||
getPackageManagerCommand,
|
||||
joinPathFragments,
|
||||
names,
|
||||
offsetFromRoot,
|
||||
type PluginConfiguration,
|
||||
readNxJson,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateJson,
|
||||
updateNxJson,
|
||||
updateProjectConfiguration,
|
||||
writeJson,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
getRelativePathToRootTsConfig,
|
||||
initGenerator as jsInitGenerator,
|
||||
} from '@nx/js';
|
||||
import {
|
||||
swcCoreVersion,
|
||||
getNpmScope,
|
||||
addProjectToTsSolutionWorkspace,
|
||||
isUsingTsSolutionSetup,
|
||||
updateTsconfigFiles,
|
||||
} from '@nx/js/internal';
|
||||
import { join } from 'path';
|
||||
import {
|
||||
nxVersion,
|
||||
swcLoaderVersion,
|
||||
tsLibVersion,
|
||||
typesNodeVersion,
|
||||
} from '../../utils/versions';
|
||||
import { webInitGenerator } from '../init/init';
|
||||
import { Schema } from './schema';
|
||||
import { hasWebpackPlugin } from '../../utils/has-webpack-plugin';
|
||||
import staticServeConfiguration from '../static-serve/static-serve-configuration';
|
||||
import type { PackageJson } from 'nx/src/utils/package-json';
|
||||
|
||||
interface NormalizedSchema extends Schema {
|
||||
projectName: string;
|
||||
importPath: string;
|
||||
appProjectRoot: string;
|
||||
e2eProjectName: string;
|
||||
e2eProjectRoot: string;
|
||||
names: ReturnType<typeof names>;
|
||||
parsedTags: string[];
|
||||
isUsingTsSolutionConfig: boolean;
|
||||
}
|
||||
|
||||
function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
|
||||
const rootTsConfigPath = getRelativePathToRootTsConfig(
|
||||
tree,
|
||||
options.appProjectRoot
|
||||
);
|
||||
if (options.bundler === 'vite') {
|
||||
generateFiles(
|
||||
tree,
|
||||
join(__dirname, './files/app-vite'),
|
||||
options.appProjectRoot,
|
||||
{
|
||||
...options,
|
||||
tmpl: '',
|
||||
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
|
||||
rootTsConfigPath,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const rootOffset = offsetFromRoot(options.appProjectRoot);
|
||||
generateFiles(
|
||||
tree,
|
||||
join(__dirname, './files/app-webpack'),
|
||||
options.appProjectRoot,
|
||||
{
|
||||
...options,
|
||||
tmpl: '',
|
||||
offsetFromRoot: rootOffset,
|
||||
rootTsConfigPath,
|
||||
webpackPluginOptions: hasWebpackPlugin(tree)
|
||||
? {
|
||||
compiler: options.compiler,
|
||||
target: 'web',
|
||||
outputPath: options.isUsingTsSolutionConfig
|
||||
? 'dist'
|
||||
: joinPathFragments(
|
||||
rootOffset,
|
||||
'dist',
|
||||
options.appProjectRoot !== '.'
|
||||
? options.appProjectRoot
|
||||
: options.projectName
|
||||
),
|
||||
tsConfig: './tsconfig.app.json',
|
||||
main: './src/main.ts',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
styles: [`./src/styles.${options.style}`],
|
||||
}
|
||||
: null,
|
||||
}
|
||||
);
|
||||
if (options.unitTestRunner === 'none') {
|
||||
tree.delete(
|
||||
join(options.appProjectRoot, './src/app/app.element.spec.ts')
|
||||
);
|
||||
}
|
||||
}
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
updateJson(
|
||||
tree,
|
||||
joinPathFragments(options.appProjectRoot, 'tsconfig.json'),
|
||||
() => ({
|
||||
extends: rootTsConfigPath,
|
||||
files: [],
|
||||
include: [],
|
||||
references: [
|
||||
{
|
||||
path: './tsconfig.app.json',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
} else {
|
||||
updateJson(
|
||||
tree,
|
||||
joinPathFragments(options.appProjectRoot, 'tsconfig.json'),
|
||||
(json) => {
|
||||
return {
|
||||
...json,
|
||||
compilerOptions: {
|
||||
...(json.compilerOptions || {}),
|
||||
strict: options.strict,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBundler(tree: Tree, options: NormalizedSchema) {
|
||||
const main = joinPathFragments(options.appProjectRoot, 'src/main.ts');
|
||||
const tsConfig = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'tsconfig.app.json'
|
||||
);
|
||||
const assets = [
|
||||
joinPathFragments(options.appProjectRoot, 'src/favicon.ico'),
|
||||
joinPathFragments(options.appProjectRoot, 'src/assets'),
|
||||
];
|
||||
|
||||
if (options.bundler === 'webpack') {
|
||||
const { configurationGenerator } = ensurePackage<
|
||||
typeof import('@nx/webpack')
|
||||
>('@nx/webpack', nxVersion);
|
||||
await configurationGenerator(tree, {
|
||||
target: 'web',
|
||||
project: options.projectName,
|
||||
main,
|
||||
tsConfig,
|
||||
compiler: options.compiler ?? 'babel',
|
||||
devServer: true,
|
||||
webpackConfig: joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'webpack.config.js'
|
||||
),
|
||||
skipFormat: true,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
const project = readProjectConfiguration(tree, options.projectName);
|
||||
if (project.targets?.build) {
|
||||
const prodConfig = project.targets.build.configurations.production;
|
||||
const buildOptions = project.targets.build.options;
|
||||
buildOptions.assets = assets;
|
||||
buildOptions.index = joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
'src/index.html'
|
||||
);
|
||||
buildOptions.baseHref = '/';
|
||||
buildOptions.styles = [
|
||||
joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
`src/styles.${options.style}`
|
||||
),
|
||||
];
|
||||
// We can delete that, because this projest is an application
|
||||
// and applications have a .babelrc file in their root dir.
|
||||
// So Nx will find it and use it
|
||||
delete buildOptions.babelUpwardRootMode;
|
||||
buildOptions.scripts = [];
|
||||
prodConfig.fileReplacements = [
|
||||
{
|
||||
replace: joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
`src/environments/environment.ts`
|
||||
),
|
||||
with: joinPathFragments(
|
||||
options.appProjectRoot,
|
||||
`src/environments/environment.prod.ts`
|
||||
),
|
||||
},
|
||||
];
|
||||
prodConfig.optimization = true;
|
||||
prodConfig.outputHashing = 'all';
|
||||
prodConfig.sourceMap = false;
|
||||
prodConfig.namedChunks = false;
|
||||
prodConfig.extractLicenses = true;
|
||||
prodConfig.vendorChunk = false;
|
||||
updateProjectConfiguration(tree, options.projectName, project);
|
||||
}
|
||||
// TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html).
|
||||
} else if (options.bundler === 'none') {
|
||||
const project = readProjectConfiguration(tree, options.projectName);
|
||||
addBuildTargetDefaults(tree, `@nx/js:${options.compiler}`);
|
||||
project.targets ??= {};
|
||||
project.targets.build = {
|
||||
executor: `@nx/js:${options.compiler}`,
|
||||
outputs: ['{options.outputPath}'],
|
||||
options: {
|
||||
main,
|
||||
outputPath: joinPathFragments('dist', options.appProjectRoot),
|
||||
tsConfig,
|
||||
},
|
||||
};
|
||||
updateProjectConfiguration(tree, options.projectName, project);
|
||||
} else {
|
||||
throw new Error('Unsupported bundler type');
|
||||
}
|
||||
}
|
||||
|
||||
async function addProject(tree: Tree, options: NormalizedSchema) {
|
||||
const packageJson: PackageJson = {
|
||||
name: options.importPath,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
};
|
||||
|
||||
if (!options.useProjectJson) {
|
||||
if (options.projectName !== options.importPath) {
|
||||
packageJson.nx = { name: options.projectName };
|
||||
}
|
||||
if (options.parsedTags?.length) {
|
||||
packageJson.nx ??= {};
|
||||
packageJson.nx.tags = options.parsedTags;
|
||||
}
|
||||
} else {
|
||||
addProjectConfiguration(tree, options.projectName, {
|
||||
projectType: 'application',
|
||||
root: options.appProjectRoot,
|
||||
sourceRoot: joinPathFragments(options.appProjectRoot, 'src'),
|
||||
tags: options.parsedTags,
|
||||
targets: {},
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
|
||||
writeJson(
|
||||
tree,
|
||||
joinPathFragments(options.appProjectRoot, 'package.json'),
|
||||
packageJson
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setDefaults(tree: Tree, options: NormalizedSchema) {
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.generators = nxJson.generators || {};
|
||||
nxJson.generators['@nx/web:application'] = {
|
||||
style: options.style,
|
||||
linter: options.linter,
|
||||
unitTestRunner: options.unitTestRunner,
|
||||
e2eTestRunner: options.e2eTestRunner,
|
||||
...nxJson.generators['@nx/web:application'],
|
||||
};
|
||||
updateNxJson(tree, nxJson);
|
||||
}
|
||||
|
||||
export async function applicationGenerator(host: Tree, schema: Schema) {
|
||||
return await applicationGeneratorInternal(host, {
|
||||
addPlugin: false,
|
||||
...schema,
|
||||
});
|
||||
}
|
||||
|
||||
export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
|
||||
const options = await normalizeOptions(host, schema);
|
||||
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
await addProjectToTsSolutionWorkspace(host, options.appProjectRoot);
|
||||
}
|
||||
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
|
||||
const jsInitTask = await jsInitGenerator(host, {
|
||||
js: false,
|
||||
skipFormat: true,
|
||||
platform: 'web',
|
||||
});
|
||||
tasks.push(jsInitTask);
|
||||
const webTask = await webInitGenerator(host, {
|
||||
...options,
|
||||
skipFormat: true,
|
||||
});
|
||||
tasks.push(webTask);
|
||||
|
||||
await addProject(host, options);
|
||||
|
||||
if (options.bundler !== 'vite') {
|
||||
await setupBundler(host, options);
|
||||
}
|
||||
|
||||
createApplicationFiles(host, options);
|
||||
|
||||
if (options.linter === 'eslint') {
|
||||
const { lintProjectGenerator } = ensurePackage<typeof import('@nx/eslint')>(
|
||||
'@nx/eslint',
|
||||
nxVersion
|
||||
);
|
||||
const lintTask = await lintProjectGenerator(host, {
|
||||
linter: options.linter,
|
||||
project: options.projectName,
|
||||
tsConfigPaths: [
|
||||
joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
|
||||
],
|
||||
unitTestRunner: options.unitTestRunner,
|
||||
skipFormat: true,
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
tasks.push(lintTask);
|
||||
|
||||
// Add out-tsc ignore pattern when using TS solution setup
|
||||
if (options.isUsingTsSolutionConfig) {
|
||||
// CommonJS `require` instead of dynamic ESM `import` — `ensurePackage`
|
||||
// exposes the temp install via `Module._initPaths`, which ESM ignores.
|
||||
const {
|
||||
addIgnoresToLintConfig,
|
||||
}: typeof import('@nx/eslint/internal') = require('@nx/eslint/internal');
|
||||
addIgnoresToLintConfig(host, options.appProjectRoot, ['**/out-tsc']);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.bundler === 'vite') {
|
||||
const { viteConfigurationGenerator, createOrEditViteConfig } =
|
||||
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
|
||||
// We recommend users use `import.meta.env.MODE` and other variables in their code to differentiate between production and development.
|
||||
// See: https://vite.dev/guide/env-and-mode.html
|
||||
if (
|
||||
host.exists(joinPathFragments(options.appProjectRoot, 'src/environments'))
|
||||
) {
|
||||
host.delete(
|
||||
joinPathFragments(options.appProjectRoot, 'src/environments')
|
||||
);
|
||||
}
|
||||
|
||||
const viteTask = await viteConfigurationGenerator(host, {
|
||||
uiFramework: 'none',
|
||||
project: options.projectName,
|
||||
newProject: true,
|
||||
includeVitest: options.unitTestRunner === 'vitest',
|
||||
inSourceTests: options.inSourceTests,
|
||||
skipFormat: true,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
tasks.push(viteTask);
|
||||
createOrEditViteConfig(
|
||||
host,
|
||||
{
|
||||
project: options.projectName,
|
||||
includeLib: false,
|
||||
includeVitest: options.unitTestRunner === 'vitest',
|
||||
inSourceTests: options.inSourceTests,
|
||||
useEsmExtension: true,
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
|
||||
const { createOrEditViteConfig } = ensurePackage<typeof import('@nx/vite')>(
|
||||
'@nx/vite',
|
||||
nxVersion
|
||||
);
|
||||
ensurePackage('@nx/vitest', nxVersion);
|
||||
// CommonJS `require` instead of dynamic ESM `import` — `ensurePackage`
|
||||
// exposes the temp install via `Module._initPaths`, which ESM ignores.
|
||||
const {
|
||||
configurationGenerator,
|
||||
}: typeof import('@nx/vitest/generators') = require('@nx/vitest/generators');
|
||||
const vitestTask = await configurationGenerator(host, {
|
||||
uiFramework: 'none',
|
||||
project: options.projectName,
|
||||
coverageProvider: 'v8',
|
||||
inSourceTests: options.inSourceTests,
|
||||
skipFormat: true,
|
||||
addPlugin: options.addPlugin,
|
||||
compiler: options.compiler,
|
||||
});
|
||||
tasks.push(vitestTask);
|
||||
createOrEditViteConfig(
|
||||
host,
|
||||
{
|
||||
project: options.projectName,
|
||||
includeLib: false,
|
||||
includeVitest: true,
|
||||
inSourceTests: options.inSourceTests,
|
||||
useEsmExtension: true,
|
||||
},
|
||||
true,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(options.bundler === 'vite' || options.unitTestRunner === 'vitest') &&
|
||||
options.inSourceTests
|
||||
) {
|
||||
host.delete(
|
||||
joinPathFragments(options.appProjectRoot, `src/app/app.element.spec.ts`)
|
||||
);
|
||||
}
|
||||
|
||||
const nxJson = readNxJson(host);
|
||||
let hasPlugin: PluginConfiguration | undefined;
|
||||
let buildPlugin: string;
|
||||
let buildConfigFile: string;
|
||||
if (options.bundler === 'webpack' || options.bundler === 'vite') {
|
||||
buildPlugin = `@nx/${options.bundler}/plugin`;
|
||||
buildConfigFile =
|
||||
options.bundler === 'webpack' ? 'webpack.config.js' : `vite.config.ts`;
|
||||
hasPlugin = nxJson.plugins?.find((p) =>
|
||||
typeof p === 'string' ? p === buildPlugin : p.plugin === buildPlugin
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPlugin) {
|
||||
await staticServeConfiguration(host, {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
spa: true,
|
||||
});
|
||||
}
|
||||
|
||||
let e2eWebServerInfo: E2EWebServerDetails = {
|
||||
e2eWebServerAddress: `http://localhost:4200`,
|
||||
e2eWebServerCommand: `${getPackageManagerCommand().exec} nx run ${
|
||||
options.projectName
|
||||
}:serve`,
|
||||
e2eCiWebServerCommand: `${getPackageManagerCommand().exec} nx run ${
|
||||
options.projectName
|
||||
}:serve-static`,
|
||||
e2eCiBaseUrl: `http://localhost:4200`,
|
||||
e2eDevServerTarget: `${options.projectName}:serve`,
|
||||
};
|
||||
|
||||
if (options.bundler === 'webpack') {
|
||||
const { getWebpackE2EWebServerInfo } = ensurePackage<
|
||||
typeof import('@nx/webpack')
|
||||
>('@nx/webpack', nxVersion);
|
||||
e2eWebServerInfo = await getWebpackE2EWebServerInfo(
|
||||
host,
|
||||
options.projectName,
|
||||
joinPathFragments(options.appProjectRoot, `webpack.config.js`),
|
||||
options.addPlugin,
|
||||
4200
|
||||
);
|
||||
} else if (options.bundler === 'vite') {
|
||||
const { getViteE2EWebServerInfo } = ensurePackage<
|
||||
typeof import('@nx/vite')
|
||||
>('@nx/vite', nxVersion);
|
||||
e2eWebServerInfo = await getViteE2EWebServerInfo(
|
||||
host,
|
||||
options.projectName,
|
||||
joinPathFragments(options.appProjectRoot, `vite.config.ts`),
|
||||
options.addPlugin,
|
||||
4200
|
||||
);
|
||||
}
|
||||
|
||||
if (options.e2eTestRunner === 'cypress') {
|
||||
const { configurationGenerator } = ensurePackage<
|
||||
typeof import('@nx/cypress')
|
||||
>('@nx/cypress', nxVersion);
|
||||
|
||||
const packageJson: PackageJson = {
|
||||
name: options.e2eProjectName,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
};
|
||||
|
||||
if (!options.useProjectJson) {
|
||||
packageJson.nx = {
|
||||
implicitDependencies: [options.projectName],
|
||||
};
|
||||
} else {
|
||||
addProjectConfiguration(host, options.e2eProjectName, {
|
||||
root: options.e2eProjectRoot,
|
||||
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
tags: [],
|
||||
implicitDependencies: [options.projectName],
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
|
||||
writeJson(
|
||||
host,
|
||||
joinPathFragments(options.e2eProjectRoot, 'package.json'),
|
||||
packageJson
|
||||
);
|
||||
}
|
||||
|
||||
const cypressTask = await configurationGenerator(host, {
|
||||
...options,
|
||||
project: options.e2eProjectName,
|
||||
devServerTarget: e2eWebServerInfo.e2eDevServerTarget,
|
||||
baseUrl: e2eWebServerInfo.e2eWebServerAddress,
|
||||
directory: 'src',
|
||||
skipFormat: true,
|
||||
webServerCommands: {
|
||||
default: e2eWebServerInfo.e2eWebServerCommand,
|
||||
production: e2eWebServerInfo.e2eCiWebServerCommand,
|
||||
},
|
||||
ciWebServerCommand: e2eWebServerInfo.e2eCiWebServerCommand,
|
||||
ciBaseUrl: e2eWebServerInfo.e2eCiBaseUrl,
|
||||
});
|
||||
|
||||
tasks.push(cypressTask);
|
||||
} else if (options.e2eTestRunner === 'playwright') {
|
||||
const { configurationGenerator: playwrightConfigGenerator } = ensurePackage<
|
||||
typeof import('@nx/playwright')
|
||||
>('@nx/playwright', nxVersion);
|
||||
|
||||
const packageJson: PackageJson = {
|
||||
name: options.e2eProjectName,
|
||||
version: '0.0.1',
|
||||
private: true,
|
||||
};
|
||||
|
||||
if (!options.useProjectJson) {
|
||||
packageJson.nx = {
|
||||
implicitDependencies: [options.projectName],
|
||||
};
|
||||
} else {
|
||||
addProjectConfiguration(host, options.e2eProjectName, {
|
||||
root: options.e2eProjectRoot,
|
||||
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
|
||||
projectType: 'application',
|
||||
targets: {},
|
||||
tags: [],
|
||||
implicitDependencies: [options.projectName],
|
||||
});
|
||||
}
|
||||
|
||||
if (!options.useProjectJson || options.isUsingTsSolutionConfig) {
|
||||
writeJson(
|
||||
host,
|
||||
joinPathFragments(options.e2eProjectRoot, 'package.json'),
|
||||
packageJson
|
||||
);
|
||||
}
|
||||
|
||||
const playwrightTask = await playwrightConfigGenerator(host, {
|
||||
project: options.e2eProjectName,
|
||||
skipFormat: true,
|
||||
skipPackageJson: false,
|
||||
directory: 'src',
|
||||
js: false,
|
||||
linter: options.linter,
|
||||
setParserOptionsProject: options.setParserOptionsProject,
|
||||
webServerCommand: e2eWebServerInfo.e2eCiWebServerCommand,
|
||||
webServerAddress: e2eWebServerInfo.e2eCiBaseUrl,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
|
||||
tasks.push(playwrightTask);
|
||||
}
|
||||
if (options.unitTestRunner === 'jest') {
|
||||
const { configurationGenerator } = ensurePackage<typeof import('@nx/jest')>(
|
||||
'@nx/jest',
|
||||
nxVersion
|
||||
);
|
||||
const jestTask = await configurationGenerator(host, {
|
||||
project: options.projectName,
|
||||
skipSerializers: true,
|
||||
setupFile: 'web-components',
|
||||
compiler: options.compiler,
|
||||
skipFormat: true,
|
||||
addPlugin: options.addPlugin,
|
||||
});
|
||||
tasks.push(jestTask);
|
||||
}
|
||||
|
||||
if (options.compiler === 'swc') {
|
||||
writeJson(host, joinPathFragments(options.appProjectRoot, '.swcrc'), {
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
},
|
||||
target: 'es2016',
|
||||
},
|
||||
});
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
host,
|
||||
{},
|
||||
{ '@swc/core': swcCoreVersion, 'swc-loader': swcLoaderVersion }
|
||||
);
|
||||
tasks.push(installTask);
|
||||
} else {
|
||||
writeJson(host, joinPathFragments(options.appProjectRoot, '.babelrc'), {
|
||||
presets: ['@nx/js/babel'],
|
||||
});
|
||||
}
|
||||
|
||||
setDefaults(host, options);
|
||||
|
||||
tasks.push(
|
||||
addDependenciesToPackageJson(
|
||||
host,
|
||||
{ tslib: tsLibVersion },
|
||||
{ '@types/node': typesNodeVersion }
|
||||
)
|
||||
);
|
||||
|
||||
updateTsconfigFiles(
|
||||
host,
|
||||
options.appProjectRoot,
|
||||
'tsconfig.app.json',
|
||||
{
|
||||
module: 'esnext',
|
||||
moduleResolution: 'bundler',
|
||||
},
|
||||
options.linter === 'eslint'
|
||||
? ['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs']
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(host);
|
||||
}
|
||||
|
||||
tasks.push(() => {
|
||||
logShowProjectCommand(options.projectName);
|
||||
});
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
async function normalizeOptions(
|
||||
host: Tree,
|
||||
options: Schema
|
||||
): Promise<NormalizedSchema> {
|
||||
await ensureRootProjectName(options, 'application');
|
||||
const {
|
||||
projectName,
|
||||
projectRoot: appProjectRoot,
|
||||
importPath,
|
||||
} = await determineProjectNameAndRootOptions(host, {
|
||||
name: options.name,
|
||||
projectType: 'application',
|
||||
directory: options.directory,
|
||||
});
|
||||
const nxJson = readNxJson(host);
|
||||
const addPluginDefault =
|
||||
process.env.NX_ADD_PLUGINS !== 'false' &&
|
||||
nxJson.useInferencePlugins !== false;
|
||||
options.addPlugin ??= addPluginDefault;
|
||||
|
||||
const isUsingTsSolutionConfig = isUsingTsSolutionSetup(host);
|
||||
const appProjectName =
|
||||
!isUsingTsSolutionConfig || options.name ? projectName : importPath;
|
||||
|
||||
const e2eProjectName = `${appProjectName}-e2e`;
|
||||
const e2eProjectRoot = `${appProjectRoot}-e2e`;
|
||||
|
||||
const npmScope = getNpmScope(host);
|
||||
|
||||
const parsedTags = options.tags
|
||||
? options.tags.split(',').map((s) => s.trim())
|
||||
: [];
|
||||
|
||||
options.style = options.style || 'css';
|
||||
options.linter = options.linter || 'eslint';
|
||||
options.unitTestRunner = options.unitTestRunner || 'jest';
|
||||
options.e2eTestRunner = options.e2eTestRunner || 'playwright';
|
||||
|
||||
return {
|
||||
...options,
|
||||
prefix: options.prefix ?? npmScope ?? 'app',
|
||||
compiler: options.compiler ?? 'babel',
|
||||
bundler: options.bundler ?? 'webpack',
|
||||
projectName: appProjectName,
|
||||
importPath,
|
||||
strict: options.strict ?? true,
|
||||
appProjectRoot,
|
||||
e2eProjectRoot,
|
||||
e2eProjectName,
|
||||
parsedTags,
|
||||
names: names(projectName),
|
||||
isUsingTsSolutionConfig,
|
||||
useProjectJson: options.useProjectJson ?? !isUsingTsSolutionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export default applicationGenerator;
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"presets": [
|
||||
"@nx/js/babel"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title><%= names.className %></title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/src/styles.<%= style %>" />
|
||||
</head>
|
||||
<body>
|
||||
<<%= prefix %>-root></<%= prefix %>-root>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Remove template code below
|
||||
*/
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
svg {
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
overflow: scroll;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
#welcome {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
#welcome h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
#welcome span {
|
||||
display: block;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 2.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#hero {
|
||||
align-items: center;
|
||||
background-color: hsla(214, 62%, 21%, 1);
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#hero .text-container {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
#hero .text-container h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
#hero .text-container h2 svg {
|
||||
color: hsla(162, 47%, 50%, 1);
|
||||
height: 2rem;
|
||||
left: -0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
#hero .text-container h2 span {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
#hero .text-container a {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
#hero .logo-container svg {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
width: 66.666667%;
|
||||
}
|
||||
|
||||
#middle-content {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
|
||||
#learning-materials {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#learning-materials h2 {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.list-item-link {
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-link svg:first-child {
|
||||
margin-right: 1rem;
|
||||
height: 1.5rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.list-item-link > span {
|
||||
flex-grow: 1;
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link > span > span {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
line-height: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link svg:last-child {
|
||||
height: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1rem;
|
||||
}
|
||||
.list-item-link:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: hsla(162, 47%, 50%, 1);
|
||||
}
|
||||
.list-item-link:hover > span {
|
||||
}
|
||||
.list-item-link:hover > span > span {
|
||||
color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
.list-item-link:hover svg:last-child {
|
||||
transform: translateX(0.25rem);
|
||||
}
|
||||
|
||||
#other-links {
|
||||
}
|
||||
.button-pill {
|
||||
padding: 1.5rem 2rem;
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.button-pill svg {
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
.button-pill > span {
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.button-pill span span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.button-pill:hover svg,
|
||||
.button-pill:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
#nx-console:hover {
|
||||
background-color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console svg {
|
||||
color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console-jetbrains {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
#nx-console-jetbrains:hover {
|
||||
background-color: rgba(255, 49, 140, 1);
|
||||
}
|
||||
#nx-console-jetbrains svg {
|
||||
color: rgba(255, 49, 140, 1);
|
||||
}
|
||||
#nx-repo:hover {
|
||||
background-color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-repo svg {
|
||||
color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
|
||||
#nx-cloud {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#nx-cloud > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#nx-cloud > div svg {
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
#nx-cloud > div h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#nx-cloud > div h2 span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
#nx-cloud p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#nx-cloud a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#commands {
|
||||
padding: 2.5rem 2rem;
|
||||
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#commands h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#commands p {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
details pre > span {
|
||||
color: rgba(181, 181, 181, 1);
|
||||
}
|
||||
summary {
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
summary:hover {
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
summary svg {
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
#love {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 3.5rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
#love svg {
|
||||
color: rgba(252, 165, 165, 1);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: inline;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
#hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: flex;
|
||||
}
|
||||
#middle-content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { AppElement } from './app.element';
|
||||
|
||||
describe('AppElement', () => {
|
||||
let app: AppElement;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new AppElement();
|
||||
});
|
||||
|
||||
it('should create successfully', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a greeting', () => {
|
||||
app.connectedCallback();
|
||||
|
||||
expect(app.querySelector('h1').innerHTML).toContain(
|
||||
'Welcome <%= projectName %>'
|
||||
);
|
||||
});
|
||||
});
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
import './app.element.<%= style %>';
|
||||
|
||||
export class AppElement extends HTMLElement {
|
||||
public static observedAttributes = [
|
||||
|
||||
];
|
||||
|
||||
connectedCallback() {
|
||||
const title = '<%= projectName %>';
|
||||
this.innerHTML = `
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<!-- WELCOME -->
|
||||
<div id="welcome">
|
||||
<h1>
|
||||
<span> Hello there, </span>
|
||||
Welcome ${title} 👋
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- HERO -->
|
||||
<div id="hero" class="rounded">
|
||||
<div class="text-container">
|
||||
<h2>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
||||
/>
|
||||
</svg>
|
||||
<span>You're up and running</span>
|
||||
</h2>
|
||||
<a href="#commands"> What's next? </a>
|
||||
</div>
|
||||
<div class="logo-container">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MIDDLE CONTENT -->
|
||||
<div id="middle-content">
|
||||
<div id="learning-materials" class="rounded shadow">
|
||||
<h2>Learning materials</h2>
|
||||
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Documentation
|
||||
<span> Everything is in there </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Blog
|
||||
<span> Changelog, features & events </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
YouTube channel
|
||||
<span> Nx Show, talks & tutorials </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Interactive tutorials
|
||||
<span> Create an app, step-by-step </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||
<path
|
||||
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Video courses
|
||||
<span> Nx custom courses </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div id="other-links">
|
||||
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Visual Studio Code</title>
|
||||
<path
|
||||
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Install Nx Console for VSCode
|
||||
<span>The official VSCode extension for Nx.</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
id="nx-console-jetbrains"
|
||||
class="button-pill rounded shadow"
|
||||
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
height="48"
|
||||
width="48"
|
||||
viewBox="20 20 60 60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m22.5 22.5h60v60h-60z" />
|
||||
<g fill="#fff">
|
||||
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
|
||||
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
|
||||
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
|
||||
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
|
||||
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
|
||||
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
|
||||
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
|
||||
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
|
||||
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
|
||||
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>
|
||||
Install Nx Console for JetBrains
|
||||
<span>
|
||||
Available for WebStorm, Intellij IDEA Ultimate and more!
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<div id="nx-cloud" class="rounded shadow">
|
||||
<div>
|
||||
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
|
||||
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
|
||||
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
|
||||
</svg>
|
||||
<h2>
|
||||
Nx Cloud
|
||||
<span>
|
||||
Enable faster CI & better DX
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
You can activate distributed tasks executions and caching by
|
||||
running:
|
||||
</p>
|
||||
<pre>nx connect</pre>
|
||||
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
|
||||
</div>
|
||||
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Nx is open source
|
||||
<span> Love Nx? Give us a star! </span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COMMANDS -->
|
||||
<div id="commands" class="rounded shadow">
|
||||
<h2>Next steps</h2>
|
||||
<p>Here are some things you can do with Nx:</p>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Add UI library
|
||||
</summary>
|
||||
<pre><span># Generate UI lib</span>
|
||||
nx g @nx/angular:lib ui
|
||||
|
||||
<span># Add a component</span>
|
||||
nx g @nx/angular:component ui/src/lib/button</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
View interactive project graph
|
||||
</summary>
|
||||
<pre>nx graph</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Run affected commands
|
||||
</summary>
|
||||
<pre><span># see what's been affected by changes</span>
|
||||
nx affected:graph
|
||||
|
||||
<span># run tests for current changes</span>
|
||||
nx affected:test
|
||||
|
||||
<span># run e2e tests for current changes</span>
|
||||
nx affected:e2e</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<p id="love">
|
||||
Carefully crafted with
|
||||
<svg
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('<%= prefix %>-root', AppElement);
|
||||
@@ -0,0 +1 @@
|
||||
import './app/app.element';
|
||||
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
|
||||
"types": ["node", "vite/client"]
|
||||
},
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"extends": "<%= rootTsConfigPath %>",
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
+424
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
* Remove template code below
|
||||
*/
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
svg {
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
'Liberation Mono', 'Courier New', monospace;
|
||||
overflow: scroll;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
#welcome {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
#welcome h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
#welcome span {
|
||||
display: block;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 2.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#hero {
|
||||
align-items: center;
|
||||
background-color: hsla(214, 62%, 21%, 1);
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#hero .text-container {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
#hero .text-container h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
#hero .text-container h2 svg {
|
||||
color: hsla(162, 47%, 50%, 1);
|
||||
height: 2rem;
|
||||
left: -0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
#hero .text-container h2 span {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
#hero .text-container a {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
#hero .logo-container svg {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
width: 66.666667%;
|
||||
}
|
||||
|
||||
#middle-content {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
|
||||
#learning-materials {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#learning-materials h2 {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.list-item-link {
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-link svg:first-child {
|
||||
margin-right: 1rem;
|
||||
height: 1.5rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.list-item-link > span {
|
||||
flex-grow: 1;
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link > span > span {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
line-height: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link svg:last-child {
|
||||
height: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1rem;
|
||||
}
|
||||
.list-item-link:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: hsla(162, 47%, 50%, 1);
|
||||
}
|
||||
.list-item-link:hover > span {
|
||||
}
|
||||
.list-item-link:hover > span > span {
|
||||
color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
.list-item-link:hover svg:last-child {
|
||||
transform: translateX(0.25rem);
|
||||
}
|
||||
|
||||
#other-links {
|
||||
}
|
||||
.button-pill {
|
||||
padding: 1.5rem 2rem;
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.button-pill svg {
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
.button-pill > span {
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.button-pill span span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.button-pill:hover svg,
|
||||
.button-pill:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
#nx-console:hover {
|
||||
background-color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console svg {
|
||||
color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console-jetbrains {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
#nx-console-jetbrains:hover {
|
||||
background-color: rgba(255, 49, 140, 1);
|
||||
}
|
||||
#nx-console-jetbrains svg {
|
||||
color: rgba(255, 49, 140, 1);
|
||||
}
|
||||
#nx-repo:hover {
|
||||
background-color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-repo svg {
|
||||
color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
|
||||
#nx-cloud {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#nx-cloud > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#nx-cloud > div svg {
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
#nx-cloud > div h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#nx-cloud > div h2 span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
#nx-cloud p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#nx-cloud a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#commands {
|
||||
padding: 2.5rem 2rem;
|
||||
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#commands h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#commands p {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
details pre > span {
|
||||
color: rgba(181, 181, 181, 1);
|
||||
}
|
||||
summary {
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
summary:hover {
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
summary svg {
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
#love {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 3.5rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
#love svg {
|
||||
color: rgba(252, 165, 165, 1);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: inline;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
#hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: flex;
|
||||
}
|
||||
#middle-content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { AppElement } from './app.element';
|
||||
|
||||
describe('AppElement', () => {
|
||||
let app: AppElement;
|
||||
|
||||
beforeEach(() => {
|
||||
app = new AppElement();
|
||||
});
|
||||
|
||||
it('should create successfully', () => {
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a greeting', () => {
|
||||
app.connectedCallback();
|
||||
|
||||
expect(app.querySelector('h1').innerHTML).toContain(
|
||||
'Welcome <%= projectName %>'
|
||||
);
|
||||
});
|
||||
});
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
import './app.element.<%= style %>';
|
||||
|
||||
export class AppElement extends HTMLElement {
|
||||
public static observedAttributes = [
|
||||
|
||||
];
|
||||
|
||||
connectedCallback() {
|
||||
const title = '<%= projectName %>';
|
||||
this.innerHTML = `
|
||||
<div class="wrapper">
|
||||
<div class="container">
|
||||
<!-- WELCOME -->
|
||||
<div id="welcome">
|
||||
<h1>
|
||||
<span> Hello there, </span>
|
||||
Welcome ${title} 👋
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- HERO -->
|
||||
<div id="hero" class="rounded">
|
||||
<div class="text-container">
|
||||
<h2>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
|
||||
/>
|
||||
</svg>
|
||||
<span>You're up and running</span>
|
||||
</h2>
|
||||
<a href="#commands"> What's next? </a>
|
||||
</div>
|
||||
<div class="logo-container">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MIDDLE CONTENT -->
|
||||
<div id="middle-content">
|
||||
<div id="learning-materials" class="rounded shadow">
|
||||
<h2>Learning materials</h2>
|
||||
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Documentation
|
||||
<span> Everything is in there </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Blog
|
||||
<span> Changelog, features & events </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>YouTube</title>
|
||||
<path
|
||||
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
YouTube channel
|
||||
<span> Nx Show, talks & tutorials </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Interactive tutorials
|
||||
<span> Create an app, step-by-step </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M12 14l9-5-9-5-9 5 9 5z" />
|
||||
<path
|
||||
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Video courses
|
||||
<span> Nx custom courses </span>
|
||||
</span>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div id="other-links">
|
||||
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Visual Studio Code</title>
|
||||
<path
|
||||
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Install Nx Console for VSCode
|
||||
<span>The official VSCode extension for Nx.</span>
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
id="nx-console-jetbrains"
|
||||
class="button-pill rounded shadow"
|
||||
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
height="48"
|
||||
width="48"
|
||||
viewBox="20 20 60 60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="m22.5 22.5h60v60h-60z" />
|
||||
<g fill="#fff">
|
||||
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
|
||||
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
|
||||
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
|
||||
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
|
||||
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
|
||||
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
|
||||
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
|
||||
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
|
||||
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
|
||||
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
|
||||
</g>
|
||||
</svg>
|
||||
<span>
|
||||
Install Nx Console for JetBrains
|
||||
<span>
|
||||
Available for WebStorm, Intellij IDEA Ultimate and more!
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
<div id="nx-cloud" class="rounded shadow">
|
||||
<div>
|
||||
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
|
||||
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
|
||||
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
|
||||
</svg>
|
||||
<h2>
|
||||
Nx Cloud
|
||||
<span>
|
||||
Enable faster CI & better DX
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p>
|
||||
You can activate distributed tasks executions and caching by
|
||||
running:
|
||||
</p>
|
||||
<pre>nx connect</pre>
|
||||
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
|
||||
</div>
|
||||
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Nx is open source
|
||||
<span> Love Nx? Give us a star! </span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- COMMANDS -->
|
||||
<div id="commands" class="rounded shadow">
|
||||
<h2>Next steps</h2>
|
||||
<p>Here are some things you can do with Nx:</p>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Add UI library
|
||||
</summary>
|
||||
<pre><span># Generate UI lib</span>
|
||||
nx g @nx/angular:lib ui
|
||||
|
||||
<span># Add a component</span>
|
||||
nx g @nx/angular:component ui/src/lib/button</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
View interactive project graph
|
||||
</summary>
|
||||
<pre>nx graph</pre>
|
||||
</details>
|
||||
<details>
|
||||
<summary>
|
||||
<svg
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
Run affected commands
|
||||
</summary>
|
||||
<pre><span># see what's been affected by changes</span>
|
||||
nx affected:graph
|
||||
|
||||
<span># run tests for current changes</span>
|
||||
nx affected:test
|
||||
|
||||
<span># run e2e tests for current changes</span>
|
||||
nx affected:e2e</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<p id="love">
|
||||
Carefully crafted with
|
||||
<svg
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
customElements.define('<%= prefix %>-root', AppElement);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title><%= names.className %></title>
|
||||
<base href="/" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<<%= prefix %>-root></<%= prefix %>-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
import './app/app.element';
|
||||
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
|
||||
"types": ["node", "@nx/web/typings/style.d.ts"]
|
||||
},
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "<%= rootTsConfigPath %>",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<% if (webpackPluginOptions) { %>
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'),
|
||||
clean: true,
|
||||
},
|
||||
devServer: {
|
||||
port: 4200
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
tsConfig: '<%= webpackPluginOptions.tsConfig %>',
|
||||
compiler: '<%= webpackPluginOptions.compiler %>',
|
||||
main: '<%= webpackPluginOptions.main %>',
|
||||
index: '<%= webpackPluginOptions.index %>',
|
||||
baseHref: '<%= webpackPluginOptions.baseHref %>',
|
||||
assets: <%- JSON.stringify(webpackPluginOptions.assets) %>,
|
||||
styles: <%- JSON.stringify(webpackPluginOptions.styles) %>,
|
||||
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
|
||||
optimization: process.env['NODE_ENV'] === 'production',
|
||||
})
|
||||
],
|
||||
};
|
||||
<% } else { %>
|
||||
const { composePlugins, withNx, withWeb } = require('@nx/webpack');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx(),
|
||||
withWeb(),
|
||||
(config) => {
|
||||
// Update the webpack config as needed here.
|
||||
// e.g. `config.plugins.push(new MyPlugin())`
|
||||
config.output.clean = true;
|
||||
return config;
|
||||
}
|
||||
);
|
||||
<% } %>
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Linter, LinterType } from '@nx/eslint';
|
||||
|
||||
export interface Schema {
|
||||
directory: string;
|
||||
name?: string;
|
||||
prefix?: string;
|
||||
style?: string;
|
||||
bundler?: 'webpack' | 'none' | 'vite';
|
||||
compiler?: 'babel' | 'swc';
|
||||
skipFormat?: boolean;
|
||||
tags?: string;
|
||||
unitTestRunner?: 'jest' | 'vitest' | 'none';
|
||||
inSourceTests?: boolean;
|
||||
e2eTestRunner?: 'cypress' | 'playwright' | 'none';
|
||||
linter?: Linter | LinterType;
|
||||
setParserOptionsProject?: boolean;
|
||||
strict?: boolean;
|
||||
addPlugin?: boolean;
|
||||
useProjectJson?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"cli": "nx",
|
||||
"$id": "NxWebApp",
|
||||
"title": "Create a Web Application for Nx",
|
||||
"description": "Create a web application using `swc` or `babel` as compiler.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
"type": "string",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "Which directory do you want to create the application in?"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name of the application.",
|
||||
"type": "string",
|
||||
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"style": {
|
||||
"description": "The file extension to be used for style files.",
|
||||
"type": "string",
|
||||
"default": "css",
|
||||
"enum": ["css", "scss"],
|
||||
"x-prompt": {
|
||||
"message": "Which stylesheet format would you like to use?",
|
||||
"type": "list",
|
||||
"items": [
|
||||
{
|
||||
"value": "css",
|
||||
"label": "CSS"
|
||||
},
|
||||
{
|
||||
"value": "scss",
|
||||
"label": "SASS(.scss) [ https://sass-lang.com ]"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"compiler": {
|
||||
"type": "string",
|
||||
"description": "The compiler to use",
|
||||
"enum": ["swc", "babel"],
|
||||
"default": "swc",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"description": "The bundler to use.",
|
||||
"enum": ["vite", "webpack", "none"],
|
||||
"default": "vite",
|
||||
"x-prompt": "Which bundler do you want to use?",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"linter": {
|
||||
"description": "The tool to use for running lint checks.",
|
||||
"type": "string",
|
||||
"enum": ["eslint", "none"],
|
||||
"default": "none",
|
||||
"x-prompt": "Which linter would you like to use?"
|
||||
},
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"unitTestRunner": {
|
||||
"type": "string",
|
||||
"enum": ["vitest", "jest", "none"],
|
||||
"default": "none",
|
||||
"description": "Test runner to use for unit tests. Default value is 'jest' when using 'webpack' or 'none' as the bundler and 'vitest' when using 'vite' as the bundler",
|
||||
"x-prompt": "What unit test runner should be used?"
|
||||
},
|
||||
"inSourceTests": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
|
||||
},
|
||||
"e2eTestRunner": {
|
||||
"type": "string",
|
||||
"enum": ["playwright", "cypress", "none"],
|
||||
"x-prompt": "Which E2E test runner would you like to use?",
|
||||
"description": "Test runner to use for end to end (e2e) tests",
|
||||
"default": "playwright"
|
||||
},
|
||||
"tags": {
|
||||
"type": "string",
|
||||
"description": "Add tags to the application (used for linting)"
|
||||
},
|
||||
"setParserOptionsProject": {
|
||||
"type": "boolean",
|
||||
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
|
||||
"default": false
|
||||
},
|
||||
"strict": {
|
||||
"type": "boolean",
|
||||
"description": "Creates an application with strict mode and strict type checking.",
|
||||
"default": true
|
||||
},
|
||||
"useProjectJson": {
|
||||
"type": "boolean",
|
||||
"description": "Use a `project.json` configuration file instead of inlining the Nx configuration in the `package.json` file."
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"examplesFile": "../../../docs/application-examples.md"
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { addDependenciesToPackageJson, readJson, Tree } from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
|
||||
import webInitGenerator from './init';
|
||||
|
||||
describe('init', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should add web dependencies', async () => {
|
||||
const existing = 'existing';
|
||||
const existingVersion = '1.0.0';
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{
|
||||
'@nx/web': nxVersion,
|
||||
[existing]: existingVersion,
|
||||
},
|
||||
{
|
||||
[existing]: existingVersion,
|
||||
}
|
||||
);
|
||||
await webInitGenerator(tree, {});
|
||||
const packageJson = readJson(tree, 'package.json');
|
||||
expect(packageJson.devDependencies['@nx/web']).toBeDefined();
|
||||
expect(packageJson.devDependencies[existing]).toBeDefined();
|
||||
expect(packageJson.dependencies['@nx/web']).toBeUndefined();
|
||||
expect(packageJson.dependencies[existing]).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
GeneratorCallback,
|
||||
removeDependenciesFromPackageJson,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
} from '@nx/devkit';
|
||||
import { nxVersion } from '../../utils/versions';
|
||||
import { Schema } from './schema';
|
||||
|
||||
function updateDependencies(tree: Tree, schema: Schema) {
|
||||
const tasks: GeneratorCallback[] = [];
|
||||
tasks.push(removeDependenciesFromPackageJson(tree, ['@nx/web'], []));
|
||||
tasks.push(
|
||||
addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ '@nx/web': nxVersion },
|
||||
undefined,
|
||||
schema.keepExistingVersions
|
||||
)
|
||||
);
|
||||
|
||||
return runTasksInSerial(...tasks);
|
||||
}
|
||||
|
||||
export async function webInitGenerator(tree: Tree, schema: Schema) {
|
||||
let installTask: GeneratorCallback = () => {};
|
||||
if (!schema.skipPackageJson) {
|
||||
installTask = updateDependencies(tree, schema);
|
||||
}
|
||||
|
||||
if (!schema.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return installTask;
|
||||
}
|
||||
|
||||
export default webInitGenerator;
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Schema {
|
||||
skipFormat?: boolean;
|
||||
skipPackageJson?: boolean;
|
||||
keepExistingVersions?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebInit",
|
||||
"cli": "nx",
|
||||
"title": "Init Web Plugin",
|
||||
"description": "Init Web Plugin.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"skipFormat": {
|
||||
"description": "Skip formatting files",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"skipPackageJson": {
|
||||
"description": "Do not add dependencies to `package.json`.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"x-priority": "internal"
|
||||
},
|
||||
"keepExistingVersions": {
|
||||
"type": "boolean",
|
||||
"x-priority": "internal",
|
||||
"description": "Keep existing dependencies versions",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebStaticServe",
|
||||
"cli": "nx",
|
||||
"title": "Static Serve Configuration",
|
||||
"description": "Add a new serve target to serve a build apps static files. This allows for faster serving of the static build files by reusing the case. Helpful when reserving the app over and over again like in e2e tests.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"buildTarget": {
|
||||
"type": "string",
|
||||
"description": "Name of the build target to serve"
|
||||
},
|
||||
"outputPath": {
|
||||
"type": "string",
|
||||
"description": "Path to the directory of the built files. This is only needed if buildTarget doesn't specify an outputPath executor option."
|
||||
},
|
||||
"targetName": {
|
||||
"type": "string",
|
||||
"description": "Name of the serve target to add. Defaults to 'serve-static'.",
|
||||
"default": "serve-static"
|
||||
},
|
||||
"spa": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to set the 'spa' flag on the generated target.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["buildTarget"]
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import {
|
||||
addProjectConfiguration,
|
||||
readProjectConfiguration,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { webStaticServeGenerator } from './static-serve-configuration';
|
||||
|
||||
describe('Static serve configuration generator', () => {
|
||||
let tree: Tree;
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should add a `serve-static` target to the project', async () => {
|
||||
addReactConfig(tree, 'react-app');
|
||||
addAngularConfig(tree, 'angular-app');
|
||||
addStorybookConfig(tree, 'storybook');
|
||||
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'react-app:build',
|
||||
});
|
||||
|
||||
expect(readProjectConfiguration(tree, 'react-app').targets['serve-static'])
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "react-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'angular-app:build',
|
||||
});
|
||||
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "angular-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'storybook:build-storybook',
|
||||
});
|
||||
expect(readProjectConfiguration(tree, 'storybook').targets['serve-static'])
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependsOn": [
|
||||
"build-storybook",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "storybook:build-storybook",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/storybook/storybook",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support custom target name', async () => {
|
||||
addReactConfig(tree, 'react-app');
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'react-app:build',
|
||||
targetName: 'serve-static-custom',
|
||||
});
|
||||
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'react-app').targets['serve-static-custom']
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "react-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should infer outputPath via the buildTarget#outputs', async () => {
|
||||
addAngularConfig(tree, 'angular-app');
|
||||
const projectConfig = readProjectConfiguration(tree, 'angular-app');
|
||||
delete projectConfig.targets.build.options.outputPath;
|
||||
projectConfig.targets.build.outputs = ['{options.myPath}'];
|
||||
projectConfig.targets.build.options.myPath = 'dist/angular-app';
|
||||
|
||||
updateProjectConfiguration(tree, 'angular-app', projectConfig);
|
||||
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'angular-app:build',
|
||||
});
|
||||
|
||||
expect(
|
||||
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"dependsOn": [
|
||||
"build",
|
||||
],
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "angular-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/angular-app",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not override targets', async () => {
|
||||
addStorybookConfig(tree, 'storybook');
|
||||
|
||||
const pc = readProjectConfiguration(tree, 'storybook');
|
||||
pc.targets['serve-static'] = {
|
||||
executor: 'custom:executor',
|
||||
};
|
||||
|
||||
updateProjectConfiguration(tree, 'storybook', pc);
|
||||
|
||||
expect(() => {
|
||||
return webStaticServeGenerator(tree, {
|
||||
buildTarget: 'storybook:build-storybook',
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Project storybook already has a 'serve-static' target configured.
|
||||
Either rename or remove the existing 'serve-static' target and try again.
|
||||
Optionally, you can provide a different name with the --target-name option other than 'serve-static'"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
function addReactConfig(tree: Tree, name: string) {
|
||||
addProjectConfiguration(tree, name, {
|
||||
name,
|
||||
projectType: 'application',
|
||||
root: `${name}`,
|
||||
sourceRoot: `${name}/src`,
|
||||
targets: {
|
||||
build: {
|
||||
executor: '@nx/vite:build',
|
||||
outputs: ['{options.outputPath}'],
|
||||
defaultConfiguration: 'production',
|
||||
options: {
|
||||
outputPath: `dist/${name}`,
|
||||
},
|
||||
configurations: {
|
||||
development: {
|
||||
mode: 'development',
|
||||
},
|
||||
production: {
|
||||
mode: 'production',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function addAngularConfig(tree: Tree, name: string) {
|
||||
addProjectConfiguration(tree, name, {
|
||||
name,
|
||||
projectType: 'application',
|
||||
root: `${name}`,
|
||||
sourceRoot: `${name}/src`,
|
||||
targets: {
|
||||
build: {
|
||||
executor: '@angular-devkit/build-angular:browser',
|
||||
outputs: ['{options.outputPath}'],
|
||||
options: {
|
||||
outputPath: `dist/${name}`,
|
||||
index: `${name}/src/index.html`,
|
||||
main: `${name}/src/main.ts`,
|
||||
polyfills: [`zone.js`],
|
||||
tsConfig: `${name}/tsconfig.app.json`,
|
||||
inlineStyleLanguage: `scss`,
|
||||
assets: [`${name}/src/favicon.ico`, `${name}/src/assets`],
|
||||
styles: [`${name}/src/styles.scss`],
|
||||
scripts: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function addStorybookConfig(tree: Tree, name: string) {
|
||||
addProjectConfiguration(tree, name, {
|
||||
name,
|
||||
projectType: 'application',
|
||||
root: `${name}`,
|
||||
sourceRoot: `${name}/src`,
|
||||
targets: {
|
||||
'build-storybook': {
|
||||
executor: '@storybook/angular:build-storybook',
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: `dist/${name}/storybook`,
|
||||
configDir: `${name}/.storybook`,
|
||||
browserTarget: `storybook:build-storybook`,
|
||||
compodoc: false,
|
||||
},
|
||||
configurations: {
|
||||
ci: {
|
||||
quiet: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
createProjectGraphAsync,
|
||||
logger,
|
||||
parseTargetString,
|
||||
type ProjectGraph,
|
||||
readCachedProjectGraph,
|
||||
readProjectConfiguration,
|
||||
stripIndents,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import type { Schema as FileServerExecutorSchema } from '../../executors/file-server/schema.d';
|
||||
|
||||
interface WebStaticServeSchema {
|
||||
buildTarget: string;
|
||||
outputPath?: string;
|
||||
targetName?: string;
|
||||
spa?: boolean;
|
||||
}
|
||||
|
||||
interface NormalizedWebStaticServeSchema extends WebStaticServeSchema {
|
||||
projectName: string;
|
||||
targetName: string;
|
||||
spa: boolean;
|
||||
parsedBuildTarget: string;
|
||||
}
|
||||
|
||||
export async function webStaticServeGenerator(
|
||||
tree: Tree,
|
||||
options: WebStaticServeSchema
|
||||
) {
|
||||
const opts = await normalizeOptions(tree, options);
|
||||
addStaticConfig(tree, opts);
|
||||
}
|
||||
|
||||
async function normalizeOptions(
|
||||
tree: Tree,
|
||||
options: WebStaticServeSchema
|
||||
): Promise<NormalizedWebStaticServeSchema> {
|
||||
let projectGraph: ProjectGraph;
|
||||
try {
|
||||
projectGraph = readCachedProjectGraph();
|
||||
} catch (e) {
|
||||
projectGraph = await createProjectGraphAsync();
|
||||
}
|
||||
const target = parseTargetString(options.buildTarget, projectGraph);
|
||||
const opts: NormalizedWebStaticServeSchema = {
|
||||
...options,
|
||||
targetName: options.targetName || 'serve-static',
|
||||
projectName: target.project,
|
||||
spa: options.spa ?? true,
|
||||
parsedBuildTarget: target.target,
|
||||
};
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, target.project);
|
||||
const buildTargetConfig = projectConfig?.targets?.[target.target];
|
||||
if (!buildTargetConfig) {
|
||||
throw new Error(stripIndents`Unable to read the target configuration for the provided build target, ${opts.buildTarget}
|
||||
Are you sure this target exists?`);
|
||||
}
|
||||
|
||||
if (projectConfig.targets[opts.targetName]) {
|
||||
throw new Error(stripIndents`Project ${target.project} already has a '${opts.targetName}' target configured.
|
||||
Either rename or remove the existing '${opts.targetName}' target and try again.
|
||||
Optionally, you can provide a different name with the --target-name option other than '${opts.targetName}'`);
|
||||
}
|
||||
|
||||
// NOTE: @nx/web:file-server only looks for the outputPath option
|
||||
if (!buildTargetConfig.options?.outputPath && !opts.outputPath) {
|
||||
// attempt to find the suitable path from the outputs
|
||||
let maybeOutputValue: any;
|
||||
for (const o of buildTargetConfig?.outputs || []) {
|
||||
const isInterpolatedOutput = o.trim().startsWith('{options.');
|
||||
if (!isInterpolatedOutput) {
|
||||
continue;
|
||||
}
|
||||
const noBracketParts = o.replace(/[{}]/g, '').split('.');
|
||||
|
||||
if (noBracketParts.length === 2 && noBracketParts?.[1]) {
|
||||
const key = noBracketParts[1].trim();
|
||||
const value = buildTargetConfig.options?.[key];
|
||||
if (value) {
|
||||
maybeOutputValue = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: outputDir is the storybook option.
|
||||
opts.outputPath = buildTargetConfig.options?.outputDir || maybeOutputValue;
|
||||
if (opts.outputPath) {
|
||||
logger.warn(`Automatically detected the output path to be ${opts.outputPath}.
|
||||
If this is incorrect, the update the staticFilePath option in the ${target.project}:${opts.targetName} target configuration`);
|
||||
} else {
|
||||
logger.warn(
|
||||
stripIndents`${opts.buildTarget} did not have an outputPath property set and --output-path was not provided.
|
||||
Without either options, the static serve will most likely be unable to serve your project.
|
||||
It's recommend to provide a --output-path option in this case.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function addStaticConfig(tree: Tree, opts: NormalizedWebStaticServeSchema) {
|
||||
const projectConfig = readProjectConfiguration(tree, opts.projectName);
|
||||
|
||||
const staticServeOptions: TargetConfiguration<
|
||||
Partial<FileServerExecutorSchema>
|
||||
> = {
|
||||
executor: '@nx/web:file-server',
|
||||
dependsOn: [opts.parsedBuildTarget],
|
||||
options: {
|
||||
buildTarget: opts.buildTarget,
|
||||
staticFilePath: opts.outputPath,
|
||||
spa: opts.spa,
|
||||
},
|
||||
};
|
||||
|
||||
projectConfig.targets[opts.targetName] = staticServeOptions;
|
||||
|
||||
updateProjectConfiguration(tree, opts.projectName, projectConfig);
|
||||
}
|
||||
|
||||
export default webStaticServeGenerator;
|
||||
@@ -0,0 +1,129 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import type { Tree } from '@nx/devkit';
|
||||
import update, {
|
||||
rewriteSubpathImports,
|
||||
} from './rewrite-internal-subpath-imports';
|
||||
|
||||
describe('rewrite-@nx/web-internal-subpath-imports migration', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
describe('rewriteSubpathImports (pure)', () => {
|
||||
it('routes an internal-symbol import to @nx/web/internal', () => {
|
||||
const src = `import { waitForPortOpen } from '@nx/web/src/some/internal';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { waitForPortOpen } from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a public-symbol import to @nx/web', () => {
|
||||
const src = `import { webInitGenerator } from '@nx/web/src/some/path';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { webInitGenerator } from '@nx/web';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('splits a mixed public/internal import into two declarations', () => {
|
||||
const src = `import { webInitGenerator, waitForPortOpen } from '@nx/web/src/some/path';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { webInitGenerator } from '@nx/web';\nimport { waitForPortOpen } from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('classifies aliased bindings by their original name', () => {
|
||||
const src = `import { webInitGenerator as aliased } from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import { webInitGenerator as aliased } from '@nx/web';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves the type modifier when splitting', () => {
|
||||
const src = `import type { webInitGenerator, waitForPortOpen } from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import type { webInitGenerator } from '@nx/web';\nimport type { waitForPortOpen } from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes export { ... } from by symbol', () => {
|
||||
const src = `export { webInitGenerator, waitForPortOpen } from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`export { webInitGenerator } from '@nx/web';\nexport { waitForPortOpen } from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a namespace import to @nx/web/internal', () => {
|
||||
const src = `import * as ns from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import * as ns from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes a default import to @nx/web/internal', () => {
|
||||
const src = `import thing from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`import thing from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes export * to @nx/web/internal', () => {
|
||||
const src = `export * from '@nx/web/src/x';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`export * from '@nx/web/internal';\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes require() to @nx/web/internal', () => {
|
||||
const src = `const m = require('@nx/web/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = require('@nx/web/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes dynamic import() to @nx/web/internal', () => {
|
||||
const src = `const m = await import('@nx/web/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = await import('@nx/web/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes typeof import() type queries to @nx/web/internal', () => {
|
||||
const src = `let m: typeof import('@nx/web/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`let m: typeof import('@nx/web/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('rewrites both runtime and type args of a require()-with-cast', () => {
|
||||
const src = `const m = require('@nx/web/src/x') as typeof import('@nx/web/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`const m = require('@nx/web/internal') as typeof import('@nx/web/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('routes jest.mock() to @nx/web/internal', () => {
|
||||
const src = `jest.mock('@nx/web/src/x');\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(
|
||||
`jest.mock('@nx/web/internal');\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves non-@nx/web imports untouched', () => {
|
||||
const src = `import { foo } from '@nx/devkit';\nimport { bar } from '@nx/web';\n`;
|
||||
expect(rewriteSubpathImports(src)).toBe(src);
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites files across the tree and formats', async () => {
|
||||
tree.write(
|
||||
'libs/app/src/x.ts',
|
||||
`import { waitForPortOpen } from '@nx/web/src/some/internal';\n`
|
||||
);
|
||||
await update(tree);
|
||||
expect(tree.read('libs/app/src/x.ts', 'utf-8')).toContain(
|
||||
`from '@nx/web/internal'`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
applyChangesToString,
|
||||
ChangeType,
|
||||
ensurePackage,
|
||||
formatFiles,
|
||||
logger,
|
||||
type StringChange,
|
||||
type Tree,
|
||||
visitNotIgnoredFiles,
|
||||
} from '@nx/devkit';
|
||||
import type {
|
||||
CallExpression,
|
||||
ExportDeclaration,
|
||||
ExportSpecifier,
|
||||
ImportDeclaration,
|
||||
ImportSpecifier,
|
||||
ImportTypeNode,
|
||||
Node,
|
||||
SourceFile,
|
||||
StringLiteral,
|
||||
} from 'typescript';
|
||||
|
||||
const TS_EXTENSIONS = ['.ts', '.tsx', '.cts', '.mts'] as const;
|
||||
const FROM_PREFIX = '@nx/web/src/';
|
||||
const TO_PUBLIC = '@nx/web';
|
||||
const TO_INTERNAL = '@nx/web/internal';
|
||||
|
||||
// Symbols exported from `@nx/web`'s public entry (packages/web/index.ts).
|
||||
// A named import/export of one of these from `@nx/web/src/*` is routed to
|
||||
// the public `@nx/web` entry; everything else goes to `@nx/web/internal`.
|
||||
const PUBLIC_SYMBOLS: ReadonlySet<string> = new Set([
|
||||
'applicationGenerator',
|
||||
'webInitGenerator',
|
||||
'webStaticServeGenerator',
|
||||
]);
|
||||
|
||||
// Methods on `jest` and `vi` that take a module specifier as their first arg.
|
||||
const MOCK_HELPER_METHODS: ReadonlySet<string> = new Set([
|
||||
'mock',
|
||||
'unmock',
|
||||
'doMock',
|
||||
'dontMock',
|
||||
'requireActual',
|
||||
'requireMock',
|
||||
'importActual',
|
||||
'importMock',
|
||||
]);
|
||||
|
||||
let ts: typeof import('typescript') | undefined;
|
||||
|
||||
export default async function rewriteInternalSubpathImports(
|
||||
tree: Tree
|
||||
): Promise<void> {
|
||||
let touchedCount = 0;
|
||||
|
||||
visitNotIgnoredFiles(tree, '.', (filePath) => {
|
||||
if (!TS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
|
||||
return;
|
||||
}
|
||||
const original = tree.read(filePath, 'utf-8');
|
||||
if (!original || !original.includes(FROM_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
const updated = rewriteSubpathImports(original);
|
||||
if (updated !== original) {
|
||||
tree.write(filePath, updated);
|
||||
touchedCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (touchedCount > 0) {
|
||||
logger.info(
|
||||
`Rewrote @nx/web/src/* imports in ${touchedCount} file(s) ` +
|
||||
`(public symbols to @nx/web, internals to @nx/web/internal).`
|
||||
);
|
||||
}
|
||||
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
export function rewriteSubpathImports(source: string): string {
|
||||
ts ??= ensurePackage<typeof import('typescript')>('typescript', '*');
|
||||
const sourceFile = ts.createSourceFile(
|
||||
'tmp.ts',
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
/* setParentNodes */ true,
|
||||
ts.ScriptKind.TSX
|
||||
);
|
||||
|
||||
const changes: StringChange[] = [];
|
||||
for (const stmt of sourceFile.statements) {
|
||||
if (ts.isImportDeclaration(stmt)) {
|
||||
collectImportRewrite(sourceFile, stmt, changes);
|
||||
} else if (ts.isExportDeclaration(stmt)) {
|
||||
collectExportRewrite(sourceFile, stmt, changes);
|
||||
}
|
||||
}
|
||||
collectCallExpressionRewrites(sourceFile, changes);
|
||||
|
||||
return changes.length > 0 ? applyChangesToString(source, changes) : source;
|
||||
}
|
||||
|
||||
function isSubpathSpecifier(node: Node): node is StringLiteral {
|
||||
return ts!.isStringLiteral(node) && node.text.startsWith(FROM_PREFIX);
|
||||
}
|
||||
|
||||
function collectImportRewrite(
|
||||
sourceFile: SourceFile,
|
||||
stmt: ImportDeclaration,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
if (!isSubpathSpecifier(stmt.moduleSpecifier)) {
|
||||
return;
|
||||
}
|
||||
const clause = stmt.importClause;
|
||||
// Pure named imports (`import { a, b } from '...'`) can be split by symbol.
|
||||
// A default or namespace import grabs the whole module, so it can't be
|
||||
// split — route it wholesale to the internal entry.
|
||||
if (
|
||||
clause &&
|
||||
!clause.name &&
|
||||
clause.namedBindings &&
|
||||
ts!.isNamedImports(clause.namedBindings)
|
||||
) {
|
||||
rewriteNamedDeclaration(
|
||||
sourceFile,
|
||||
stmt,
|
||||
stmt.moduleSpecifier,
|
||||
clause.isTypeOnly,
|
||||
clause.namedBindings.elements,
|
||||
'import',
|
||||
changes
|
||||
);
|
||||
return;
|
||||
}
|
||||
replaceSpecifier(sourceFile, stmt.moduleSpecifier, TO_INTERNAL, changes);
|
||||
}
|
||||
|
||||
function collectExportRewrite(
|
||||
sourceFile: SourceFile,
|
||||
stmt: ExportDeclaration,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
if (!stmt.moduleSpecifier || !isSubpathSpecifier(stmt.moduleSpecifier)) {
|
||||
return;
|
||||
}
|
||||
// `export { a, b } from '...'` can be split; `export * from '...'` cannot.
|
||||
if (stmt.exportClause && ts!.isNamedExports(stmt.exportClause)) {
|
||||
rewriteNamedDeclaration(
|
||||
sourceFile,
|
||||
stmt,
|
||||
stmt.moduleSpecifier,
|
||||
stmt.isTypeOnly,
|
||||
stmt.exportClause.elements,
|
||||
'export',
|
||||
changes
|
||||
);
|
||||
return;
|
||||
}
|
||||
replaceSpecifier(sourceFile, stmt.moduleSpecifier, TO_INTERNAL, changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition the named bindings of an import/export declaration into the ones
|
||||
* that resolve to `@nx/web`'s public entry and the ones that don't. If both
|
||||
* groups are non-empty, the single declaration is split into two.
|
||||
*/
|
||||
function rewriteNamedDeclaration(
|
||||
sourceFile: SourceFile,
|
||||
decl: ImportDeclaration | ExportDeclaration,
|
||||
specifier: StringLiteral,
|
||||
isTypeOnly: boolean,
|
||||
elements: readonly (ImportSpecifier | ExportSpecifier)[],
|
||||
keyword: 'import' | 'export',
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const publicEls: (ImportSpecifier | ExportSpecifier)[] = [];
|
||||
const internalEls: (ImportSpecifier | ExportSpecifier)[] = [];
|
||||
for (const el of elements) {
|
||||
// `propertyName` is the original name in `orig as alias`; fall back to
|
||||
// `name` for the plain `orig` form.
|
||||
const importedName = (el.propertyName ?? el.name).text;
|
||||
(PUBLIC_SYMBOLS.has(importedName) ? publicEls : internalEls).push(el);
|
||||
}
|
||||
|
||||
if (publicEls.length === 0) {
|
||||
replaceSpecifier(sourceFile, specifier, TO_INTERNAL, changes);
|
||||
return;
|
||||
}
|
||||
if (internalEls.length === 0) {
|
||||
replaceSpecifier(sourceFile, specifier, TO_PUBLIC, changes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mixed — replace the whole declaration with one statement per target.
|
||||
const quote = sourceFile.text.charAt(specifier.getStart(sourceFile));
|
||||
const start = decl.getStart(sourceFile);
|
||||
const end = decl.getEnd();
|
||||
const semicolon = sourceFile.text.charAt(end - 1) === ';' ? ';' : '';
|
||||
const prefix = isTypeOnly ? `${keyword} type` : keyword;
|
||||
const render = (
|
||||
els: (ImportSpecifier | ExportSpecifier)[],
|
||||
target: string
|
||||
): string =>
|
||||
`${prefix} { ${els
|
||||
.map((el) => el.getText(sourceFile))
|
||||
.join(', ')} } from ${quote}${target}${quote}${semicolon}`;
|
||||
|
||||
changes.push(
|
||||
{ type: ChangeType.Delete, start, length: end - start },
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: start,
|
||||
text: `${render(publicEls, TO_PUBLIC)}\n${render(
|
||||
internalEls,
|
||||
TO_INTERNAL
|
||||
)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function collectCallExpressionRewrites(
|
||||
sourceFile: SourceFile,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const visit = (node: Node): void => {
|
||||
if (
|
||||
ts!.isCallExpression(node) &&
|
||||
shouldRewriteCallExpression(node) &&
|
||||
node.arguments.length >= 1 &&
|
||||
isSubpathSpecifier(node.arguments[0])
|
||||
) {
|
||||
// `require(...)`, dynamic `import(...)` and `jest.mock(...)` reference
|
||||
// the module as a whole and can't be symbol-split, so they go to the
|
||||
// internal entry.
|
||||
replaceSpecifier(
|
||||
sourceFile,
|
||||
node.arguments[0] as StringLiteral,
|
||||
TO_INTERNAL,
|
||||
changes
|
||||
);
|
||||
} else if (ts!.isImportTypeNode(node)) {
|
||||
// `typeof import('...')` parses as an `ImportTypeNode`, not a
|
||||
// CallExpression — its argument is `LiteralTypeNode<StringLiteral>`.
|
||||
// The whole module is referenced, so it can't be symbol-split.
|
||||
const literal = getImportTypeStringLiteral(node);
|
||||
if (literal && literal.text.startsWith(FROM_PREFIX)) {
|
||||
replaceSpecifier(sourceFile, literal, TO_INTERNAL, changes);
|
||||
}
|
||||
}
|
||||
ts!.forEachChild(node, visit);
|
||||
};
|
||||
visit(sourceFile);
|
||||
}
|
||||
|
||||
function getImportTypeStringLiteral(
|
||||
node: ImportTypeNode
|
||||
): StringLiteral | undefined {
|
||||
const arg = node.argument;
|
||||
if (arg && ts!.isLiteralTypeNode(arg) && ts!.isStringLiteral(arg.literal)) {
|
||||
return arg.literal;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldRewriteCallExpression(call: CallExpression): boolean {
|
||||
const callee = call.expression;
|
||||
// `require('...')`
|
||||
if (ts!.isIdentifier(callee) && callee.text === 'require') return true;
|
||||
// dynamic `import('...')` (runtime form parses as a CallExpression whose
|
||||
// callee is the `import` keyword). The `typeof import('...')` type-position
|
||||
// form is an `ImportTypeNode` (handled in `collectCallExpressionRewrites`).
|
||||
if (callee.kind === ts!.SyntaxKind.ImportKeyword) return true;
|
||||
// `jest.mock(...)` / `vi.mock(...)` and friends.
|
||||
if (ts!.isPropertyAccessExpression(callee)) {
|
||||
const obj = callee.expression;
|
||||
if (
|
||||
ts!.isIdentifier(obj) &&
|
||||
(obj.text === 'jest' || obj.text === 'vi') &&
|
||||
MOCK_HELPER_METHODS.has(callee.name.text)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function replaceSpecifier(
|
||||
sourceFile: SourceFile,
|
||||
literal: StringLiteral,
|
||||
target: string,
|
||||
changes: StringChange[]
|
||||
): void {
|
||||
const start = literal.getStart(sourceFile);
|
||||
const end = literal.getEnd();
|
||||
const quote = sourceFile.text.charAt(start);
|
||||
changes.push(
|
||||
{ type: ChangeType.Delete, start, length: end - start },
|
||||
{
|
||||
type: ChangeType.Insert,
|
||||
index: start,
|
||||
text: `${quote}${target}${quote}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { WebpackNxBuildCoordinationPlugin } from '@nx/webpack/internal';
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
findUp,
|
||||
findAllNodeModules,
|
||||
deleteOutputDir,
|
||||
} from '@nx/webpack/internal';
|
||||
@@ -0,0 +1,10 @@
|
||||
import { readNxJson, Tree } from '@nx/devkit';
|
||||
|
||||
export function hasVitePlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
return !!nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string'
|
||||
? p === '@nx/vite/plugin'
|
||||
: p.plugin === '@nx/vite/plugin'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { readNxJson, Tree } from '@nx/devkit';
|
||||
|
||||
export function hasWebpackPlugin(tree: Tree) {
|
||||
const nxJson = readNxJson(tree);
|
||||
return !!nxJson.plugins?.some((p) =>
|
||||
typeof p === 'string'
|
||||
? p === '@nx/webpack/plugin'
|
||||
: p.plugin === '@nx/webpack/plugin'
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { normalizeOptions, normalizePluginPath } from '@nx/webpack';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { join } from 'path';
|
||||
export const nxVersion = require(join('@nx/web', 'package.json')).version;
|
||||
|
||||
export const swcLoaderVersion = '0.1.15';
|
||||
export const typesNodeVersion = '^22.0.0';
|
||||
export const tsLibVersion = '^2.3.0';
|
||||
@@ -0,0 +1,50 @@
|
||||
import { logger } from '@nx/devkit';
|
||||
import * as net from 'net';
|
||||
|
||||
export function waitForPortOpen(
|
||||
port: number,
|
||||
options: { host?: string; retries?: number; retryDelay?: number } = {}
|
||||
): Promise<void> {
|
||||
const host = options.host ?? '127.0.0.1';
|
||||
const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT'];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkPort = (retries = options.retries ?? 120) => {
|
||||
const client = new net.Socket();
|
||||
const cleanupClient = () => {
|
||||
client.removeAllListeners('connect');
|
||||
client.removeAllListeners('error');
|
||||
client.end();
|
||||
client.destroy();
|
||||
client.unref();
|
||||
};
|
||||
client.once('connect', () => {
|
||||
cleanupClient();
|
||||
resolve();
|
||||
});
|
||||
|
||||
client.once('error', (err) => {
|
||||
if (retries === 0 || !allowedErrorCodes.includes(err['code'])) {
|
||||
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
|
||||
logger.info(
|
||||
`Error connecting on ${host}:${port}: ${err['code'] || err}`
|
||||
);
|
||||
}
|
||||
cleanupClient();
|
||||
reject(err);
|
||||
} else {
|
||||
setTimeout(() => checkPort(retries - 1), options.retryDelay ?? 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Node will use IPv6 if it is available, but this can cause issues if the server is only listening on IPv4.
|
||||
// Hard-coding to look on 127.0.0.1 to avoid using the IPv6 loopback address "::1".
|
||||
if (process.env['NX_VERBOSE_LOGGING'] === 'true') {
|
||||
logger.info(`Connecting on ${host}:${port}`);
|
||||
}
|
||||
client.connect({ port, host });
|
||||
};
|
||||
|
||||
checkPort();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user