1062 lines
38 KiB
JavaScript
1062 lines
38 KiB
JavaScript
import * as t from '@babel/types';
|
|
import path from 'path';
|
|
import _generator from '@babel/generator';
|
|
|
|
// This is just a Pascal heuristic
|
|
// we only assume a function is a component
|
|
// if the first character is in uppercase
|
|
function isComponentishName(name) {
|
|
return name[0] >= 'A' && name[0] <= 'Z';
|
|
}
|
|
function getImportSpecifierName(specifier) {
|
|
if (t.isIdentifier(specifier.imported)) {
|
|
return specifier.imported.name;
|
|
}
|
|
return specifier.imported.value;
|
|
}
|
|
|
|
// Source of solid-refresh (for import)
|
|
const SOLID_REFRESH_MODULE = 'solid-refresh';
|
|
// Exported names from solid-refresh that will be imported
|
|
const IMPORT_REGISTRY = {
|
|
kind: 'named',
|
|
name: '$$registry',
|
|
source: SOLID_REFRESH_MODULE,
|
|
};
|
|
const IMPORT_REFRESH = {
|
|
kind: 'named',
|
|
name: '$$refresh',
|
|
source: SOLID_REFRESH_MODULE,
|
|
};
|
|
const IMPORT_COMPONENT = {
|
|
kind: 'named',
|
|
name: '$$component',
|
|
source: SOLID_REFRESH_MODULE,
|
|
};
|
|
const IMPORT_CONTEXT = {
|
|
kind: 'named',
|
|
name: '$$context',
|
|
source: SOLID_REFRESH_MODULE,
|
|
};
|
|
const IMPORT_DECLINE = {
|
|
kind: 'named',
|
|
name: '$$decline',
|
|
source: SOLID_REFRESH_MODULE,
|
|
};
|
|
const IMPORT_SPECIFIERS = [
|
|
{
|
|
type: 'render',
|
|
definition: { name: 'render', kind: 'named', source: 'solid-js/web' },
|
|
},
|
|
{
|
|
type: 'render',
|
|
definition: { name: 'hydrate', kind: 'named', source: 'solid-js/web' },
|
|
},
|
|
{
|
|
type: 'createContext',
|
|
definition: {
|
|
name: 'createContext',
|
|
kind: 'named',
|
|
source: 'solid-js',
|
|
},
|
|
},
|
|
{
|
|
type: 'createContext',
|
|
definition: {
|
|
name: 'createContext',
|
|
kind: 'named',
|
|
source: 'solid-js/web',
|
|
},
|
|
},
|
|
];
|
|
|
|
function getHotIdentifier(state) {
|
|
switch (state.bundler) {
|
|
// vite/esm uses `import.meta.hot`
|
|
case 'esm':
|
|
case 'vite':
|
|
return t.memberExpression(t.memberExpression(t.identifier('import'), t.identifier('meta')), t.identifier('hot'));
|
|
// webpack 5 uses `import.meta.webpackHot`
|
|
// rspack does as well
|
|
case 'webpack5':
|
|
case 'rspack-esm':
|
|
return t.memberExpression(t.memberExpression(t.identifier('import'), t.identifier('meta')), t.identifier('webpackHot'));
|
|
default:
|
|
// `module.hot` is the default.
|
|
return t.memberExpression(t.identifier('module'), t.identifier('hot'));
|
|
}
|
|
}
|
|
|
|
function getImportIdentifier(state, path, registration) {
|
|
const name = registration.kind === 'named' ? registration.name : 'default';
|
|
const target = `${registration.source}[${name}]`;
|
|
const current = state.imports.get(target);
|
|
if (current) {
|
|
return current;
|
|
}
|
|
const programParent = path.scope.getProgramParent();
|
|
const uid = programParent.generateUidIdentifier(name);
|
|
programParent.registerDeclaration(programParent.path.unshiftContainer('body', t.importDeclaration([
|
|
registration.kind === 'named'
|
|
? t.importSpecifier(uid, t.identifier(registration.name))
|
|
: t.importDefaultSpecifier(uid),
|
|
], t.stringLiteral(registration.source)))[0]);
|
|
state.imports.set(target, uid);
|
|
return uid;
|
|
}
|
|
|
|
function getRootStatementPath(path) {
|
|
let current = path.parentPath;
|
|
while (current) {
|
|
const next = current.parentPath;
|
|
if (next && t.isProgram(next.node)) {
|
|
return current;
|
|
}
|
|
current = next;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
const REGISTRY = 'REGISTRY';
|
|
function createRegistry(state, path) {
|
|
const current = state.imports.get(REGISTRY);
|
|
if (current) {
|
|
return current;
|
|
}
|
|
const root = getRootStatementPath(path);
|
|
const identifier = path.scope.generateUidIdentifier(REGISTRY);
|
|
root.scope.registerDeclaration(root.insertBefore(t.variableDeclaration('const', [
|
|
t.variableDeclarator(identifier, t.callExpression(getImportIdentifier(state, path, IMPORT_REGISTRY), [])),
|
|
]))[0]);
|
|
const pathToHot = getHotIdentifier(state);
|
|
const statements = [
|
|
t.expressionStatement(t.callExpression(getImportIdentifier(state, path, IMPORT_REFRESH), [
|
|
t.stringLiteral(state.bundler),
|
|
pathToHot,
|
|
identifier,
|
|
])),
|
|
];
|
|
// Vite's importAnalysis statically lexes for `import.meta.hot.accept` to
|
|
// mark modules as self-accepting. The actual accept logic is in $$refreshESM,
|
|
// but Vite needs this direct call for server-side HMR boundary detection.
|
|
if (state.bundler === 'vite') {
|
|
statements.unshift(t.expressionStatement(t.callExpression(t.memberExpression(pathToHot, t.identifier('accept')), [])));
|
|
}
|
|
path.scope.getProgramParent().path.pushContainer('body', [
|
|
t.ifStatement(pathToHot, t.blockStatement(statements)),
|
|
]);
|
|
state.imports.set(REGISTRY, identifier);
|
|
return identifier;
|
|
}
|
|
|
|
// https://github.com/babel/babel/issues/15269
|
|
let generator;
|
|
if (typeof _generator !== 'function') {
|
|
generator = _generator.default;
|
|
}
|
|
else {
|
|
generator = _generator;
|
|
}
|
|
function generateCode(node) {
|
|
return generator(node).code;
|
|
}
|
|
|
|
function isPathValid(path, key) {
|
|
return key(path.node);
|
|
}
|
|
function isNestedExpression(node) {
|
|
switch (node.type) {
|
|
case 'ParenthesizedExpression':
|
|
case 'TypeCastExpression':
|
|
case 'TSAsExpression':
|
|
case 'TSSatisfiesExpression':
|
|
case 'TSNonNullExpression':
|
|
case 'TSTypeAssertion':
|
|
case 'TSInstantiationExpression':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
function unwrapNode(node, key) {
|
|
if (key(node)) {
|
|
return node;
|
|
}
|
|
if (isNestedExpression(node)) {
|
|
return unwrapNode(node.expression, key);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isForeignBinding(source, current, name) {
|
|
if (source === current) {
|
|
return true;
|
|
}
|
|
if (current.scope.hasOwnBinding(name)) {
|
|
return false;
|
|
}
|
|
if (current.parentPath) {
|
|
return isForeignBinding(source, current.parentPath, name);
|
|
}
|
|
return true;
|
|
}
|
|
function isInTypescript(path) {
|
|
let parent = path.parentPath;
|
|
while (parent) {
|
|
if (t.isTypeScript(parent.node) && !t.isExpression(parent.node)) {
|
|
return true;
|
|
}
|
|
parent = parent.parentPath;
|
|
}
|
|
return false;
|
|
}
|
|
function getForeignBindings(path) {
|
|
const identifiers = new Set();
|
|
path.traverse({
|
|
ReferencedIdentifier(p) {
|
|
// Check identifiers that aren't in a TS expression
|
|
if (!isInTypescript(p) && isForeignBinding(path, p, p.node.name)) {
|
|
if (isPathValid(p, t.isIdentifier) ||
|
|
isPathValid(p.parentPath, t.isJSXMemberExpression)) {
|
|
identifiers.add(p.node.name);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
const collected = [];
|
|
for (const identifier of identifiers) {
|
|
collected.push(t.identifier(identifier));
|
|
}
|
|
return collected;
|
|
}
|
|
|
|
function getHMRDeclineCall(state, path) {
|
|
const pathToHot = getHotIdentifier(state);
|
|
if (state.bundler === 'vite') {
|
|
return t.ifStatement(pathToHot, t.blockStatement([
|
|
t.expressionStatement(t.callExpression(t.memberExpression(pathToHot, t.identifier('accept')), [
|
|
t.arrowFunctionExpression([], t.callExpression(t.memberExpression(pathToHot, t.identifier('invalidate')), [])),
|
|
])),
|
|
]));
|
|
}
|
|
return t.ifStatement(pathToHot, t.blockStatement([
|
|
t.expressionStatement(t.callExpression(getImportIdentifier(state, path, IMPORT_DECLINE), [
|
|
t.stringLiteral(state.bundler),
|
|
pathToHot,
|
|
])),
|
|
]));
|
|
}
|
|
|
|
function getStatementPath(path) {
|
|
if (t.isStatement(path.node)) {
|
|
return path;
|
|
}
|
|
if (path.parentPath) {
|
|
return getStatementPath(path.parentPath);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isStatementTopLevel(path) {
|
|
let blockParent = path.scope.getBlockParent();
|
|
const programParent = path.scope.getProgramParent();
|
|
// a FunctionDeclaration binding refers to itself as the block parent
|
|
if (blockParent.path === path) {
|
|
blockParent = blockParent.parent;
|
|
}
|
|
return programParent === blockParent;
|
|
}
|
|
|
|
function isIdentifierValidCallee(state, path, callee, target) {
|
|
const binding = path.scope.getBindingIdentifier(callee.name);
|
|
if (binding) {
|
|
const result = state.registrations.identifiers.get(binding);
|
|
if (result && result.type === target) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isPropertyValidCallee(result, target, propName) {
|
|
for (let i = 0, len = result.length; i < len; i++) {
|
|
const registration = result[i];
|
|
if (registration.type === target) {
|
|
if (registration.definition.kind === 'named') {
|
|
if (registration.definition.name === propName) {
|
|
return true;
|
|
}
|
|
}
|
|
else if (propName === 'default') {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function isMemberExpressionValidCallee(state, path, member, target) {
|
|
if (!t.isIdentifier(member.property)) {
|
|
return false;
|
|
}
|
|
const trueObject = unwrapNode(member.object, t.isIdentifier);
|
|
if (!trueObject) {
|
|
return false;
|
|
}
|
|
const binding = path.scope.getBindingIdentifier(trueObject.name);
|
|
if (!binding) {
|
|
return false;
|
|
}
|
|
const result = state.registrations.namespaces.get(binding);
|
|
if (!result) {
|
|
return false;
|
|
}
|
|
return isPropertyValidCallee(result, target, member.property.name);
|
|
}
|
|
function isValidCallee(state, path, { callee }, target) {
|
|
if (t.isV8IntrinsicIdentifier(callee)) {
|
|
return false;
|
|
}
|
|
const trueCallee = unwrapNode(callee, t.isIdentifier);
|
|
if (trueCallee) {
|
|
return isIdentifierValidCallee(state, path, trueCallee, target);
|
|
}
|
|
const trueMember = unwrapNode(callee, t.isMemberExpression);
|
|
if (trueMember && !trueMember.computed) {
|
|
return isMemberExpressionValidCallee(state, path, trueMember, target);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function registerImportSpecifier(state, id, specifier) {
|
|
if (t.isImportDefaultSpecifier(specifier)) {
|
|
if (id.definition.kind === 'default') {
|
|
state.registrations.identifiers.set(specifier.local, id);
|
|
}
|
|
return;
|
|
}
|
|
if (t.isImportSpecifier(specifier)) {
|
|
if (specifier.importKind === 'type' || specifier.importKind === 'typeof') {
|
|
return;
|
|
}
|
|
const name = getImportSpecifierName(specifier);
|
|
if ((id.definition.kind === 'named' && name === id.definition.name) ||
|
|
(id.definition.kind === 'default' && name === 'default')) {
|
|
state.registrations.identifiers.set(specifier.local, id);
|
|
}
|
|
return;
|
|
}
|
|
let current = state.registrations.namespaces.get(specifier.local);
|
|
if (!current) {
|
|
current = [];
|
|
}
|
|
current.push(id);
|
|
state.registrations.namespaces.set(specifier.local, current);
|
|
}
|
|
function registerImportSpecifiers(state, path, definitions) {
|
|
for (let i = 0, len = definitions.length; i < len; i++) {
|
|
const id = definitions[i];
|
|
if (path.node.source.value === id.definition.source) {
|
|
for (let k = 0, klen = path.node.specifiers.length; k < klen; k++) {
|
|
registerImportSpecifier(state, id, path.node.specifiers[k]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function generateUniqueName(path, name) {
|
|
let uid;
|
|
let i = 1;
|
|
do {
|
|
uid = name + '_' + i;
|
|
i++;
|
|
} while (path.scope.hasLabel(uid) ||
|
|
path.scope.hasBinding(uid) ||
|
|
path.scope.hasGlobal(uid) ||
|
|
path.scope.hasReference(uid));
|
|
const program = path.scope.getProgramParent();
|
|
program.references[uid] = true;
|
|
program.uids[uid] = true;
|
|
return t.identifier(uid);
|
|
}
|
|
|
|
function getDescriptiveName(path, defaultName) {
|
|
let current = path;
|
|
while (current) {
|
|
switch (current.node.type) {
|
|
case 'FunctionDeclaration':
|
|
case 'FunctionExpression': {
|
|
if (current.node.id) {
|
|
return current.node.id.name;
|
|
}
|
|
break;
|
|
}
|
|
case 'VariableDeclarator': {
|
|
if (current.node.id.type === 'Identifier') {
|
|
return current.node.id.name;
|
|
}
|
|
break;
|
|
}
|
|
case 'ClassPrivateMethod':
|
|
case 'ClassMethod':
|
|
case 'ObjectMethod': {
|
|
switch (current.node.key.type) {
|
|
case 'Identifier':
|
|
return current.node.key.name;
|
|
case 'PrivateName':
|
|
return current.node.key.id.name;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
current = current.parentPath;
|
|
}
|
|
return defaultName;
|
|
}
|
|
|
|
const REFRESH_JSX_SKIP = /^\s*@refresh jsx-skip\s*$/;
|
|
function shouldSkipJSX(node) {
|
|
// Node without leading comments shouldn't be skipped
|
|
if (node.leadingComments) {
|
|
for (let i = 0, len = node.leadingComments.length; i < len; i++) {
|
|
if (REFRESH_JSX_SKIP.test(node.leadingComments[i].value)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function skippableJSX(node) {
|
|
return t.addComment(node, 'leading', '@refresh jsx-skip');
|
|
}
|
|
function pushAttribute(state, replacement) {
|
|
const key = 'v' + state.attributes.length;
|
|
state.attributes.push(t.jsxAttribute(t.jsxIdentifier(key), t.jsxExpressionContainer(replacement)));
|
|
return key;
|
|
}
|
|
function pushAttributeAndReplace(state, target, replacement) {
|
|
const key = pushAttribute(state, replacement);
|
|
target.replaceWith(t.memberExpression(state.props, t.identifier(key)));
|
|
}
|
|
function extractJSXExpressionFromNormalAttribute(state, attr) {
|
|
const value = attr.get('value');
|
|
if (isPathValid(value, t.isJSXElement) ||
|
|
isPathValid(value, t.isJSXFragment)) {
|
|
value.replaceWith(t.jsxExpressionContainer(value.node));
|
|
}
|
|
if (isPathValid(value, t.isJSXExpressionContainer)) {
|
|
extractJSXExpressionsFromJSXExpressionContainer(state, value);
|
|
}
|
|
}
|
|
function extractJSXExpressionFromRef(state, attr) {
|
|
const value = attr.get('value');
|
|
if (isPathValid(value, t.isJSXExpressionContainer)) {
|
|
const expr = value.get('expression');
|
|
if (isPathValid(expr, t.isExpression)) {
|
|
const unwrappedIdentifier = unwrapNode(expr.node, t.isIdentifier);
|
|
let replacement;
|
|
if (unwrappedIdentifier) {
|
|
const arg = expr.scope.generateUidIdentifier('arg');
|
|
const binding = expr.scope.getBinding(unwrappedIdentifier.name);
|
|
const cannotAssignKind = ['const', 'module'];
|
|
const isConst = binding && cannotAssignKind.includes(binding.kind);
|
|
replacement = t.arrowFunctionExpression([arg], t.blockStatement([
|
|
t.ifStatement(t.binaryExpression('===', t.unaryExpression('typeof', unwrappedIdentifier), t.stringLiteral('function')), t.blockStatement([
|
|
t.expressionStatement(t.callExpression(unwrappedIdentifier, [arg])),
|
|
]),
|
|
// fix the new usage of `ref` attribute,
|
|
// if use `Signals as refs`, the `else` branch will throw an error with `Cannot assign to "setter" because it is a constant` message
|
|
// issue: https://github.com/solidjs/solid-refresh/issues/66
|
|
// docs: https://docs.solidjs.com/concepts/refs#signals-as-refs
|
|
isConst
|
|
? null
|
|
: t.blockStatement([
|
|
t.expressionStatement(t.assignmentExpression('=', unwrappedIdentifier, arg)),
|
|
])),
|
|
]));
|
|
}
|
|
else {
|
|
replacement = expr.node;
|
|
}
|
|
pushAttributeAndReplace(state, expr, replacement);
|
|
}
|
|
}
|
|
}
|
|
function extractJSXExpressionFromUseDirective(state, id, attr) {
|
|
const value = attr.get('value');
|
|
if (isPathValid(value, t.isJSXExpressionContainer)) {
|
|
extractJSXExpressionsFromJSXExpressionContainer(state, value);
|
|
}
|
|
const key = pushAttribute(state, t.identifier(id.name));
|
|
state.vars.push(t.variableDeclarator(t.identifier(id.name), t.memberExpression(state.props, t.identifier(key))));
|
|
}
|
|
function extractJSXExpressionFromAttribute(state, attr) {
|
|
const key = attr.get('name');
|
|
if (isPathValid(key, t.isJSXIdentifier)) {
|
|
if (key.node.name === 'ref') {
|
|
extractJSXExpressionFromRef(state, attr);
|
|
}
|
|
else {
|
|
extractJSXExpressionFromNormalAttribute(state, attr);
|
|
}
|
|
}
|
|
else if (isPathValid(key, t.isJSXNamespacedName)) {
|
|
if (key.node.namespace.name === 'use') {
|
|
extractJSXExpressionFromUseDirective(state, key.node.name, attr);
|
|
}
|
|
else {
|
|
extractJSXExpressionFromNormalAttribute(state, attr);
|
|
}
|
|
}
|
|
}
|
|
function extractJSXExpressionsFromAttributes(state, path) {
|
|
const openingElement = path.get('openingElement');
|
|
const attrs = openingElement.get('attributes');
|
|
for (let i = 0, len = attrs.length; i < len; i++) {
|
|
const attr = attrs[i];
|
|
if (isPathValid(attr, t.isJSXAttribute)) {
|
|
extractJSXExpressionFromAttribute(state, attr);
|
|
}
|
|
if (isPathValid(attr, t.isJSXSpreadAttribute)) {
|
|
const arg = attr.get('argument');
|
|
pushAttributeAndReplace(state, arg, arg.node);
|
|
}
|
|
}
|
|
}
|
|
function convertJSXOpeningToExpression(node) {
|
|
if (t.isJSXIdentifier(node)) {
|
|
return t.identifier(node.name);
|
|
}
|
|
return t.memberExpression(convertJSXOpeningToExpression(node.object), convertJSXOpeningToExpression(node.property));
|
|
}
|
|
const COMPONENT_PATTERN = /^[A-Z_]/;
|
|
function extractJSXExpressionsFromJSXElement(state, path) {
|
|
const openingElement = path.get('openingElement');
|
|
const openingName = openingElement.get('name');
|
|
if ((isPathValid(openingName, t.isJSXIdentifier) &&
|
|
COMPONENT_PATTERN.test(openingName.node.name)) ||
|
|
isPathValid(openingName, t.isJSXMemberExpression)) {
|
|
if (isPathValid(openingName, t.isJSXIdentifier)) {
|
|
const binding = path.scope.getBinding(openingName.node.name);
|
|
if (binding) {
|
|
const statementPath = binding.path.getStatementParent();
|
|
if (statementPath && isStatementTopLevel(statementPath)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
const key = pushAttribute(state, convertJSXOpeningToExpression(openingName.node));
|
|
const replacement = t.jsxMemberExpression(t.jsxIdentifier(state.props.name), t.jsxIdentifier(key));
|
|
openingName.replaceWith(replacement);
|
|
const closingElement = path.get('closingElement');
|
|
if (isPathValid(closingElement, t.isJSXClosingElement)) {
|
|
closingElement.get('name').replaceWith(replacement);
|
|
}
|
|
}
|
|
}
|
|
function extractJSXExpressionsFromJSXExpressionContainer(state, child) {
|
|
const expr = child.get('expression');
|
|
if (isPathValid(expr, t.isExpression)) {
|
|
pushAttributeAndReplace(state, expr, expr.node);
|
|
}
|
|
}
|
|
function extractJSXExpressionsFromJSXSpreadChild(state, child) {
|
|
const arg = child.get('expression');
|
|
pushAttributeAndReplace(state, arg, arg.node);
|
|
}
|
|
function extractJSXExpressions(state, path) {
|
|
if (isPathValid(path, t.isJSXElement)) {
|
|
extractJSXExpressionsFromJSXElement(state, path);
|
|
extractJSXExpressionsFromAttributes(state, path);
|
|
}
|
|
const children = path.get('children');
|
|
for (let i = 0, len = children.length; i < len; i++) {
|
|
const child = children[i];
|
|
if (isPathValid(child, t.isJSXElement) ||
|
|
isPathValid(child, t.isJSXFragment)) {
|
|
extractJSXExpressions(state, child);
|
|
}
|
|
else if (isPathValid(child, t.isJSXExpressionContainer)) {
|
|
extractJSXExpressionsFromJSXExpressionContainer(state, child);
|
|
}
|
|
else if (isPathValid(child, t.isJSXSpreadChild)) {
|
|
extractJSXExpressionsFromJSXSpreadChild(state, child);
|
|
}
|
|
}
|
|
}
|
|
function transformJSX(path) {
|
|
if (shouldSkipJSX(path.node)) {
|
|
return;
|
|
}
|
|
const state = {
|
|
props: path.scope.generateUidIdentifier('props'),
|
|
attributes: [],
|
|
vars: [],
|
|
};
|
|
extractJSXExpressions(state, path);
|
|
const descriptiveName = getDescriptiveName(path, 'template');
|
|
const id = generateUniqueName(path, isComponentishName(descriptiveName)
|
|
? descriptiveName
|
|
: 'JSX_' + descriptiveName);
|
|
const rootPath = getRootStatementPath(path);
|
|
let template = skippableJSX(t.cloneNode(path.node));
|
|
if (state.vars.length) {
|
|
template = t.blockStatement([
|
|
t.variableDeclaration('const', state.vars),
|
|
t.returnStatement(template),
|
|
]);
|
|
}
|
|
const templateComp = t.arrowFunctionExpression([state.props], template);
|
|
if (path.node.loc) {
|
|
templateComp.loc = path.node.loc;
|
|
}
|
|
rootPath.scope.registerDeclaration(rootPath.insertBefore(t.variableDeclaration('const', [t.variableDeclarator(id, templateComp)]))[0]);
|
|
path.replaceWith(skippableJSX(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(id.name), [...state.attributes], true), t.jsxClosingElement(t.jsxIdentifier(id.name)), [], true)));
|
|
}
|
|
|
|
// @ts-nocheck
|
|
/**
|
|
* Copyright (c) 2019 Jason Dent
|
|
* https://github.com/Jason3S/xxhash
|
|
*/
|
|
const PRIME32_1 = 2654435761;
|
|
const PRIME32_2 = 2246822519;
|
|
const PRIME32_3 = 3266489917;
|
|
const PRIME32_4 = 668265263;
|
|
const PRIME32_5 = 374761393;
|
|
function toUtf8(text) {
|
|
const bytes = [];
|
|
for (let i = 0, n = text.length; i < n; ++i) {
|
|
const c = text.charCodeAt(i);
|
|
if (c < 0x80) {
|
|
bytes.push(c);
|
|
}
|
|
else if (c < 0x800) {
|
|
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
}
|
|
else if (c < 0xd800 || c >= 0xe000) {
|
|
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
}
|
|
else {
|
|
const cp = 0x10000 + (((c & 0x3ff) << 10) | (text.charCodeAt(++i) & 0x3ff));
|
|
bytes.push(0xf0 | ((cp >> 18) & 0x7), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
|
}
|
|
}
|
|
return new Uint8Array(bytes);
|
|
}
|
|
/**
|
|
*
|
|
* @param buffer - byte array or string
|
|
* @param seed - optional seed (32-bit unsigned);
|
|
*/
|
|
function xxHash32(buffer, seed = 0) {
|
|
buffer = typeof buffer === 'string' ? toUtf8(buffer) : buffer;
|
|
const b = buffer;
|
|
/*
|
|
Step 1. Initialize internal accumulators
|
|
Each accumulator gets an initial value based on optional seed input. Since the seed is optional, it can be 0.
|
|
```
|
|
u32 acc1 = seed + PRIME32_1 + PRIME32_2;
|
|
u32 acc2 = seed + PRIME32_2;
|
|
u32 acc3 = seed + 0;
|
|
u32 acc4 = seed - PRIME32_1;
|
|
```
|
|
Special case : input is less than 16 bytes
|
|
When input is too small (< 16 bytes), the algorithm will not process any stripe. Consequently, it will not
|
|
make use of parallel accumulators.
|
|
In which case, a simplified initialization is performed, using a single accumulator :
|
|
u32 acc = seed + PRIME32_5;
|
|
The algorithm then proceeds directly to step 4.
|
|
*/
|
|
let acc = (seed + PRIME32_5) & 0xffffffff;
|
|
let offset = 0;
|
|
if (b.length >= 16) {
|
|
const accN = [
|
|
(seed + PRIME32_1 + PRIME32_2) & 0xffffffff,
|
|
(seed + PRIME32_2) & 0xffffffff,
|
|
(seed + 0) & 0xffffffff,
|
|
(seed - PRIME32_1) & 0xffffffff,
|
|
];
|
|
/*
|
|
Step 2. Process stripes
|
|
A stripe is a contiguous segment of 16 bytes. It is evenly divided into 4 lanes, of 4 bytes each.
|
|
The first lane is used to update accumulator 1, the second lane is used to update accumulator 2, and so on.
|
|
Each lane read its associated 32-bit value using little-endian convention.
|
|
For each {lane, accumulator}, the update process is called a round, and applies the following formula :
|
|
```
|
|
accN = accN + (laneN * PRIME32_2);
|
|
accN = accN <<< 13;
|
|
accN = accN * PRIME32_1;
|
|
```
|
|
This shuffles the bits so that any bit from input lane impacts several bits in output accumulator.
|
|
All operations are performed modulo 2^32.
|
|
Input is consumed one full stripe at a time. Step 2 is looped as many times as necessary to consume
|
|
the whole input, except the last remaining bytes which cannot form a stripe (< 16 bytes). When that
|
|
happens, move to step 3.
|
|
*/
|
|
const b = buffer;
|
|
const limit = b.length - 16;
|
|
let lane = 0;
|
|
for (offset = 0; (offset & 0xfffffff0) <= limit; offset += 4) {
|
|
const i = offset;
|
|
const laneN0 = b[i + 0] + (b[i + 1] << 8);
|
|
const laneN1 = b[i + 2] + (b[i + 3] << 8);
|
|
const laneNP = laneN0 * PRIME32_2 + ((laneN1 * PRIME32_2) << 16);
|
|
let acc = (accN[lane] + laneNP) & 0xffffffff;
|
|
acc = (acc << 13) | (acc >>> 19);
|
|
const acc0 = acc & 0xffff;
|
|
const acc1 = acc >>> 16;
|
|
accN[lane] = (acc0 * PRIME32_1 + ((acc1 * PRIME32_1) << 16)) & 0xffffffff;
|
|
lane = (lane + 1) & 0x3;
|
|
}
|
|
/*
|
|
Step 3. Accumulator convergence
|
|
All 4 lane accumulators from previous steps are merged to produce a single remaining accumulator
|
|
of same width (32-bit). The associated formula is as follows :
|
|
```
|
|
acc = (acc1 <<< 1) + (acc2 <<< 7) + (acc3 <<< 12) + (acc4 <<< 18);
|
|
```
|
|
*/
|
|
acc =
|
|
(((accN[0] << 1) | (accN[0] >>> 31)) +
|
|
((accN[1] << 7) | (accN[1] >>> 25)) +
|
|
((accN[2] << 12) | (accN[2] >>> 20)) +
|
|
((accN[3] << 18) | (accN[3] >>> 14))) &
|
|
0xffffffff;
|
|
}
|
|
/*
|
|
Step 4. Add input length
|
|
The input total length is presumed known at this stage. This step is just about adding the length to
|
|
accumulator, so that it participates to final mixing.
|
|
```
|
|
acc = acc + (u32)inputLength;
|
|
```
|
|
*/
|
|
acc = (acc + buffer.length) & 0xffffffff;
|
|
/*
|
|
Step 5. Consume remaining input
|
|
There may be up to 15 bytes remaining to consume from the input. The final stage will digest them according
|
|
to following pseudo-code :
|
|
```
|
|
while (remainingLength >= 4) {
|
|
lane = read_32bit_little_endian(input_ptr);
|
|
acc = acc + lane * PRIME32_3;
|
|
acc = (acc <<< 17) * PRIME32_4;
|
|
input_ptr += 4; remainingLength -= 4;
|
|
}
|
|
```
|
|
This process ensures that all input bytes are present in the final mix.
|
|
*/
|
|
const limit = buffer.length - 4;
|
|
for (; offset <= limit; offset += 4) {
|
|
const i = offset;
|
|
const laneN0 = b[i + 0] + (b[i + 1] << 8);
|
|
const laneN1 = b[i + 2] + (b[i + 3] << 8);
|
|
const laneP = laneN0 * PRIME32_3 + ((laneN1 * PRIME32_3) << 16);
|
|
acc = (acc + laneP) & 0xffffffff;
|
|
acc = (acc << 17) | (acc >>> 15);
|
|
acc =
|
|
((acc & 0xffff) * PRIME32_4 + (((acc >>> 16) * PRIME32_4) << 16)) &
|
|
0xffffffff;
|
|
}
|
|
/*
|
|
```
|
|
while (remainingLength >= 1) {
|
|
lane = read_byte(input_ptr);
|
|
acc = acc + lane * PRIME32_5;
|
|
acc = (acc <<< 11) * PRIME32_1;
|
|
input_ptr += 1; remainingLength -= 1;
|
|
}
|
|
```
|
|
*/
|
|
for (; offset < b.length; ++offset) {
|
|
const lane = b[offset];
|
|
acc = acc + lane * PRIME32_5;
|
|
acc = (acc << 11) | (acc >>> 21);
|
|
acc =
|
|
((acc & 0xffff) * PRIME32_1 + (((acc >>> 16) * PRIME32_1) << 16)) &
|
|
0xffffffff;
|
|
}
|
|
/*
|
|
Step 6. Final mix (avalanche)
|
|
The final mix ensures that all input bits have a chance to impact any bit in the output digest,
|
|
resulting in an unbiased distribution. This is also called avalanche effect.
|
|
```
|
|
acc = acc xor (acc >> 15);
|
|
acc = acc * PRIME32_2;
|
|
acc = acc xor (acc >> 13);
|
|
acc = acc * PRIME32_3;
|
|
acc = acc xor (acc >> 16);
|
|
```
|
|
*/
|
|
acc = acc ^ (acc >>> 15);
|
|
acc =
|
|
(((acc & 0xffff) * PRIME32_2) & 0xffffffff) +
|
|
(((acc >>> 16) * PRIME32_2) << 16);
|
|
acc = acc ^ (acc >>> 13);
|
|
acc =
|
|
(((acc & 0xffff) * PRIME32_3) & 0xffffffff) +
|
|
(((acc >>> 16) * PRIME32_3) << 16);
|
|
acc = acc ^ (acc >>> 16);
|
|
// turn any negatives back into a positive number;
|
|
return acc < 0 ? acc + 4294967296 : acc;
|
|
}
|
|
|
|
const CWD = process.cwd();
|
|
function getFile(filename) {
|
|
return path.relative(CWD, filename);
|
|
}
|
|
function createSignatureValue(node) {
|
|
const code = generateCode(node);
|
|
const result = xxHash32(code).toString(16);
|
|
return result;
|
|
}
|
|
function captureIdentifiers(state, path) {
|
|
path.traverse({
|
|
ImportDeclaration(p) {
|
|
if (!(p.node.importKind === 'type' || p.node.importKind === 'typeof')) {
|
|
registerImportSpecifiers(state, p, state.specifiers);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
function checkValidRenderCall(path) {
|
|
let currentPath = path.parentPath;
|
|
while (currentPath) {
|
|
if (t.isProgram(currentPath.node)) {
|
|
return true;
|
|
}
|
|
if (!t.isStatement(currentPath.node)) {
|
|
return false;
|
|
}
|
|
currentPath = currentPath.parentPath;
|
|
}
|
|
return false;
|
|
}
|
|
function fixRenderCalls(state, path) {
|
|
path.traverse({
|
|
ExpressionStatement(p) {
|
|
const trueCallExpr = unwrapNode(p.node.expression, t.isCallExpression);
|
|
if (trueCallExpr &&
|
|
checkValidRenderCall(p) &&
|
|
isValidCallee(state, p, trueCallExpr, 'render')) {
|
|
// Replace with variable declaration
|
|
const id = p.scope.generateUidIdentifier('cleanup');
|
|
p.replaceWith(t.variableDeclaration('const', [
|
|
t.variableDeclarator(id, p.node.expression),
|
|
]));
|
|
const pathToHot = getHotIdentifier(state);
|
|
p.insertAfter(t.ifStatement(pathToHot, t.expressionStatement(t.callExpression(t.memberExpression(pathToHot, t.identifier('dispose')), [id]))));
|
|
p.skip();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
function wrapComponent(state, path, identifier, component, original = component) {
|
|
const statementPath = getStatementPath(path);
|
|
if (statementPath) {
|
|
const registry = createRegistry(state, statementPath);
|
|
const hotName = t.stringLiteral(identifier.name);
|
|
const componentCall = getImportIdentifier(state, statementPath, IMPORT_COMPONENT);
|
|
const properties = [];
|
|
if (state.filename && original.loc) {
|
|
const filePath = getFile(state.filename);
|
|
properties.push(t.objectProperty(t.identifier('location'), t.stringLiteral(`${filePath}:${original.loc.start.line}:${original.loc.start.column}`)));
|
|
}
|
|
if (state.granular) {
|
|
properties.push(t.objectProperty(t.identifier('signature'), t.stringLiteral(createSignatureValue(component))));
|
|
const dependencies = getForeignBindings(path);
|
|
if (dependencies.length) {
|
|
const dependencyKeys = [];
|
|
let id;
|
|
for (let i = 0, len = dependencies.length; i < len; i++) {
|
|
id = dependencies[i];
|
|
dependencyKeys.push(t.objectProperty(id, id, false, true));
|
|
}
|
|
properties.push(t.objectProperty(t.identifier('dependencies'), t.arrowFunctionExpression([], t.objectExpression(dependencyKeys))));
|
|
}
|
|
}
|
|
return t.callExpression(componentCall, [
|
|
registry,
|
|
hotName,
|
|
component,
|
|
t.objectExpression(properties),
|
|
]);
|
|
}
|
|
return component;
|
|
}
|
|
function wrapContext(state, path, identifier, context) {
|
|
const statementPath = getStatementPath(path);
|
|
if (statementPath) {
|
|
const registry = createRegistry(state, statementPath);
|
|
const hotName = t.stringLiteral(identifier.name);
|
|
const contextCall = getImportIdentifier(state, statementPath, IMPORT_CONTEXT);
|
|
return t.callExpression(contextCall, [registry, hotName, context]);
|
|
}
|
|
return context;
|
|
}
|
|
const SKIP_PATTERN = /^\s*@refresh skip\s*$/;
|
|
const RELOAD_PATTERN = /^\s*@refresh reload\s*$/;
|
|
function setupProgram(state, path, comments) {
|
|
let shouldSkip = false;
|
|
let isDone = false;
|
|
if (comments) {
|
|
for (const { value: comment } of comments) {
|
|
if (SKIP_PATTERN.test(comment)) {
|
|
isDone = true;
|
|
shouldSkip = true;
|
|
break;
|
|
}
|
|
if (RELOAD_PATTERN.test(comment)) {
|
|
isDone = true;
|
|
path.pushContainer('body', getHMRDeclineCall(state, path));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!shouldSkip && state.fixRender) {
|
|
captureIdentifiers(state, path);
|
|
fixRenderCalls(state, path);
|
|
}
|
|
return isDone;
|
|
}
|
|
function isValidFunction(node) {
|
|
return t.isArrowFunctionExpression(node) || t.isFunctionExpression(node);
|
|
}
|
|
function transformVariableDeclarator(state, path) {
|
|
if (path.parentPath.isVariableDeclaration() &&
|
|
!isStatementTopLevel(path.parentPath)) {
|
|
return;
|
|
}
|
|
const identifier = path.node.id;
|
|
const init = path.node.init;
|
|
if (!(init && t.isIdentifier(identifier))) {
|
|
return;
|
|
}
|
|
if (isComponentishName(identifier.name)) {
|
|
const trueFuncExpr = unwrapNode(init, isValidFunction);
|
|
// Check for valid FunctionExpression or ArrowFunctionExpression
|
|
if (trueFuncExpr &&
|
|
// Must not be async or generator
|
|
!(trueFuncExpr.async || trueFuncExpr.generator) &&
|
|
// Might be component-like, but the only valid components
|
|
// have zero or one parameter
|
|
trueFuncExpr.params.length < 2) {
|
|
path.node.init = wrapComponent(state, path, identifier, trueFuncExpr);
|
|
}
|
|
}
|
|
// For `createContext` calls
|
|
const trueCallExpr = unwrapNode(init, t.isCallExpression);
|
|
if (trueCallExpr &&
|
|
isValidCallee(state, path, trueCallExpr, 'createContext')) {
|
|
path.node.init = wrapContext(state, path, identifier, trueCallExpr);
|
|
}
|
|
path.skip();
|
|
}
|
|
function transformFunctionDeclaration(state, path) {
|
|
if (isStatementTopLevel(path)) {
|
|
const decl = path.node;
|
|
// Check if declaration is FunctionDeclaration
|
|
if (
|
|
// Check if the declaration has an identifier, and then check
|
|
decl.id &&
|
|
// if the name is component-ish
|
|
isComponentishName(decl.id.name) &&
|
|
!(decl.generator || decl.async) &&
|
|
// Might be component-like, but the only valid components
|
|
// have zero or one parameter
|
|
decl.params.length < 2) {
|
|
path.scope.registerDeclaration(path.replaceWith(t.variableDeclaration('const', [
|
|
t.variableDeclarator(decl.id, wrapComponent(state, path, decl.id, t.functionExpression(decl.id, decl.params, decl.body), decl)),
|
|
]))[0]);
|
|
path.skip();
|
|
}
|
|
}
|
|
}
|
|
function bubbleFunctionDeclaration(program, path) {
|
|
if (isStatementTopLevel(path)) {
|
|
const decl = path.node;
|
|
// Check if declaration is FunctionDeclaration
|
|
if (
|
|
// Check if the declaration has an identifier, and then check
|
|
decl.id &&
|
|
// if the name is component-ish
|
|
isComponentishName(decl.id.name) &&
|
|
!(decl.generator || decl.async) &&
|
|
// Might be component-like, but the only valid components
|
|
// have zero or one parameter
|
|
decl.params.length < 2) {
|
|
if (path.parentPath.isExportNamedDeclaration()) {
|
|
path.parentPath.replaceWith(t.exportNamedDeclaration(undefined, [
|
|
t.exportSpecifier(decl.id, decl.id),
|
|
]));
|
|
}
|
|
else if (path.parentPath.isExportDefaultDeclaration()) {
|
|
path.replaceWith(decl.id);
|
|
}
|
|
else {
|
|
path.remove();
|
|
}
|
|
const [tmp] = program.unshiftContainer('body', [decl]);
|
|
program.scope.registerDeclaration(tmp);
|
|
tmp.skip();
|
|
}
|
|
}
|
|
}
|
|
function solidRefreshPlugin() {
|
|
return {
|
|
name: 'solid-refresh',
|
|
visitor: {
|
|
Program(programPath, context) {
|
|
var _a, _b, _c;
|
|
const state = {
|
|
jsx: (_a = context.opts.jsx) !== null && _a !== void 0 ? _a : true,
|
|
granular: (_b = context.opts.granular) !== null && _b !== void 0 ? _b : true,
|
|
opts: context.opts,
|
|
specifiers: [...IMPORT_SPECIFIERS],
|
|
imports: new Map(),
|
|
registrations: {
|
|
identifiers: new Map(),
|
|
namespaces: new Map(),
|
|
},
|
|
filename: context.filename,
|
|
bundler: context.opts.bundler || 'standard',
|
|
fixRender: (_c = context.opts.fixRender) !== null && _c !== void 0 ? _c : true,
|
|
};
|
|
if (setupProgram(state, programPath, context.file.ast.comments)) {
|
|
return;
|
|
}
|
|
programPath.traverse({
|
|
FunctionDeclaration(path) {
|
|
bubbleFunctionDeclaration(programPath, path);
|
|
},
|
|
});
|
|
programPath.scope.crawl();
|
|
if (state.jsx) {
|
|
programPath.traverse({
|
|
JSXElement(path) {
|
|
transformJSX(path);
|
|
},
|
|
JSXFragment(path) {
|
|
transformJSX(path);
|
|
},
|
|
});
|
|
programPath.scope.crawl();
|
|
}
|
|
programPath.traverse({
|
|
VariableDeclarator(path) {
|
|
transformVariableDeclarator(state, path);
|
|
},
|
|
FunctionDeclaration(path) {
|
|
transformFunctionDeclaration(state, path);
|
|
},
|
|
});
|
|
// TODO anything simpler than this?
|
|
// This is to fix an issue with webpack
|
|
programPath.scope.crawl();
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
export { solidRefreshPlugin as default };
|
|
//# sourceMappingURL=babel.mjs.map
|