UNPKG

@nx/storybook

Version:

The Nx Plugin for Storybook contains executors and generators for allowing your workspace to use the powerful Storybook integration testing & documenting capabilities.

269 lines (268 loc) • 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodes = exports.createNodesV2 = exports.createDependencies = void 0; const devkit_1 = require("@nx/devkit"); const path_1 = require("path"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const fs_1 = require("fs"); const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const js_1 = require("@nx/js"); const config_utils_1 = require("@nx/devkit/src/utils/config-utils"); const file_hasher_1 = require("nx/src/hasher/file-hasher"); const tsquery_1 = require("@phenomnomnominal/tsquery"); function readTargetsCache(cachePath) { return (0, fs_1.existsSync)(cachePath) ? (0, devkit_1.readJsonFile)(cachePath) : {}; } function writeTargetsToCache(cachePath, results) { (0, devkit_1.writeJsonFile)(cachePath, results); } /** * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'. */ const createDependencies = () => { return []; }; exports.createDependencies = createDependencies; const storybookConfigGlob = '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}'; exports.createNodesV2 = [ storybookConfigGlob, async (configFilePaths, options, context) => { const normalizedOptions = normalizeOptions(options); const optionsHash = (0, file_hasher_1.hashObject)(normalizedOptions); const cachePath = (0, path_1.join)(cache_directory_1.workspaceDataDirectory, `storybook-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); try { return await (0, devkit_1.createNodesFromFiles)((configFile, _, context) => createNodesInternal(configFile, normalizedOptions, context, targetsCache), configFilePaths, normalizedOptions, context); } finally { writeTargetsToCache(cachePath, targetsCache); } }, ]; exports.createNodes = [ storybookConfigGlob, (configFilePath, options, context) => { devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'); return createNodesInternal(configFilePath, normalizeOptions(options), context, {}); }, ]; async function createNodesInternal(configFilePath, options, context, targetsCache) { let projectRoot = ''; if (configFilePath.includes('/.storybook')) { projectRoot = (0, path_1.dirname)(configFilePath).replace('/.storybook', ''); } else { projectRoot = (0, path_1.dirname)(configFilePath).replace('.storybook', ''); } if (projectRoot === '') { projectRoot = '.'; } // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(context.workspaceRoot, projectRoot)); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return {}; } const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(projectRoot, options, context, [(0, js_1.getLockFileName)((0, devkit_1.detectPackageManager)(context.workspaceRoot))]); const projectName = buildProjectName(projectRoot, context.workspaceRoot); targetsCache[hash] ??= await buildStorybookTargets(configFilePath, projectRoot, options, context, projectName); const result = { projects: { [projectRoot]: { root: projectRoot, targets: targetsCache[hash], }, }, }; return result; } async function buildStorybookTargets(configFilePath, projectRoot, options, context, projectName) { const buildOutputs = getOutputs(); const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context); // First attempt to do a very fast lookup for the framework // If that fails, the framework might be inherited, so do a very heavyweight lookup const storybookFramework = (await getStorybookFramework(configFilePath, context)) || (await getStorybookFullyResolvedFramework(configFilePath, context)); const frameworkIsAngular = storybookFramework === '@storybook/angular'; if (frameworkIsAngular && !projectName) { throw new Error(`Could not find a name for the project at '${projectRoot}'. Please make sure that the project has a package.json or project.json file with name specified.`); } const targets = {}; targets[options.buildStorybookTargetName] = buildTarget(namedInputs, buildOutputs, projectRoot, frameworkIsAngular, projectName, configFilePath); targets[options.serveStorybookTargetName] = serveTarget(projectRoot, frameworkIsAngular, projectName, configFilePath); if (isStorybookTestRunnerInstalled()) { targets[options.testStorybookTargetName] = testTarget(projectRoot); } targets[options.staticStorybookTargetName] = serveStaticTarget(options, projectRoot); return targets; } function buildTarget(namedInputs, outputs, projectRoot, frameworkIsAngular, projectName, configFilePath) { let targetConfig; if (frameworkIsAngular) { targetConfig = { executor: '@storybook/angular:build-storybook', options: { configDir: `${(0, path_1.dirname)(configFilePath)}`, browserTarget: `${projectName}:build-storybook`, compodoc: false, outputDir: (0, devkit_1.joinPathFragments)(projectRoot, 'storybook-static'), }, cache: true, outputs, inputs: [ ...('production' in namedInputs ? ['production', '^production'] : ['default', '^default']), { externalDependencies: [ 'storybook', '@storybook/angular', isStorybookTestRunnerInstalled() ? '@storybook/test-runner' : undefined, ].filter(Boolean), }, ], }; } else { targetConfig = { command: `storybook build`, options: { cwd: projectRoot }, cache: true, outputs, inputs: [ ...('production' in namedInputs ? ['production', '^production'] : ['default', '^default']), { externalDependencies: [ 'storybook', isStorybookTestRunnerInstalled() ? '@storybook/test-runner' : undefined, ].filter(Boolean), }, ], }; } return targetConfig; } function serveTarget(projectRoot, frameworkIsAngular, projectName, configFilePath) { if (frameworkIsAngular) { return { continuous: true, executor: '@storybook/angular:start-storybook', options: { configDir: `${(0, path_1.dirname)(configFilePath)}`, browserTarget: `${projectName}:build-storybook`, compodoc: false, }, }; } else { return { continuous: true, command: `storybook dev`, options: { cwd: projectRoot }, }; } } function testTarget(projectRoot) { const targetConfig = { command: `test-storybook`, options: { cwd: projectRoot }, inputs: [ { externalDependencies: ['storybook', '@storybook/test-runner'], }, ], }; return targetConfig; } function serveStaticTarget(options, projectRoot) { const targetConfig = { dependsOn: [`${options.buildStorybookTargetName}`], continuous: true, executor: '@nx/web:file-server', options: { buildTarget: `${options.buildStorybookTargetName}`, staticFilePath: (0, devkit_1.joinPathFragments)(projectRoot, 'storybook-static'), }, }; return targetConfig; } async function getStorybookFramework(configFilePath, context) { const resolvedPath = (0, path_1.join)(context.workspaceRoot, configFilePath); const mainTsJs = (0, fs_1.readFileSync)(resolvedPath, 'utf-8'); const importDeclarations = tsquery_1.tsquery.query(mainTsJs, 'ImportDeclaration:has(ImportSpecifier:has([text="StorybookConfig"]))')?.[0]; if (!importDeclarations) { return parseFrameworkName(mainTsJs); } const storybookConfigImportPackage = tsquery_1.tsquery.query(importDeclarations, 'StringLiteral')?.[0]; if (storybookConfigImportPackage?.getText() === `'@storybook/core-common'`) { return parseFrameworkName(mainTsJs); } return storybookConfigImportPackage?.getText(); } function parseFrameworkName(mainTsJs) { const frameworkPropertyAssignment = tsquery_1.tsquery.query(mainTsJs, `PropertyAssignment:has(Identifier:has([text="framework"]))`)?.[0]; if (!frameworkPropertyAssignment) { return undefined; } const propertyAssignments = tsquery_1.tsquery.query(frameworkPropertyAssignment, `PropertyAssignment:has(Identifier:has([text="name"]))`); const namePropertyAssignment = propertyAssignments?.find((expression) => { return expression.getText().startsWith('name'); }); if (!namePropertyAssignment) { const storybookConfigImportPackage = tsquery_1.tsquery.query(frameworkPropertyAssignment, 'StringLiteral')?.[0]; return storybookConfigImportPackage?.getText(); } return tsquery_1.tsquery.query(namePropertyAssignment, `StringLiteral`)?.[0]?.getText(); } async function getStorybookFullyResolvedFramework(configFilePath, context) { const resolvedPath = (0, path_1.join)(context.workspaceRoot, configFilePath); const { framework } = await (0, config_utils_1.loadConfigFile)(resolvedPath); return typeof framework === 'string' ? framework : framework.name; } function getOutputs() { const outputs = [ `{projectRoot}/storybook-static`, `{options.output-dir}`, `{options.outputDir}`, `{options.o}`, ]; return outputs; } function normalizeOptions(options) { return { buildStorybookTargetName: options.buildStorybookTargetName ?? 'build-storybook', serveStorybookTargetName: options.serveStorybookTargetName ?? 'storybook', testStorybookTargetName: options.testStorybookTargetName ?? 'test-storybook', staticStorybookTargetName: options.staticStorybookTargetName ?? 'static-storybook', }; } function buildProjectName(projectRoot, workspaceRoot) { const packageJsonPath = (0, path_1.join)(workspaceRoot, projectRoot, 'package.json'); const projectJsonPath = (0, path_1.join)(workspaceRoot, projectRoot, 'project.json'); let name; if ((0, fs_1.existsSync)(projectJsonPath)) { const projectJson = (0, devkit_1.parseJson)((0, fs_1.readFileSync)(projectJsonPath, 'utf-8')); name = projectJson.name; } else if ((0, fs_1.existsSync)(packageJsonPath)) { const packageJson = (0, devkit_1.parseJson)((0, fs_1.readFileSync)(packageJsonPath, 'utf-8')); name = packageJson.name; } return name; } function isStorybookTestRunnerInstalled() { try { require.resolve('@storybook/test-runner'); return true; } catch (e) { return false; } }