slicejs-web-framework 3.4.4 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/Slice/Components/Structural/Controller/Controller.js +135 -0
  2. package/Slice/Components/Structural/EventManager/EventManager.js +289 -4
  3. package/Slice/Components/Structural/EventManager/EventManagerDebugger.js +368 -30
  4. package/Slice/Components/Structural/LeakInspector/LeakInspector.js +296 -0
  5. package/Slice/Components/Structural/StylesManager/ThemeManager/ThemeManager.js +56 -0
  6. package/Slice/Slice.js +14 -1
  7. package/Slice/tests/event-emitters.test.js +79 -0
  8. package/Slice/tests/event-registry.test.js +141 -0
  9. package/Slice/tests/leak-orphans.test.js +131 -0
  10. package/api/{middleware → framework}/securityMiddleware.js +18 -74
  11. package/api/framework/server.js +333 -0
  12. package/api/index.js +5 -279
  13. package/api/tests/public-env-resolver.test.js +1 -1
  14. package/package.json +4 -2
  15. package/sliceConfig.schema.json +10 -1
  16. package/src/App/index.html +31 -1
  17. package/src/manifest.json +15 -0
  18. package/src/robots.txt +4 -0
  19. package/src/sitemap.xml +8 -0
  20. package/src/sliceConfig.json +5 -1
  21. package/types/index.d.ts +12 -0
  22. package/slicejs-cli/.github/workflows/ci.yml +0 -43
  23. package/slicejs-cli/LICENSE +0 -21
  24. package/slicejs-cli/assets/Slice.js-logo.png +0 -0
  25. package/slicejs-cli/client.js +0 -740
  26. package/slicejs-cli/commands/Print.js +0 -164
  27. package/slicejs-cli/commands/Validations.js +0 -92
  28. package/slicejs-cli/commands/build/build.js +0 -40
  29. package/slicejs-cli/commands/buildProduction/buildProduction.js +0 -577
  30. package/slicejs-cli/commands/bundle/bundle.js +0 -236
  31. package/slicejs-cli/commands/createComponent/VisualComponentTemplate.js +0 -55
  32. package/slicejs-cli/commands/createComponent/createComponent.js +0 -128
  33. package/slicejs-cli/commands/deleteComponent/deleteComponent.js +0 -81
  34. package/slicejs-cli/commands/doctor/doctor.js +0 -516
  35. package/slicejs-cli/commands/getComponent/getComponent.js +0 -695
  36. package/slicejs-cli/commands/init/init.js +0 -473
  37. package/slicejs-cli/commands/listComponents/listComponents.js +0 -172
  38. package/slicejs-cli/commands/startServer/startServer.js +0 -240
  39. package/slicejs-cli/commands/startServer/watchServer.js +0 -66
  40. package/slicejs-cli/commands/types/types.js +0 -583
  41. package/slicejs-cli/commands/utils/LocalCliDelegation.js +0 -53
  42. package/slicejs-cli/commands/utils/PackageManager.js +0 -148
  43. package/slicejs-cli/commands/utils/PathHelper.js +0 -75
  44. package/slicejs-cli/commands/utils/VersionChecker.js +0 -169
  45. package/slicejs-cli/commands/utils/bundling/BundleGenerator.js +0 -2525
  46. package/slicejs-cli/commands/utils/bundling/DependencyAnalyzer.js +0 -933
  47. package/slicejs-cli/commands/utils/loadConfig.js +0 -31
  48. package/slicejs-cli/commands/utils/sliceScripts.js +0 -48
  49. package/slicejs-cli/commands/utils/updateManager.js +0 -471
  50. package/slicejs-cli/package.json +0 -71
  51. package/slicejs-cli/playwright.config.js +0 -51
  52. package/slicejs-cli/pnpm-lock.yaml +0 -1629
  53. package/slicejs-cli/tests/build-command-integration.test.js +0 -87
  54. package/slicejs-cli/tests/build-mangling-e2e.test.js +0 -290
  55. package/slicejs-cli/tests/build-production-e2e.test.js +0 -140
  56. package/slicejs-cli/tests/builder-edge-cases.test.js +0 -322
  57. package/slicejs-cli/tests/bundle-generate-e2e.test.js +0 -115
  58. package/slicejs-cli/tests/bundle-generator.test.js +0 -691
  59. package/slicejs-cli/tests/bundle-v2-register-output.test.js +0 -470
  60. package/slicejs-cli/tests/bundling-dependency-edges.test.js +0 -127
  61. package/slicejs-cli/tests/bundling-imports-unit.test.js +0 -267
  62. package/slicejs-cli/tests/client-launcher-contract.test.js +0 -211
  63. package/slicejs-cli/tests/commands-component-crud.test.js +0 -128
  64. package/slicejs-cli/tests/commands-doctor.test.js +0 -152
  65. package/slicejs-cli/tests/commands-version-checker.test.js +0 -37
  66. package/slicejs-cli/tests/component-registry-parse.test.js +0 -34
  67. package/slicejs-cli/tests/dependency-analyzer.test.js +0 -24
  68. package/slicejs-cli/tests/e2e/bundles.spec.js +0 -91
  69. package/slicejs-cli/tests/e2e/dependency-scenarios.spec.js +0 -56
  70. package/slicejs-cli/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  71. package/slicejs-cli/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  72. package/slicejs-cli/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  73. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  74. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  75. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  76. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  77. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  78. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  79. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  80. package/slicejs-cli/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  81. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  82. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  83. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  84. package/slicejs-cli/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  85. package/slicejs-cli/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  86. package/slicejs-cli/tests/e2e/fixtures/components/registry.json +0 -12
  87. package/slicejs-cli/tests/e2e/fixtures/vendor-components.mjs +0 -65
  88. package/slicejs-cli/tests/e2e/navigation.spec.js +0 -44
  89. package/slicejs-cli/tests/e2e/render.spec.js +0 -34
  90. package/slicejs-cli/tests/e2e/serve.mjs +0 -264
  91. package/slicejs-cli/tests/e2e/shared-deps.spec.js +0 -61
  92. package/slicejs-cli/tests/e2e/unminified.spec.js +0 -33
  93. package/slicejs-cli/tests/e2e-serve.test.js +0 -148
  94. package/slicejs-cli/tests/fixtures/components.js +0 -8
  95. package/slicejs-cli/tests/fixtures/sliceConfig.json +0 -74
  96. package/slicejs-cli/tests/getcomponent.test.js +0 -435
  97. package/slicejs-cli/tests/helpers/setup.js +0 -102
  98. package/slicejs-cli/tests/init-command-contract.test.js +0 -101
  99. package/slicejs-cli/tests/init-e2e.test.js +0 -82
  100. package/slicejs-cli/tests/init-project-isolation.test.js +0 -98
  101. package/slicejs-cli/tests/local-cli-delegation.test.js +0 -81
  102. package/slicejs-cli/tests/package-manager.test.js +0 -127
  103. package/slicejs-cli/tests/path-helper.test.js +0 -206
  104. package/slicejs-cli/tests/perf-budget.test.js +0 -84
  105. package/slicejs-cli/tests/postinstall-command.test.js +0 -103
  106. package/slicejs-cli/tests/property-mangling-regression.test.js +0 -182
  107. package/slicejs-cli/tests/types-breakage.test.js +0 -491
  108. package/slicejs-cli/tests/types-generator-errors.test.js +0 -361
  109. package/slicejs-cli/tests/types-generator.test.js +0 -348
  110. /package/api/{utils → framework}/publicEnvResolver.js +0 -0
