Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/@lwc/compiler/src/transformers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import type { BabelFileResult } from '@babel/core';
import type { CompilerDiagnostic } from '@lwc/errors';

/** The object returned after transforming code. */
export interface TransformResult {
/** The compiled source code. */
code: string;
/** The generated source map. */
map: unknown;
map: BabelFileResult['map'];
/** Any diagnostic warnings that may have occurred. */
warnings?: CompilerDiagnostic[];
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/@lwc/compiler/src/transformers/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import * as styleCompiler from '@lwc/style-compiler';
import { normalizeToCompilerError, TransformerErrors, CompilerAggregateError } from '@lwc/errors';

import type { BabelFileResult } from '@babel/core';
import type { NormalizedTransformOptions } from '../options';
import type { TransformResult } from './shared';

Expand Down Expand Up @@ -63,6 +64,6 @@ export default function styleTransform(
// the styles doesn't make sense, the transform returns an empty mappings.
return {
code: res.code,
map: { mappings: '' },
map: { mappings: '' } as BabelFileResult['map'],
};
}
3 changes: 2 additions & 1 deletion packages/@lwc/compiler/src/transformers/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '@lwc/errors';
import { compile } from '@lwc/template-compiler';

import type { BabelFileResult } from '@babel/core';
import type { NormalizedTransformOptions } from '../options';
import type { TransformResult } from './shared';

Expand Down Expand Up @@ -93,7 +94,7 @@ export default function templateTransform(
// the template doesn't make sense, the transform returns an empty mappings.
return {
code: result.code,
map: { mappings: '' },
map: { mappings: '' } as BabelFileResult['map'],
warnings,
cssScopeTokens: result.cssScopeTokens,
};
Expand Down
131 changes: 80 additions & 51 deletions packages/@lwc/rollup-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import pluginUtils from '@rollup/pluginutils';
import { transformSync } from '@lwc/compiler';
import { resolveModule, RegistryType } from '@lwc/module-resolver';
import { getAPIVersionFromNumber } from '@lwc/shared';
import type { Plugin, SourceMapInput, RollupLog } from 'rollup';
import type { Plugin, RollupLog, TransformResult } from 'rollup';
import type { FilterPattern } from '@rollup/pluginutils';
import type { StylesheetConfig, DynamicImportConfig } from '@lwc/compiler';
import type { ModuleRecord } from '@lwc/module-resolver';
Expand Down Expand Up @@ -79,10 +79,13 @@ export interface RollupLwcOptions {

const PLUGIN_NAME = 'rollup-plugin-lwc-compiler';

const IMPLICIT_DEFAULT_HTML_PATH = ['@lwc', 'resources', 'empty_html.js'].join(path.sep);
const IMPLICIT_DEFAULT_HTML_PATH = path.join('@lwc', 'resources', 'empty_html.js');
const EMPTY_IMPLICIT_HTML_CONTENT = 'export default void 0';
const IMPLICIT_DEFAULT_CSS_PATH = ['@lwc', 'resources', 'empty_css.css'].join(path.sep);
const IMPLICIT_DEFAULT_CSS_PATH = path.join('@lwc', 'resources', 'empty_css.css');
const EMPTY_IMPLICIT_CSS_CONTENT = '';
const IMPLICIT_DEFAULT_JS_PATH = path.join('@lwc', 'resources', 'empty_js.js');
const EMPTY_IMPLICIT_JS_CONTENT =
'import {LightningElement} from "lwc";export default class extends LightningElement{}';
const SCRIPT_FILE_EXTENSIONS = ['.js', '.mjs', '.jsx', '.ts', '.mts', '.tsx'];

const DEFAULT_MODULES = [
Expand Down Expand Up @@ -110,27 +113,32 @@ function isImplicitCssImport(importee: string, importer: string): boolean {
}

interface Descriptor {
/** The filename for the component. May be a virtual `@lwc/resources` path. */
filename: string;
/** Whether the component uses scoped CSS. */
scoped: boolean;
/** The component's specifier, e.g. `x/cmp`. */
specifier: string | null;
/** For a template-only component, the real path to the template. */
templateOnlyEntry: string | null;
}

function parseDescriptorFromFilePath(id: string): Descriptor {
const [filename, query] = id.split('?', 2);
const params = new URLSearchParams(query);
const scoped = params.has('scoped');
const specifier = params.get('specifier');
return {
filename,
specifier,
scoped,
specifier: params.get('specifier'),
scoped: params.has('scoped'),
templateOnlyEntry: params.get('template'),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this null if there is a js or ts file for the component?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, it's only set for template-only HTML files

};
}

function appendAliasSpecifierQueryParam(id: string, specifier: string): string {
function appendQueryParams(id: string, specifier: string, template?: string): string {
const [filename, query] = id.split('?', 2);
const params = new URLSearchParams(query);
params.set('specifier', specifier);
if (template) params.set('template', template);
return `${filename}?${params.toString()}`;
}

Expand Down Expand Up @@ -232,7 +240,11 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
resolveId(importee, importer) {
if (importer) {
// Importer has been resolved already and may contain an alias specifier
const { filename: importerFilename } = parseDescriptorFromFilePath(importer);
const importerDescriptor = parseDescriptorFromFilePath(importer);
const importerFilename =
// If `templateOnlyEntry` is set, then it points to the real filename and
// `filename` is just the virtual JS file
importerDescriptor.templateOnlyEntry ?? importerDescriptor.filename;

// Normalize relative import to absolute import
// Note that in @rollup/plugin-node-resolve v13, relative imports will sometimes
Expand Down Expand Up @@ -285,6 +297,12 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
rootDir,
});

if (entry.endsWith('.html')) {
// If the entrypoint is `.html`, then it's a template-only component.
// We need to inject a virtual JS file for the component to function.
return appendQueryParams(IMPLICIT_DEFAULT_JS_PATH, specifier, entry);
}

if (type === RegistryType.alias) {
// specifier must be in in namespace/name format
const [namespace, name, ...rest] = specifier.split('/');
Expand All @@ -293,11 +311,10 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
// Verify 3 things about the alias specifier:
// 1. The namespace is a non-empty string
// 2. The name is an non-empty string
// 3. The specifier was in a namespace / name format, ie no extra '/' (this is what rest checks)
const hasValidSpecifier =
!!namespace?.length && !!name?.length && !rest?.length;
// 3. The specifier was in a `namespace/name` format, i.e. no extra '/' (this is what rest checks)
const hasValidSpecifier = namespace && name && rest.length === 0;
if (hasValidSpecifier) {
return appendAliasSpecifierQueryParam(entry, specifier);
return appendQueryParams(entry, specifier);
}
}

Expand All @@ -314,15 +331,18 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
load(id) {
if (id === IMPLICIT_DEFAULT_HTML_PATH) {
return EMPTY_IMPLICIT_HTML_CONTENT;
}

if (id === IMPLICIT_DEFAULT_CSS_PATH) {
} else if (id === IMPLICIT_DEFAULT_CSS_PATH) {
return EMPTY_IMPLICIT_CSS_CONTENT;
}

// Have to parse the `?scoped=true` in `load`, because it's not guaranteed
// that `resolveId` will always be called (e.g. if another plugin resolves it first)
const { filename, specifier: hasAlias } = parseDescriptorFromFilePath(id);
// Not checking `id` directly because it's expected to have query params
if (filename === IMPLICIT_DEFAULT_JS_PATH) {
return EMPTY_IMPLICIT_JS_CONTENT;
}

const isCSS = path.extname(filename) === '.css';

if (isCSS || hasAlias) {
Expand All @@ -340,19 +360,22 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
}
},

transform(src, id) {
const { scoped, filename, specifier } = parseDescriptorFromFilePath(id);
transform(src, id): TransformResult {
const { scoped, filename, specifier, templateOnlyEntry } =
parseDescriptorFromFilePath(id);
// If `templateOnlyEntry` is set, then `filename` is the virtual JS file
const realFilename = templateOnlyEntry ?? filename;

// Filter user-land config and lwc import
if (!filter(filename)) {
if (!filter(realFilename)) {
return;
}

// Extract module name and namespace from file path.
// Specifier will only exist for modules with alias paths.
// Otherwise, use the file directory structure to resolve namespace and name.
const [namespace, name] =
specifier?.split('/') ?? path.dirname(filename).split(path.sep).slice(-2);
specifier?.split('/') ?? path.dirname(realFilename).split(path.sep).slice(-2);

/* v8 ignore next */
if (!namespace || !name) {
Expand All @@ -364,48 +387,54 @@ export default function lwc(pluginOptions: RollupLwcOptions = {}): Plugin {
name
)}) could not be determined from the specifier ${JSON.stringify(
specifier
)} or filename ${JSON.stringify(path.dirname(filename))}`
)} or filename ${JSON.stringify(path.dirname(realFilename))}`
);
}

const apiVersionToUse = getAPIVersionFromNumber(apiVersion);

const { code, map, warnings } = transformSync(src, filename, {
name,
namespace,
outputConfig: { sourcemap },
stylesheetConfig,
experimentalDynamicComponent,
experimentalDynamicDirective,
enableDynamicComponents,
enableSyntheticElementInternals,
enableLwcOn,
enableLightningWebSecurityTransforms,
// TODO [#3370]: remove experimental template expression flag
experimentalComplexExpressions,
preserveHtmlComments,
scopedStyles: scoped,
disableSyntheticShadowSupport,
apiVersion: apiVersionToUse,
enableStaticContentOptimization:
// {enableStaticContentOptimization:undefined} behaves like `false`
// but {} (prop unspecified) behaves like `true`
'enableStaticContentOptimization' in pluginOptions
? pluginOptions.enableStaticContentOptimization
: true,
targetSSR,
ssrMode,
componentFeatureFlagModulePath,
});
const { code, map, warnings } = transformSync(
src,
// If `templateOnlyEntry` is set, then the extension of `realFilename` is .html,
// but we're transforming the virtual JS file as if it is the component's JS file
// (which doesn't actually exist)
templateOnlyEntry ? `${realFilename.slice(0, -5)}.js` : realFilename,
{
name,
namespace,
outputConfig: { sourcemap },
stylesheetConfig,
experimentalDynamicComponent,
experimentalDynamicDirective,
enableDynamicComponents,
enableSyntheticElementInternals,
enableLwcOn,
enableLightningWebSecurityTransforms,
// TODO [#3370]: remove experimental template expression flag
experimentalComplexExpressions,
preserveHtmlComments,
scopedStyles: scoped,
disableSyntheticShadowSupport,
apiVersion: apiVersionToUse,
enableStaticContentOptimization:
// {enableStaticContentOptimization:undefined} behaves like `false`
// but {} (prop unspecified) behaves like `true`
'enableStaticContentOptimization' in pluginOptions
? pluginOptions.enableStaticContentOptimization
: true,
targetSSR,
ssrMode,
componentFeatureFlagModulePath,
}
);

if (warnings) {
for (const warning of warnings) {
this.warn(transformWarningToRollupLog(warning, src, filename));
this.warn(transformWarningToRollupLog(warning, src, realFilename));
}
}

const rollupMap = map as SourceMapInput;
return { code, map: rollupMap };
return { code, map };
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/ssr-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { ComponentTransformOptions, TemplateTransformOptions } from './shar

export interface CompilationResult {
code: string;
map: unknown;
map: undefined;
}

export function compileComponentForSSR(
Expand Down
3 changes: 0 additions & 3 deletions playground/src/modules/x/app/app.js

This file was deleted.

Loading