Feat: optimize runtime size (#6848)

* feat: optimize runtime size

* fix: log error instead of throw

* chore: fix test case

* chore: clean up useless params
This commit is contained in:
ClarkXia 2024-04-02 16:25:08 +08:00 committed by GitHub
parent ee1496261e
commit 44ef63fcf1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 90 additions and 476 deletions

View File

@ -39,7 +39,7 @@ async function buildCustomOutputs(
bundleOptions: Pick<BundlerOptions, 'userConfig' | 'appConfig' | 'routeManifest'>,
) {
const { userConfig, appConfig, routeManifest } = bundleOptions;
const { ssg, output: { distType, prependCode } } = userConfig;
const { ssg } = userConfig;
const routeType = appConfig?.router?.type;
const {
outputPaths = [],
@ -51,8 +51,6 @@ async function buildCustomOutputs(
documentOnly: !ssg,
renderMode: ssg ? 'SSG' : undefined,
routeType: appConfig?.router?.type,
distType,
prependCode,
routeManifest,
});
if (routeType === 'memory' && userConfig?.routes?.injectInitialEntry) {

View File

@ -70,6 +70,15 @@ export const RUNTIME_EXPORTS = [
'usePageLifecycle',
'unstable_useDocumentData',
'dynamic',
// Document API
'Meta',
'Title',
'Links',
'Scripts',
'FirstChunkCache',
'Data',
'Main',
'usePageAssets',
],
alias: {
usePublicAppContext: 'useAppContext',

View File

@ -7,15 +7,12 @@ import { Context } from 'build-scripts';
import type { CommandArgs, CommandName } from 'build-scripts';
import type { Config } from '@ice/shared-config/types';
import type { AppConfig } from '@ice/runtime/types';
import fse from 'fs-extra';
import webpack from '@ice/bundles/compiled/webpack/index.js';
import type {
DeclarationData,
PluginData,
ExtendsPluginAPI,
TargetDeclarationData,
} from './types/index.js';
import { DeclarationType } from './types/index.js';
import Generator from './service/runtimeGenerator.js';
import { createServerCompiler } from './service/serverCompiler.js';
import createWatch from './service/watchSource.js';
@ -41,6 +38,7 @@ import addPolyfills from './utils/runtimePolyfill.js';
import webpackBundler from './bundler/webpack/index.js';
import rspackBundler from './bundler/rspack/index.js';
import getDefaultTaskConfig from './plugins/task.js';
import hasDocument from './utils/hasDocument.js';
const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -75,38 +73,23 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
let entryCode = 'render();';
const generatorAPI = {
addExport: (declarationData: Omit<DeclarationData, 'declarationType'>) => {
generator.addDeclaration('framework', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
addExport: (declarationData: DeclarationData) => {
generator.addDeclaration('framework', declarationData);
},
addTargetExport: (declarationData: Omit<TargetDeclarationData, 'declarationType'>) => {
generator.addDeclaration('framework', {
...declarationData,
declarationType: DeclarationType.TARGET,
});
addTargetExport: () => {
logger.error('`addTargetExport` is deprecated, please use `addExport` instead.');
},
addExportTypes: (declarationData: Omit<DeclarationData, 'declarationType'>) => {
generator.addDeclaration('frameworkTypes', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
addExportTypes: (declarationData: DeclarationData) => {
generator.addDeclaration('frameworkTypes', declarationData);
},
addRuntimeOptions: (declarationData: Omit<DeclarationData, 'declarationType'>) => {
generator.addDeclaration('runtimeOptions', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
addRuntimeOptions: (declarationData: DeclarationData) => {
generator.addDeclaration('runtimeOptions', declarationData);
},
removeRuntimeOptions: (removeSource: string | string[]) => {
generator.removeDeclaration('runtimeOptions', removeSource);
},
addRouteTypes: (declarationData: Omit<DeclarationData, 'declarationType'>) => {
generator.addDeclaration('routeConfigTypes', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
addRouteTypes: (declarationData: DeclarationData) => {
generator.addDeclaration('routeConfigTypes', declarationData);
},
addRenderFile: generator.addRenderFile,
addRenderTemplate: generator.addTemplateFiles,
@ -114,17 +97,11 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
entryCode = callback(entryCode);
},
addEntryImportAhead: (declarationData: Pick<DeclarationData, 'source'>) => {
generator.addDeclaration('entry', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
generator.addDeclaration('entry', declarationData);
},
modifyRenderData: generator.modifyRenderData,
addDataLoaderImport: (declarationData: DeclarationData) => {
generator.addDeclaration('dataLoaderImport', {
...declarationData,
declarationType: DeclarationType.NORMAL,
});
generator.addDeclaration('dataLoaderImport', declarationData);
},
getExportList: (registerKey: string) => {
return generator.getExportList(registerKey);
@ -239,7 +216,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
// get userConfig after setup because of userConfig maybe modified by plugins
const { userConfig } = ctx;
const { routes: routesConfig, server, syntaxFeatures, polyfill, output: { distType } } = userConfig;
const { routes: routesConfig, server, syntaxFeatures, polyfill } = userConfig;
const coreEnvKeys = getCoreEnvKeys();
@ -286,8 +263,7 @@ async function createService({ rootDir, command, commandArgs }: CreateServiceOpt
// Enable react-router for web as default.
enableRoutes: true,
entryCode,
jsOutput: distType.includes('javascript'),
hasDocument: fse.existsSync(path.join(rootDir, 'src/document.tsx')) || fse.existsSync(path.join(rootDir, 'src/document.jsx')) || fse.existsSync(path.join(rootDir, 'src/document.js')),
hasDocument: hasDocument(rootDir),
dataLoader: userConfig.dataLoader,
routeImports,
routeDefinition,

View File

@ -6,33 +6,8 @@ import { logger } from '../../utils/logger.js';
const plugin: Plugin = () => ({
name: 'plugin-web',
setup: ({ registerTask, onHook, context, generator }) => {
setup: ({ registerTask, onHook, context }) => {
const { commandArgs, command, userConfig } = context;
generator.addTargetExport({
specifier: [
'Meta',
'Title',
'Links',
'Scripts',
'FirstChunkCache',
'Data',
'Main',
'usePageAssets',
],
types: [
'MetaType',
'TitleType',
'LinksType',
'ScriptsType',
'FirstChunkCacheType',
'DataType',
'MainType',
],
source: '@ice/runtime',
target: 'web',
});
const removeExportExprs = ['serverDataLoader', 'staticDataLoader'];
// Remove dataLoader exports only when build in production
// and configure to generate data-loader.js.

View File

@ -18,7 +18,6 @@ import type {
RenderTemplate,
RenderData,
DeclarationData,
TargetDeclarationData,
Registration,
TemplateOptions,
} from '../types/generator.js';
@ -37,76 +36,36 @@ interface Options {
templates?: (string | TemplateOptions)[];
}
function isDeclarationData(data: TargetDeclarationData | DeclarationData): data is DeclarationData {
return data.declarationType === 'normal';
}
function isTargetDeclarationData(data: TargetDeclarationData | DeclarationData): data is TargetDeclarationData {
return data.declarationType === 'target';
}
export function generateDeclaration(exportList: Array<TargetDeclarationData | DeclarationData>) {
const targetImportDeclarations: Array<string> = [];
export function generateDeclaration(exportList: DeclarationData[]) {
const importDeclarations: Array<string> = [];
const exportDeclarations: Array<string> = [];
const exportNames: Array<string> = [];
const variables: Map<string, string> = new Map();
let moduleId = 0;
exportList.forEach(data => {
// Deal with target.
if (isTargetDeclarationData(data)) {
const { specifier, source, target, types = [] } = data;
const isDefaultImport = !Array.isArray(specifier);
const specifiers = isDefaultImport ? [specifier] : specifier;
const arrTypes: Array<string> = Array.isArray(types) ? types : [types];
const { specifier, source, alias, type } = data;
const isDefaultImport = !Array.isArray(specifier);
const specifiers = isDefaultImport ? [specifier] : specifier;
const symbol = type ? ';' : ',';
moduleId++;
const moduleName = `${target}Module${moduleId}`;
targetImportDeclarations.push(`if (import.meta.target === '${target}') {
${specifiers.map(item => `${item} = ${moduleName}.${item};`).join('\n ')}
}
`);
if (specifier) {
importDeclarations.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifiers.map(specifierStr => ((alias && alias[specifierStr]) ? `${specifierStr} as ${alias[specifierStr]}` : specifierStr)).join(', ')} }`} from '${source}';`);
importDeclarations.push(`import ${isDefaultImport ? moduleName : `* as ${moduleName}`} from '${source}';`);
if (arrTypes.length) {
importDeclarations.push(`import type { ${arrTypes.join(', ')}} from '${source}';`);
}
specifiers.forEach((specifierStr, index) => {
if (!variables.has(specifierStr)) {
variables.set(specifierStr, arrTypes[index] || 'any');
specifiers.forEach((specifierStr) => {
if (alias && alias[specifierStr]) {
exportDeclarations.push(`${alias[specifierStr]}${symbol}`);
exportNames.push(alias[specifierStr]);
} else {
exportDeclarations.push(`${specifierStr}${symbol}`);
exportNames.push(specifierStr);
}
});
} else if (isDeclarationData(data)) {
const { specifier, source, alias, type } = data;
const isDefaultImport = !Array.isArray(specifier);
const specifiers = isDefaultImport ? [specifier] : specifier;
const symbol = type ? ';' : ',';
if (specifier) {
importDeclarations.push(`import ${type ? 'type ' : ''}${isDefaultImport ? specifier : `{ ${specifiers.map(specifierStr => ((alias && alias[specifierStr]) ? `${specifierStr} as ${alias[specifierStr]}` : specifierStr)).join(', ')} }`} from '${source}';`);
specifiers.forEach((specifierStr) => {
if (alias && alias[specifierStr]) {
exportDeclarations.push(`${alias[specifierStr]}${symbol}`);
exportNames.push(alias[specifierStr]);
} else {
exportDeclarations.push(`${specifierStr}${symbol}`);
exportNames.push(specifierStr);
}
});
} else {
importDeclarations.push(`import '${source}';`);
}
} else {
importDeclarations.push(`import '${source}';`);
}
});
return {
targetImportStr: targetImportDeclarations.join('\n'),
importStr: importDeclarations.join('\n'),
targetExportStr: Array.from(variables.keys()).join(',\n '),
/**
* Add two whitespace character in order to get the formatted code. For example:
* export {
@ -116,39 +75,30 @@ export function generateDeclaration(exportList: Array<TargetDeclarationData | De
*/
exportStr: exportDeclarations.join('\n '),
exportNames,
variablesStr: Array.from(variables.entries()).map(item => `let ${item[0]}: ${item[1]};`).join('\n'),
};
}
export function checkExportData(
currentList: (DeclarationData | TargetDeclarationData)[],
exportData: (DeclarationData | TargetDeclarationData) | (DeclarationData | TargetDeclarationData)[],
currentList: DeclarationData[],
exportData: DeclarationData | DeclarationData[],
apiName: string,
) {
(Array.isArray(exportData) ? exportData : [exportData]).forEach((data) => {
const exportNames = (Array.isArray(data.specifier) ? data.specifier : [data.specifier]).map((specifierStr) => {
if (isDeclarationData(data)) {
return data?.alias?.[specifierStr] || specifierStr;
} else {
return specifierStr;
}
return data?.alias?.[specifierStr] || specifierStr;
});
currentList.forEach((item) => {
if (isTargetDeclarationData(item)) return;
const { specifier, alias, source } = item;
if (isDeclarationData(item)) {
const { specifier, alias, source } = item;
// check exportName and specifier
const currentExportNames = (Array.isArray(specifier) ? specifier : [specifier]).map((specifierStr) => {
return alias?.[specifierStr] || specifierStr || source;
});
// check exportName and specifier
const currentExportNames = (Array.isArray(specifier) ? specifier : [specifier]).map((specifierStr) => {
return alias?.[specifierStr] || specifierStr || source;
});
if (currentExportNames.some((name) => exportNames.includes(name))) {
logger.error('specifier:', specifier, 'alias:', alias);
logger.error('duplicate with', data);
throw new Error(`duplicate export data added by ${apiName}`);
}
if (currentExportNames.some((name) => exportNames.includes(name))) {
logger.error('specifier:', specifier, 'alias:', alias);
logger.error('duplicate with', data);
throw new Error(`duplicate export data added by ${apiName}`);
}
});
});
@ -253,18 +203,12 @@ export default class Generator {
importStr,
exportStr,
exportNames,
targetExportStr,
targetImportStr,
variablesStr,
} = generateDeclaration(exportList);
const [importStrKey, exportStrKey, targetImportStrKey, targetExportStrKey] = dataKeys;
const [importStrKey, exportStrKey] = dataKeys;
return {
[importStrKey]: importStr,
[exportStrKey]: exportStr,
exportNames,
variablesStr,
[targetImportStrKey]: targetImportStr,
[targetExportStrKey]: targetExportStr,
};
};

View File

@ -1,22 +1,8 @@
export enum DeclarationType {
NORMAL = 'normal',
TARGET = 'target',
}
export interface DeclarationData {
specifier?: string | string[];
source: string;
type?: boolean;
alias?: Record<string, string>;
declarationType?: DeclarationType;
}
export interface TargetDeclarationData {
specifier: string | string[];
source: string;
target: string;
types?: string | string[];
declarationType?: DeclarationType;
}
export type RenderData = Record<string, any>;

View File

@ -7,17 +7,16 @@ import type { Config } from '@ice/shared-config/types';
import type { AppConfig, AssetsManifest } from '@ice/runtime/types';
import type ServerCompileTask from '../utils/ServerCompileTask.js';
import type { CreateLogger } from '../utils/logger.js';
import type { DeclarationData, TargetDeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport, Render } from './generator.js';
import type { DeclarationData, AddRenderFile, AddTemplateFiles, ModifyRenderData, AddDataLoaderImport, Render } from './generator.js';
export type { CreateLoggerReturnType } from '../utils/logger.js';
type AddExport = (exportData: DeclarationData) => void;
type AddTargetExport = (exportData: TargetDeclarationData) => void;
type AddEntryCode = (callback: (code: string) => string) => void;
type AddEntryImportAhead = (exportData: Pick<DeclarationData, 'source'>) => void;
type RemoveExport = (removeSource: string | string[]) => void;
type EventName = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
type GetExportList = (key: string, target?: string) => (DeclarationData | TargetDeclarationData)[];
type GetExportList = (key: string, target?: string) => DeclarationData[];
type ServerCompilerBuildOptions = Pick<
esbuild.BuildOptions,
@ -138,7 +137,11 @@ export interface ExtendsPluginAPI {
registerTask: RegisterTask<Config>;
generator: {
addExport: AddExport;
addTargetExport: AddTargetExport;
/**
* @deprecated
* API will be removed in the next major version.
*/
addTargetExport: () => void;
addExportTypes: AddExport;
addRuntimeOptions: AddExport;
removeRuntimeOptions: RemoveExport;

View File

@ -58,7 +58,15 @@ export interface UserConfig {
abortcontroller?: boolean | string;
};
output?: {
/**
* @deprecated
* output. distType is deprecated, it will be removed in the future.
*/
distType: Array<DistType> | DistType;
/**
* @deprecated
* output.prependCode is deprecated, it will be removed in the future.
*/
prependCode?: string;
};
/**

View File

@ -1,7 +1,6 @@
import * as path from 'path';
import fse from 'fs-extra';
import type { ServerContext, RenderMode, AppConfig, DistType } from '@ice/runtime';
import type { UserConfig } from '../types/userConfig.js';
import type { ServerContext, RenderMode, AppConfig } from '@ice/runtime';
import dynamicImport from './dynamicImport.js';
import { logger } from './logger.js';
import type RouteManifest from './routeManifest.js';
@ -13,8 +12,6 @@ interface Options {
documentOnly: boolean;
routeType: AppConfig['router']['type'];
renderMode?: RenderMode;
distType: UserConfig['output']['distType'];
prependCode: string;
routeManifest: RouteManifest;
}
@ -30,13 +27,10 @@ export default async function generateEntry(options: Options): Promise<EntryResu
documentOnly,
renderMode,
routeType,
prependCode = '',
routeManifest,
} = options;
const distType = typeof options.distType === 'string' ? [options.distType] : options.distType;
let serverEntry;
let serverEntry: string;
try {
serverEntry = await dynamicImport(entry);
} catch (error) {
@ -51,9 +45,7 @@ export default async function generateEntry(options: Options): Promise<EntryResu
const routePath = paths[i];
const {
htmlOutput,
jsOutput,
sourceMap,
} = await renderEntry({ routePath, serverEntry, documentOnly, renderMode, distType, prependCode });
} = await renderEntry({ routePath, serverEntry, documentOnly, renderMode });
const generateOptions = { rootDir, routePath, outputDir };
if (htmlOutput) {
const path = await generateFilePath({ ...generateOptions, type: 'html' });
@ -63,24 +55,6 @@ export default async function generateEntry(options: Options): Promise<EntryResu
);
outputPaths.push(path);
}
if (sourceMap) {
const path = await generateFilePath({ ...generateOptions, type: 'js.map' });
await writeFile(
path,
sourceMap,
);
outputPaths.push(path);
}
if (jsOutput) {
const path = await generateFilePath({ ...generateOptions, type: 'js' });
await writeFile(
path,
jsOutput,
);
outputPaths.push(path);
}
}
return {
@ -124,16 +98,12 @@ async function renderEntry(
routePath,
serverEntry,
documentOnly,
distType = ['html'],
prependCode = '',
renderMode,
}: {
routePath: string;
serverEntry: any;
documentOnly: boolean;
distType?: DistType;
renderMode?: RenderMode;
prependCode?: string;
},
) {
const serverContext: ServerContext = {
@ -141,25 +111,16 @@ async function renderEntry(
url: routePath,
} as any,
};
// renderToEntry exported when disType includes javascript .
const render = distType.includes('javascript') ? serverEntry.renderToEntry : serverEntry.renderToHTML;
const {
value,
jsOutput,
sourceMap,
} = await render(serverContext, {
} = await serverEntry.renderToHTML(serverContext, {
renderMode,
documentOnly,
routePath,
serverOnlyBasename: '/',
distType,
prependCode,
});
return {
htmlOutput: value,
jsOutput,
sourceMap,
};
}

View File

@ -0,0 +1,9 @@
import * as path from 'path';
import fg from 'fast-glob';
export default function hasDocument(rootDir: string) {
const document = fg.sync('document.{tsx,ts,jsx.js}', {
cwd: path.join(rootDir, 'src'),
});
return document.length > 0;
}

View File

@ -9,7 +9,7 @@ import * as Document from '@/document';
<% } else { -%>
import * as Document from './document';
<% } -%>
import type { RenderMode, DistType } from '@ice/runtime';
import type { RenderMode } from '@ice/runtime';
import type { RenderToPipeableStreamOptions } from 'react-dom/server';
// @ts-ignore
import assetsManifest from 'virtual:assets-manifest.json';
@ -49,7 +49,6 @@ interface RenderOptions {
serverOnlyBasename?: string;
routePath?: string;
disableFallback?: boolean;
distType?: DistType;
publicPath?: string;
serverData?: any;
streamOptions?: RenderToPipeableStreamOptions;
@ -71,16 +70,6 @@ export async function renderToResponse(requestContext, options: RenderOptions =
return runtime.renderToResponse(requestContext, mergedOptions);
}
<% if (jsOutput) { -%>
export async function renderToEntry(requestContext, options: RenderOptions = {}) {
const { renderMode = 'SSR' } = options;
setRuntimeEnv(renderMode);
const mergedOptions = mergeOptions(options);
return await runtime.renderToEntry(requestContext, mergedOptions);
}
<% } -%>
function mergeOptions(options) {
const { renderMode = 'SSR', basename, publicPath } = options;

View File

@ -3,10 +3,6 @@ import '<%= globalStyle %>'
<% } -%>
import { definePageConfig, defineRunApp } from './type-defines';
<%- framework.imports %>
<%- framework.variablesStr %>
<%- framework.targetImport %>
export {
definePageConfig,
defineRunApp,

View File

@ -3,7 +3,6 @@
*/
import { expect, it, describe } from 'vitest';
import { generateDeclaration, checkExportData, removeDeclarations } from '../src/service/runtimeGenerator';
import { DeclarationType } from '../src/types/generator';
describe('generateDeclaration', () => {
it('basic usage', () => {
@ -11,7 +10,6 @@ describe('generateDeclaration', () => {
source: 'react-router',
specifier: 'Router',
type: false,
declarationType: DeclarationType.NORMAL,
}]);
expect(importStr).toBe('import Router from \'react-router\';');
expect(exportStr).toBe('Router,');
@ -21,7 +19,6 @@ describe('generateDeclaration', () => {
source: 'react-router',
specifier: 'Router',
type: true,
declarationType: DeclarationType.NORMAL,
}]);
expect(importStr).toBe('import type Router from \'react-router\';');
expect(exportStr).toBe('Router;');
@ -30,7 +27,6 @@ describe('generateDeclaration', () => {
const { importStr, exportStr } = generateDeclaration([{
source: 'react-router',
specifier: ['Switch', 'Route'],
declarationType: DeclarationType.NORMAL,
}]);
expect(importStr).toBe('import { Switch, Route } from \'react-router\';');
expect(exportStr).toBe(['Switch,', 'Route,'].join('\n '));
@ -43,7 +39,6 @@ describe('generateDeclaration', () => {
alias: {
Helmet: 'Head',
},
declarationType: DeclarationType.NORMAL,
}]);
expect(importStr).toBe('import { Helmet as Head } from \'react-helmet\';');
expect(exportStr).toBe('Head,');
@ -53,11 +48,9 @@ describe('generateDeclaration', () => {
const defaultExportData = [{
source: 'react-router',
specifier: ['Switch', 'Route'],
declarationType: DeclarationType.NORMAL,
}, {
source: 'react-helmet',
specifier: 'Helmet',
declarationType: DeclarationType.NORMAL,
}];
describe('checkExportData', () => {

1
packages/runtime/document.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from './esm/Document';

View File

@ -21,7 +21,8 @@
"./react": "./esm/react.js",
"./react/jsx-runtime": "./esm/jsx-runtime.js",
"./react/jsx-dev-runtime": "./esm/jsx-dev-runtime.js",
"./data-loader": "./esm/dataLoader.js"
"./data-loader": "./esm/dataLoader.js",
"./document": "./esm/Document.js"
},
"files": [
"esm",
@ -57,13 +58,9 @@
"@ice/shared": "^1.0.2",
"@remix-run/router": "1.14.2",
"abortcontroller-polyfill": "1.7.5",
"ejs": "^3.1.6",
"fs-extra": "^10.0.0",
"history": "^5.3.0",
"htmlparser2": "^8.0.1",
"react-router-dom": "6.21.3",
"semver": "^7.4.0",
"source-map": "^0.7.4"
"semver": "^7.4.0"
},
"peerDependencies": {
"react": "^18.1.0",

View File

@ -1,2 +1,2 @@
export { renderToResponse, renderToHTML, renderToEntry } from './runServerApp.js';
export { renderToResponse, renderToHTML } from './runServerApp.js';
export * from './index.js';

View File

@ -9,7 +9,6 @@ import type {
AppProvider,
RouteWrapper,
RenderMode,
DistType,
Loader,
RouteWrapperConfig,
} from './types.js';
@ -160,7 +159,6 @@ export type {
AppProvider,
RouteWrapper,
RenderMode,
DistType,
Loader,
RunClientAppOptions,
MetaType,

View File

@ -1,96 +0,0 @@
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as htmlparser2 from 'htmlparser2';
import ejs from 'ejs';
import fse from 'fs-extra';
import __createElement from './domRender.js';
import { generateSourceMap } from './sourcemap.js';
let dirname;
if (typeof __dirname === 'string') {
dirname = __dirname;
} else {
dirname = path.dirname(fileURLToPath(import.meta.url));
}
export async function renderHTMLToJS(html, {
prependCode = '',
}) {
let jsOutput = '';
const dom = htmlparser2.parseDocument(html);
const sourceMapInfo = {
sourceMapFileList: [],
extraLine: prependCode.split('\n').length,
extraColumn: 0,
};
let headElement;
let bodyElement;
function findElement(node) {
if (headElement && bodyElement) return;
if (node.name === 'head') {
headElement = node;
} else if (node.name === 'body') {
bodyElement = node;
}
const {
children = [],
} = node;
children.forEach(findElement);
}
findElement(dom);
const extraScript = [];
function parse(node) {
const {
name,
attribs,
data,
children,
} = node;
let resChildren = [];
if (children) {
if (name === 'script' && children[0] && children[0].data) {
extraScript.push(`(function(){${children[0].data}})();`);
// The path of sourcemap file.
if (attribs['data-sourcemap']) {
sourceMapInfo.sourceMapFileList.push(attribs['data-sourcemap']);
}
delete attribs['data-sourcemap'];
} else {
resChildren = node.children.map(parse);
}
}
return {
tagName: name,
attributes: attribs,
children: resChildren,
text: data,
};
}
const head = parse(headElement);
const body = parse(bodyElement);
const templateContent = fse.readFileSync(path.join(dirname, '../templates/js-entry.js.ejs'), 'utf-8');
jsOutput = ejs.render(templateContent, {
createElement: __createElement,
head,
body,
extraScript,
prependCode,
});
// Generate sourcemap for entry js.
const sourceMap = await generateSourceMap(sourceMapInfo);
return {
jsOutput,
sourceMap,
};
}

View File

@ -30,7 +30,6 @@ import getRequestContext from './requestContext.js';
import matchRoutes from './matchRoutes.js';
import getCurrentRoutePath from './utils/getCurrentRoutePath.js';
import ServerRouter from './ServerRouter.js';
import { renderHTMLToJS } from './renderHTMLToJS.js';
import addLeadingSlash from './utils/addLeadingSlash.js';
export interface RenderOptions {
@ -53,8 +52,6 @@ export interface RenderOptions {
[key: string]: PageConfig;
};
runtimeOptions?: Record<string, any>;
distType?: Array<'html' | 'javascript'>;
prependCode?: string;
serverData?: any;
streamOptions?: RenderToPipeableStreamOptions;
}
@ -70,42 +67,6 @@ interface Response {
headers?: Record<string, string>;
}
/**
* Render and send the result with both entry bundle and html.
*/
export async function renderToEntry(
requestContext: ServerContext,
renderOptions: RenderOptions,
) {
const result = await renderToHTML(requestContext, renderOptions);
const { value } = result;
let jsOutput;
let sourceMap;
const {
distType = ['html'],
prependCode = '',
} = renderOptions;
if (value && distType.includes('javascript')) {
const res = await renderHTMLToJS(value, {
prependCode,
});
jsOutput = res.jsOutput;
sourceMap = res.sourceMap;
}
let htmlOutput;
if (distType.includes('html')) {
htmlOutput = result;
}
return {
...htmlOutput,
jsOutput,
sourceMap,
};
}
/**
* Render and return the result as html string.
*/

View File

@ -1,62 +0,0 @@
import fse from 'fs-extra';
import { SourceMapConsumer, SourceMapGenerator } from 'source-map';
// Starting with extra script, it's a fixed line.
const BASE_LINE = 28;
// Starting with end of '(function(){', it's a fixed column.
const BASE_COLUMN = 12;
export async function generateSourceMap({
sourceMapFileList = [],
extraLine = 0,
extraColumn = 0,
}) {
if (!sourceMapFileList.length) {
return '';
}
const generator = new SourceMapGenerator({
file: '',
sourceRoot: '',
});
await Promise.all(sourceMapFileList.map((sourceMapFile) => {
return new Promise((resolve) => {
if (!fse.existsSync(sourceMapFile)) {
resolve(true);
}
const content = fse.readFileSync(sourceMapFile, 'utf-8');
const contentLines = content.split('\n').length;
SourceMapConsumer.with(content, null, consumer => {
// Set content by source.
consumer.sources.forEach((source) => {
generator.setSourceContent(source, consumer.sourceContentFor(source));
});
// Get each map from script, and set it to the new map.
consumer.eachMapping((mapping) => {
// No need to add mapping if no name and no line or no column.
if (!mapping.name) return;
generator.addMapping({
generated: {
line: mapping.generatedLine + BASE_LINE + extraLine + contentLines,
column: mapping.generatedColumn + BASE_COLUMN + extraColumn,
},
original: {
line: mapping.originalLine,
column: mapping.originalColumn,
},
source: mapping.source,
name: mapping.name,
});
});
resolve(true);
});
});
}));
return generator.toString();
}

View File

@ -300,8 +300,6 @@ export interface RouteMatch {
export type RenderMode = 'SSR' | 'SSG' | 'CSR';
export type DistType = Array<'html' | 'javascript'>;
declare global {
interface ImportMeta {
// The build target for ice.js

View File

@ -1,19 +0,0 @@
<%- prependCode %>
(function () {
<%- createElement %>
<% if (head && head.children) {-%>
<%- JSON.stringify(head.children) %>.forEach(ele => {
__ICE__CREATE_ELEMENT(ele, document.head);
});
<% } -%>
<% if (body && body.children) {-%>
<%- JSON.stringify(body.children) %>.forEach(ele => {
__ICE__CREATE_ELEMENT(ele, document.body);
});
<% } -%>
})();
<% if (extraScript) {-%>
<% extraScript.forEach((script, index) => { -%>
<%- script %>
<% }) -%>
<% } -%>

View File

@ -2356,27 +2356,15 @@ importers:
abortcontroller-polyfill:
specifier: 1.7.5
version: 1.7.5
ejs:
specifier: ^3.1.6
version: 3.1.8
fs-extra:
specifier: ^10.0.0
version: 10.1.0
history:
specifier: ^5.3.0
version: 5.3.0
htmlparser2:
specifier: ^8.0.1
version: 8.0.1
react-router-dom:
specifier: 6.21.3
version: 6.21.3(react-dom@18.2.0)(react@18.2.0)
semver:
specifier: ^7.4.0
version: 7.4.0
source-map:
specifier: ^0.7.4
version: 0.7.4
devDependencies:
'@remix-run/web-fetch':
specifier: ^4.3.3
@ -21203,6 +21191,7 @@ packages:
/source-map@0.7.4:
resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==}
engines: {node: '>= 8'}
dev: true
/source-map@0.8.0-beta.0:
resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}