@@ -1,181 +0,0 @@
1
- export default class Route extends HTMLElement {
2
- constructor(props) {
3
- super();
4
- this.props = props;
5
- this.rendered = false;
6
- }
7
-
8
- init() {
9
- if (!this.props.path) {
10
- this.props.path = ' ';
11
- }
12
-
13
- // If no component is given, derive it from routes.js (the Router's pathToRouteMap).
14
- if (!this.props.component) {
15
- this.props.component = slice.router.pathToRouteMap.get(this.props.path)?.component || ' ';
16
- }
17
- // NOTE: Route does NOT register itself in the Router. `routes.js` is the single source of
18
- // truth for what the Router knows. The Router resolves the URL on first load / refresh /
19
- // deep-link BEFORE this component mounts, so a path that only lived in a Route would 404
20
- // on a direct load. Declare every path in `routes.js`.
21
- }
22
-
23
- get path() {
24
- return this.props.path;
25
- }
26
-
27
- set path(value) {
28
- this.props.path = value;
29
- }
30
-
31
- get component() {
32
- return this.props.component;
33
- }
34
-
35
- set component(value) {
36
- this.props.component = value;
37
- }
38
-
39
- /**
40
- * Verifica si el path actual coincide con el path del Route
41
- * Soporta rutas estáticas y dinámicas con parámetros ${param}
42
- */
43
- matchesCurrentPath() {
44
- // Normalize trailing slash so '/about/' behaves like '/about' (keep root '/').
45
- const raw = window.location.pathname;
46
- const currentPath = raw.length > 1 ? raw.replace(/\/+$/, '') : raw;
47
- const routePath = this.props.path.length > 1 ? this.props.path.replace(/\/+$/, '') : this.props.path;
48
-
49
- // 1. Match exacto, case-insensitive ('/About' coincide con '/about')
50
- if (routePath.toLowerCase() === currentPath.toLowerCase()) {
51
- return { matches: true, params: {} };
52
- }
53
-
54
- // 2. Si la ruta tiene parámetros dinámicos ${param}
55
- if (this.props.path.includes('${')) {
56
- const { regex, paramNames } = this.compilePathPattern(this.props.path);
57
- const match = currentPath.match(regex);
58
-
59
- if (match) {
60
- // Extraer parámetros de la URL
61
- const params = {};
62
- paramNames.forEach((name, i) => {
63
- params[name] = match[i + 1];
64
- });
65
-
66
- return { matches: true, params };
67
- }
68
- }
69
-
70
- return { matches: false, params: {} };
71
- }
72
-
73
- /**
74
- * Convierte un patrón de ruta con ${param} en una expresión regular
75
- * Ejemplo: "/user/${id}" -> /^\/user\/([^/]+)$/
76
- */
77
- compilePathPattern(pattern) {
78
- const paramNames = [];
79
- const regexPattern = '^' + pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
80
- paramNames.push(paramName);
81
- return '([^/]+)'; // Captura cualquier caracter excepto /
82
- }) + '$';
83
-
84
- return {
85
- // 'i': case-insensitive path matching. Captured param values keep their original case.
86
- regex: new RegExp(regexPattern, 'i'),
87
- paramNames
88
- };
89
- }
90
-
91
- async render(params = {}) {
92
- const metadata = this.props.metadata || {};
93
-
94
- if (Route.componentCache[this.props.component]) {
95
- const cachedComponent = Route.componentCache[this.props.component];
96
- this.innerHTML = '';
97
-
98
- // Actualizar props del componente cacheado
99
- if (cachedComponent.props) {
100
- cachedComponent.props = {
101
- ...cachedComponent.props,
102
- params: params,
103
- metadata: metadata // ✅ Incluir metadata
104
- };
105
- }
106
-
107
- if (cachedComponent.update) {
108
- await cachedComponent.update();
109
- }
110
-
111
- this.appendChild(cachedComponent);
112
- } else {
113
- if (!this.props.component) {
114
- return;
115
- }
116
-
117
- if (!slice.controller.componentCategories.has(this.props.component)) {
118
- slice.logger.logError(`${this.sliceId}`, `Component ${this.props.component} not found`);
119
- return;
120
- }
121
-
122
- // Crear el componente con los parámetros y metadata de la ruta
123
- const component = await slice.build(this.props.component, {
124
- sliceId: this.props.component,
125
- params: params, // ✅ Pasar los parámetros al componente
126
- metadata: metadata // ✅ Pasar metadata al componente
127
- });
128
-
129
- this.innerHTML = '';
130
- this.appendChild(component);
131
- Route.componentCache[this.props.component] = component;
132
- }
133
- this.rendered = true;
134
- }
135
-
136
- async renderIfCurrentRoute() {
137
- const { matches, params } = this.matchesCurrentPath();
138
-
139
- if (matches) {
140
- if (this.rendered) {
141
- if (Route.componentCache[this.props.component]) {
142
- const cachedComponent = Route.componentCache[this.props.component];
143
-
144
- // Actualizar params y metadata en el componente cacheado
145
- if (cachedComponent.props) {
146
- cachedComponent.props = {
147
- ...cachedComponent.props,
148
- params: params,
149
- metadata: this.props.metadata || {}
150
- };
151
- }
152
-
153
- if (cachedComponent.update) {
154
- await cachedComponent.update();
155
- }
156
- return true;
157
- }
158
- }
159
- await this.render(params);
160
- return true;
161
- }
162
- return false;
163
- }
164
-
165
- removeComponent() {
166
- delete Route.componentCache[this.props.component];
167
- this.innerHTML = '';
168
- this.rendered = false;
169
- }
170
-
171
- /**
172
- * Cleanup cuando el componente se destruye
173
- */
174
- destroy() {
175
- this.removeComponent();
176
- }
177
- }
178
-
179
- Route.componentCache = {};
180
-
181
- customElements.define('slice-route', Route);
@@ -1,12 +0,0 @@
1
- {
2
- "Button": "Visual",
3
- "Link": "Visual",
4
- "Loading": "Visual",
5
- "MultiRoute": "Visual",
6
- "Navbar": "Visual",
7
- "NotFound": "Visual",
8
- "Route": "Visual",
9
- "FetchManager": "Service",
10
- "IndexedDbManager": "Service",
11
- "LocalStorageManager": "Service"
12
- }
@@ -1,65 +0,0 @@
1
- // One-time dev utility: downloads the starter Visual/Service components from the
2
- // official Slice.js visual library and persists them under ./components so the
3
- // browser E2E can assemble a complete, renderable starter app hermetically
4
- // (no network at test time). Re-run with `node tests/e2e/fixtures/vendor-components.mjs`
5
- // to refresh the fixture.
6
- import fs from 'fs-extra';
7
- import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
-
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const OUT = path.join(__dirname, 'components');
12
- const BASE = 'https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components';
13
-
14
- // Mirrors the starter set installed by `slice init`.
15
- const VISUAL = ['Button', 'Link', 'Loading', 'MultiRoute', 'Navbar', 'NotFound', 'Route'];
16
- const SERVICE = ['FetchManager', 'IndexedDbManager', 'LocalStorageManager'];
17
- // Logical routing components ship JS-only (mirrors getAvailableComponents()).
18
- const JS_ONLY = new Set(['Route', 'MultiRoute', 'Link']);
19
-
20
- function filesFor(name, category) {
21
- if (category === 'Service' || JS_ONLY.has(name)) return [`${name}.js`];
22
- return [`${name}.js`, `${name}.html`, `${name}.css`];
23
- }
24
-
25
- async function fetchText(url) {
26
- const res = await fetch(url);
27
- if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
28
- return res.text();
29
- }
30
-
31
- async function vendor(name, category) {
32
- const dir = path.join(OUT, category, name);
33
- await fs.ensureDir(dir);
34
- for (const file of filesFor(name, category)) {
35
- const required = file.endsWith('.js');
36
- try {
37
- const content = await fetchText(`${BASE}/${category}/${name}/${file}`);
38
- await fs.writeFile(path.join(dir, file), content, 'utf8');
39
- process.stdout.write(` ✓ ${category}/${name}/${file} (${content.length}b)\n`);
40
- } catch (err) {
41
- if (required) throw err;
42
- // Optional html/css may not exist for simple components (e.g. NotFound).
43
- process.stdout.write(` - ${category}/${name}/${file} (skipped: ${err.message.split(' for ')[0]})\n`);
44
- }
45
- }
46
- }
47
-
48
- async function main() {
49
- await fs.emptyDir(OUT);
50
- for (const name of VISUAL) await vendor(name, 'Visual');
51
- for (const name of SERVICE) await vendor(name, 'Service');
52
-
53
- // Persist a registry manifest so the harness can register them in components.js.
54
- const registry = {};
55
- for (const name of VISUAL) registry[name] = 'Visual';
56
- for (const name of SERVICE) registry[name] = 'Service';
57
- await fs.writeJson(path.join(OUT, 'registry.json'), registry, { spaces: 2 });
58
-
59
- console.log(`\nVendored ${VISUAL.length} Visual + ${SERVICE.length} Service components into ${OUT}`);
60
- }
61
-
62
- main().catch((err) => {
63
- console.error(err.message);
64
- process.exit(1);
65
- });
@@ -1,44 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- test.describe('navigation (App Shell + MultiRoute)', () => {
4
- test('navigates Home -> About via the in-page Button', async ({ page }) => {
5
- await page.goto('/');
6
- await expect(page.locator('slice-home-section h1.home__title')).toBeVisible();
7
-
8
- await page.locator('slice-home-section slice-button button').click();
9
-
10
- await expect(page).toHaveURL(/\/about$/);
11
- await expect(page.locator('slice-about-section h1')).toHaveText('About');
12
- // The shell (navbar) persists across the content swap.
13
- await expect(page.locator('slice-nav-bar')).toBeAttached();
14
- });
15
-
16
- test('deep-links directly to /about', async ({ page }) => {
17
- await page.goto('/about');
18
- await expect(page.locator('slice-app-shell')).toBeAttached();
19
- await expect(page.locator('slice-about-section h1')).toHaveText('About');
20
- });
21
-
22
- test('renders the NotFound page for the /404 route', async ({ page }) => {
23
- await page.goto('/404');
24
- await expect(page.locator('slice-notfound')).toBeAttached();
25
- });
26
-
27
- test('navbar links swap sections while keeping the shell mounted', async ({ page }) => {
28
- await page.goto('/about');
29
- await expect(page.locator('slice-about-section')).toBeAttached();
30
-
31
- await page.locator('slice-nav-bar').getByText('Home', { exact: true }).click();
32
-
33
- await expect(page).toHaveURL(/127\.0\.0\.1:\d+\/$/);
34
- await expect(page.locator('slice-home-section')).toBeAttached();
35
- await expect(page.locator('slice-nav-bar')).toBeAttached();
36
- });
37
-
38
- test('survives a full reload on a deep route', async ({ page }) => {
39
- await page.goto('/about');
40
- await expect(page.locator('slice-about-section')).toBeAttached();
41
- await page.reload();
42
- await expect(page.locator('slice-about-section h1')).toHaveText('About');
43
- });
44
- });
@@ -1,34 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
-
3
- // Collect console errors / uncaught page errors for the whole test.
4
- function trackErrors(page) {
5
- const errors = [];
6
- page.on('console', (msg) => {
7
- if (msg.type() === 'error') errors.push(msg.text());
8
- });
9
- page.on('pageerror', (err) => errors.push(String(err)));
10
- return errors;
11
- }
12
-
13
- test.describe('initial app render (production build)', () => {
14
- test('boots the framework and renders the home page with no console errors', async ({ page }) => {
15
- const errors = trackErrors(page);
16
-
17
- await page.goto('/');
18
-
19
- // The App Shell mounts, with its persistent navbar.
20
- await expect(page.locator('slice-app-shell')).toBeAttached();
21
- await expect(page.locator('slice-nav-bar')).toBeAttached();
22
-
23
- // The Home section renders its real content...
24
- await expect(page.locator('slice-home-section h1.home__title')).toHaveText(
25
- /Welcome to your Slice app/
26
- );
27
- // ...including a child Button built via slice.build with its label.
28
- await expect(
29
- page.locator('slice-home-section slice-button .slice_button_value')
30
- ).toHaveText(/Go to About/);
31
-
32
- expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
33
- });
34
- });
@@ -1,264 +0,0 @@
1
- // E2E web server: assembles a complete App-Shell + MultiRoute starter
2
- // (framework src/api + the vendored starter components), runs the real
3
- // production build, and serves dist/ over the production serving contract.
4
- // Used as Playwright's `webServer`. Build artifacts live in an os.tmpdir,
5
- // so nothing is written into the repo.
6
- import http from 'node:http';
7
- import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import fs from 'fs-extra';
10
- import { createTestProject } from '../helpers/setup.js';
11
-
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
- const FIXTURES = path.join(__dirname, 'fixtures', 'components');
14
- const FRAMEWORK_SLICE_JS = path.resolve(
15
- __dirname,
16
- '../../node_modules/slicejs-web-framework/Slice/Slice.js'
17
- );
18
- const PORT = process.env.E2E_PORT ? Number(process.env.E2E_PORT) : 3210;
19
-
20
- const CONTENT_TYPES = {
21
- '.js': 'application/javascript; charset=utf-8',
22
- '.mjs': 'application/javascript; charset=utf-8',
23
- '.json': 'application/json; charset=utf-8',
24
- '.html': 'text/html; charset=utf-8',
25
- '.css': 'text/css; charset=utf-8',
26
- '.svg': 'image/svg+xml',
27
- '.ico': 'image/x-icon',
28
- '.png': 'image/png',
29
- '.woff2': 'font/woff2',
30
- };
31
-
32
- // A shared module imported by two route components that land in different route
33
- // bundles — this is what triggers the vendor-shared bundle. It must transform to
34
- // >2KB (minVendorSharedTransformedSize), so it embeds a data table that survives
35
- // minification.
36
- function buildSharedKitSource() {
37
- const rows = [];
38
- for (let i = 0; i < 60; i += 1) {
39
- rows.push(
40
- ` { id: ${i}, key: 'shared_item_${i}', label: 'Shared registry entry number ${i} reused across multiple routes', enabled: ${i % 2 === 0}, weight: ${i * 7} },`
41
- );
42
- }
43
- return `// Shared kit deliberately imported by several routes.
44
- export const SHARED_TAG = 'shared-kit-v1';
45
-
46
- export const SHARED_TABLE = [
47
- ${rows.join('\n')}
48
- ];
49
-
50
- export const SHARED_PALETTE = { primary: '#4f46e5', secondary: '#06b6d4', surface: '#0b1020', text: '#e6e9f5' };
51
-
52
- export function sharedBadge(label) {
53
- return '[' + SHARED_TAG + '] ' + String(label);
54
- }
55
-
56
- export function sharedLookup(id) {
57
- return SHARED_TABLE.find((row) => row.id === id) || null;
58
- }
59
-
60
- export default { SHARED_TAG, SHARED_TABLE, SHARED_PALETTE, sharedBadge, sharedLookup };
61
- `;
62
- }
63
-
64
- const SHARED_KIT_SOURCE = buildSharedKitSource();
65
-
66
- // Shared modules for the extra dependency scenarios.
67
- const LEAF_SOURCE = `export const LEAF = 'leaf-value';
68
- export function leafTag() { return 'leaf:' + LEAF; }
69
- `;
70
- // mid.js imports leaf.js -> a transitive dependency of any component using mid.
71
- const MID_SOURCE = `import { LEAF, leafTag } from './leaf.js';
72
- export function midValue() { return 'mid(' + LEAF + ')[' + leafTag() + ']'; }
73
- `;
74
- const APP_CONFIG_SOURCE = `const TITLE = 'Configured';
75
- export const VERSION = 3;
76
- export default { title: TITLE, version: VERSION, tagline: 'default-export-works' };
77
- `;
78
-
79
- function componentJs(name, { imports = '', initBody = '' } = {}) {
80
- return `${imports ? imports + '\n\n' : ''}export default class ${name} extends HTMLElement {
81
- constructor(props) {
82
- super();
83
- slice.attachTemplate(this);
84
- slice.controller.setComponentProps(this, props);
85
- }
86
-
87
- init() {
88
- ${initBody}
89
- }
90
- }
91
-
92
- customElements.define('slice-${name.toLowerCase()}', ${name});
93
- `;
94
- }
95
-
96
- async function scaffold(app, createComponent, name, { js, html, css } = {}) {
97
- const ok = createComponent(name, 'Visual');
98
- if (!ok) throw new Error(`[e2e] createComponent failed for ${name}`);
99
- const dir = path.join(app, 'src', 'Components', 'Visual', name);
100
- if (js != null) await fs.writeFile(path.join(dir, `${name}.js`), js, 'utf8');
101
- if (html != null) await fs.writeFile(path.join(dir, `${name}.html`), html, 'utf8');
102
- if (css != null) await fs.writeFile(path.join(dir, `${name}.css`), css, 'utf8');
103
- }
104
-
105
- async function addScenarios(app) {
106
- const createComponent = (await import('../../commands/createComponent/createComponent.js')).default;
107
- const sharedDir = path.join(app, 'src', 'shared');
108
- await fs.ensureDir(sharedDir);
109
- await fs.writeFile(path.join(sharedDir, 'sharedKit.js'), SHARED_KIT_SOURCE, 'utf8');
110
- await fs.writeFile(path.join(sharedDir, 'leaf.js'), LEAF_SOURCE, 'utf8');
111
- await fs.writeFile(path.join(sharedDir, 'mid.js'), MID_SOURCE, 'utf8');
112
- await fs.writeFile(path.join(sharedDir, 'appConfig.js'), APP_CONFIG_SOURCE, 'utf8');
113
-
114
- // (a) vendor-shared: two route components import the same (>2KB) module and
115
- // fall into different route bundles (services vs routing categories).
116
- for (const name of ['ServicesPage', 'RoutingPage']) {
117
- await scaffold(app, createComponent, name, {
118
- js: componentJs(name, {
119
- imports: "import { SHARED_TAG, sharedBadge } from '../../../shared/sharedKit.js';",
120
- initBody:
121
- ` this.dataset.sharedTag = SHARED_TAG;\n this.dataset.sharedBadge = sharedBadge('${name}');`,
122
- }),
123
- });
124
- }
125
-
126
- // (b) transitive dependency: mid.js itself imports leaf.js.
127
- await scaffold(app, createComponent, 'TransitivePage', {
128
- js: componentJs('TransitivePage', {
129
- imports: "import { midValue } from '../../../shared/mid.js';",
130
- initBody: ' this.dataset.transitive = midValue();',
131
- }),
132
- });
133
-
134
- // (c) default-export dependency.
135
- await scaffold(app, createComponent, 'DefaultDepPage', {
136
- js: componentJs('DefaultDepPage', {
137
- imports: "import cfg from '../../../shared/appConfig.js';",
138
- initBody: ' this.dataset.cfgTitle = cfg.title;\n this.dataset.cfgTagline = cfg.tagline;',
139
- }),
140
- });
141
-
142
- // (d) CSS application.
143
- await scaffold(app, createComponent, 'CssProbePage', {
144
- js: componentJs('CssProbePage'),
145
- html: '<div class="css-probe-marker">styled by Slice</div>',
146
- css: '.css-probe-marker { color: rgb(7, 113, 219); font-weight: 700; }',
147
- });
148
-
149
- // Wire all extra routes (keeping the 404 route last).
150
- const entries = [
151
- { path: '/services', component: 'ServicesPage', title: 'Services' },
152
- { path: '/routing', component: 'RoutingPage', title: 'Routing' },
153
- { path: '/transitive', component: 'TransitivePage', title: 'Transitive' },
154
- { path: '/defaultdep', component: 'DefaultDepPage', title: 'DefaultDep' },
155
- { path: '/cssprobe', component: 'CssProbePage', title: 'CssProbe' },
156
- ];
157
- const inserted = entries
158
- .map((e) => ` { path: '${e.path}', component: '${e.component}', metadata: { title: '${e.title}' } },`)
159
- .join('\n');
160
-
161
- const routesPath = path.join(app, 'src', 'routes.js');
162
- let routes = await fs.readFile(routesPath, 'utf8');
163
- routes = routes.replace(/(\n)(\s*)\{ path: '\/404'/, `\n${inserted}\n$2{ path: '/404'`);
164
- await fs.writeFile(routesPath, routes, 'utf8');
165
- }
166
-
167
- async function assembleAndBuild() {
168
- process.env.NODE_ENV = 'production';
169
-
170
- // Framework src + api copied into a throwaway project.
171
- const app = await createTestProject();
172
-
173
- // The bundler discovers the framework's structural components under
174
- // node_modules/slicejs-web-framework — make the installed package resolvable
175
- // from the assembled project so the framework bundle is generated.
176
- const fwPkg = path.resolve(__dirname, '../../node_modules/slicejs-web-framework');
177
- await fs.ensureDir(path.join(app, 'node_modules'));
178
- await fs.ensureSymlink(fwPkg, path.join(app, 'node_modules', 'slicejs-web-framework'), 'dir')
179
- .catch(() => fs.copy(fwPkg, path.join(app, 'node_modules', 'slicejs-web-framework')));
180
-
181
- // Drop in the vendored starter Visual/Service components.
182
- await fs.copy(path.join(FIXTURES, 'Visual'), path.join(app, 'src', 'Components', 'Visual'));
183
- await fs.copy(path.join(FIXTURES, 'Service'), path.join(app, 'src', 'Components', 'Service'));
184
-
185
- process.env.INIT_CWD = app;
186
-
187
- // Add the dependency scenarios (vendor-shared, transitive, default-export, CSS)
188
- // so those bundle paths are built and exercised by the browser specs.
189
- await addScenarios(app);
190
-
191
- // Regenerate components.js from disk (the real `slice component list`).
192
- const listComponents = (await import('../../commands/listComponents/listComponents.js')).default;
193
- listComponents();
194
-
195
- // Real production build. E2E_MINIFY=false exercises the unminified bundle path.
196
- const minify = process.env.E2E_MINIFY !== 'false';
197
- const build = (await import('../../commands/build/build.js')).default;
198
- const ok = await build({ minify, obfuscate: minify });
199
- if (!ok) {
200
- console.error('[e2e] build failed');
201
- process.exit(1);
202
- }
203
-
204
- return path.join(app, 'dist');
205
- }
206
-
207
- function startServer(distDir) {
208
- const server = http.createServer(async (req, res) => {
209
- try {
210
- const pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
211
-
212
- if (pathname === '/slice-env.json') {
213
- res.setHeader('Content-Type', CONTENT_TYPES['.json']);
214
- res.end(JSON.stringify({ mode: 'production', env: {} }));
215
- return;
216
- }
217
- if (pathname === '/Slice/Slice.js') {
218
- const body = await fs.readFile(FRAMEWORK_SLICE_JS).catch(() => null);
219
- if (!body) { res.statusCode = 404; res.end('Slice.js not found'); return; }
220
- res.setHeader('Content-Type', CONTENT_TYPES['.js']);
221
- res.end(body);
222
- return;
223
- }
224
-
225
- const filePath = path.join(distDir, pathname);
226
- if (!filePath.startsWith(distDir)) { res.statusCode = 403; res.end('forbidden'); return; }
227
-
228
- const stat = await fs.stat(filePath).catch(() => null);
229
- if (stat && stat.isFile()) {
230
- res.setHeader('Content-Type', CONTENT_TYPES[path.extname(filePath)] || 'application/octet-stream');
231
- res.end(await fs.readFile(filePath));
232
- return;
233
- }
234
-
235
- // A missing file WITH an extension is a genuine 404 (don't mask asset
236
- // 404s as the SPA shell — that produces confusing MIME errors).
237
- if (path.extname(pathname)) { res.statusCode = 404; res.end('not found'); return; }
238
-
239
- // Extensionless paths are client routes -> SPA fallback.
240
- const index = await fs.readFile(path.join(distDir, 'App', 'index.html')).catch(() => null);
241
- if (index) { res.setHeader('Content-Type', CONTENT_TYPES['.html']); res.end(index); return; }
242
- res.statusCode = 404;
243
- res.end('not found');
244
- } catch (error) {
245
- res.statusCode = 500;
246
- res.end(String(error));
247
- }
248
- });
249
-
250
- server.listen(PORT, '127.0.0.1', () => {
251
- console.log(`[e2e] app server ready at http://127.0.0.1:${PORT} (dist: ${distDir})`);
252
- });
253
-
254
- const shutdown = () => server.close(() => process.exit(0));
255
- process.on('SIGTERM', shutdown);
256
- process.on('SIGINT', shutdown);
257
- }
258
-
259
- assembleAndBuild()
260
- .then(startServer)
261
- .catch((err) => {
262
- console.error('[e2e] fatal:', err);
263
- process.exit(1);
264
- });
@@ -1,61 +0,0 @@
1
- import { test, expect } from '@playwright/test';
2
- import { parse } from '@babel/parser';
3
-
4
- // Two routes (/services, /routing) live in different bundles and import the same
5
- // module, so the build extracts it into a vendor-shared bundle. These specs
6
- // validate both the produced artifact and that the shared dependency resolves at
7
- // runtime (via window.__SLICE_SHARED_DEPS__) when the pages render.
8
-
9
- test.describe('vendor-shared bundle (shared dependency across routes)', () => {
10
- test('the config advertises a real vendor-shared bundle wired into both routes', async ({ request }) => {
11
- const cfg = await (await request.get('/bundles/bundle.config.json')).json();
12
-
13
- expect(cfg.bundles.vendorShared, 'vendorShared present').toBeTruthy();
14
- expect(cfg.bundles.vendorShared.file).toBe('slice-bundle.vendor-shared.js');
15
- expect(cfg.bundles.vendorShared.dependencyCount).toBeGreaterThanOrEqual(1);
16
-
17
- expect(cfg.routeBundles['/services']).toContain('vendor-shared');
18
- expect(cfg.routeBundles['/routing']).toContain('vendor-shared');
19
- });
20
-
21
- test('the vendor-shared bundle is served as a valid, contract-compliant module', async ({ request }) => {
22
- const res = await request.get('/bundles/slice-bundle.vendor-shared.js');
23
- expect(res.status()).toBe(200);
24
- expect(res.headers()['content-type'] || '').toMatch(/javascript/);
25
-
26
- const code = await res.text();
27
- expect(() => parse(code, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow();
28
- expect(code).toContain('SLICE_BUNDLE_META');
29
- expect(code).toContain('registerAll');
30
- expect(code).toContain('SLICE_BUNDLE_DEPENDENCIES');
31
- // The shared module's own content lives here, not duplicated per route.
32
- expect(code).toContain('shared-kit-v1');
33
- });
34
-
35
- test('/services renders and resolves the shared dependency at runtime', async ({ page }) => {
36
- const errors = [];
37
- page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));
38
- page.on('pageerror', (e) => errors.push(String(e)));
39
-
40
- await page.goto('/services');
41
- await expect(page.locator('slice-servicespage')).toBeAttached();
42
- // The data attribute is set in init() from the shared module — its presence
43
- // proves the vendor-shared dependency was registered and resolved.
44
- await expect(page.locator('slice-servicespage')).toHaveAttribute('data-shared-tag', 'shared-kit-v1');
45
- await expect(page.locator('slice-servicespage')).toHaveAttribute('data-shared-badge', /ServicesPage/);
46
-
47
- expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
48
- });
49
-
50
- test('/routing renders and resolves the same shared dependency', async ({ page }) => {
51
- const errors = [];
52
- page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));
53
- page.on('pageerror', (e) => errors.push(String(e)));
54
-
55
- await page.goto('/routing');
56
- await expect(page.locator('slice-routingpage')).toBeAttached();
57
- await expect(page.locator('slice-routingpage')).toHaveAttribute('data-shared-tag', 'shared-kit-v1');
58
-
59
- expect(errors, `console errors:\n${errors.join('\n')}`).toEqual([]);
60
- });
61
- });