diff --git a/packages/atomic/.storybook/main.mts b/packages/atomic/.storybook/main.mts index e07c6ec577e..21349e5fe80 100644 --- a/packages/atomic/.storybook/main.mts +++ b/packages/atomic/.storybook/main.mts @@ -45,6 +45,8 @@ const isCDN = process.env.DEPLOYMENT_ENVIRONMENT === 'CDN'; const config: StorybookConfig = { stories: ['../src/**/*.new.stories.tsx', '../src/**/*.mdx'], staticDirs: [ + {from: '../dist/atomic/assets', to: '/assets'}, + {from: '../dist/atomic/lang', to: '/lang'}, {from: '../dist/atomic', to: './assets'}, {from: '../dist/atomic/lang', to: './lang'}, ], diff --git a/packages/atomic/jest/setup.cjs b/packages/atomic/jest/setup.cjs index ece895386e3..52ab8bb9702 100644 --- a/packages/atomic/jest/setup.cjs +++ b/packages/atomic/jest/setup.cjs @@ -1,4 +1,6 @@ module.exports = async () => { // Mocking the ShadowRoot class to avoid errors in tests globalThis.ShadowRoot = class ShadowRoot {}; + // TODO: get rid of the mock once no longer running stencil tests + jest.mock('../src/utils/resource-url'); }; diff --git a/packages/atomic/scripts/asset-path-transformer.mjs b/packages/atomic/scripts/asset-path-transformer.mjs new file mode 100644 index 00000000000..313d5d66c1f --- /dev/null +++ b/packages/atomic/scripts/asset-path-transformer.mjs @@ -0,0 +1,51 @@ +import { + isPropertyAccessExpression, + isIdentifier, + isMetaProperty, + visitEachChild, + visitNode, + SyntaxKind, +} from 'typescript'; + +/** + * Transforms occurrences of `import.meta.env.RESOURCE_URL` in the source file + * to `import.meta.url`. + */ +function resourceUrlTransformer(context) { + const {factory} = context; + return (sourceFile) => { + function visitor(node) { + // Check for import.meta.env.RESOURCE_URL + if ( + isPropertyAccessExpression(node) && + isIdentifier(node.name) && + node.name.text === 'RESOURCE_URL' + ) { + const envNode = node.expression; + if ( + isPropertyAccessExpression(envNode) && + isIdentifier(envNode.name) && + envNode.name.text === 'env' + ) { + const metaNode = envNode.expression; + if ( + isMetaProperty(metaNode) && + metaNode.keywordToken === SyntaxKind.ImportKeyword + ) { + return factory.createPropertyAccessExpression( + factory.createMetaProperty( + SyntaxKind.ImportKeyword, + factory.createIdentifier('meta') + ), + 'url' + ); + } + } + } + return visitEachChild(node, visitor, context); + } + return visitNode(sourceFile, visitor); + }; +} + +export default resourceUrlTransformer; diff --git a/packages/atomic/scripts/build.mjs b/packages/atomic/scripts/build.mjs index 8d3a9208d33..02c59c65118 100644 --- a/packages/atomic/scripts/build.mjs +++ b/packages/atomic/scripts/build.mjs @@ -1,4 +1,5 @@ -import {dirname, basename} from 'path'; +import chalk from 'chalk'; +import {dirname, basename, relative} from 'path'; import {argv} from 'process'; import { readConfigFile, @@ -8,7 +9,9 @@ import { getPreEmitDiagnostics, createProgram, flattenDiagnosticMessageText, + DiagnosticCategory, } from 'typescript'; +import resourceUrlTransformer from './asset-path-transformer.mjs'; import pathTransformer from './path-transform.mjs'; import svgTransformer from './svg-transform.mjs'; @@ -18,6 +21,12 @@ if (configArg === undefined) { throw new Error('Missing --config=[PATH] argument'); } const tsConfigPath = configArg.split('=')[1]; +const isCDN = process.env.DEPLOYMENT_ENVIRONMENT === 'CDN'; +const transformers = [ + svgTransformer, + pathTransformer, + ...(isCDN ? [resourceUrlTransformer] : []), +]; function loadTsConfig(configPath) { const configFile = readConfigFile(configPath, sys.readFile); @@ -39,7 +48,7 @@ function emit(program) { const writeFile = undefined; const emitOnlyDtsFiles = false; const customTransformers = { - before: [svgTransformer, pathTransformer], + before: transformers, afterDeclarations: [pathTransformer], }; @@ -62,7 +71,10 @@ function emit(program) { * Info: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#a-minimal-compiler */ function compileWithTransformer() { - console.log('Using tsconfig:', basename(tsConfigPath)); + console.log( + chalk.blue('Using tsconfig:'), + chalk.green(basename(tsConfigPath)) + ); const {options, fileNames} = loadTsConfig(tsConfigPath); const program = createProgram(fileNames, options); const emitResult = emit(program); @@ -71,6 +83,8 @@ function compileWithTransformer() { emitResult.diagnostics ); + let hasError = false; + allDiagnostics.forEach((diagnostic) => { if (diagnostic.file) { const {line, character} = getLineAndCharacterOfPosition( @@ -83,14 +97,20 @@ function compileWithTransformer() { ); console.log( - `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}` + `${chalk.cyan(relative(process.cwd(), diagnostic.file.fileName))}:${chalk.yellow(line + 1)}:${chalk.yellow(character + 1)} - ${chalk.red('error')} ${chalk.gray(message)}` ); } else { - console.error(flattenDiagnosticMessageText(diagnostic.messageText, '\n')); + console.error( + chalk.red(flattenDiagnosticMessageText(diagnostic.messageText, '\n')) + ); + } + + if (diagnostic.category === DiagnosticCategory.Error) { + hasError = true; } }); - let exitCode = emitResult.emitSkipped ? 1 : 0; + let exitCode = emitResult.emitSkipped || hasError ? 1 : 0; console.log(`Process exiting with code '${exitCode}'.`); process.exit(exitCode); } diff --git a/packages/atomic/src/types/node.d.ts b/packages/atomic/src/types/node.d.ts new file mode 100644 index 00000000000..fc95af0c1ad --- /dev/null +++ b/packages/atomic/src/types/node.d.ts @@ -0,0 +1,7 @@ +interface ImportMetaEnv { + readonly RESOURCE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/packages/atomic/src/utils/__mocks__/resource-url.ts b/packages/atomic/src/utils/__mocks__/resource-url.ts new file mode 100644 index 00000000000..f57eb643649 --- /dev/null +++ b/packages/atomic/src/utils/__mocks__/resource-url.ts @@ -0,0 +1 @@ +export const getResourceUrl = () => undefined; diff --git a/packages/atomic/src/utils/resource-url.ts b/packages/atomic/src/utils/resource-url.ts new file mode 100644 index 00000000000..9cb1034834c --- /dev/null +++ b/packages/atomic/src/utils/resource-url.ts @@ -0,0 +1,5 @@ +// TODO: get rid of the mock once no longer running stencil tests + +// import.meta is an ESM only feature and cannot be used in CJS. +// To avoid errors such as "SyntaxError: Cannot use 'import.meta' outside a module error" when running stencil tests, we need to mock this function in the tests. +export const getResourceUrl = () => import.meta.env?.RESOURCE_URL; diff --git a/packages/atomic/src/utils/utils.ts b/packages/atomic/src/utils/utils.ts index 2edc2512424..4213ac8220f 100644 --- a/packages/atomic/src/utils/utils.ts +++ b/packages/atomic/src/utils/utils.ts @@ -1,5 +1,5 @@ -import {getAssetPath} from '@stencil/core'; import DOMPurify from 'dompurify'; +import {getResourceUrl} from './resource-url'; /** * Returns a function that can be executed only once @@ -83,6 +83,19 @@ export function containsVisualElement(node: Node) { return false; } +export function getAssetPath(path: string): string { + const resourceUrl = getResourceUrl(); + const baseUrl = + resourceUrl !== undefined + ? new URL('./', resourceUrl).href + : new URL('./', window.document.baseURI).href; + const assetUrl = new URL(path, baseUrl); + + return assetUrl.origin !== window.location.origin + ? assetUrl.href + : assetUrl.pathname; +} + export function parseAssetURL(url: string, assetPath = './assets') { const [, protocol, remainder] = url.match(/^([a-z]+):\/\/(.*?)(\.svg)?$/) || []; diff --git a/packages/atomic/tsconfig.lit.json b/packages/atomic/tsconfig.lit.json index e29793360e4..ebbe8c71b50 100644 --- a/packages/atomic/tsconfig.lit.json +++ b/packages/atomic/tsconfig.lit.json @@ -12,7 +12,8 @@ "outDir": "dist/atomic/components", "paths": { "@/*": ["./*"] - } + }, + "typeRoots": ["node_modules/@types", "src/types"] }, "files": [ diff --git a/packages/atomic/vitest.config.ts b/packages/atomic/vitest.config.ts index 4b66ce4a8a2..03d41a8acf7 100644 --- a/packages/atomic/vitest.config.ts +++ b/packages/atomic/vitest.config.ts @@ -1,7 +1,16 @@ import path from 'node:path'; import {defineConfig} from 'vitest/config'; +const port = 63315; +const resourceUrl = `http://localhost:${port}/`; + export default defineConfig({ + define: { + 'import.meta.env.RESOURCE_URL': `"${resourceUrl}"`, + }, + server: { + port: port, + }, resolve: { alias: { '@': path.resolve(import.meta.dirname, './'),